Tweening/Interpolating Between Two Cgpaths/Uibeziers

Tweening / Interpolating between two CGPaths / UIBeziers

This is actually a lot simpler then you would first think and uses animations with "no" speed (paused). Now with the paused animation added to the layer you can change the time offset to jump to a specific time within the animation.

If you are not planning on running the animation by itself (i.e. only control it manually) I would suggest that you change the duration of the animation to 1. This means that you change the time offset from 0 to 1 when moving from 0% to 100%. If your duration was 0.3 (as in your example) then you would set the time offset to a value between 0 and 0.3 instead.

Implementation

As I said above, there is very little code involved in this little trick.

  1. (Optional) Set the duration to 1.0
  2. Set the speed of the layer (yes they conform to CAMediaTiming) or the animation to 0.0
  3. During the drag gesture or slider (as in my example) set the timeOffset of the layer or animation (depending on what you did in step 2).

This is how I configured my animation + shape layer

CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:@"path"];
morph.duration = 1.; // Step 1
morph.fromValue = (__bridge id)fromPath;
morph.toValue = (__bridge id)toPath;
[self.shapeLayer addAnimation:morph forKey:@"morph shape back and forth"];

self.shapeLayer.speed = 0.0; // Step 2

And the slider action:

- (IBAction)sliderChanged:(UISlider *)sender {
self.shapeLayer.timeOffset = sender.value; // Step 3
}

You can see the end result below. Happy coding!

Sample Image

Interpolating CGAffineTransform

If you want to be able to interact with the animation (maybe using a gesture or a slider or some other mechanism) then you can use a little trick where you "pause" the animation by setting the speed of the view's layer to 0 and then move the animation to a specific point in time by setting the timeOffset of the layer.

I have an explanation of this in an answer to a similar (but animating a different property) question and I a more detailed explanation in the context of animation timing in a blog post.


So that this isn't just a link answer, this is the very little code that you would need to do this interaction. I'm assuming a slider in this case. The blog post shows how to use it with scroll events and if you want to do it with gestures then I'm sure you can figure it out :)

In you animation setup (note that I'm using the CATransform3D instead of CGAffineTransform)

CABasicAnimation *yourAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
yourAnimation.duration = 1.; // For convenience (so that timeOffset is from 0.0 to 1.0)
yourAnimation.fromValue = [NSValue valueWithCATransform3D:fromTransform];
yourAnimation.toValue = [NSValue valueWithCATransform3D:toTransform];
[self.yourView.layer addAnimation: yourAnimation forKey:@"Your Animation"];

self.yourView.layer.speed = 0.0; // Pause every animation on that layer

And the slider to drive the interaction of the animation (slider is configured to go from 0.0 to 1.0):

- (IBAction)sliderChanged:(UISlider *)sender {
self.yourView.layer.timeOffset = sender.value; // Change the "current time" of the animation
}

Filling colour between two concentric ovals

If you are just trying to fill the smaller oval with a white colour, change your fillColor2 code to this:

UIColor *fillColor2 = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];

or this:

UIColor *fillColor2 = [UIColor whiteColor];

The smaller circle is showing up black because filling red, green, and blue values with 0 results in black.

iOS: CAShape Layer Path Transformation

By carefully choosing your control points, etc, you can probably cook up a path which draws as a triangle, but which has the same number of segments and control points as the circle you want to draw. Like this (all numeric values assume iPhone screen, but the point is general):

@implementation ViewController
{
CAShapeLayer* shapeLayer;
}

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

shapeLayer = [[CAShapeLayer alloc] init];

CGRect bounds = self.view.bounds;
bounds.origin.x += 0.25 * bounds.size.width;
bounds.size.width *= 0.5;
bounds.origin.y += 0.25 * bounds.size.height;
bounds.size.height *= 0.5;

shapeLayer.frame = bounds;
shapeLayer.backgroundColor = [[UIColor redColor] CGColor];

[self.view.layer addSublayer: shapeLayer];
[self toCircle: nil];
}

