Get position of path at time

2019-02-17 12:34发布

问题:

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

回答1:

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:

  • Use CGPathApply on the path you want to introspect along with a custom CGPathApplierFunction function.
  • Your 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).
  • You will be given one, two, three and four points for kCGPathElementMoveToPoint, kCGPathElementAddLineToPoint, kCGPathElementAddQuadCurveToPoint and kCGPathElementAddCurveToPoint respectively.
  • Store these points internally in a representation of your choosing. You only need to use the CGPathApply once per path and this step is extremely fast.

Now, onto the math:

  • Based on the time you wish to find the position at, say t, get the element (more on this later) and its constituent points.
  • If the element type is kCGPathElementMoveToPoint, its a linear interpolation p0 + t * (p1 - p0) (for x and y)
  • If the element type is kCGPathElementAddQuadCurveToPoint, its quadratic ((1 - t) * (1 - t)) * p0 + 2 * (1 - t) * t * p1 + t * t * p2
  • If the element type is 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.



回答2:

Building on Matt's display link answer, you can track the position of the end point by creating a second "invisible" keyframe animation.

NOTES:

  1. with this technique you don't need to calculate the position of the end point yourself
  2. you can use any path shape.
  3. this was written in the view controller class of a basic iOS Single View Application template in Xcode

We start with 3 properties:

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, strong) CAShapeLayer *pathLayer;
@property (nonatomic, strong) CALayer *trackingLayer;
@end

The displayLink will allow us to run code every time the screen updates. The pathLayer provides the visuals, the one that we'll animate. The trackingLayer provides an invisible layer that we'll use to track the position of the strokeEnd animation on the pathLayer.

We open our view controller like so:

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self createDisplayLink];
    [self createPathLayer];
    [self createTrackingLayer];
    [self startAnimating];
}
...

With the following methods...

We first create the display link and add it to the run loop (as per Matt's code):

-(void)createDisplayLink {
    _displayLink = [CADisplayLink
                    displayLinkWithTarget:self
                    selector:
                    @selector(displayLinkDidUpdate:)];

    [_displayLink
     addToRunLoop:[NSRunLoop mainRunLoop]
     forMode:NSDefaultRunLoopMode];
}

We then create the visible layer:

-(void)createPathLayer {
    //create and style the path layer
    //add it to the root layer of the view controller's view
    _pathLayer = [CAShapeLayer layer];
    _pathLayer.bounds = CGRectMake(0,0,100,100);
    _pathLayer.path = CGPathCreateWithEllipseInRect(_pathLayer.bounds, nil);
    _pathLayer.fillColor = [UIColor clearColor].CGColor;
    _pathLayer.lineWidth = 5;
    _pathLayer.strokeColor = [UIColor blackColor].CGColor;
    _pathLayer.position = self.view.center;
    [self.view.layer addSublayer:_pathLayer];
}

We then create an "invisible" (i.e. via a frame with no dimensions) layer to track:

-(void)createTrackingLayer {
    _trackingLayer = [CALayer layer];

    //set the frame (NOT bounds) so that we can see the layer
    //uncomment the following two lines to see the tracking layer
    //_trackingLayer.frame = CGRectMake(0,0,5,5);
    //_trackingLayer.backgroundColor = [UIColor redColor].CGColor;

    //we add the blank layer to the PATH LAYER
    //so that its coordinates are always in the path layer's coordinate system
    [_pathLayer addSublayer:_trackingLayer];
}

We then create a method that grabs the position of the tracking layer:

- (void)displayLinkDidUpdate:(CADisplayLink *)sender {
    //grab the presentation layer of the blank layer
    CALayer *presentationLayer = [_trackingLayer presentationLayer];
    //grab the position of the blank layer
    //convert it to the main view's layer coordinate system
    CGPoint position = [self.view.layer convertPoint:presentationLayer.position
                                           fromLayer:_trackingLayer];
    //print it out, or do something else with it
    NSLog(@"%4.2f,%4.2f",position.x,position.y);
}

... and the startAnimating method:

-(void)startAnimating {
    //begin the animation transaction
    [CATransaction begin];
    //create the stroke animation
    CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    //from 0
    strokeEndAnimation.fromValue = @(0);
    //to 1
    strokeEndAnimation.toValue = @(1);
    //1s animation
    strokeEndAnimation.duration = 10.0f;
    //repeat forever
    strokeEndAnimation.repeatCount = HUGE_VAL;
    //ease in / out
    strokeEndAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    //apply to the pathLayer
    [_pathLayer addAnimation:strokeEndAnimation forKey:@"strokeEndAnimation"];

    //NOTE: we don't actually TRACK above animation, its there only for visual effect

    //begin the follow path animation
    CAKeyframeAnimation *followPathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    //set the path for the keyframe animation
    followPathAnimation.path = _pathLayer.path;
    //add an array of times that match the NUMBER of points in the path
    //for custom paths, you'll need to know the number of points and calc this yourself
    //for an ellipse there are 5 points exactly
    followPathAnimation.keyTimes = @[@(0),@(0.25),@(0.5),@(0.75),@(1)];
    //copy the timing function
    followPathAnimation.timingFunction = strokeEndAnimation.timingFunction;
    //copy the duration
    followPathAnimation.duration = strokeEndAnimation.duration;
    //copy the repeat count
    followPathAnimation.repeatCount = strokeEndAnimation.repeatCount;
    //add the animation to the layer
    [_trackingLayer addAnimation:followPathAnimation forKey:@"postionAnimation"];
    [CATransaction commit];
}

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:

  1. different/custom paths can be used (not just ellipses...)
  2. different media timing functions can be used (you don't have to figure out the math yourself for ease in, out, or linear, etc...)
  3. you can start / stop animating the tracking layer at any time (i.e. doesn't have to run continuously)
  4. you can start / stop the display link at any time
  5. converting between different layer coordinates is quite easy, so you can have layer inside layers inside layers and still transfer their coordinates to any other layer

EDIT Here's a link to a github repo: https://github.com/C4Code/layerTrackPosition

Here's an image of my simulator:



回答3:

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.