Custom animation for CAEmitterCell - ios

I have a CAEmitterLayer and I'd like to have a simple animation run over the course of each particle's life.
As soon as particle pops in, I'd like it to scale up to about 1.2, then after a short time have it scale back to 1.0 and stay that way until it's lifetime expires.
I know about the scale, scaleRange and scaleSpeed properties of the CAEmitterCell but they're way too random for what I need.
Is this possible to do? I've tried adding a CABasicAnimation like this (my CAEmitterCell's name is "heart"):
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"emitterCells.heart.scale"];
anim.fromValue = #(1.0);
anim.toValue = #(2.0);
anim.duration = 3.0;
anim.fillMode = kCAFillModeForwards;
anim.repeatCount = CGFLOAT_MAX;
[self.heartsEmitter addAnimation:anim forKey:#"scaleAnimation"];
but it doesn't work, the particles just appear at a random scale, they don't animate at all.

I'm not completely sure, but it seems to me that you are applying the animation to the emitter instead of the cells.
If the CAEmitterCell's name is heart try this: [self.heart addAnimation:anim forKey:#"scaleAnimation"];. Does this help?

Related

CAAnimation is overriding previous CAAnimation animation

I have one this scenario:
I add to a layer CAAnimation that transforms it to a specific frame. with a starting time of 0.
Then I add another CAAnimation that transforms it to a different frame. with a starting time of 0.5.
what happens is that the layer immediately gets the second frame (with no animation) and after the first animation time passes the second animation is completed correctly.
This is the animation creation code:
+ (CAAnimation *)transformAnimation:(CALayer *)layer
fromFrame:(CGRect)fromFrame
toFrame:(CGRect)toFrame
fromAngle:(CGFloat)fromAngle
toAngle:(CGFloat)toAngle
anchor:(CGPoint)anchor
vertical:(BOOL)vertical
begin:(CFTimeInterval)begin
duration:(CFTimeInterval)duration {
CATransform3D fromTransform = makeTransform(layer, fromFrame, fromAngle, anchor, vertical);
CATransform3D midTransform1 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.33, vertical);
CATransform3D midTransform2 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.66, vertical);
CATransform3D toTransform = makeTransform(layer, toFrame, toAngle, anchor, vertical);
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:fromTransform],
[NSValue valueWithCATransform3D:midTransform1],
[NSValue valueWithCATransform3D:midTransform2],
[NSValue valueWithCATransform3D:toTransform]
];
animation.beginTime = begin;
animation.duration = duration;
animation.fillMode = kCAFillModeBoth;
animation.calculationMode = kCAAnimationPaced;
animation.removedOnCompletion = NO;
return animation;
}
EDIT
in most scenarios, this code works well and the animations are sequenced correctly. But if I set 1 transform animation to start after 2 seconds and then set another transform to start after 4 seconds. the first transform is applied immediately to the layer and the second animation starts from there.
Any Idea how can I separate the animation to run one after the other?
(I prefer not using a completion block)
Thanks
The easiest and most glaring early fix would be to change the fill mode so that the second animation is not clamped on both ends overriding the previous animation.
animation.fillMode = kCAFillModeForwards;
Also I would adjust the begin time to be
animation.beginTime = CACurrentMediaTime() + begin;
If it is a matter of overlapping begin times and durations and not this let me know and I can provide that as well.

Is CABasicAnimation practice different for UI objects and CALayers?

