How do I animate a caret shape into a smooth arc? - ios

I want to create an animation where a caret shape (">") animates into a circle arc. It's for an application where there is a caret that transforms into a 3/4 circular arc, which then spins while a network transaction runs.
The animation should look like this:
Any time I try to do path animations like this, I get very strange results. How do I create a smooth animation?

The secret to this sort of animation is to install a path into a CAShapeLayer and use a CABasicAnimation on the path that transforms the path from its starting state to it's end state. The tricky part is that the starting and ending paths need to have the same number and type of control points.
I wrote a demo app that creates exactly the animation above. You can download it from Github (link)
Here is the readme from the repo, which explains how it works in some detail:
CaretToArcAnimation
This project animates a simple transform of a caret symbol to an arc that spans 3/4 of a circle. It looks like this:
It works by animating the path installed into a CAShapeLayer.
In order for a path animation to work correctly, the starting and ending paths need to have the same number and type of control points.
To animate from the caret to the arc, it creates the caret as 2 cubic bezier curves. The first "curve" (Which is actually a straight line) starts at the lower left of the caret, passes through points 1/3 and 2/3 of the way along the lower line segment, and ends at the "bend" in the caret. That creates a Bezier curve that renders as a line segment. The second curve is also a straight line Bezier curve. The second one starts at the bend in the caret and ends at the top left corner.
The arc is also composed of 2 cubic bezier curves, drawn in the same direction as those in the caret symbol. However, the control points for the arc's Bezier curves are chosen so that the resulting curve closely approximates an arc of 3π/2 radians, or 270 degrees, or 3/4 of a cicrcle. The arc is slightly larger than the caret it replaces, and faces the same way.
It's easier to understand if you add a visual representation of the Bezier control points, like this:
The red dots that land on the caret/curve are the endpoints of the 2 Bezier curves. The red dots that are on the outside of the arc are the control points that define the shape of the curves. For the caret shape, the control points are on the lines, which causes the Bezier curve to take a straight line shape.
I used this article to get the control points for the two Bezier curves. I didn't feel like figuring out the math, so I just set the curve on that page's interactive arc renderer to draw 3/8 of a circle, wrote down the coordinates of all the control points, and then flipped them to get the second Bezier curve.
Here is a screenshot from the Bezier Arc approximator web simulation I used to find the Bezier control points:
(In that screenshot, the angle slider is expressed in radians. One half of a 3/4 circle arc is 3/8 of a full circle, or 3/8 of 2π. 2π * 3/8 is about 2.36, so that is the arc angle I chose.)
The CaretToArcAnimation app defines a CaretToArcView class which is a subclass of UIView.
Most of the interesting work is done in the CaretToArcView class.
It has a static var layerClass which returns CAShapeLayer.self. This causes the view's backing layer to be set up as a CAShapeLayer.
class override var layerClass: AnyClass {
return CAShapeLayer.self
}
The CaretToArcView.swift file defines an enum ViewState:
enum ViewState: Int {
case none
case caret
case arc
}
The custom view class has a var viewState of type ViewState. Its initial value is .none, meaning no path is installed in the view.
If you set the state to .caret or .arc, it checks to see if the current layer path is nil. If it is, it installs the appropriate path into the layer without animation.
If the previous path was the other path type, it builds a path with the new shape, and creates a CABasicAnimation with a fromState of the previous path, and toState of the new path. The animation's duration is set using an instance variable, animationDuration. The animation uses ease-in, ease-out timing, although it would be easy to change.
The class also has public function rotate(_ doRotate: Bool). If you call it with doRotate == false, it removes all animations from the view's layer. If you call it with doRotate == true, it adds an infinitely repeating rotation animation to the layer, rotating it 360°, over and over, using linear timing.
The app's view controller drives the settings in the CaretToArcView, and it has logic that prevents the custom view from invoking both animations at the same time (Strange things would happen if you did that. Don't.)
The app's screen looks like the image below.
The view controller disables both the button and the switch during a toggle animation, and disables the "Toggle" button when the rotate switch is turned on and the shape is rotating.

Related

CAShapeLayer draw partially through path end points

When I use CAShapeLayer and create a rectangle shape then the path starts at rectangle's origin (top-left corner) and draws clockwise. Now if I want to draw only part of the shape then I'm using strokeStart and strokeEnd properties. The problem comes when I want to draw a part consisting the path's end points. In this case the path is closed and it starts and ends on rectangle's top-left corner. When I'm setting strokeStart=0.8 and strokeEnd=0.2 I'm hoping it to draw the last section of the path and a bit from the beginning of the path. However, this is not working. Are there any ideas or tricks how to do this?
Update:
Adding an image to clarify what I mean above. I want an animation which draws a small amount of rectangle and that drawn part circles over the rectangle:
The short answer is that I don't think you can do that, at least not with a single path that will draw any of the segments in your examples. I'm pretty sure that strokeStart must be less than strokeEnd.
If you want to draw your last segment you'd need to create a custom rectangle path that started at the lower left corner and wrapped around.

Masking performance

