iOS: CAShape Layer Path Transformation
Asked Answered
I

3

2

I have used CAShape Layer before doing basic transformation on paths - from a smaller circle into a larger circle. Good enough, but then I tried to transform a triangle into a circle; it worked, but the transformation is weird. Other words, from one shape to another, it "flips", "twists" before the final shape is formed. With the same shapes, there is no problem - from a circle to a circle - but with different shapes, the transformation is weird.

I wonder if this is the expected way? Or is there other tricks or workarounds where we can smoothly and proportionally transform one shape into another different shape using CAShape Layer(or is there other ways of doing this; it doesn't have to be CAShape Layer)? Thanks in advance.

Interaction answered 25/7, 2013 at 13:0 Comment(0)
B
10

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.

Briefless answered 25/7, 2013 at 17:8 Comment(3)
+1 Nice, you did the work I though of but never would have the energy to figure out :)Havens
@ipmcc: Very nice. I kind of thought about the same as well, afterward. I will accept your answer.Interaction
If any of you can somehow refine this further, please come back and share.Interaction
E
1

From the documentation of the path property on CAShapeLayer:

If the two paths have a different number of control points or segments the results are undefined.

So yes, I would say that this is expected since a circle and a triangle are different shapes.

This article describe custom path animations in great detail.

Ensile answered 25/7, 2013 at 13:3 Comment(0)
D
1

I'm guessing here! You can define a circle using 3 points. That way you will have the same number of points as the triangle. To define the circle this will be your best friend

+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
Dibasic answered 25/7, 2013 at 13:7 Comment(4)
@DavidRönnqvist Ah.. You say the bezierPathWithArcCenter-method may use control points under the hood? Didn't think about that!Dibasic
I was more referring to what the documentation say: "If the two paths have a different number of control points or segments the results are undefined." 3 lines are the same number of segments as 3 arcs but they may not have the same number of control points.Havens
@DavidRönnqvist Yep, you are correct. I'm curious about Unheilig's findings. Maybe it works? Though the documentation does not guarantee anything in this case :)Dibasic
@Interaction Certainly. They have a toll-free-bridging between eachother. Have you tried yet? I'm curious about the result :)Dibasic

© 2022 - 2024 — McMap. All rights reserved.