45

I have a UIBezierPath that I need to take a list of points from.

In Qt there is a function called pointAtPercent that would fit my needs but I can't find anything equivalent in Objective-C.

Does anyone know how to do this?

tread
  • 10,133
  • 17
  • 95
  • 170
Alexandre
  • 807
  • 2
  • 9
  • 15

7 Answers7

68

You might try this:

UIBezierPath *yourPath; // Assume this has some points in it
CGPath yourCGPath = yourPath.CGPath;
NSMutableArray *bezierPoints = [NSMutableArray array];
CGPathApply(yourCGPath, bezierPoints, MyCGPathApplierFunc);

The path applier function will be handed each of the path's path elements in turn.
Check out CGPathApplierFunction and CGPathApply.

The path applier function might look something like this

void MyCGPathApplierFunc (void *info, const CGPathElement *element) {
    NSMutableArray *bezierPoints = (NSMutableArray *)info;

    CGPoint *points = element->points;
    CGPathElementType type = element->type;

    switch(type) {
        case kCGPathElementMoveToPoint: // contains 1 point
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            break;
        
        case kCGPathElementAddLineToPoint: // contains 1 point
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];            
            break;
        
        case kCGPathElementAddQuadCurveToPoint: // contains 2 points
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[1]]];            
            break;
        
        case kCGPathElementAddCurveToPoint: // contains 3 points
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[1]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[2]]];
            break;
        
        case kCGPathElementCloseSubpath: // contains no point
            break;
    }
}
Moritz
  • 3,235
  • 26
  • 17
  • 2
    This worked for me. With ARC I had to use a bridge cast on the caller with CGPathApply(path.CGPath, (__bridge void *) bezierPoints, processPathElement); and inside the receiver method as NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; – Christopher Oct 13 '12 at 20:55
  • 24
    Note that this doesn't give you points on the curve, just an array of vertices and control points. You really can't do anything with this array, because the information on the nature of points (moveTo vs. controlPoint) is lost. But that's a starting point. You can use wykobi to evaluate points that lie on the curve. – alecail Nov 02 '12 at 15:03
  • 1
    ARC also uses `CGPathRef yourCGPath` rather than `CGPath`. – pkamb Aug 15 '14 at 01:47
  • @alecail If you really want the metadata, you can adapt this to return an array of `CGPathElementType`s instead. That just offloads the work in the `switch` statement to the callee. For me, I just needed the points and this worked great. – Echelon Aug 27 '15 at 07:36
  • @alecail I did a comparison of the points and points decoded from CGPathApply(a freehand drawing), they are identical. Why did you say it doesn't give points on the curve? – nyus2006 Jul 09 '18 at 13:34
  • because this gives you points on the control polygon of the curve, not on the curve itself. Unless all of your points are aligned, or you curve is really.. a polygon, those curves are different... – alecail Jul 10 '18 at 14:15
18

@Moritz Great answer! To find a Swift like solution I strumbled across an approach in this post and implemented a CGPath extension like this to get the points from the path:

extension CGPath {
    func points() -> [CGPoint]
    {
        var bezierPoints = [CGPoint]()
        forEach(body: { (element: CGPathElement) in
            let numberOfPoints: Int = {
                switch element.type {
                case .moveToPoint, .addLineToPoint: // contains 1 point
                    return 1
                case .addQuadCurveToPoint: // contains 2 points
                    return 2
                case .addCurveToPoint: // contains 3 points
                    return 3
                case .closeSubpath:
                    return 0
                }
            }()
            for index in 0..<numberOfPoints {
                let point = element.points[index]
                bezierPoints.append(point)
            }
        })
        return bezierPoints
    }
    
