CAKeyframeAnimation rotation don't draw real shadow - ios

I want to rotate an UIImageView with CAKeyframeAnimation. The problem is that the shadow rotates with the object, making a non-real effect... The shadow must remain under the image, not rotating around it
This is my code. Very simple:
let imagen:UIImageView = UIImageView(image:UIImage(named: "material6.png"))
self.view.addSubview(imagen)
imagen.center = CGPoint(x: 100, y: 100)
imagen.layer.shadowOpacity = 0.75
imagen.layer.shadowOffset = CGSize(width: 0, height: 15)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)
Any solution?

If the shadow is rotating along with the image. You can solve it by adding an UIView with same dimensions of your UIImageView below your UIImageView. If you add shadow to your UIView instead of UIImageView, you can achieve the desired effect in this case.
let imagen:UIImageView = UIImageView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
imagen.center = CGPoint(x: 100, y: 100)
imagen.backgroundColor = UIColor.red
let belowView: UIView = UIView(frame: imagen.frame)
belowView.backgroundColor = UIColor.white
belowView.layer.shadowOpacity = 0.75
belowView.layer.shadowOffset = CGSize(width: 0, height: 15)
self.view.addSubview(belowView)
self.view.addSubview(imagen)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)

Related

ReplicationLayer stopping animation once the first instance finishes animating, but the other instance animations are cut short

I have attached a gif of what the animation looks like when setup as described below. How can I ensure that all 6 instances of the replicatorLayer finish animating and then remove the leftover coin image (i feel using dispatchQueue.asyncAfter is insufficient for animationPurposes, but am unaware of another solution).
I have a CAReplicationLayer as the first and only subLayer of my views primary CALayer object.
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.instanceDelay = TimeInterval(0.1)
replicatorLayer.instanceCount = 6
replicatorLayer.frame.size = frame.size
layer.addSublayer(replicatorLayer)
I create another CALayer for an image and some text and add it as a sublayer replicatorLayer
let coinWithTextLayer = CALayer()
coinWithTextLayer.frame = CGRect(x: circleCenter.x, y: circleCenter.y, width: 61, height: 67)
coinWithTextLayer.masksToBounds = false
let imageLayer = CALayer()
imageLayer.contents = image.cgImage
imageLayer.frame = CGRect(x: 0, y: 20, width: 49, height: 49)
imageLayer.contentsGravity = .resizeAspect
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 34, y: 0, width: 40, height: 19)
textLayer.fontSize = 20
textLayer.string = "+ \(5)"
coinWithTextLayer.addSublayer(imageLayer)
coinWithTextLayer.addSublayer(textLayer)
replicatorLayer.addSublayer(coinWithTextLayer)
I then apply an animation group to the coinWithTextLayer that animates it up and to the right, while also scaling from 0.1 to 1
let positionAnim1 = CABasicAnimation(keyPath: "position")
positionAnim1.fromValue = circleCenter
positionAnim1.toValue = CGPoint(x: circleCenter.x + 24, y: circleCenter.y - 24)
let scaleAnim1 = CABasicAnimation(keyPath: "transform.scale")
scaleAnim1.fromValue = 0.1
scaleAnim1.toValue = 1
scaleAnim1.delegate = self
let firstAnimationGroup = CAAnimationGroup()
firstAnimationGroup.beginTime = 0
firstAnimationGroup.duration = 2
firstAnimationGroup.autoreverses = false
firstAnimationGroup.animations = [positionAnim1, scaleAnim1]
firstAnimationGroup.delegate = self
firstAnimationGroup.fillMode = .backwards
coinWithTextLayer.add(firstAnimationGroup, forKey: "tapCoinAnim1")
/*DispatchQueue.main.asyncAfter(seconds: 0.5 + (0.1 * 6)) { // 0.1 * 6 to account for the delay between instances of the replicatorLayer
coinWithTextLayer.removeFromSuperlayer()
coinWithTextLayer.removeAllAnimations()
}*/ // Commented out because it does successfuly remove the coin after the animation, but feels like an incorrect/inefficient way of doing so

iOS Swift: create a mask from a UILabel

