【问题标题】:Convert UIBezierPath to PKStrokePath swift将 UIBezierPath 转换为 PKStrokePath swift
【发布时间】:2021-08-15 08:04:59
【问题描述】:

有没有办法将 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))

【问题讨论】:

    标签: swift uibezierpath pencilkit


    【解决方案1】:

    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: $0, p1: startPoint, p2: endPoint)
                    }
                case .addQuadCurveToPoint:
                    pointCalculationFunc = {
                        calculateQuad(t: $0, p1: startPoint, p2: command.controlPoints[0], p3: endPoint)
                    }
                case .addCurveToPoint:
                    pointCalculationFunc = {
                        calculateCube(t: $0, 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 { $0.position < $1.position }
                        .map { $0.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]
    }
    
    // http://stackoverflow.com/a/38743318/1321917
    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
            )
        }
    )
    

    【讨论】:

    • 你的解决方案非常适合曲线,谢谢
    【解决方案2】:

    我建议捕获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)
    

    产量:

    【讨论】:

    • 谢谢,但我注意到它与直线完美搭配,但如果我有曲线,它会显示出奇怪的形状
    • 如果有addCurve就不行,比如let path = UIBezierPath() path.move(to: CGPoint(x: 11.458, y: 11.458)) path.addLine(to: CGPoint(x: 11.458, y: 125.816)) path.addCurve(to: CGPoint(x: 97.479, y: 60.973), controlPoint1: CGPoint(x: 11.458, y: 125.816), controlPoint2: CGPoint(x: 129.531, y: 94.336)) path.addCurve(to: CGPoint(x: 41.153999999999996, y: 67.304), controlPoint1: CGPoint(x: 65.42699999999999, y: 27.61), controlPoint2: CGPoint(x: 62.344, y: 27.677999999999997))
    • 这个形状应该是这样的:a.cl.ly/kpunmeqG
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-31
    • 2014-10-06
    • 2017-10-10
    相关资源
    最近更新 更多