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.
Related
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.
I am working on creating a way for a user to move polygons on screen and change their shape by dragging their corner vertices. I then need to be able to re-draw those modified polygons on other devices - so I need to be able to get final positions of all vertices for all polygons on screen.
This is what I am currently doing:
I draw polygons using UIBezierPath for each shape in the override of the drawRect function. I then allow user to drag them around the screen by applying CGAffineTransformMakeTranslation function and the x and y coordinate deltas in touchedMoved function override. I have the initial control points from which initial polygons are drawn (as described here). But once a path instance is moved on screen, those values don't change - so I am only able to get initial values.
Is there something built - in in the Core Graphics framework that will allow me to grab a set of current control points in a UIBezierPath instance? I am trying to avoid keeping track of those points manually. I will consider using other ways to draw if:
there is a built - in way to detect if a point lies within that polygon (such as UIBezierPath#contains method
A way to easily introduce constraints so user can't move a polygon out of bounds of the superview (I need the whole polygon to be visible)
A way to grab all points easily when user is done
Everything can run under 60fps on iPhone 5.
Thanks for your time!
As you're only applying the transform to the view/layer, to get the transformed control points from the path, simply apply that same transform to the path, and then fetch the control points. Something like:
UIBezierPath* copiedPath = [myPath copy];
[copiedPath applyTransform:[view transform]];
[self yourExistingMethodToFetchPointsFromPath:copiedPath];
The way that you're currently pulling out points from a path is unfortunately at the only API available for re-fetching points from an input UIBezierPath. However - you might be interested in a library I wrote to make working with Bezier path's much simpler: PerformanceBezier. This library makes it significantly easier to get the points from a path:
for(NSInteger i=0;i<[path elementCount];i++){
CGPathElement element = [path elementAtIndex:n];
// now you know the element.type and the element.points
}
In addition to adding functionality to make paths easier to work with, it also adds a caching layer on top of the existing API to make the performance hit of working with paths much much smaller. Depending on how much CPU time you're spending on UIBezierPath methods, this library will make a significant improvement. I saw between 2x and 10x improvement, depending on the operations I was using.
I have stumbled upon an interesting problem and I am not sure how to solve it. I have a line graph on which data points are marked with circular dots. At the moment I am creating one CAShapeLayer for the line itself and also CAShapeLayer for each dot.
The line is being animated with the strokeEnd animation key path. And it is working. However, the slightly negative effect is that once I open the graph I see all the data points already drawn and then a line is being animated through them.
Ideally I would like to change this behaviour in such a way that at the beginning nothing is shown and as the line gets animated data points are being drawn once the line passes through them.
I was thinking about this problem for some time now and I cannot find an elegant way of solving it. It seems to me that there should be a fairly easy way of achieving what I want. Maybe I should create a compounded path (but then how do I specify that a line needs to be stroked whereas dots need to be filled?).
Could anyone please guide me in the right direction?
There are lots of ways to handle this.
Assuming that the X increments of your graph are constant, and you're using linear timing, you could simply divide the total animation time by the number of data-points, and calculate the times when you need to add points to the graph when the line progresses to that X position. Simply add dot shape layers to the parent view's layer at the appropriate time intervals.
You could also change your drawing method to add new line segments to your graph path one at a time, on a timer, and add dot shapes at the same time. That would give you a step-by-step animation rather than a smooth line drawing.
So say I wanted to animate drawing this shape:
Animate it something like the disney logo which seems to "write" the letters.
Ideally, I'd also like to have control over how fast it was drawn (maybe just easing), how would I go about getting that done? Specifically imagine if that link had a line of varying widths.
Not too familiar with drawing things in objective-c so perhaps someone can help point me in the right direction in terms of strategy.
Create a CAShapeLayer and set that shape as its path. Use a CABasicAnimation or a CAKeyframeAnimation to animate its strokeEnd property.
For an app I am creating, I am using a UIBezierPath to store a path. The problem is that this path continually increases in length, and at a point, the app lags and becomes jerky. Because the path is continually increasing in length, I am constantly redrawing the view 100 times a second (if there is a better way to do this, please let me know). At a certain point, the app just becomes extremely jerky. I think it is because it takes too long to stroke the path.
You can see in my drawRect method that the graphics are translated. Very little of the path is on the screen, so is there a way to only stroke the part of the path that is visible? That is what I thought I was doing with the CGContextClip method, but it had no noticeable improvement.
- (void)drawRect:(CGRect)rect {
CGContextRef myContext = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(myContext, 0, yTranslation);
[[UIColor whiteColor] setStroke];
[bPath addLineToPoint:currentPoint];
[bPath setLineWidth:lineWidth];
CGContextClip(myContext);
[bPath stroke];
}
Thank you.
A couple of thoughts:
IMHO, you shouldn't be adding data points at a regular interval at all. You should be adding data points if and only if there are new data points to be added (e.g. touchesMoved or gesture recognizer gets called with UIGestureStateChanged). This reduces the number of points in bezier and defers the point at which performance problems impose themselves upon you. The process should be driven by the touch events, not a timer.
If some of your drawing is off screen, you can probably speed it up by checking to see if either of the points falls within the visible portion of the view (e.g CGRectContainsPoint). You should probably check for intersections of line segments with the CGRect (as it's theoretically possible for neither the start nor ends to be inside the visible rectangle, but for the line segment between them to intersect the rectangle).
You can also design this process so that it determines which segments are visible only when the view port moves. This can save you from needing to constantly iterate through a very large array.
At some point, there are diminishing returns of drawing individual paths versus a bitmap image, even just for the visible portion. Sometimes it's useful to render old paths as an image, and then draw new paths over that snapshot rather than redrawing the whole path every time. For example, I've used approach where when I'm starting a new gesture, I snapshot old image and only draw new gesture on top of that.
Caching is a possible solution: Draw the curve once on an in memory image with transparent background. Update this image only when the curve changes. Overlay this cached image on whatever you are drawing on. It should be cheaper in processing power.
The other possibility is to remove the unneeded points from the bezier curve after determining which ones will affect the current view, then render the resulting bezier curve.