Hi I'm trying to use a label as a mask for a particle emitter layer.
My particles emitter is already set up, but I'm having a problem to get a mask from a label, this is my code that doesn't work so well.
func emitter() {
// define emitter layer as centered w 80% of smallest dimension
let image = emitterImage
let origin = CGPoint(x: view.bounds.midX - view.bounds.width / 2, y: view.bounds.midY - view.bounds.height / 2)
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let size = CGSize(width: view.bounds.width, height: view.bounds.height)
let rect = CGRect(origin: origin, size: size)
let emitterLayer = CAEmitterLayer()
emitterLayer.emitterShape = CAEmitterLayerEmitterShape.rectangle
emitterLayer.emitterSize = rect.size
emitterLayer.emitterPosition = center
// define cells
let cell = CAEmitterCell()
cell.birthRate = Float(size.width * size.height / 10)
cell.lifetime = 1
cell.velocity = 10
cell.scale = 0.1
cell.scaleSpeed = -0.1
cell.emissionRange = .pi * 2
cell.contents = image.cgImage
emitterLayer.emitterCells = [cell]
// add the layer
view.layer.addSublayer(emitterLayer)
// mask
let font = UIFont(name: "HelveticaNeue", size: 64)!
var unichars = [UniChar]("Text".utf16)
var glyphs = [CGGlyph](repeating: 0, count: unichars.count)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)!
let path = UIBezierPath(cgPath: cgpath)
let mask = CAShapeLayer()
mask.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = 10.0
mask.path = path.cgPath
emitterLayer.mask = mask
}
}
Problem 1: I got just the first letter ("T") how could I attach all the characters path in one?
if gotGlyphs {
var paths: [UIBezierPath] = []
for glyph in glyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyph, nil)!
let path = UIBezierPath(cgPath: cgpath)
paths.append(path)
}
In this way I got an array of all chars path, but how can I attach them??
Problem 2: The path is rotated by 180 degrees (why???)
Solved Using CATextLayer to create the mask from a label.
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 0, y: view.bounds.height / 6 * 1, width: yourLabel.frame.width, height: yourLabel.frame.height)
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.fontSize = 30
textLayer.font = yourLabel.font
textLayer.isWrapped = true
textLayer.truncationMode = CATextLayerTruncationMode.end
textLayer.string = yourLabel.text
emitterLayer.mask = textLayer

Swift - CABasicAnimation not working with CALayer

Here is my code about add a layer with animation added.
let myLayer: CALayer = .init()
let myAnimation: CABasicAnimation = .init(keyPath: "bounds.size.height")
// for myLayer to center of superview
let centerPoint: CGPoint = .init(x: mySuperview.bounds.width / 2, y: mySuperview.bounds.height / 2)
myLayer.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
myLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
myLayer.position = centerPoint
myLayer.backgroundColor = UIColor.red.cgColor
myAnimation.fromValue = 20
myAnimation.toValue = 100
myAnimation.duration = 1
myAnimation.beginTime = CACurrentMediaTime() + 1.0
myAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
myLayer.add(myAnimation, forKey: "bounds.size.height")
mySuperview.layer.addSublayer(myLayer)
and myLayer was appeared at correct position. but added animation isn't working.
not only about bounds.size.height. frame.size.height, transform, opacity... I tried all of them but It isn't changing.
I am confused because I have a recollection of working well easily by above code.
Did I skip anything?

How to animate a stitched image?