    func forEach(body: @escaping @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        
        func callback(info: UnsafeMutableRawPointer?, element: UnsafePointer<CGPathElement>) {
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        apply(info: unsafeBody, function: callback)
    }
}
Maciek Czarnik
  • 5,950
  • 2
  • 37
  • 50
FBente
  • 2,160
  • 23
  • 34
8

I think you were trying to do something like this:

https://math.stackexchange.com/questions/26846/is-there-an-explicit-form-for-cubic-bézier-curves

y=u0(1−x^3)+3u1(1−x^2)x+3u2(1−x)x^2+u3x^3

This is my method for printing all values of that function.

- (void)logXY {

    float u0 = 0;
    float u1 = 0.05;
    float u2 = 0.25;
    float u3 = 1;

    for (float x = 0; x <= 10.0; x = x + 0.1) {
        float y = u0 * (1 - x * x * x) + 3 * u1 * (1 - x * x) * x + 3 * u2 * (1 - x) * x * x + u3 * x * x * x;

        NSLog(@"x: %f\ty: %f", x, y);
    }
}

And output is:

x: 0.000000 y: 0.000000
x: 0.100000 y: 0.022600
x: 0.200000 y: 0.060800
x: 0.300000 y: 0.115200
x: 0.400000 y: 0.186400
x: 0.500000 y: 0.275000
x: 0.600000 y: 0.381600
x: 0.700000 y: 0.506800
x: 0.800000 y: 0.651200
x: 0.900000 y: 0.815400
x: 1.000000 y: 1.000000
x: 1.100000 y: 1.205600
x: 1.200000 y: 1.432800
x: 1.300000 y: 1.682200
x: 1.400000 y: 1.954401
x: 1.500000 y: 2.250001
x: 1.600000 y: 2.569601
x: 1.700000 y: 2.913801
x: 1.800000 y: 3.283201
x: 1.900000 y: 3.678401
x: 2.000000 y: 4.100001
x: 2.100000 y: 4.548601
x: 2.200000 y: 5.024800
x: 2.300000 y: 5.529200
x: 2.400000 y: 6.062399
x: 2.500000 y: 6.625000
x: 2.600000 y: 7.217597
x: 2.700000 y: 7.840797
x: 2.799999 y: 8.495197
x: 2.899999 y: 9.181394
x: 2.999999 y: 9.899996
Community
  • 1
  • 1
Adam
  • 158
  • 3
  • 8
4

I wrote an article about finding the closest point on UIBezierPath, which is very similar to what the OP is looking for. I've also created a demo project on Swift: https://github.com/agordeev/BezierPathClosestPoint

Andrey Gordeev
  • 30,606
  • 13
  • 135
  • 162
3

i have modified @FBente post to work with swift 3

extension CGPath {
func points() -> [CGPoint]
{
    var bezierPoints = [CGPoint]()
    self.forEach(body: { (element: CGPathElement) in
        let numberOfPoints: Int = {
            switch element.type {
            case .moveToPoint, .addLineToPoint: // contains 1 point
                return 1
            case .addQuadCurveToPoint: // contains 2 points
                return 2
            case .addCurveToPoint: // contains 3 points
                return 3
            case .closeSubpath:
                return 0
            }
        }()
        for index in 0..<numberOfPoints {
            let point = element.points[index]
            bezierPoints.append(point)
        }
    })
    return bezierPoints
}

func forEach( body: @convention(block) (CGPathElement) -> Void) {
    typealias Body = @convention(block) (CGPathElement) -> Void
    func callback(info: UnsafeMutableRawPointer, element: UnsafePointer<CGPathElement>) {
        let body = unsafeBitCast(info, to: Body.self)
        body(element.pointee)
    }
    let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
    self.apply(info: unsafeBody, function: callback as! CGPathApplierFunction)
}
}
1

swift 4.0:

    var bezierPoints = NSMutableArray()
    yourPath.apply(info: &bezierPoints, function: { info, element in

        guard let resultingPoints = info?.assumingMemoryBound(to: NSMutableArray.self) else {
            return
        }

        let points = element.pointee.points
        let type = element.pointee.type

        switch type {
        case .moveToPoint:
            resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])

        case .addLineToPoint:
            resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])

        case .addQuadCurveToPoint:
            resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
            resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])

        case .addCurveToPoint:
            resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
            resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
            resultingPoints.pointee.add([NSNumber(value: Float(points[2].x)), NSNumber(value: Float(points[2].y))])

        case .closeSubpath:
            break
        }
    })
Rodrigo Fava
  • 318
  • 3
  • 12
0

Updated FBente's extension for Swift 3:

extension CGPath {
  func points() -> [CGPoint]
  {
    var bezierPoints = [CGPoint]()
    self.forEach({ (element: CGPathElement) in
      let numberOfPoints: Int = {
        switch element.type {
        case .moveToPoint, .addLineToPoint: // contains 1 point
          return 1
        case .addQuadCurveToPoint: // contains 2 points
          return 2
        case .addCurveToPoint: // contains 3 points
          return 3
        case .closeSubpath:
          return 0
        }
      }()
      for index in 0..<numberOfPoints {
        let point = element.points[index]
        bezierPoints.append(point)
      }
    })
    return bezierPoints
  }

  func forEach(_ body: @convention(block) (CGPathElement) -> Void) {
    typealias Body = @convention(block) (CGPathElement) -> Void
    func callback(info: UnsafeMutableRawPointer, element: UnsafePointer<CGPathElement>) {
      let body = unsafeBitCast(info, to: Body.self)
      body(element.pointee)
    }
    let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
    self.apply(info: unsafeBody, function: callback as! CGPathApplierFunction)
  }
}
Joel
  • 423
  • 1
  • 6
  • 14