将 UIBezierPath 转换为 PKStrokePath swift

Convert UIBezierPath to PKStrokePath swift

有没有办法将 UIBezierPath 转换为 PKStrokePath?

例如,我将这些路径设置为 UIBezierPath 我怎样才能将它转换为 PKStrokePath 以便在 PKDrawing 中使用它?

let shape = UIBezierPath()
shape.move(to: CGPoint(x: 30.2, y: 37.71))
shape.addLine(to: CGPoint(x: 15.65, y: 2.08))
shape.addLine(to: CGPoint(x: 2.08, y: 37.33))
shape.move(to: CGPoint(x: 23.46, y: 21.21))
shape.addLine(to: CGPoint(x: 8.36, y: 21.02))

我建议捕获 CGPoint 的数组,例如

let pointArrays = [
    [CGPoint(x: 30.2, y: 37.71), CGPoint(x: 15.65, y: 2.08), CGPoint(x: 2.08, y: 37.33)], 
    [CGPoint(x: 23.46, y: 21.21), CGPoint(x: 8.36, y: 21.02)]
]

由此,您可以分别创建适当的 UIBezierPathPKStrokePath

例如:

let path = UIBezierPath()
for stroke in pointArrays where pointArrays.count > 1 {
    path.move(to: stroke.first!)
    for point in stroke.dropFirst() {
        path.addLine(to: point)
    }
}

将产生(lineWidth 为 1 且描边颜色为红色):

而使用 PencilKit,

let ink = PKInk(.pen, color: .blue)
let strokes = pointArrays.compactMap { stroke -> PKStroke? in
    guard stroke.count > 1 else { return nil }
    let controlPoints = stroke.enumerated().map { index, point in
        PKStrokePoint(location: point, timeOffset: 0.1 * TimeInterval(index), size: CGSize(width: 3, height: 3), opacity: 2, force: 1, azimuth: 0, altitude: 0)
    }
    let path = PKStrokePath(controlPoints: controlPoints, creationDate: Date())
    return PKStroke(ink: ink, path: path)
}
let drawing = PKDrawing(strokes: strokes)

将产生:

显然,如两个图所示,UIBezierPath 中的一系列点与 PKStrokePath 中的一系列控制点不同。

如果你想要PKStrokePath匹配,你应该为每个线段创建单独的路径,例如

let ink = PKInk(.pen, color: .blue)
var strokes: [PKStroke] = []

for points in pointArrays where points.count > 1 {
    let strokePoints = points.enumerated().map { index, point in
        PKStrokePoint(location: point, timeOffset: 0.1 * TimeInterval(index), size: CGSize(width: 3, height: 3), opacity: 2, force: 1, azimuth: 0, altitude: 0)
    }

    var startStrokePoint = strokePoints.first!

    for strokePoint in strokePoints {
        let path = PKStrokePath(controlPoints: [startStrokePoint, strokePoint], creationDate: Date())
        strokes.append(PKStroke(ink: ink, path: path))
        startStrokePoint = strokePoint
    }
}
let drawing = PKDrawing(strokes: strokes)

产量:

UIBezierPath PKStrokePath 是完全不同的对象

UIBezierPath可以有点、线、曲线和跳到另一个点,而PKStrokePath只能有点连接,但它们有不同的参数,如大小、力、等等

因此,要创建 PKStrokePath,我们需要根据 UIBezierPath 计算点数。如果路径中有 move 个步骤

,单个 UIBezierPath 可能会产生多个 PKStrokePath

这是一条棘手但可行的道路

作为基地我选择了Finding the closest point on UIBezierPath。本文讲的是计算UIBezierPath

虽然这篇文章的作者为每条 UIBezierPath 曲线取 100 个点,但我采取了下一步并将二进制搜索思想添加到算法中:我希望曲线上的点距离不大于stopDistance(你可以玩这个值)

extension UIBezierPath {
    func generatePathPoints() -> [[CGPoint]] {
        let points = cgPath.points()
        guard points.count > 0 else {
            return []
        }
        var paths = [[CGPoint]]()
        var pathPoints = [CGPoint]()
        var previousPoint: CGPoint?
        let stopDistance: CGFloat = 10
        for command in points {
            let endPoint = command.point
            defer {
                previousPoint = endPoint
            }
            guard let startPoint = previousPoint else {
                continue
            }
            let pointCalculationFunc: (CGFloat) -> CGPoint
            switch command.type {
            case .addLineToPoint:
                // Line
                pointCalculationFunc = {
                    calculateLinear(t: [=10=], p1: startPoint, p2: endPoint)
                }
            case .addQuadCurveToPoint:
                pointCalculationFunc = {
                    calculateQuad(t: [=10=], p1: startPoint, p2: command.controlPoints[0], p3: endPoint)
                }
            case .addCurveToPoint:
                pointCalculationFunc = {
                    calculateCube(t: [=10=], p1: startPoint, p2: command.controlPoints[0], p3: command.controlPoints[1], p4: endPoint)
                }
            case .closeSubpath:
                previousPoint = nil
                fallthrough
            case .moveToPoint:
                if !pathPoints.isEmpty {
                    paths.append(pathPoints)
                    pathPoints = []
                }
                continue
            @unknown default:
                continue
            }
            
            let initialCurvePoints = [
                CurvePoint(position: 0, cgPointGenerator: pointCalculationFunc),
                CurvePoint(position: 1, cgPointGenerator: pointCalculationFunc),
            ]
            let curvePoints = calculatePoints(
                tRange: 0...1,
                pointCalculationFunc: pointCalculationFunc,
                leftPoint: initialCurvePoints[0].cgPoint,
                stopDistance: stopDistance
            ) + initialCurvePoints
            pathPoints.append(
                contentsOf:
                    curvePoints
                    .sorted { [=10=].position < .position }
                    .map { [=10=].cgPoint }
            )
            previousPoint = endPoint
        }
        if !pathPoints.isEmpty {
            paths.append(pathPoints)
            pathPoints = []
        }
        return paths
    }
    