I'm not sure how to create this animation. Would you somehow split the 1 jpg file evenly in 3 pieces and animate that? Or would you have to make multiple copies of the jpg and do something with them?
Any help would be awesome!
UPDATE
Since you want a crossfade, it's probably easiest to do this by splitting the image into separate cel images:
import UIKit
import PlaygroundSupport
extension UIImage {
func subImage(inUnitRect unitRect: CGRect) -> UIImage? {
guard imageOrientation == .up, let cgImage = self.cgImage else { return nil }
let cgImageWidth = CGFloat(cgImage.width)
let cgImageHeight = CGFloat(cgImage.height)
let scaledRect = CGRect(x: unitRect.origin.x * cgImageWidth, y: unitRect.origin.y * cgImageHeight, width: unitRect.size.width * cgImageWidth, height: unitRect.size.height * cgImageHeight)
guard let croppedCgImage = cgImage.cropping(to: scaledRect) else { return nil }
return UIImage(cgImage: croppedCgImage, scale: scale, orientation: .up)
}
}
let image = #imageLiteral(resourceName: "image.png")
let celCount: CGFloat = 3
let cels = stride(from: 0, to: celCount, by: 1).map({ (i) -> UIImage in
image.subImage(inUnitRect: CGRect(x: i / celCount, y: 0, width: 1/3, height: 1))!
})
Then we can use a keyframe animation to crossfade the layer contents:
let imageView = UIImageView(image: cels[0])
PlaygroundPage.current.liveView = imageView
let animation = CAKeyframeAnimation(keyPath: "contents")
var values = [CGImage]()
var keyTimes = [Double]()
for (i, cel) in cels.enumerated() {
keyTimes.append(Double(i) / Double(cels.count))
values.append(cel.cgImage!)
// The 0.9 means 90% of the time will be spent *outside* of crossfade.
keyTimes.append((Double(i) + 0.9) / Double(cels.count))
values.append(cel.cgImage!)
}
values.append(cels[0].cgImage!)
keyTimes.append(1.0)
animation.keyTimes = keyTimes.map({ NSNumber(value: $0) })
animation.values = values
animation.repeatCount = .infinity
animation.duration = 5
imageView.layer.add(animation, forKey: animation.keyPath)
Result:
ORIGINAL
There are multiple ways you can do this. One is by setting or animating the contentsRect property of the image view's layer.
In your image, there are three cels, and each occupies exactly 1/3 of the image. The contentsRect is in the unit coordinate space, which makes computation easy. The contentsRect for cel i is CGRect(x: i/3, y: 0, width: 1/3, height: 0).
You want discrete jumps between cels, instead of smooth sliding transitions, so you need to use a keyframe animation with a kCAAnimationDiscrete calculationMode.
import UIKit
import PlaygroundSupport
let image = #imageLiteral(resourceName: "image.png")
let celSize = CGSize(width: image.size.width / 3, height: image.size.height)
let imageView = UIImageView()
imageView.frame = CGRect(origin: .zero, size: celSize)
imageView.image = image
PlaygroundPage.current.liveView = imageView
let animation = CAKeyframeAnimation(keyPath: "contentsRect")
animation.duration = 1.5
animation.calculationMode = kCAAnimationDiscrete
animation.repeatCount = .infinity
animation.values = [
CGRect(x: 0, y: 0, width: 1/3.0, height: 1),
CGRect(x: 1/3.0, y: 0, width: 1/3.0, height: 1),
CGRect(x: 2/3.0, y: 0, width: 1/3.0, height: 1)
] as [CGRect]
imageView.layer.add(animation, forKey: animation.keyPath!)
Result:

CAKeyframeAnimation counter-clockwise

I want to animate along a bezierPath counter-clockwise.
At the moment the code below only allows me to do it clockwise.
let arrowHead = UIImageView()
arrowHead.image = UIImage(named: "arrowHead.png")
arrowHead.frame = CGRect(x: 55, y: 300, width: 12, height: 12)
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(50, 240, 320, 240))
let ovalShapeLayer = CAShapeLayer()
ovalShapeLayer.fillColor = UIColor.clearColor().CGColor
ovalShapeLayer.strokeColor = UIColor.lightGrayColor().CGColor
ovalShapeLayer.lineWidth = 1.5
ovalShapeLayer.path = ovalPath.CGPath
self.view.layer.addSublayer(ovalShapeLayer)
let myKeyFrameAnimation = CAKeyframeAnimation(keyPath: "position")
myKeyFrameAnimation.path = ovalPath.CGPath
myKeyFrameAnimation.rotationMode = kCAAnimationRotateAuto
myKeyFrameAnimation.repeatCount = Float.infinity
myKeyFrameAnimation.duration = 6.0
// add the animation
arrowHead.layer.addAnimation(myKeyFrameAnimation, forKey: "animate position along path")
self.view.addSubview(arrowHead)
any ideas?
Ok got it.
myKeyFrameAnimation.speed = -1
Was very simple...
At alternative solution is to use bezierPathWithArcCenter... as documented here which allows you to specify the direction of the circle. Using -M_PI_2 as the start angle and 3 * M_PI_2 as the end angle will give a circle beginning and ending at the top.

Resources