iOS: how to animate the fill in uibezier path? - ios

I am working on a kids letter tracing app - see attached screen shot.
I was able to display the letter by using the bezierpath identified by the font glyphs and allow writing the touches inside the bezierpath.
Now, i want to add an help animation so that it shows how to write this letter from start to finish.
How to do that?
Consider we display "A" and i want to show animation shows how to write "A" for kids.
Any pointers / ideas.
thanks much.

I used this animation for my app. Created a circular progress bar that fills up dot clockwise
CABasicAnimation *pathAnim = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnim.toValue = (id)newPath.CGPath;
// -- initialize CAAnimation group with duration of 0.2secs and beginTime will begin after another.
CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 0.2f;
anims.beginTime = CACurrentMediaTime() + anims.duration*i;
anims.fillMode = kCAFillModeForwards;
[anims setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[progressLayer addAnimation:anims forKey:nil];

There is no straightforward way to do this. I imagine the following would be easiest to implement:
Create a path per character that will trace how the stroke will be written. If you have limited number of characters like in the alphabet, it's possible to do this by hand. You can implement something like this to capture the path: http://code.tutsplus.com/tutorials/smooth-freehand-drawing-on-ios--mobile-13164
Use your current path (in red in your photo) as a clipping mask so the fill does not overflow.
Stroke the path you created in 1. with a very thick stroke to do the fill.

I can't find a "built-in" way to animate the fill as you want. There is a built-in way to animate the stroke, however:
Use a UIView with a CAShapeLayer backing it. CAShapeLayer has a path property. You can then apply an animation to the layer's strokeStart and/or strokeEnd properties.
Hope it helps.

After loading your bezierpath add init the animation:
CAKeyframeAnimation *shapeKeyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"strokeEnd"];
shapeKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
shapeKeyframeAnimation.keyTimes = #[#0.0f, #0.6f, #1.0f];
shapeKeyframeAnimation.values = #[#0.0f, #1.0f, #1.0f];
shapeKeyframeAnimation.duration = 4.0f;
shapeKeyframeAnimation.repeatCount = CGFLOAT_MAX;
then add it to your shape layer:
[shapeLayer addAnimation:_loadingKeyframeAnimation forKey:#"stoke"];

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.

Animate fillColor property

I am trying to achieve the filling of a circle(something like a pie).
I want to fill it from 0 to 360
This is what i have done
CAShapeLayer *circleLayer=[CAShapeLayer layer];
circleLayer.path=[UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150) radius:100 startAngle:0 endAngle:360 clockwise:YES].CGPath;
circleLayer.strokeColor=[UIColor redColor].CGColor;
circleLayer.fillColor=[UIColor blueColor].CGColor;
[self.view.layer addSublayer:circleLayer];
//adding animation
CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:#"fillColor"];
basicAnimation.fillMode=kCAFillModeForwards;
basicAnimation.fromValue=(id)[UIColor clearColor].CGColor;
basicAnimation.toValue=(id)[UIColor yellowColor].CGColor;
basicAnimation.duration=1.5f;
basicAnimation.repeatCount=10;
basicAnimation.autoreverses=YES;
[circleLayer addAnimation:basicAnimation forKey:#"fillColor"];
I am not able to figure out,how do i acheive the effect of color filling from start angle to end angle.
Thanks
Your description of what you want to do is unclear. Are you trying to create a "clock wipe" effect, where the color change sweeps like the second hand of a clock?
If so, check out this demo app I created on github:
github animation demo project
Look in particular at the description of the Clock Wipe animation in the read me file.
Here is what the clock wipe animation looks like:
This project animates the mask of an image view to do a clock wipe reveal animation, but it would be a simple matter to animate a shape layer as a content layer of a view instead of making it a mask.
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 0.7f;
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circleLayer addAnimation:animation forKey:#"strokeEnd"];
I have implemented a shape layer subclass for similar requirements. Here is the Github link. For your requirement the arc start radius is 0.0f and end radius is the circle radius. You can also refer the sample project in that link.

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"];

how can I animate a CAlayer's bounds to progressively reveal an image?

For certain reasons, I'm trying to avoid using a CAScrollLayer to do this. The effect I'm going after is to progressively reveal (from bottom to top) a CALayer's content (a png I previously loaded in). So I thought about doing this:
layer.anchorPoint = CGPointMake(.5, 1);
CABasicAnimation* a = [CABasicAnimation animationWithKeyPath:#"bounds.size.height"];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
a.fillMode = kCAFillModeBoth;
a.removedOnCompletion = NO;
a.duration = 1;
a.fromValue = [NSNumber numberWithFloat:0.];
a.toValue = [NSNumber numberWithFloat:layer.bounds.size.height];
[layer addAnimation:a forKey:nil];
The problem with this is you can tell the layer's content is scaled with the bounds. I was trying for the bounds to change but the content to stay always the original size, so that effectively the bounds clip the image and as I increase bounds.height, the image "Reveals" itself.
Any ideas as to how to pull it off or what might I be missing?
Ok I got it to work, but I basically had to update the layer's frame too, to reflect the change in anchor point:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
layer.contentsGravity = kCAGravityTop;
layer.masksToBounds = YES;
layer.anchorPoint = CGPointMake(.5, 1);
CGRect newFrame = layer.frame;
newFrame.origin.y += newFrame.size.height / 2;
layer.frame = newFrame;
[CATransaction setValue:(id)kCFBooleanFalse forKey:kCATransactionDisableActions];
a.toValue = [NSNumber numberWithFloat:layer.bounds.size.height];
[layer addAnimation:a forKey:nil];
"Dad" has the right answer.
You want to create a CAShapeLayer, and install that as the mask on your layer.
You create a CGPath that is just a rectangle and install that path into the shape layer. The contents of the path determine what areas of the masked layer show up. If the path is a triangle in the middle of the layer, then only the triangle appears.
You then create an animation that animates the path.
To reveal your image from the bottom, you'd set up a path that was a 0 height rectangle at the bottom of the layer, and then you'd create a CAAnimation where the toValue is the same rectangle with a hight of the full layer you want to reveal. The system would generate an animation that reveals the image in a sweep.
You can use this same technique to achieve all kinds of cool effects, like barn doors, venetian blinds, "iris wipes", etc.
What if you changed the clipping mask instead? (or use a mask layer).
You could put another image over the target image and move it up like a stage curtain.

CABasicAnimation rotate returns to original position

i'm rotating a CALayer using CABasicAnimation and works fine. The problem is, when I try to rotate the same layer, it returns back to its original position before it will rotate. My expected output is that, for the next rotation, it should start from where it has ended. Here's my code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.fromValue = 0;
animation.toValue = [NSNumber numberWithFloat:3.0];
animation.duration = 3.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.autoreverses = NO;
[calayer addAnimation:animation forKey:#"rotate"];
Is there anything missing on my code? thanks
What's happening is that you're seeing the animation in the presentation layer. However, that doesn't update the actual position of your layer. So, once the animation finishes, you see the layer as it was because it hasn't changed.
It's really worth reading the "Core Animation Rendering Architecture". Otherwise this can be very confusing.
To fix it, set a delegate to your CABasicAnimation as follows:
[animation setDelegate:self];
Then, create a method to set your target properties that you want when the animation completes. Now, here's the confusing part. You should do this on animationDidStart not animationDidStop. Otherwise, the presentation layer animation will finish, and you'll get a flicker as you see the calayer in the original position then it jumps - without animation - to the target position. Try it with animationDidStop and you'll see what I mean.
I hope that's not too confusing!
- (void)animationDidStart:(CAAnimation *)theAnimation
{
[calayer setWhateverPropertiesExpected];
}
EDIT:
I later discovered that Apple recommend a much better way to do this.
Oleg Begemann has a nice description of the correct technique in his blog post Prevent Layers from Snapping Back to Original Values When Using Explicit CAAnimations
Basically what you do is before you start the animation, you take a note of the layer's current value, i.e., the original value:
// Save the original value
CGFloat originalY = layer.position.y;
Next, set the toValue on the layer's model. Therefore the layer model has the final value of whatever animation you are about to do:
// Change the model value
layer.position = CGPointMake(layer.position.x, 300.0);
Then, you set the animation up with the animation fromValue being that original value that you noted above:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
// Now specify the fromValue for the animation because
// the current model value is already the correct toValue
animation.fromValue = #(originalY);
animation.duration = 1.0;
// Use the name of the animated property as key
// to override the implicit animation
[layer addAnimation:animation forKey:#"position"];
Note that code in edit above was copy/pasted from Ole Begemann's blog for clarity
If you want the animation to start from where it has ended, then set the fromValue property to the CALayer's current rotation.
Obtaining that value is tricky, but this SO post shows you how: https://stackoverflow.com/a/6706604/1072846

Resources