Animate a CAShapeLayer circle into a rounded triangle

I would like to animate a figure when it moves from a circle to a triangle with a rounded corner.

TL; DR: How do I animate the CAShapeLayer path between two forms of CGPath ? I know that they must have the same number of breakpoints, but I think I am doing this - what is wrong with this code?

The initial and final states will look something like this: transition http://clrk.it/4vJS+

Here is what I have tried so far: I am using CAShapeLayer and animating the changes to the path property.

According to the documentation (primary focus):

The path object can be animated using any of the specific subclasses of CAPropertyAnimation . Paths will interpolate as a linear mixture of on-line points; โ€œautonomousโ€ points can be interpolated nonlinearly (for example, to maintain the continuity of the derivative curve). If two paths have a different number of control points or segments, the results are undefined.

In an attempt to make the circle and triangle have the same number of control points, I made them like four-pointed shapes: the circle is a strongly rounded rectangle, and the triangle โ€œhidesโ€ the fourth control point on one side.

Here is my code:

 self.view.backgroundColor = .blueColor() //CREATE A CASHAPELAYER let shape = CAShapeLayer() shape.frame = CGRect(x: 50, y: 50, width: 200, height: 200) shape.fillColor = UIColor.redColor().CGColor self.view.layer.addSublayer(shape) let bounds = shape.bounds //CREATE THE SQUIRCLE let squareRadius: CGFloat = CGRectGetWidth(shape.bounds)/2 let topCorner = CGPointMake(bounds.midX, bounds.minY) let rightCorner = CGPointMake(bounds.maxX, bounds.midY) let bottomCorner = CGPointMake(bounds.midX, bounds.maxY) let leftCorner = CGPointMake(bounds.minX, bounds.midY) let squarePath = CGPathCreateMutable() let squareStartingPoint = midPoint(leftCorner, point2: topCorner) CGPathMoveToPoint(squarePath, nil, squareStartingPoint.x, squareStartingPoint.y) addArcToPoint(squarePath, aroundPoint: topCorner, onWayToPoint: rightCorner, radius: squareRadius) addArcToPoint(squarePath, aroundPoint: rightCorner, onWayToPoint: bottomCorner, radius: squareRadius) addArcToPoint(squarePath, aroundPoint: bottomCorner, onWayToPoint: leftCorner, radius: squareRadius) addArcToPoint(squarePath, aroundPoint: leftCorner, onWayToPoint: topCorner, radius: squareRadius) CGPathCloseSubpath(squarePath) let square = UIBezierPath(CGPath: squarePath) //CREATE THE (FAKED) TRIANGLE let triangleRadius: CGFloat = 25.0 let trianglePath = CGPathCreateMutable() let triangleStartingPoint = midPoint(topCorner, point2: rightCorner) let startingPoint = midPoint(topCorner, point2: leftCorner) CGPathMoveToPoint(trianglePath, nil, startingPoint.x, startingPoint.y) let cheatPoint = midPoint(topCorner, point2:bottomCorner) addArcToPoint(trianglePath, aroundPoint: topCorner, onWayToPoint: cheatPoint, radius: triangleRadius) addArcToPoint(trianglePath, aroundPoint: cheatPoint, onWayToPoint: bottomCorner, radius: triangleRadius) addArcToPoint(trianglePath, aroundPoint: bottomCorner, onWayToPoint: leftCorner, radius: triangleRadius) addArcToPoint(trianglePath, aroundPoint: leftCorner, onWayToPoint: topCorner, radius: triangleRadius) CGPathCloseSubpath(trianglePath) let triangle = UIBezierPath(CGPath: trianglePath) shape.path = square.CGPath 

... and later, in viewDidAppear :

 let animation = CABasicAnimation(keyPath: "path") animation.fromValue = self.square.CGPath animation.toValue = self.triangle.CGPath animation.duration = 3 self.shape.path = self.triangle.CGPath self.shape.addAnimation(animation, forKey: "animationKey") 

I have two quick little functions for clearer code:

 func addArcToPoint(path: CGMutablePath!, aroundPoint: CGPoint, onWayToPoint: CGPoint, radius: CGFloat) { CGPathAddArcToPoint(path, nil, aroundPoint.x, aroundPoint.y, onWayToPoint.x, onWayToPoint.y, radius) } func midPoint(point1: CGPoint, point2: CGPoint) -> CGPoint { return CGPointMake((point1.x + point2.x)/2, (point1.y + point2.y)/2) } 

My current result is as follows:

triangle-to-square

NOTE. . I am trying to build a circle from a very rounded square, trying to get the same number of control points in vector form. In the above GIF, the angle radius has been reduced to make the conversion more noticeable.

What do you think is going on? How else can I achieve this effect?

+7
ios animation cabasicanimation cashapelayer
source share
1 answer

You do not get the same number of control points, because addArcToPoint() skips line segment creation if the current waypoint and the first argument are equal.

From the documentation (highlighted by me):

If the current point and the first tangent point of the arc (the starting point) are not equal , quartz joins the line segment from the current point to the first tangent point.

These two calls in your triangle code will not create line segments, while the corresponding calls making squircle will be:

 addArcToPoint(trianglePath, aroundPoint: cheatPoint, onWayToPoint: bottomCorner, radius: triangleRadius) addArcToPoint(trianglePath, aroundPoint: bottomCorner, onWayToPoint: leftCorner, radius: triangleRadius) 

You can manually create rows by calling CGPathAddLineToPoint . And you can test your work using CGPathApplierFunction , which allows you to list breakpoints.


Also ~~~ I could assume that rotating the image link and writing a function that sets the desired shape for each t โˆˆ [0, 1] is likely to become a more understandable and more convenient service solution.

+6
source share

All Articles