如何从UIBezierPath获取点列表?

45

我有一个UIBezierPath,需要从中获取一系列点的列表。

Qt中,有一个函数称为pointAtPercent可以满足我的需求,但是我找不到与之相等的Objective-C函数。

是否有人知道如何实现这个功能?

7个回答

68

你可以尝试这个:

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

路径应用函数将依次处理路径中的每个路径元素。
请查看 CGPathApplierFunctionCGPathApply

路径应用函数可能类似于这样

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;
    }
}

2
这对我有用。在使用ARC时,我必须在调用者上使用桥接转换,如CGPathApply(path.CGPath, (__bridge void *) bezierPoints, processPathElement); 并且在接收方法内部,使用NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; - Christopher
25
请注意,这不会给你曲线上的点数,只会提供一个顶点和控制点的数组。你不能通过这个数组做任何事情,因为有关点(moveTo vs. controlPoint)性质的信息已经丢失了。但这是一个起点。您可以使用Wykobi来评估位于曲线上的点。 - alecail
1
ARC也使用CGPathRef yourCGPath而非CGPath - pkamb
如果您真的想要元数据,您可以将其调整为返回CGPathElementType数组。这只是将工作转移给被调用方而已。对我来说,我只需要点,这个方法非常好用。 - Echelon
@alecail,我对点和从CGPathApply(一个手绘图)解码的点进行了比较,它们是相同的。你为什么说它不会给出曲线上的点? - nyus2006
因为这会在曲线的控制多边形上给你点,而不是在曲线本身上。除非你所有的点都对齐,或者你的曲线真的是一个多边形,否则这些曲线是不同的... - alecail

18

@Moritz 很棒的回答!为了找到类似于Swift的解决方案,我在这个帖子中偶然发现了一种方法,并实现了一个CGPath扩展来从路径中获取点:

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)
    }
}

8
我认为您试图做类似于以下内容的事情: 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
- (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);
    }
}

输出结果为:

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

2
终于,在完成课程三年后,学会了工程数学应用!哈哈 - Rajamohan S

4

3
我已经修改了@FBente的帖子,使其适用于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)
}
}

使用Xcode 8.3.2,在调用self.apply(::)时,forEach(:)会崩溃。 - Womble

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
        }
    })

0

更新了针对Swift 3的FBente扩展:

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)
  }
}

使用Xcode 8.3.2,在调用self.apply(::)时,forEach(:)会崩溃。 - Womble

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接