CAKeyFrameAnimation along a CGPath defined by SVG file is jerky. - ios

I want to animate an object along a path. I got it to work, but the movement is not linear. (it slows down a lot along minor bends in the path.)
Here are the main steps.
1) Import the path from an SVG file using PockeSVG
-(CGMutablePathRef)makeMutablePathFromSVG:(NSString *)svgFileName{
PocketSVG *myVectorDrawing0 = [[PocketSVG alloc] initFromSVGFileNamed:svgFileName];
UIBezierPath *myBezierPath0 = myVectorDrawing0.bezier;
return CGPathCreateMutableCopy([myBezierPath0 CGPath]);
}
2) Create the CAKeyFrameAnimation
CAKeyframeAnimation *moveAlongPath = [CAKeyframeAnimation animationWithKeyPath:#"position"];
CGMutablePathRef animationPath = CGPathCreateMutableCopy(pathForwardTo5);
[moveAlongPath setPath:animationPath];
[moveAlongPath setDuration:1.0f];
moveAlongPath.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveAlongPath.delegate = self;
[[trackButton layer] addAnimation:moveAlongPath forKey:#"moveButtonAlongPath"];
CFRelease(animationPath);
I tried using other CAMediaTimingFunctions and they all exhibit this kind of behavior.
Question: The animation itself works, but it doesn't follow a smooth and consistent speed. Any idea why?

The slow downs that you are seeing is due to the default calculation mode where each segment of the animation takes the same time. This means that very short segments are going to move slower.
If you look a the documentation for the path property, you will see how to achieve a constant velocity along the path:
How the animation proceeds along the path is dependent on the value in the calculationMode property. To achieve a smooth, constant velocity animation along the path, set the calculationMode property to kCAAnimationPaced or kCAAnimationCubicPaced.
So, to get a constant pace throughout the animation you should set the calculationMode to kCAAnimationPaced.

Related

CABasicAnimation to path strange behaviour [duplicate]

I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.

CAKeyFrameAnimation is not animating when using CGPath

I am trying to animate a dot around a rect in a circular fashion.
For that I created an oval UIBezierPath, created a CAKeyFrameAnimation and set the path for keyPath 'position'.
I setup a project to experiment with animations and got something working nicely.
The problem is, when I add the code to my app, the dot isn't animating.
I tried to add that same code to other apps and it work just fine. It doesn't work just for that one app.
The strange thing is that if I replace the path by an array of value containing CGPoint then the dot animates. It's just not working for CGPath for that specific app.
Is there a hidden setting somewhere that blocks CAKeyFrameAnimation using CGPath?
Is there another way to do it? Using values it doesn't curve nicely like it does with a path.
Here is the code I used if that helps:
CAKeyframeAnimation *orbit = [CAKeyframeAnimation animation];
orbit.keyPath = #"position";
orbit.path = [self foregroundArcPath].CGPath;
orbit.duration = 0.7f;
orbit.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
orbit.repeatCount = HUGE_VALF;
orbit.calculationMode = kCAAnimationPaced;
[self.foregroundDot addAnimation:orbit forKey:#"orbit"];
Like I said, this code works, just not in this one app.
It's my first post here, sorry if it's poorly formulated.
Perhaps foregroundArcPath is nil or an empty path?
I apologize for giving an answer with little detail. I would have just commented but I am not yet able to do that.
I encounter exactly problem as you did. Perhaps your project import some NSObject override class, such as RMMapper.
-(instancetype)copyWithZone:(NSZone *)zone {
typeof(self) copiedObj = [[[self class] allocWithZone:zone] init];
CGPath is not suitable for the copy zone.
Hoping this tip will help you.

Using CABasicAnimation to rotate a UIImageView more than once

I am using a CABasicAnimation to rotate a UIImageView 90 degrees clockwise, but I need to have it rotate a further 90 degrees later on from its position after the initial 90 degree rotation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 10;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(0)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(90)];
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Using the code above works initially, the image stays at a 90 degree angle. If I call this again to make it rotate a further 90 degrees the animation starts by jumping back to 0 and rotating 90 degrees, not 90 to 180 degrees.
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
Any ideas?
tl;dr: It is very easy to misuse removeOnCompletion = NO and most people don't realize the consequences of doing so. The proper solution is to change the model value "for real".
First of all: I'm not trying to judge or be mean to you. I see the same misunderstanding over and over and I can see why it happens. By explaining why things happen I hope that everyone who experience the same issues and sees this answer learn more about what their code is doing.
What went wrong
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
That is very true and it's exactly what happens. Computers are funny in that sense. They always to exactly what you tell them and not what you want them to do.
removeOnCompletion = NO can be a bitch
In your case the villain is this line of code:
animation.removedOnCompletion = NO;
It is often misused to keep the final value of the animation after the animation completes. The only problem is that it happens by not removing the animation from the view. Animations in Core Animation doesn't alter the underlying property that they are animating, they just animate it on screen. If you look at the actual value during the animation you will never see it change. Instead the animation works on what is called the presentation layer.
Normally when the animation completes it is removed from the layer and the presentation layer goes away and the model layer appears on screen again. However, when you keep the animation attached to the layer everything looks as it should on screen but you have introduced a difference between what the property says is the transform and how the layer appears to be rotated on screen.
When you configure the animation to be additive that means that the from and to values are added to the existing value, just as you said. The problem is that the value of that property is 0. You never change it, you just animate it. The next time you try and add that animation to the same layer the value still won't be changed but the animation is doing exactly what it was configured to do: "animate additively from the current value of the model".
The solution
Skip that line of code. The result is however that the rotation doesn't stick. The better way to make it stick is to change the model. Set the new end value of the rotation before animating the rotation so that the model looks as it should when the animation gets removed.
byValue is like magic
There is a very handy property (that I'm going to use) on CABasicAnimation that is called byValue that can be used to make relative animations. It can be combined with either toValue and fromValue to do many different kinds of animations. The different combinations are all specified in its documentation (under the section). The combination I'm going to use is:
byValue and toValue are non-nil. Interpolates between (toValue - byValue) and toValue.
Some actual code
With an explicit toValue of 0 the animation happens from "currentValue-byValue" to "current value". By changing the model first current value is the end value.
NSString *zRotationKeyPath = #"transform.rotation.z"; // The killer of typos
// Change the model to the new "end value" (key path can work like this but properties don't)
CGFloat currentAngle = [[_myview.layer valueForKeyPath:zRotationKeyPath] floatValue];
CGFloat angleToAdd = M_PI_2; // 90 deg = pi/2
[_myview.layer setValue:#(currentAngle+angleToAdd) forKeyPath:zRotationKeyPath];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:zRotationKeyPath];
animation.duration = 10;
// #( ) is fancy NSNumber literal syntax ...
animation.toValue = #(0.0); // model value was already changed. End at that value
animation.byValue = #(angleToAdd); // start from - this value (it's toValue - byValue (see above))
// Add the animation. Once it completed it will be removed and you will see the value
// of the model layer which happens to be the same value as the animation stopped at.
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Small disclaimer:
I didn't run this code but am fairly certain that it runs as it should and that I didn't do any typos. Correct me if I did. The entire discussion is still valid.
pass incremental value of angle see my code
static int imgAngle=0;
- (void)doAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 5;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle+90)];
[self.imgView.layer addAnimation:animation forKey:#"90rotation"];
imgAngle+=90;
if (imgAngle>360) {
imgAngle = 0;
}
}
Above code is just for idea. Its not tested

Smoothly animated hole in CALayer?

Ok, I figured out how to do this based on various posts here on SO, and it works great. I'm working on an overlay which will basically mask the whole window except for a small region. This is for drawing attention to a specific area of my app. I'm using a bunch of calls to moveToPoint: and addLineToPoint: like so (this is in my CALayer subclass' drawInContext:):
....
// inner path (CW)
[holePath moveToPoint:CGPointMake(x, y)];
[holePath addLineToPoint:CGPointMake(x + w, y)];
[holePath addLineToPoint:CGPointMake(x + w, y + h)];
[holePath addLineToPoint:CGPointMake(x, y+h)];
// outer path (CCW)
[holePath moveToPoint:CGPointMake(xBounds, yBounds)];
[holePath addLineToPoint:CGPointMake(xBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds)];
// put the path in the context
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0, 0);
CGContextAddPath(ctx, holePath.CGPath);
CGContextClosePath(ctx);
// set the color
CGContextSetFillColorWithColor(ctx, self.overlayColor.CGColor);
// draw the overlay
CGContextDrawPath(ctx, kCGPathFillStroke);
(holePath is an instance of UIBezierPath.)
So far so good. The next step is animation. In order to do this (I also found this technique here on SO) I made a method as follows
-(CABasicAnimation *)makeAnimationForKey:(NSString *)key {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:key];
anim.fromValue = [[self presentationLayer] valueForKey:key];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 0.5;
return anim;
}
and overrode actionForKey:, initWithLayer: and needsDisplayForKey: (returning the result of makeAnimationForKey: in actionForKey:. Now, I get a nice "hole layer", which has a property holeRect which is animatable using implicit CAAnimations! Unfortunately, it's SUPER choppy. I get something like 2 or 3 frames per second. I thought that perhaps the problem was the background, and tried replacing it with a snapshot, but no dice. Then, I used Instruments to profile, and discovered that the HUGE hog here is the call to CGContextDrawPath().
tl;dr I guess my question comes down to this: is there a simpler way to create this layer with a hole in it which will redraw faster? My hunch is that, if I could simplify the path I'm using, drawing the path would be lighter. Or possibly masking? Please help!
Ok, I tried phix23's suggestion, and it totally did the trick! At first, I was subclassing CALayer and adding a CAShapeLayer as a sublayer, but I couldn't get it to work properly and I'm pretty tired at this point, so I gave up and just replaced my subclass completely with CAShapeLayer! I used the above code in its own method, returning the UIBezierPath, and animated like so:
UIBezierPath* oldPath = [self pathForHoleRect:self.holeRect];
UIBezierPath* newPath = [self pathForHoleRect:rectToHighlight];
self.holeRect = rectToHighlight;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 0.5;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (id)oldPath.CGPath;
animation.toValue = (id)newPath.CGPath;
[self.holeLayer addAnimation:animation forKey:#"animatePath"];
self.holeLayer.path = newPath.CGPath;
Interesting side note -- path is NOT implicitly animatable. I guess that makes sense.
Redrawing is expensive, animation is not. When you set the value for "holeRect" you are instructing the entire layer to redraw. THIS IS NOT THE SAME AS A TYPICAL PERFORMANCE OPTIMIZED PROPERTY ANIMATION. If the layer is the size of the entire screen, then you are effectively recreating a new version of the layer on every frame. This is one of the things that apple does to ensure smooth animations. You want to prevent redraws as much as possible.
I would suggest creating a layer with the hole in the center, and make sure that the layer is large enough to cover the entire screen no matter where the hole is centered. Then the animation should animate the "position" property of the layer. While it may seem wasteful to have this massive layer of which only ~ 30% will ever be used at once, it is much less wasteful then redrawing on every frame. If you would like to maintain your "holeRect" interface you can, but the layer with "holeRect" property should contain a sublayer or sublayers that are animated in the way that I described (in layoutSubviews, not drawInContext:).
In summary, make sure you are animating position, opacity, transform as these are among the most efficient animations on layers, and redraw only when necessary.
In your example you use a rectangular hole. Implementing such animation could be more efficient if you use four rectangular layers (see attached image). To animate the hole rectangle position you'll have to animate the blue layers' widths, and red layers' heights. (if the hole rectangle can change width, the red layers' widths will also have to be animated)
If you need a non-rectangular hole, you could place another layer in the middle of these, which has the hole inside and change only the position of this layer (see the 2nd image). Resizing this non-rectangular hole will result in recreating only the middle layer's contents, so it should be a bit faster than in your original case where this layer was of the size of the screen if I understood correctly.
I am posting an anwser because I am not able to comment on your question (yet) due to reputation.
My question is whether this has to be created programmatically? If it is just about creating an attention area you could use another approach.
Why not just use a black png with a transparent hole in it? Then you do not have performance issues during animation and to a certain extent if you choose your original hole size well, you could even resize it. The image has to be just big enough so that it covers every part of the view indenpendently of the current position of the hole. The part outside the hole could also include transparency resulting in a shadow effect outside the attention area.

How to animate one image over the unusual curve path in my iPad application?

I have two separate images.
CurvedPath image (Attached Below)
PersonImage
As you can see this path is not having perfect arc for single angle.
I have another PersonImage that I want to animate exactly above the center line for this arc image with zoom-in effect.
This animation should be start from bottom-left point to top-right point.
How to achieve this kind of animation?
I have read about QuartzCore and BeizerPath animation, but as I am having less knowledge about those, it will quiet difficult to me to achieve this quickly.
Moving along a path
As long as you can get the exact path that you want to animate the image align you can do it using Core Animation and CAKeyframeAnimation.
Create a key frame animation for the position property and set it do animate along your path
CAKeyframeAnimation *moveAlongPath = [CAKeyframeAnimation animationWithKeyPath:#"position"];
[moveAlongPath setPath:myPath]; // As a CGPath
If you created your path as a UIBezierPath then you can easily get the CGPath by calling CGPath on the bezier path.
Next you configure the animation with duration etc.
[moveAlongPath setDuration:5.0]; // 5 seconds
// some other configurations here maybe...
Now you add the animation to your imageView's layer and it will animate along the path.
[[myPersonImageView layer] addAnimation:moveAlongPath forKey:#"movePersonAlongPath"];
If you've never used Core Animation before, you need to add QuartzCore.framework to your project and add #import <QuartzCore/QuartzCore.h> at the top of your implementation.
Creating a UIBezierPath
If you don't know what a bezier path is, look at the Wikipedia site. Once you know your control points you can create a simple bezier path like this (where all the points are normal CGPoints):
UIBezierPath *arcingPath = [UIBezierPath bezierPath];
[arcingPath moveToPoint:startPoint];
[arcingPath addCurveToPoint:endPoint
controlPoint1:controlPoint1
controlPoint2:controlPoint2];
CGPathRef animationPath = [arcingPath CGPath]; // The path you animate along
Zooming up
To achieve the zoom effect you can apply a similar animation using a CABasicAnimation for the transform of the layer. Simply animate from a 0-scaled transform (infinitely small) to a 1-scaled transform (normal size).
CABasicAnimation *zoom = [CABasicAnimation animationWithKeyPath:#"transform"];
[zoom setFromValue:[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.0, 0.0, 1.0);];
[zoom setToValue:[NSValue valueWithCATransform3D:CATransform3DIdentity];
[zoom setDuration:5.0]; // 5 seconds
// some other configurations here maybe...
[[myPersonImageView layer] addAnimation:zoom forKey:#"zoomPersonToNormalSize"];
Both at the same time
To have both animations run at the same time you add them to an animation group and add that to the person image view instead. If you do this then you configure the animation group (with duration and such instead of the individual animations).
CAAnimationGroup *zoomAndMove = [CAAnimationGroup animation];
[zoomAndMove setDuration:5.0]; // 5 seconds
// some other configurations here maybe...
[zoomAndMove setAnimations:[NSArray arrayWithObjects:zoom, moveAlongPath, nil]];
[[myPersonImageView layer] addAnimation:zoomAndMove forKey:#"bothZoomAndMove"];

Resources