Get position of path at time

前端 未结 3 1806
终归单人心
终归单人心 2021-01-13 06:17

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 ani

相关标签:
3条回答
  • 2021-01-13 06:54

    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.

    0 讨论(0)
  • 2021-01-13 06:58

    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.

    0 讨论(0)
  • 2021-01-13 07:03

    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:

    tracking the position of a calayer that is being animated

    0 讨论(0)
提交回复
热议问题