CGPoint AveragePoints(CGPoint a, CGPoint b)
{
return CGPointMake((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
}

- (IBAction)toCircle:(id)sender
{
UIBezierPath* p = [[UIBezierPath alloc] init];

[p moveToPoint: CGPointMake(80, 56)];
[p addCurveToPoint:CGPointMake(144, 120) controlPoint1:CGPointMake(115.34622, 56) controlPoint2:CGPointMake(144, 84.653778)];
[p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:CGPointMake(144, 131.23434) controlPoint2:CGPointMake(141.0428, 142.27077)];
[p addCurveToPoint:CGPointMake(48, 175.42563) controlPoint1:CGPointMake(117.75252, 182.61073) controlPoint2:CGPointMake(78.610725, 193.09874)];
[p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:CGPointMake(38.270771, 169.80846) controlPoint2:CGPointMake(30.191547, 161.72923)];
[p addCurveToPoint:CGPointMake(47.999996, 64.574379) controlPoint1:CGPointMake(6.9012618, 121.38927) controlPoint2:CGPointMake(17.389269, 82.24749)];
[p addCurveToPoint:CGPointMake(80, 56) controlPoint1:CGPointMake(57.729225, 58.957207) controlPoint2:CGPointMake(68.765656, 56)];
[p closePath];

[CATransaction begin];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.duration = 3.f;
pathAnimation.fromValue = (id)shapeLayer.path;
pathAnimation.toValue = (id)p.CGPath;
[shapeLayer addAnimation:pathAnimation forKey:@"path"];
[CATransaction setCompletionBlock:^{
shapeLayer.path = p.CGPath;
}];
[CATransaction commit];

double delayInSeconds = 4.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self toTriangle: nil];
});

}

- (IBAction)toTriangle: (id)sender
{
UIBezierPath* p = [[UIBezierPath alloc] init];

// Triangle using the same number and kind of points...
[p moveToPoint: CGPointMake(80, 56)];
[p addCurveToPoint: AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152)) controlPoint1:CGPointMake(80, 56) controlPoint2:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152))];
[p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152)) controlPoint2:CGPointMake(135.42563, 152)];
[p addCurveToPoint:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint1:CGPointMake(135.42563, 152) controlPoint2:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152))];
[p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint2:CGPointMake(24.574375, 152)];
[p addCurveToPoint: AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint1:CGPointMake(24.574375, 152) controlPoint2:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) ];
[p addCurveToPoint:CGPointMake(80, 56) controlPoint1:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint2:CGPointMake(80, 56)];
[p closePath];

[CATransaction begin];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.duration = 3.f;
pathAnimation.fromValue = (id)shapeLayer.path;
pathAnimation.toValue = (id)p.CGPath;
[shapeLayer addAnimation:pathAnimation forKey:@"path"];
[CATransaction setCompletionBlock:^{
shapeLayer.path = p.CGPath;
}];
[CATransaction commit];

double delayInSeconds = 4.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self toCircle: nil];
});

}

Even with the number of segments and control points being the same, the animation still looks a little wonky and probably isn't what you wanted. The reason for that is that CA seems to be matching up all the segments and control points one-to-one and then linearly interpolating between them for all points. Because the relationship between control points and the resulting path is not linear (cubic in this case), interpolating control point positions linearly isn't going to result in the path moving in a linear fashion. If you run this code, you can see that as it transitions, there are weird humps in the side of the triangle where the circle path had other points in the arc.

More generally, it's just not going to be reasonable to expect CA to magically morph between two arbitrary paths in some specific way that turns out to be desirable in appearance. Even going to the effort to hand construct these paths such that they might have a prayer of morphing, they still failed to look like I thought they should.

It might be more reasonable to achieve the desired effects by working with flattened paths (i.e. paths made up of many small straight lines instead of curved path elements). Even that seems like it would be non-trivial, since you would, again, need both paths to have the same number of segments, and you would have to construct those segments such that the common points were the right number of segments along the whole path.

