is there a nice way to calculate the position of a path (CGPath or UIBezierPath) at a given time (from 0 to 1)?
Using CAShapeLayer for example, one can create an animated stroke end. I want to know the position of that stroke end at arbitrary times.
Thanks in advance, Adrian
Building on Matt's display link answer, you can track the position of the end point by creating a second "invisible" keyframe animation.
NOTES:
We start with 3 properties:
The
displayLink
will allow us to run code every time the screen updates. ThepathLayer
provides the visuals, the one that we'll animate. ThetrackingLayer
provides an invisible layer that we'll use to track the position of thestrokeEnd
animation on thepathLayer
.We open our view controller like so:
With the following methods...
We first create the display link and add it to the run loop (as per Matt's code):
We then create the visible layer:
We then create an "invisible" (i.e. via a frame with no dimensions) layer to track:
We then create a method that grabs the position of the tracking layer:
... and the
startAnimating
method:This technique is pretty useful if you have paths you want to follow, but don't want to be bothered doing the math yourself.
Some of the valuable reasons for this are:
EDIT Here's a link to a github repo: https://github.com/C4Code/layerTrackPosition
Here's an image of my simulator:
You can definitely base your approach on the CADisplayLink and a tracking layer. However, if you don't mind doing a little bit of math on your own, the solution is not too complicated. Plus, you wont have to depend on setting up a display link and extra layers. In fact, you dont even have to depend on QuartzCore.
The following will work for any CGPathRef. In case of a UIBezierPath, fetch the CGPath property of the same:
CGPathApply
on the path you want to introspect along with a customCGPathApplierFunction
function.CGPathApplierFunction
will be invoked for each component of that path. The CGPathElement (an argument to the applier) will tell you what kind of a path element it is along with the points that make that element (control points or endpoints).kCGPathElementMoveToPoint
,kCGPathElementAddLineToPoint
,kCGPathElementAddQuadCurveToPoint
andkCGPathElementAddCurveToPoint
respectively.CGPathApply
once per path and this step is extremely fast.Now, onto the math:
t
, get the element (more on this later) and its constituent points.kCGPathElementMoveToPoint
, its a linear interpolationp0 + t * (p1 - p0)
(for x and y)kCGPathElementAddQuadCurveToPoint
, its quadratic((1 - t) * (1 - t)) * p0 + 2 * (1 - t) * t * p1 + t * t * p2
kCGPathElementAddCurveToPoint
, its a cubic bezier((1 - t) * (1 - t) * (1 - t)) * p0 + 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t * p3
Now the question remains, how do you figure out the path element at time
t
. You can assume each path element gets an equal time slice or you can calculate the distance of each element and account for the fractional time (the former approach works fine for me). Also, don't forget to add the times for all previous path elements (you dont have to find the interpolations for these).As I said, this is just for completeness (and likely how Apple figures out this stuff out themselves) and only if you are willing to do the math.
If you keep records of all your points from the beginning, you can calculate the distance between them.
When you want to know at a given time which of those points are being animated on the screen, you can do this:
first, get the current value of the strokeEnd (it's between 0 and 1) like this:
CAShapeLayer *presentationLayer = (CAShapeLayer*)[_pathLayer presentationLayer];
CGFloat strokeValue = [[presentationLayer valueForKey:@"strokeEnd"] floatValue];
then calculate the distance you already drew by now:
CGFloat doneDistance = _allTheDistance * strokeValue;
after this, you have to iterate all your points and calculate the distance between them till you get that
doneDistance
This won't tell you exactly where on screen the path is, but the current point that is animated. Maybe it will help someone.