Is there a way to convert UIBezierPath to PKStrokePath ?
for example I have these path as UIBezierPath how can I convert it to PKStrokePath to use it in 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))
UIBezierPath
PKStrokePath
are totally diferrent objects
UIBezierPath
Can have points, lines, curves and jumps to an other point, while PKStrokePath
can only have points which will be connected, but they have different parameters, like size, force, etc
So to create PKStrokePath
we would need to calculate points based on a UIBezierPath
. Also a single UIBezierPath
may produce more than one PKStrokePath
, if path has move
steps inside
That's a tricky path but possible
As the base I took Finding the closest point on UIBezierPath. This article talks about calculating UIBezierPath
points
While author of this article is taking 100 points for each UIBezierPath
curve, I took a next step and added binary search idea into the algorithm: I wanted to have points on the curve with distance not greater than stopDistance
(you can play with this value)
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)
}
And the final step of converting points to list of PKStrokePath
and a 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
)
}
)
I would suggest capturing the array of arrays of CGPoint
, e.g.
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)]
]
From that, you could create the appropriate UIBezierPath
or PKStrokePath
, respectively.
For example:
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)
}
}
Will yield (with lineWidth
of 1 and a red stroke color):
Whereas with 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)
Will yield:
Obviously, as the two drawings illustrate, a series of points in a UIBezierPath
is not the same as a series of control points in a PKStrokePath
.
If you want the PKStrokePath
to match, you should create separate paths for each line segment, e.g.
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)
Yielding:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With