In sum: This is a fairly complex problem and the naive/free solution that CoreAnimation has provided is unlikely to what you want.

Ios drawing lines animated

If I've understood what you are trying to do, then I see no reason why you shouldn't be able to do that using Core Animation.

If the pattern you are moving is as simple as that then you can use a line dash pattern on a CAShapeLayer to draw the pattern. I created my layer so that it gets the line width from the height of the bounds and the shape layer's path gets its start and end points from the bounds of the layer:

CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.bounds = CGRectMake(0, 0, 200, 60);
lineLayer.position = self.view.center;

lineLayer.strokeColor = [UIColor blackColor].CGColor;
lineLayer.lineDashPattern = @[@5, @2, @2, @2, @2, @2, @2, @2, @2, @2];
lineLayer.lineWidth = CGRectGetHeight(lineLayer.bounds);

UIBezierPath *linePath = [UIBezierPath bezierPath];
[linePath moveToPoint:CGPointMake(0, CGRectGetMidY(lineLayer.bounds))];
[linePath addLineToPoint:CGPointMake(CGRectGetMaxX(lineLayer.bounds), CGRectGetMidY(lineLayer.bounds))];
lineLayer.path = linePath.CGPath;

[self.view.layer addSublayer:lineLayer];

That will produce the static drawing of the pattern. The line dash pattern in the code is the length of the alternating segments of where the line is shown and where it is not.

Sample Image

With the path drawn, I did the animation by altering the "phase" of the line dashes (shifting them in one direction or the other). To make it seem like the pattern is smoothly and continuously moving I created a linear, repeating animation that shifts the pattern by its full length:

NSNumber *totalDashLenght = [lineLayer.lineDashPattern valueForKeyPath:@"@sum.self"]; // KVC is awesome :)

CABasicAnimation *animatePhase = [CABasicAnimation animationWithKeyPath:@"lineDashPhase"];
animatePhase.byValue = totalDashLenght; // using byValue means that even if the layer has a shifted phase, it will shift on top of that.
animatePhase.duration = 3.0;
animatePhase.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animatePhase.repeatCount = INFINITY;

[lineLayer addAnimation:animatePhase forKey:@"marching ants"];

The animated line looks like this:

Sample Image

If you want different lines to animate at different speeds you change the duration of the animation. This value represents the time it takes to animate the shifting one full phase.

Creating Clickable CGPaths

You are already creating paths, you're just creating them in the context rather than creating them and then adding them to the context. You don't need to do that. You can use CGPathCreateMutable, CGPathAddArc and CGContextAddPath.

You can also use UIBezierPath, either via bezierPathWithCGPath: or directly (and then get the CGPath from them).

And, once you have bezier paths, you can call containsPoint: on them to hit test touches.

Animate image with gesture

Curve Setup:

self.trackPath = [UIBezierPath bezierPath];
self.trackPath = [UIBezierPath bezierPathWithArcCenter:P(self.scrollView.center.x,self.scrollView.center.y-20)
radius:110
startAngle:DEGREES_TO_RADIANS(70)
endAngle:DEGREES_TO_RADIANS(115)
clockwise:NO];


self.plane = [CALayer layer];
self.plane.bounds = CGRectMake(0, 0, 60.0, 60.0);
self.plane.position = CGPointMake(self.scrollView.center.x,self.scrollView.center.y-20);
self.plane.contents = (id)([UIImage imageNamed:@"profile_tag"].CGImage);
[self.view.layer addSublayer:self.plane];

CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
anim.path = self.trackPath.CGPath;
anim.duration = 320.;
[self.plane addAnimation:anim forKey:nil];
self.plane.speed = 0.0;
self.plane.hidden = YES;

When Paging:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = self.scrollView.frame.size.width;
int page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
self.pageControl.currentPage = page;

if((scrollView.contentOffset.x >= 320) && (scrollView.contentOffset.x<=640)){
self.plane.hidden = NO;
self.plane.timeOffset = scrollView.contentOffset.x - 320;
}
}


Related Topics



Leave a reply



Submit