    private func calculatePoints(
        tRange: ClosedRange<CGFloat>,
        pointCalculationFunc: (CGFloat) -> CGPoint,
        leftPoint: CGPoint,
        stopDistance: CGFloat
    ) -> [CurvePoint] {
        let middlePoint = CurvePoint(position: (tRange.lowerBound + tRange.upperBound) / 2, cgPointGenerator: pointCalculationFunc)
        if hypot(leftPoint.x - middlePoint.cgPoint.x, leftPoint.y - middlePoint.cgPoint.y) < stopDistance {
            return [middlePoint]
        }
        let leftHalfPoints = calculatePoints(tRange: tRange.lowerBound...middlePoint.position, pointCalculationFunc: pointCalculationFunc, leftPoint: leftPoint, stopDistance: stopDistance)
        let rightHalfPoints = calculatePoints(tRange: middlePoint.position...tRange.upperBound, pointCalculationFunc: pointCalculationFunc, leftPoint: middlePoint.cgPoint, stopDistance: stopDistance)
        return leftHalfPoints + rightHalfPoints + [middlePoint]
    }
}

private struct CurvePoint {
    let position: CGFloat
    let cgPoint: CGPoint
    
    init(position: CGFloat, cgPointGenerator: (CGFloat) -> CGPoint) {
        self.position = position
        self.cgPoint = cgPointGenerator(position)
    }
}

struct PathCommand {
    let type: CGPathElementType
    let point: CGPoint
    let controlPoints: [CGPoint]
}

// 
extension CGPath {
    func points() -> [PathCommand] {
        var bezierPoints = [PathCommand]()
        forEachPoint { element in
            guard element.type != .closeSubpath else {
                return
            }
            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
                @unknown default:
                    fatalError()
                }
            }()
            var points = [CGPoint]()
            for index in 0..<(numberOfPoints - 1) {
                let point = element.points[index]
                points.append(point)
            }
            let command = PathCommand(type: element.type, point: element.points[numberOfPoints - 1], controlPoints: points)
            bezierPoints.append(command)
        }
        return bezierPoints
    }
    
    private func forEachPoint(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)
        }
        withoutActuallyEscaping(body) { body in
            let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
            apply(info: unsafeBody, function: callback as CGPathApplierFunction)
        }
    }
}

/// Calculates a point at given t value, where t in 0.0...1.0
private func calculateLinear(t: CGFloat, p1: CGPoint, p2: CGPoint) -> CGPoint {
    let mt = 1 - t
    let x = mt*p1.x + t*p2.x
    let y = mt*p1.y + t*p2.y
    return CGPoint(x: x, y: y)
}

/// Calculates a point at given t value, where t in 0.0...1.0
private func calculateCube(t: CGFloat, p1: CGPoint, p2: CGPoint, p3: CGPoint, p4: CGPoint) -> CGPoint {
    let mt = 1 - t
    let mt2 = mt*mt
    let t2 = t*t
    
    let a = mt2*mt
    let b = mt2*t*3
    let c = mt*t2*3
    let d = t*t2
    
    let x = a*p1.x + b*p2.x + c*p3.x + d*p4.x
    let y = a*p1.y + b*p2.y + c*p3.y + d*p4.y
    return CGPoint(x: x, y: y)
}

/// Calculates a point at given t value, where t in 0.0...1.0
private func calculateQuad(t: CGFloat, p1: CGPoint, p2: CGPoint, p3: CGPoint) -> CGPoint {
    let mt = 1 - t
    let mt2 = mt*mt
    let t2 = t*t
    
    let a = mt2
    let b = mt*t*2
    let c = t2
    
    let x = a*p1.x + b*p2.x + c*p3.x
    let y = a*p1.y + b*p2.y + c*p3.y
    return CGPoint(x: x, y: y)
}

最后一步是将点转换为 PKStrokePathPKDrawing

的列表
let strokePaths = path.generatePathPoints().map { pathPoints in
    PKStrokePath(
        controlPoints: pathPoints.map { pathPoint in
            PKStrokePoint(
                location: pathPoint,
                timeOffset: 0,
                size: .init(width: 5, height: 5),
                opacity: 1,
                force: 1,
                azimuth: 0,
                altitude: 0
            )
        },
        creationDate: Date()
    )
    return CGPoint(x: x, y: y)
}
let drawing = PKDrawing(
    strokes: strokePaths.map { strokePath in
        PKStroke(
            ink: PKInk(.pen, color: UIColor.black),
            path: strokePath
        )
    }
)