I was just wondering if this is the correct way to animate a CALayer with a CABasicAnimation.
On Stack Overflow I have been taught how to animate UI objects by setting a new position before running a CABasicAnimation:
Animating a UI Object Example
gameTypeControl.center = CGPointMake(gameTypeControl.center.x, -slidingUpValue/2);
CABasicAnimation *removeGameTypeControl = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
[removeGameTypeControl setFromValue:[NSNumber numberWithFloat:slidingUpValue]];
[removeGameTypeControl setToValue:[NSNumber numberWithFloat:0]];
[removeGameTypeControl setDuration:1.0];
[removeGameTypeControl setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.8 :-0.8 :1.0 :1.0]];
[[gameTypeControl layer] addAnimation:removeGameTypeControl forKey:#"removeGameTypeControl"];
Now I've been tried this method on a CALayer but it seems to work differently. For me to get the same result. I have the set the ToValue to the new y position instead of using the value 0 like I've done with my UI object animations.
Animating a CALayer Example
serveBlock2.position = CGPointMake((screenBounds.size.height/4)*3, -screenBounds.size.width/2);
CABasicAnimation *updateCurrentServe2 = [CABasicAnimation animationWithKeyPath:#"position.y"];
updateCurrentServe2.fromValue = [NSNumber numberWithFloat:slidingUpValue/2];
[updateCurrentServe2 setToValue:[NSNumber numberWithFloat:-screenBounds.size.width/2]];
[updateCurrentServe2 setDuration:1.0];
[serveBlock2 addAnimation:updateCurrentServe2 forKey:#"serveBlock2 updateCurrentServe2"];
Is this correct? Am I doing this right?
The problem is that if serveBlock2 is not a view's immediately underlying layer, setting its position, as you do in the first line of the second example, starts a different animation (an implicit animation). The way to prevent that is by turning off implicit animations. Thus this example from my book:
CompassLayer* c = (CompassLayer*)self.compass.layer;
[CATransaction setDisableActions:YES]; // <=== this is important
c.arrow.transform = CATransform3DRotate(c.arrow.transform, M_PI/4.0, 0, 0, 1);
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 0.8;
[c.arrow addAnimation:anim forKey:nil];
That way, I don't have to have a fromValue or a toValue! The old value and the new value are known automatically from the presentation layer and the model layer.

CABasicAnimation - Animating between two angles

I'm attempting to create an animated CALayer that uses a CABasicAnimation in order to animate an object that rotates at discrete intervals between two angles. Note the word discrete: I'm not trying to create a continuous animation between two points, but rather I want to calculate fixed increments between each angle in order to create a discrete movement feel.
Here's a diagram:
I've looked into setting the byValue attribute of the CABasicAnimation, but I can't seem to get it to work since you can't use fromValue, ToValue, and byValue in one animation. fromValue is always from zero, so I guess that could be dropped, but it still never animates correctly when using the current endAngle as the toValue and a byValue of 0.1 (arbitrarily chosen but should work for testing). Any ideas on how to implement this? Here's code I'm using:
anim = [CABasicAnimation animationWithKeyPath:#"currentEndAngle"];
anim.duration = 0.5f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim.toValue = [NSNumber numberWithFloat:self.endAngle];
anim.byValue = [NSNumber numberWithFloat:0.1f];
anim.repeatCount = HUGE_VALF;
[anim setDelegate:self];
[self addAnimation:anim forKey:#"animKey"];
Thanks!

Chaining the animation of a layer and its sublayer (sublayer model effects superlayer animation)

I'm trying to chain the animation of a layer and its sublayer. However, the problem I'm having is that the model update of the sublayer's position for its animation is visually apparent during its superlayer's animation. Here's the code I'm using to chain animations:
// Superlayer.
CFTimeInterval now = [self.artworkContainer.layer convertTime:CACurrentMediaTime() fromLayer:nil];
CABasicAnimation *slideDown = [CABasicAnimation animationWithKeyPath:#"position"];
slideDown.duration = SLIDE_DOWN_DURATION; //SLIDE_DOWN_DURATION;
slideDown.beginTime = now;
slideDown.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
slideDown.fromValue = [self.artworkContainer.layer valueForKey:#"position"];
CGPoint finalPointOfContainer = CGPointMake(self.artworkContainer.layer.position.x, self.artworkContainer.layer.position.y+verticalDistanceOfFirstFall);
slideDown.toValue = [NSValue valueWithCGPoint:finalPointOfContainer];
self.artworkContainer.layer.position = finalPointOfContainer;
[self.artworkContainer.layer addAnimation:slideDown forKey:#"position"];
// Sublayer
CABasicAnimation *moveActualArtwork = [CABasicAnimation animationWithKeyPath:#"position"];
moveActualArtwork.duration = timeOfSecondFall;
moveActualArtwork.beginTime = now + SLIDE_DOWN_DURATION;
moveActualArtwork.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveActualArtwork.fromValue = [self.artWork.layer valueForKey:#"position"];
CGPoint finalPointOfArtwork = CGPointMake(self.artWork.layer.position.x, self.artWork.layer.position.y+verticalDistanceOfSecondFall);
moveActualArtwork.toValue = [NSValue valueWithCGPoint:finalPointOfArtwork];
self.artWork.layer.position = finalPointOfArtwork; // If this is commented out, the superlayer animation looks correct (of course this isn't a true fix because the sublayer snaps back to its original position without a model update).
moveActualArtwork.delegate = self;
[self.artWork.layer addAnimation:moveActualArtwork forKey:#"position"];
I found the answer as I was writing the question. I needed to set the fillMode property of the second animation: moveActualArtwork.fillMode = kCAFillModeBackwards;
If one were to think of the fromValue and toValue as positions on a timeline, the problem was that before the sublayer hit the fromValue spot on the timeline, it was working off of its model layer to determine its position. By using kCAFillModeBackwards, the fromValue is effectively extended to cover the timeline previous to it so iOS knows what to render before the animation actually begins.
I found the answer reviewing the WWDC 2011 video Session 421 - Core Animation Essentials, at 45:31.

how to speed up CABasicAnimation?

I am using the following CABasicAnimation. But, its very slow..is there a way to speed it up ? Thanks.
- (void)spinLayer:(CALayer *)inLayer duration:(CFTimeInterval)inDuration
direction:(int)direction
{
CABasicAnimation* rotationAnimation;
// Rotate about the z axis
rotationAnimation =
[CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// Rotate 360 degress, in direction specified
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
Core Animation animations can repeat a number of times, by setting the repeatCount property on the animation.
So if you'd like to have an animation run for a total 80 seconds, you need to figure out a duration for one pass of the animation – maybe one full spin of this layer – and then set the duration to be that value. Then let the animation repeat that full spin several times to fill out your duration.
So something like this:
rotationAnimation.repeatCount = 8.0;
Alternatively, you can use repeatDuration to achieve a similar affect:
rotationAnimation.repeatDuration = 80.0;
In either case, you need to set the duration to the time of a single spin, and then repeat it using ONE of these methods. If you set both properties, the behavior is undefined. You can check out the documentation on CAMediaTiming here.

Resources