I'm creating an animation that uncovers the underlying image. There's a virtual shape (e.g. star) moving chaotically and uncovering different parts of the image.
So I had two bitmaps so far:
mask (trace of a shape moving here'n'there)
image (underlying image)
So far in every drawRect() I was:
creating a newMask bitmap by copying the current mask
drawing a stamp on a newMask
creating a resulting bitmap (apply newMask onto an image)
drawing a resulting bitmap to screen context
I'm struggling with performance in this approach. Any ideas how to improve it?
In particular:
Is it possible to skip step 1. & 2. and draw onto mask directly (rather than clone it).
Should I start experimenting with CALayer approach (if this kind of masking is at all possible there)
Should I use OpenGL
Is there any other approach to tackle this?
No, you should not manipulate bitmaps. That is likely to be very CPU-intensive as well as jerky (not smooth animation.)
Instead you should use a CAShapeLayer as a mask and Core Animation.
With a shape layer you can install a path (a CGPath, which can be created easily from a UIBezierPath) into the layer. Then you create a CABasicAnimation that switches the path to a new path. The trick is to always keep the same number and type of control points in the starting and ending paths of the animation. (If the number and/or type of control points in the two paths are different you get very, very strange results. Note that the path calls that create arcs of circles actually generate different numbers of control points based on how much of a circle your arc covers, so circle arcs require special handling.)
I have a sample project on Github that demonstrates various Core Animation techniques, including a demonstration of a "clock wipe" animation that reveals/hides and image view much like you describe.
https://github.com/DuncanMC/iOS-CAAnimation-group-demo
The animation looks like this:
Note that the jerky nature of that image is because it's a GIF. The actual animation on a device is buttery-smooth. It's also possible to create very complex smooth animations like this one:
(That isn't a mask animation but it could be.)

iOS Circular Slider

I want to create a circular slider like below.
But i want two functionalities in addition.
1) I want to start the slider from any point,but in fig. it starts from 0.
2) I want to include multiple sliders in a single circular black plot.
I'm sharing the link of this project:
https://www.cocoacontrols.com/controls/circularsliderdemo
Can anyone help me to do these functionalities.
Thanks in advance.
Take a look at CAShapeLayer. You could create a path that is a full circle, and use the strokeStart and strokeEnd properties to only draw part of the circle. You could use core animation to animate between the beginning and the end.
There is an open source custom gesture recognizer on Github that is a one finger gesture recognizer. That would be a good start for detecting and responding to the twirl gesture that such a control would need. EDIT: It's called KTOneFingerRotationGestureRecognizer (link)
Those are some ideas to help get you started.
I have a project on github called iOS-CAAnimation-group-demo That includes a "clock wipe" animation. The clock wipe works by setting up a shape layer as the mask layer for an image view, installing a full-circle arc that's wide enough to completely fill a rectangular area, and then animate the strokeEnd property of the shape layer to reveal/hide the image view. The clock wipe is much more complex than what you need, but it would give you the seed of what you want. You'd use a shape layer with a much thinner line width, and you would use it as a content layer, not as a mask.

UICollisionBehavior treats open path as closed?

If I define an open UIBezierPath and set it as a collision boundary:
_containerPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:M_PI
endAngle:0
clockwise:NO];
[_collisionBehavior addBoundaryWithIdentifier:#"containerBoundary" forPath:_containerPath];
and then turn gravity on, objects that are released inside the "bowl" respect the lower boundary, but objects released from above the bowl come to rest on the supposedly non-existent side. Is this expected behavior?
In the picture, the red rectangle was dropped from above; the reference view for the dynamic animator is the light gray rect. It fell from above and stopped at the invisible line.
I've confirmed that if you flip the bezier path over, the red rect does in fact respect the curved boundary; I've also tried this using an open (two-sided) triangle instead of curved path - same result.
The behavior you're seeing seems to be the same as what you see for fill with a bezier path. If you draw a "V" and fill it, it behaves as if it were a closed path. With the collision boundaries, you can make an open "V" by adding two lines with addBoundaryWithIdentifier:fromPoint:toPoint:. I don't know it there's any other way around the problem. For your half circle, I presume you could approximate it with a series of straight lines added with the method above. I've approximated circles before using 50 to 100 lines that look very close to what you get with BezierPathWithOvalInRect. I don't know if this creates a serious burden on the system when used as a collision boundary.

Efficient animation of time-variant drawing using CAKeyFrameAnimation

My current goal is to animate the drawing of a picture in my iOS app so that a user could see something like http://www.youtube.com/watch?v=xlDmk5WdaHk happening on their screen.
The acceleration of my drawings vary with time so I've built my animation using CAKeyFrameAnimation and keyTimes. At the moment, I only have one Bezier path which specifies all the points of my drawing, I add the path to my CAShapeLayer object, and then pass the path off to my animation object when I start my animation.
My problem: the completed drawing immediately appears on the screen, and my pen cursor then moves along the picture according to my keyTimes values, instead of having the stroke color filling in tandem with my pen cursor movement on the screen to produce the same effect as the video.
I noticed this SO question, where the recommended solution was to produce a separate path for every point in the animation: How to Animate CoreGraphics Drawing of Shape Using CAKeyframeAnimation
Given that I have 100-300 individual points, is there a more efficient method of producing a time-variant drawing, than creating an array of a couple hundred paths (with a couple hundred values in each path) with the paths in this form:
path0 = point0...point0...point0...point0...
path1 = point0...point1...point1...point1...
path2 = point0...point1...point2...point2...
path3 = point0...point1...point2...point3...
Then taking that array of paths, passing it into myAnimation.values and animating values with my keyTimes? Or is that really the best way to go about this?
Thanks in advance
There certainly is a more efficient way if your points are on a bezier path. CAShapeLayer has two little properties called strokeStart and strokeEnd. If you initially set strokeEnd to zero then your path will not show. At 1.0 it will show completely. Increasing the values from zero to one will trace your curve accordingly. This is an animatable property so synchronize its value with the key times of your other animation and your drawing will appear as the "pen" is moving.

Resources