Cabasicanimation Creates Empty Default Value Copy of Calayer

CABasicAnimation creates empty default value copy of CALayer

Figured out the answer!

In init(layer:) you have to copy the property values to your class manually. Here's how that looks in action:

override init(layer: AnyObject) {
if let layer = layer as? RadialGradientLayer {
gradientOrigin = layer.gradientOrigin
gradientRadius = layer.gradientRadius
colors = layer.colors
locations = layer.locations
}
super.init(layer: layer)
}

CALayer during animation is using default property value instead of what should be the current instance's property value

I figured out the solution for anyone in need. When a CALayer is animated, the CALayer isn't mutated, rather it is duplicated by using CALayer's init(layer:).

I was reading the documentation on this initializer (enter link description here) and found that inside the override you are suppose to copy layer's custom properties to the new CALayer subclass.

public override init(layer: Any) {
super.init(layer: layer)

self.common()

guard let overlay = layer as? OverlayLayer else {
return
}

self.color = overlay.color
}

Here's an update Gist with the fix if you're curious

https://gist.github.com/Wowserman/ca23d9596f2979effd9cc8fa9e73c893

CABasicAnimation resets to initial value after animation completes

Here's the answer, it's a combination of my answer and Krishnan's.

cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;

The default value is kCAFillModeRemoved. (Which is the reset behavior you're seeing.)

replacing CALayer and CABasicAnimation with SKScene and SKActions

You can easily achieve this kind of animation using moveToX and the timingMode parameter.

New Swift 3 translation below at the end of this answer.

To make an example I use the Xcode Sprite-Kit "Hello, World!" official project demo:

class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Hello, World!"
myLabel.fontSize = 15
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))

self.addChild(myLabel)
mediaTimingFunctionEaseInEaseOutEmulate(myLabel)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKLabelNode) {
let actionMoveLeft = SKAction.moveToX(CGRectGetMidX(self.frame)-100, duration:1.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

let actionMoveRight = SKAction.moveToX(CGRectGetMidX(self.frame)+100, duration:1.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
}

Output:

Sample Image

Update (This part start to emulate the static ball and the dynamic ball moving left and right but without metaball animations)

class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode

if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let seq = SKAction.sequence([zoomIn,zoomOut])
ball.runAction(seq,withKey: "zoom")
}
}
i += 1
}
}
}

New update with metaball animation:

Finally I've realize this result, my goal is to make it very similar to the original :

Sample Image

is it possible to make some variations to times (for example zoomIn or zoomOut time values or actionMoveLeft, actionMoveRight time values), this is the code:

import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

let actionMoveRight = SKAction.moveToX(dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : NSTimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.moveToPoint(p1a)
pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLineToPoint(p2b)
pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLineToPoint(p1a)
pathJoinedCircles.closePath()
let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
shapeNode.strokeColor = SKColor.orangeColor()
shapeNode.fillColor = UIColor.clearColor()
addChild(shapeNode)
let wait = SKAction.waitForDuration(vanishingTime)
self.runAction(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRectMake(ball.frame.origin.x-self.radiusBall*3,ball.frame.origin.y,ball.frame.width+(self.radiusBall*6),ball.frame.height)
if CGRectContainsRect(enlargeFrame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.EaseInEaseOut
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let wait = SKAction.waitForDuration(0.8)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.runAction(seq,withKey: "zoom")
}
}
self._metaball(ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 4 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}

Swift 3:

(I've made a little change to maxDistance: 4 * self.radiusBall with maxDistance: 5 * self.radiusBall to become more similar to the original but you can change it as you wish)

import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMove(to view: SKView) {
let label = self.childNode(withName: "//helloLabel") as? SKLabelNode
label?.removeFromParent()
self.anchorPoint = CGPoint.zero
// Some parameters
let strokeColor = SKColor.orange
let dBHeight = self.frame.midY
let dBStartX = self.frame.midX-260 // extreme left
let dBStopX = self.frame.midX+260 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPoint(x:self.frame.midX, y:dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clear
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPoint(x:dBStartX+(distanceBtwBalls*CGFloat(i)), y:dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clear
if i == 0 || i == totalBalls-1 {
ball.isHidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(node: dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveTo(x: dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.easeInEaseOut

let actionMoveRight = SKAction.moveTo(x: dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.easeInEaseOut

node.run(SKAction.repeatForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : TimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(point: center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(point: center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(point: p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.move(to: p1a)
pathJoinedCircles.addCurve(to: p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLine(to: p2b)
pathJoinedCircles.addCurve(to: p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLine(to: p1a)
pathJoinedCircles.close()
let shapeNode = SKShapeNode(path: pathJoinedCircles.cgPath)
shapeNode.strokeColor = SKColor.orange
shapeNode.fillColor = UIColor.clear
addChild(shapeNode)
let wait = SKAction.wait(forDuration: vanishingTime)
self.run(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(_ currentTime: TimeInterval) {
var i = 0
self.enumerateChildNodes(withName: "ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRect(x:ball.frame.origin.x-self.radiusBall*3,y:ball.frame.origin.y,width:ball.frame.width+(self.radiusBall*6),height:ball.frame.height)
if enlargeFrame.contains(self.dBCircle.frame) {
if (ball.action(forKey: "zoom") == nil) {
let zoomIn = SKAction.scale(to: 1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.easeInEaseOut
let zoomOut = SKAction.scale(to: 1.0, duration: 0.25)
let wait = SKAction.wait(forDuration: 0.7)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.run(seq,withKey: "zoom")
}
}
self._metaball(circle2: ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 5 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}

How to animate a CALayer to have empty contents

I decided this was the cleanest approach, since there was no other answer forthcoming:

    UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

CABasicAnimation *backgroundOut = [CABasicAnimation animationWithKeyPath:@"contents"];
backgroundOut.toValue = (__bridge id)(blank.CGImage);
backgroundOut.fillMode = kCAFillModeForwards;
backgroundOut.removedOnCompletion = NO;
backgroundOut.duration = 3.0;

[_backgroundLayer addAnimation:backgroundOut forKey:@"contents"];


Related Topics



Leave a reply



Submit