I'm trying to animate a bezier curve I made with Paintcode (great app, btw) and am drawing in a custom UIView in the "drawRect" method.
The drawing works fine but I want to animate a single point in the bezier curve.
Here's my non-working method:
-(void)animateFlame{
NSLog(#"Animating");
// Create the starting path. Your curved line.
//UIBezierPath * startPath;
// Create the end path. Your straight line.
//UIBezierPath * endPath = [self generateFlame];
//[endPath moveToPoint: CGPointMake(167.47, 214)];
int displacementX = (((int)arc4random()%50))-25;
int displacementY = (((int)arc4random()%30))-15;
NSLog(#"%i %i",displacementX,displacementY);
UIBezierPath* theBezierPath = [UIBezierPath bezierPath];
[theBezierPath moveToPoint: CGPointMake(167.47, 214)];
[theBezierPath addCurveToPoint: CGPointMake(181+displacementX, 100+displacementY) controlPoint1: CGPointMake(89.74, 214) controlPoint2: CGPointMake(192.78+displacementX, 76.52+displacementY)];
[theBezierPath addCurveToPoint: CGPointMake(167.47, 214) controlPoint1: CGPointMake(169.22+displacementX, 123.48+displacementY) controlPoint2: CGPointMake(245.2, 214)];
[theBezierPath closePath];
// Create the shape layer to display and animate the line.
CAShapeLayer * myLineShapeLayer = [[CAShapeLayer alloc] init];
CABasicAnimation * pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (__bridge id)[bezierPath CGPath];
pathAnimation.toValue = (__bridge id)[theBezierPath CGPath];
pathAnimation.duration = 0.39f;
[myLineShapeLayer addAnimation:pathAnimation forKey:#"pathAnimation"];
bezierPath = theBezierPath;
}
Using this, nothing moves on the screen at all. The random displacements generated are good and the bezierPath variable is a UIBezierPath that's declared with a class scope.
Am I missing something? (The goal is to do a sort of candle-like animation)
Quick Edit
Just seen your layer code. You are mixing up several different concepts. Like drawRect, CAShapeLayer, old animation code etc...
By doing the method below you should be able to get this working.
You can't do this :( you can't animate the contents of drawRect (i.e. you can't get it to draw multiple times over the course of the animation).
You may be able to use a timer or something and create your own animation type code. (i.e. create a timer that fires 30 times a second and runs a function that calculates where you are in the animation and updates the values you want to change and calls [self setNeedsDisplay]; to trigger the draw rect method.
Other than that there isn't much you can do.
ANOTHER EDIT
Just adding another option here. Doing this with a CAShapeLayer might be very poor performance. You might be best using a UIImageView with a series of UIImages.
There are built in properties, animationImages, animationDuration, etc... on UIImageView.
POSSIBLE SOLUTION
The path property of CAShapeLayer is animatable and so you could possible use this.
Something like...
// set up properties for path
#property (nonatomic, assign) CGPath startPath;
#property (nonatomic, assign) CGPath endPath;
#property (nonatomic, strong) CAShapeLayer *pathLayer;
// create the startPath
UIBezierPath *path = [UIBezierPath //create your path using the paint code code
self.startPath = path.CGPath;
// create the end path
path = [UIBezierPath //create your path using the paint code code
self.endPath = path.CGPath;
// create the shapee layer
self.pathLayer = [CAShapeLayer layer];
self.pathLayer.path = self.startPath;
//also set line width, colour, shadows, etc...
[self.view.layer addSubLayer:self.pathLayer];
Then you should be able to animate the path like this...
- (void)animatePath
{
[UIView animateWithDuration:2.0
animations^() {
self.pathlayer.path = self.endPath;
}];
}
There are lots of notes in the docs about CAShapeLayer and animating the path property.
This should work though.
Also, get rid of the old animation code. It has been gone since iOS4.3 you should be using the updated block animations.
Related
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 2.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
Instead of the path, how can I set animation to last line ?
You cannot just animate "the last line in a path". But you can achieve the visual effect by using two entirely separate shape layers, one with the path thus far that is not to be animated (the blue path in my example below) and a completely separate layer whose path is just the last the segment of the line, which will be animated (a red path in my example). You can then:
Make the path of the blue shape layer to be the just the existing, unanimated portion of the final path.
Make the path for the red shape layer to be just the last segment of the new path;
Animate the red layer's strokeEnd from #(0.0) to #(1.0);
When that's done (e.g. in animationDidStop:finished:), remove that red shape layer's path altogether and extend the path of the blue shape layer's path; and
Repeat as needed.
When you do this, it will look like you animated another segment onto the blue shape layer, but what you really did is that you create a separate layer with just the portion to be animated, animate that, and when done, remove that layer and update the original layer's path accordingly
Note, in the above animated example, the red path and the blue paths are on different shape layers, but it almost feels like it's one path because one layer's path starts right where the other one left off. Clearly, in your final product, you would make the two shape layers appear the same, which would be less jarring than the red/blue combination above. I just used different colors here so you could understand what the two different shape layers were doing.
FYI, this is the code that generated the above drawing:
- (void)addLineToPoint:(CGPoint)point {
// create a path that consists just of the portion to be animated
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.lastPoint];
[path addLineToPoint:point];
// set the redAnimatedPathLayer's path to be this short segment
self.redAnimatedPathLayer.path = [path CGPath];
// and animate it
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.redAnimatedPathLayer.path = [path CGPath];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = #(0.0);
pathAnimation.toValue = #(1.0);
pathAnimation.delegate = self;
[self.redAnimatedPathLayer addAnimation:pathAnimation forKey:#"strokeEndKey"];
// save the point for future reference
self.lastPoint = point;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
// update the blue layer's path to include this new point
[self.mainPath addLineToPoint:self.lastPoint];
self.blueNonAnimatedLayer.path = self.mainPath.CGPath;
// reset the red layer's path
self.redAnimatedPathLayer.path = nil;
[self repeat];
}
I built this for my company: https://github.com/busycm/BZYStrokeTimer and during the course of building, I noticed an interesting "bug" that I can't seem to mitigate when using UIBezierPath. Right when the animation starts, the path jumps a certain number of pixels forward (or backwards depending if it's counterclockwise) instead of starting up with a smooth, incremental animation. And what I found that's really interesting is how much the path jumps forward is actually the value of the line width for the CAShaperLayer.
So for example, if my bezier path starts off at CGRectGetMidX(self.bounds) and the line with is 35, the animation actually starts from CGRectGetMidX(self.bounds)+35 and the larger the line width, the more noticeable the jump is. Is there any way to get rid of that so that path will smoothly animate out from the start point?
Here's a picture of the first frame. This is what it looks like immediately after the animation starts.
Then when I resume the animation and pause again, the distance moved is about 1/100th of the distance you see in the picture.
Here's my bezier path code:
- (UIBezierPath *)generatePathWithXInset:(CGFloat)dx withYInset:(CGFloat)dy clockWise:(BOOL)clockwise{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, dy/2)];
[path closePath];
return clockwise ? path : [path bezierPathByReversingPath];
}
Here's the animation code:
CABasicAnimation *wind = [self generateAnimationWithDuration:self.duration == 0 ? kDefaultDuration : self.duration fromValue:#(self.shapeLayer.strokeStart) toValue:#(self.shapeLayer.strokeEnd) withKeypath:keypath withFillMode:kCAFillModeForwards];
wind.timingFunction = [CAMediaTimingFunction functionWithName:self.timingFunction];
wind.removedOnCompletion = NO;
self.shapeLayer.path = [self generatePathWithXInset:self.lineWidth withYInset:self.lineWidth clockWise:self.clockwise].CGPath;
[self.shapeLayer addAnimation:wind forKey:#"strokeEndAnimation"];
And here's how I construct the CAShapeLayer.
- (CAShapeLayer *)shapeLayer {
return !_shapeLayer ? _shapeLayer = ({
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = kDefaultLineWidth;
layer.fillColor = UIColor.clearColor.CGColor;
layer.strokeColor = [UIColor blackColor].CGColor;
layer.lineCap = kCALineCapSquare;
layer.frame = self.bounds;
layer.strokeStart = 0;
layer.strokeEnd = 1;
layer;
}) : _shapeLayer;
}
I think what's happening here is that, in this frame of the animation, you are drawing a line that consists of a single point. Since the line has a thickness associated with it, and the line cap type is kCALineCapSquare, that'll get rendered as a square with height and width equal to the line width.
You can think of it as if you are drawing a line with a square marker, and you are going to drag the midpoint of the marker so that it goes through every point in the curve you specified. For the first point in the line, it's as if the marker touches down at that point, leaving a square behind.
Here's a visual representation the different line cap types that will hopefully make it more intuitive. You should probably change the line cap style to kCALineCapButt.
Sidenote:
After you make that change, in this line of code
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
you probably don't have to offset the x coordinate by dx/2 anymore.
I'm trying to draw a chevron shape inside my UIView subclass. The chevron appears, but the line cap style and line join styles that I'm applying aren't being reflected in the output.
- (UIBezierPath *)chevron:(CGRect)frame
{
UIBezierPath* bezierPath = [[UIBezierPath alloc]init];
[bezierPath setLineJoinStyle:kCGLineJoinRound];
[bezierPath setLineCapStyle:kCGLineCapRound];
[bezierPath moveToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMaxY(frame))];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMaxX(frame), CGRectGetMaxY(frame) * 0.5)];
[bezierPath addLineToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMinY(frame))];
return bezierPath;
}
-(void)drawRect:(CGRect)rect{
[self.color setStroke];
UIBezierPath *chevronPath = [self chevron:rect];
[chevronPath setLineWidth:self.strokeWidth];
[chevronPath stroke];
}
According to Apple's docs, they say that "After configuring the geometry and attributes of a Bezier path, you draw the path in the current graphics context using the stroke and fill methods" but that isn't working here —-- I've tried moving the setLineJoinStyle and setLineCapStyle statements around (e.g., after adding LineToPoint, inside drawRect) and it seems like no matter how many times I call them it isn't working. Any ideas what's going wrong?
Your code is applying those styles, you just can't see them because your chevron is being drawn all the way to the edge of your view then getting clipped. To see the ends of your chevron, change your call to the chevron method to this,
UIBezierPath *chevronPath = [self chevron:CGRectInset(rect, 10, 10)];
Whether 10 points is enough of an inset will depend on how wide your line is, so you may need to increase that.
Anyone know how to/if it's a good idea to animate CGPaths in a UIView's drawRect method?
For example, draw a black line from one end of the UIView to the other and then as a timer ticks over, change each individual pixel to a different colour variation to imitate a colour 'flow' of sorts (think Mexican wave, but with colour shades).
Is this doable/efficient?
Try this.
-(void)drawRect:(CGRect)rect{
[UIView animateWithDuration:0.5 animations:^{
CAShapeLayer *shapeLayer=[CAShapeLayer layer];
shapeLayer.path=[self maskPath].CGPath;
[yourview.layer setMask:shapeLayer];
}
-(UIBezierPath *)maskPath{
UIBezierPath *path=[[UIBezierPath alloc] init];
[path moveToPoint:[self normalizedCGPointInView:yourview ForX:1 Y:1]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:9 Y:1]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:9 Y:9]];
[path addLineToPoint:[self normalizedCGPointInView:yourview ForX:1 Y:9]];
return path;
}
-(CGPoint)normalizedCGPointInView:(UIView *)view ForX:(CGFloat)x Y:(CGFloat)y{
CGFloat normalizedXUnit=view.frame.size.width/10;
CGFloat normalizedYUnit=view.frame.size.height/10;
return CGPointMake(normalizedXUnit*x, normalizedYUnit*y);
}
By using a normalised co-ordinate scheme it's a lot easier to calculate the BezierPath for the mask. Additionally it means that the mask is always relative to the size of your view and not fixed to a specific size.
Hope this helps!
I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.