将 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)]
]
由此,您可以分别创建适当的 UIBezierPath
或 PKStrokePath
。
例如:
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)
}
最后一步是将点转换为 PKStrokePath
和 PKDrawing
的列表
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
)
}
)
有没有办法将 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)]
]
由此,您可以分别创建适当的 UIBezierPath
或 PKStrokePath
。
例如:
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)
}
最后一步是将点转换为 PKStrokePath
和 PKDrawing
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
)
}
)