-
Notifications
You must be signed in to change notification settings - Fork 19
/
UIBezierPath+Superpowers.swift
795 lines (616 loc) · 23.9 KB
/
UIBezierPath+Superpowers.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
//
// UIBezierPath+Superpowers.swift
// BezierPlayground
//
// Created by Maximilian Kraus on 13.07.17.
// Copyright © 2017 Maximilian Kraus. All rights reserved.
//
import UIKit
//MARK: - Settings
/// The precision with which to calculate the length of the path.
/// Higher precision is naturally more expensive to compute.
fileprivate enum LengthCalculationPrecision: Int {
case low = 50
case normal = 100
case high = 150
}
fileprivate let lengthCalculationPrecision: LengthCalculationPrecision = .normal
/// The precision with which to calculate the perpendicular points and distances.
/// Higher precision is naturally more expensive to compute.
fileprivate enum PerpendicularCalculationPrecision: CGFloat {
case low = 15
case normal = 5
case high = 2
}
fileprivate let perpendicularCalculationPrecision: PerpendicularCalculationPrecision = .normal
// -
//MARK: - Public API
@MainActor public extension UIBezierPath {
/// Call this method once to enable this extension to automatically handle path mutations.
/// Sadly, Swift does not allow us to utilize the `load` or `initialize` methods anymore, which
/// could do this automatically for you, so you have to do it manually. Sorry.
///
/// - Important:
/// Invoking this method will perform runtime method swizzling to enable the extension to
/// be notified when the path object is mutated. You should preferably call this method in
/// your AppDelegate class or another piece of code that runs when your app is started.
///
/// You **do not have to** call this method in order for this library to work.
/// However, opting out will result in the internal caching to be deactivated, which
/// may cause a major performance impact, depending on how complex your path objects are.
///
/// If you can guarantee that your path objects won't be mutated after the first time you call
/// any of the mx_* methods or properties, you can set the internal variable `swizzled` to `true` without calling
/// this method. This will result in the caching to be re-activated. Note however, that if you go down
/// that road and mutate your path objects anyway, the library will base its calculations on the intercal cache
/// which may not be in sync with the actual path object.
@MainActor static func mx_prepare() {
if swizzled { return }
let swizzlingPairs: [(Selector, Selector)] = [
(#selector(UIBezierPath.addLine), #selector(UIBezierPath.mx_addLine)),
(#selector(UIBezierPath.addCurve), #selector(UIBezierPath.mx_addCurve)),
(#selector(UIBezierPath.addQuadCurve ), #selector(UIBezierPath.mx_addQuadCurve)),
(#selector(UIBezierPath.addArc), #selector(UIBezierPath.mx_addArc)),
(#selector(UIBezierPath.close), #selector(UIBezierPath.mx_close)),
(#selector(UIBezierPath.removeAllPoints), #selector(UIBezierPath.mx_removeAllPoints)),
(#selector(UIBezierPath.append), #selector(UIBezierPath.mx_append)),
(#selector(UIBezierPath.apply), #selector(UIBezierPath.mx_apply))
]
swizzlingPairs.forEach {
swizzle(self, $0.0, $0.1)
}
swizzled = true
}
/// Returns the total length of the receiver.
///
/// Note that if you did not call `mx_prepare`, the internal calculations of this operation
/// are not cached. In this case, it is not safe to access this property frequently without creating any overhead.
var mx_length: CGFloat {
let length = calculateLength()
if !swizzled { invalidatePathCalculations() }
return length
}
/// Returns the point on the path at `t * length` in to the path.
/// If the receiver is empty, `CGPoint.zero` is returned.
///
/// - Parameters:
/// - t: The fraction of the total path length for which to return the point on the path.
func mx_point(atFractionOfLength t: CGFloat) -> CGPoint {
if isEmpty {
return .zero
}
var point: CGPoint = .zero
findPathElement(at: t) { element, t in
point = element.point(at: t)
}
if !swizzled { invalidatePathCalculations() }
return point
}
/// For a given t, returns the slope of the path at the point
/// `t * length` in to the path.
/// If the receiver is empty, `0` is returned.
///
/// - Note:
/// The slope is expressed in context of the positiv cartesian x-axis.
/// i.e. for a path starting at `{0,100}` and ending at `{100,0}`, this method
/// will return a slope of `1.0` for any `t`.
/// Keep in mind that the y-axis of the iOS coordinate system is inversed.
///
/// - Parameter t: The fraction
/// - Returns: The slope
func mx_slope(atFractionOfLength t: CGFloat) -> CGFloat {
if isEmpty {
return 0
}
var slope: CGFloat = 0
findPathElement(at: t) { element, t in
slope = element.slope(at: t)
}
if !swizzled { invalidatePathCalculations() }
// Returning -slope, because the y-axis of the iOS coordinate system is inverse.
// By default, positive slopes would go down, negative go up. This is counter intuitive.
return -slope
}
/// For a given t, returns the tangent angle of the path at the point
/// `t * length` in to the path.
/// If the receiver is empty, `0` is returned.
///
/// - Note:
/// The angle is expressed in radian unit and in context of the positiv cartesian x-axis.
/// i.e. rotating (mathematically = counter clockwise) a horizontal line that starts in the point on the path
/// which corresponds to fraction `t`, around said point by the return value of this method,
/// results in the line being the tangent of the path in that point.
///
/// - Parameter t: The fraction
/// - Returns: The tangent angle
func mx_tangentAngle(atFractionOfLength t: CGFloat) -> CGFloat {
if isEmpty {
return 0
}
var angle: CGFloat = 0
findPathElement(at: t) { element, t in
angle = element.tangentAngle(at: t)
}
if !swizzled { invalidatePathCalculations() }
// Rotating by .pi / 2, because the y-axis of the iOS coordinate system is inversed.
// Smaller values are at the top, increasing to the bottom. This is invers to the
// cartesian coordinate system.
return angle - .pi / 2
}
/// Returns the closest point on the path to a given `CGPoint`,
/// effectively letting fall a perpendicular on the path from `point`
/// and returning the point of intersection.
///
/// - Parameters:
/// - point: The point from which to letting fall the perpendicular.
func mx_perpendicularPoint(for point: CGPoint) -> CGPoint {
calculatePointLookupTable()
var closestPoint: (p: CGPoint, distance: CGFloat) = (.zero, .greatestFiniteMagnitude)
for element in extractPathElements() {
if let lookupTable = element.pointsLookupTable {
for p in lookupTable {
let distance = p.linearLineLength(to: point)
if distance < closestPoint.distance {
closestPoint = (p, distance)
}
}
}
}
if !swizzled { invalidatePathCalculations() }
return closestPoint.p
}
/// Convenience method to calculate the perpendicular distance from a
/// given `CGPoint` to the receiver. See `mx_perpendicularPoint(for:)`.
///
/// - Parameters:
/// - point: The point for which to calculate the distance.
func mx_perpendicularDistance(from point: CGPoint) -> CGFloat {
let closestPathPoint = mx_perpendicularPoint(for: point)
return closestPathPoint.linearLineLength(to: point)
}
}
// -
//MARK: - Internal
fileprivate struct BezierPathElement {
let type: CGPathElementType
var startPoint: CGPoint
var endPoint: CGPoint
var controlPoints: [CGPoint]
var pointsLookupTable: [CGPoint]?
var lengthRange: ClosedRange<CGFloat>?
private let calculatedLength: CGFloat
var length: CGFloat {
return calculatedLength == 0 ? 1 : calculatedLength
}
init(type: CGPathElementType, startPoint: CGPoint, endPoint: CGPoint, controlPoints: [CGPoint] = []) {
self.type = type
self.startPoint = startPoint
self.endPoint = endPoint
self.controlPoints = controlPoints
calculatedLength = type.calculateLength(from: startPoint, to: endPoint, controlPoints: controlPoints)
}
func point(at t: CGFloat) -> CGPoint {
switch type {
case .addLineToPoint:
return startPoint.linearBezierPoint(to: endPoint, t: t)
case .addQuadCurveToPoint:
return startPoint.quadBezierPoint(to: endPoint, controlPoint: controlPoints[0], t: t)
case .addCurveToPoint:
return startPoint.cubicBezierPoint(to: endPoint, controlPoint1: controlPoints[0], controlPoint2: controlPoints[1], t: t)
default:
return .zero
}
}
func slope(at t: CGFloat) -> CGFloat {
switch type {
case .addLineToPoint:
return startPoint.linearSlope(to: endPoint, t: t)
case .addQuadCurveToPoint:
return startPoint.quadSlope(to: endPoint, controlPoint: controlPoints[0], t: t)
case .addCurveToPoint:
return startPoint.cubicSlope(to: endPoint, controlPoint1: controlPoints[0], controlPoint2: controlPoints[1], t: t)
default:
return 0
}
}
func tangentAngle(at t: CGFloat) -> CGFloat {
switch type {
case .addLineToPoint:
return startPoint.linearTangentAngle(to: endPoint, t: t)
case .addQuadCurveToPoint:
return startPoint.quadTangentAngle(to: endPoint, controlPoint: controlPoints[0], t: t)
case .addCurveToPoint:
return startPoint.cubicTangentAngle(to: endPoint, controlPoint1: controlPoints[0], controlPoint2: controlPoints[1], t: t)
default:
return 0
}
}
mutating func apply(transform t: CGAffineTransform) {
guard t.isTranslationOnly else { return }
startPoint = startPoint.applying(t)
endPoint = endPoint.applying(t)
controlPoints = controlPoints.map { $0.applying(t) }
}
}
fileprivate typealias CGPathApplierClosure = @convention(block) (CGPathElement) -> Void
fileprivate extension CGPath {
func apply(closure: @escaping CGPathApplierClosure) {
self.apply(info: unsafeBitCast(closure, to: UnsafeMutableRawPointer.self)) { (info, element) in
let block = unsafeBitCast(info, to: CGPathApplierClosure.self)
block(element.pointee)
}
}
}
fileprivate extension CGAffineTransform {
/// Whether or not this transform solely consists of a translation.
/// Note that the value of this property is `false`, when the receiver is `.identity`.
var isTranslationOnly: Bool {
for x in [a, b, c, d] {
if x != 0 {
return false
}
}
return tx != 0 || ty != 0
}
}
fileprivate extension CGPathElement {
var mx_points: [CGPoint] {
return Array(UnsafeBufferPointer(start: points, count: type.numberOfPoints))
}
}
fileprivate extension CGPathElementType {
var numberOfPoints: Int {
switch self {
case .moveToPoint, .addLineToPoint:
return 1
case .addQuadCurveToPoint:
return 2
case .addCurveToPoint:
return 3
case .closeSubpath:
return 0
@unknown default:
fatalError()
}
}
func calculateLength(from: CGPoint, to: CGPoint, controlPoints: [CGPoint]) -> CGFloat {
switch self {
case .moveToPoint:
return 0
case .addLineToPoint, .closeSubpath:
return from.linearLineLength(to: to)
case .addQuadCurveToPoint:
return from.quadCurveLength(to: to, controlPoint: controlPoints[0])
case .addCurveToPoint:
return from.cubicCurveLength(to: to, controlPoint1: controlPoints[0], controlPoint2: controlPoints[1])
@unknown default:
fatalError()
}
}
}
@MainActor fileprivate extension UIBezierPath {
func extractPathElements() -> [BezierPathElement] {
if let pathElements = self.mx_pathElements {
return pathElements
}
var pathElements: [BezierPathElement] = []
var currentPoint: CGPoint = .zero
cgPath.apply { element in
let type = element.type
let points = element.mx_points
var endPoint: CGPoint = .zero
var controlPoints: [CGPoint] = []
// Every UIBezierPath - no matter how complex - is created through a combination of these path elements.
switch type {
case .moveToPoint, .addLineToPoint:
endPoint = points[0]
case .addQuadCurveToPoint:
endPoint = points[1]
controlPoints.append(points[0])
case .addCurveToPoint:
endPoint = points[2]
controlPoints.append(contentsOf: points[0...1])
case .closeSubpath:
break
@unknown default:
fatalError()
}
if type != .closeSubpath && type != .moveToPoint {
let pathElement = BezierPathElement(type: type, startPoint: currentPoint, endPoint: endPoint, controlPoints: controlPoints)
pathElements.append(pathElement)
}
currentPoint = endPoint
}
self.mx_pathElements = pathElements
return pathElements
}
func findPathElement(at t: CGFloat, callback: (_ e: BezierPathElement, _ t: CGFloat) -> Void) {
// Clamp between 0 and 1.0
let t = min(max(0, t), 1)
calculateLengthRanges()
for element in extractPathElements() {
if let lengthRange = element.lengthRange, lengthRange.contains(t) {
let tInElement = (t - lengthRange.lowerBound) / (lengthRange.upperBound - lengthRange.lowerBound)
callback(element, tInElement)
break
}
}
}
func calculateLength() -> CGFloat {
if let length = self.mx_pathLength {
return length
}
let pathElements = extractPathElements()
let length = pathElements.reduce(0) { $0 + $1.length }
self.mx_pathLength = length
return length
}
func calculateLengthRanges() {
if mx_lengthRangesCalculated {
return
}
var pathElements = extractPathElements()
let totalPathLength = calculateLength()
var lengthRangeStart: CGFloat = 0
for idx in pathElements.indices {
let elementLength = pathElements[idx].length
var lengthRangeEnd = lengthRangeStart + elementLength / totalPathLength
// Sometimes, the last path element will end at 0.9999999999999xx.
// The math is correct, seems to be an issue with floating point calculations.
if idx == pathElements.count - 1 {
lengthRangeEnd = 1
}
pathElements[idx].lengthRange = lengthRangeStart...lengthRangeEnd
lengthRangeStart = lengthRangeEnd
}
mx_pathElements = pathElements
mx_lengthRangesCalculated = true
}
func calculatePointLookupTable() {
if mx_pointLookupTableCalculated {
return
}
var pathElements = extractPathElements()
// Step through all path elements and calculate points.
// The start and end point of the whole path are always included.
let step = perpendicularCalculationPrecision.rawValue
var offset: CGFloat = 0
for idx in pathElements.indices {
var element = pathElements[idx]
var points: [CGPoint] = []
while offset < element.length {
points.append(element.point(at: offset / element.length))
offset += step
}
if idx == pathElements.count - 1 && offset - step < element.length {
points.append(element.point(at: 1))
}
offset -= element.length
if points.isEmpty {
points.append(element.point(at: 0.5))
}
element.pointsLookupTable = points
pathElements[idx] = element
}
mx_pathElements = pathElements
mx_pointLookupTableCalculated = true
}
}
// -
//MARK: - Black magic
@MainActor fileprivate var pathElementsKey = "mx_pathElements_key"
@MainActor fileprivate var pathLengthKey = "mx_pathLength_key"
@MainActor fileprivate var pathElementsLengthRangesCalculated = "mx_pathElementsLengthRangesCalculated_key"
@MainActor fileprivate var pathElementsPointLookupTableCalculated = "mx_pathElementsPointLookupTableCalculated_key"
@MainActor fileprivate extension UIBezierPath {
var mx_pathElements: [BezierPathElement]? {
get {
return objc_getAssociatedObject(self, &pathElementsKey) as? [BezierPathElement]
}
set {
objc_setAssociatedObject(self, &pathElementsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var mx_pathLength: CGFloat? {
get {
return objc_getAssociatedObject(self, &pathLengthKey) as? CGFloat
}
set {
objc_setAssociatedObject(self, &pathLengthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var mx_lengthRangesCalculated: Bool {
get {
return objc_getAssociatedObject(self, &pathElementsLengthRangesCalculated) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &pathElementsLengthRangesCalculated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var mx_pointLookupTableCalculated: Bool {
get {
return objc_getAssociatedObject(self, &pathElementsPointLookupTableCalculated) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &pathElementsPointLookupTableCalculated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func invalidatePathCalculations() {
mx_pathElements = nil
mx_pathLength = nil
mx_lengthRangesCalculated = false
mx_pointLookupTableCalculated = false
}
}
// -
//MARK: - Swizzled selectors
// dispatch_once is no longer available in Swift -.-
@MainActor private var swizzled = false
fileprivate func swizzle(_ c: AnyClass, _ originalSelector: Selector, _ swizzledSelector: Selector) {
guard
let originalMethod = class_getInstanceMethod(c, originalSelector),
let swizzledMethod = class_getInstanceMethod(c, swizzledSelector)
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}
@MainActor fileprivate extension UIBezierPath {
@objc func mx_addLine(to point: CGPoint) {
mx_addLine(to: point)
invalidatePathCalculations()
}
@objc func mx_addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
mx_addCurve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
invalidatePathCalculations()
}
@objc func mx_addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) {
mx_addQuadCurve(to: endPoint, controlPoint: controlPoint)
invalidatePathCalculations()
}
@objc func mx_addArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) {
mx_addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
invalidatePathCalculations()
}
@objc func mx_close() {
mx_close()
invalidatePathCalculations()
}
@objc func mx_removeAllPoints() {
mx_removeAllPoints()
invalidatePathCalculations()
}
@objc func mx_append(path: UIBezierPath) {
mx_append(path: path)
invalidatePathCalculations()
}
@objc func mx_apply(t: CGAffineTransform) {
mx_apply(t: t)
if t.isTranslationOnly {
mx_pathElements?.indices.forEach { mx_pathElements?[$0].apply(transform: t) }
} else {
invalidatePathCalculations()
}
}
}
// -
//MARK: - Math helpers
fileprivate extension CGPoint {
func linearLineLength(to: CGPoint) -> CGFloat {
return sqrt(pow(to.x - x, 2) + pow(to.y - y, 2))
}
func linearBezierPoint(to: CGPoint, t: CGFloat) -> CGPoint {
let dx = to.x - x;
let dy = to.y - y;
let px = x + (t * dx);
let py = y + (t * dy);
return CGPoint(x: px, y: py)
}
func linearSlope(to: CGPoint, t: CGFloat) -> CGFloat {
let dx = to.x - x;
let dy = to.y - y;
return dy / dx
}
func linearTangentAngle(to: CGPoint, t: CGFloat) -> CGFloat {
let dx = to.x - x;
let dy = to.y - y;
return atan2(dx, dy)
}
func quadCurveLength(to: CGPoint, controlPoint c: CGPoint) -> CGFloat {
let iterations = lengthCalculationPrecision.rawValue;
var length: CGFloat = 0;
for idx in 0..<iterations {
let t = CGFloat(idx) * (1 / CGFloat(iterations))
let tt = t + (1 / CGFloat(iterations))
let p = self.quadBezierPoint(to: to, controlPoint: c, t: t)
let pp = self.quadBezierPoint(to: to, controlPoint: c, t: tt)
length += p.linearLineLength(to: pp)
}
return length
}
func quadBezierPoint(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGPoint {
let x = _quadBezier(t, self.x, controlPoint.x, to.x);
let y = _quadBezier(t, self.y, controlPoint.y, to.y);
return CGPoint(x: x, y: y);
}
func quadSlope(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGFloat {
let dx = _quadSlope(t, self.x, controlPoint.x, to.x);
let dy = _quadSlope(t, self.y, controlPoint.y, to.y);
return dy / dx
}
func quadTangentAngle(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGFloat {
let dx = _quadSlope(t, self.x, controlPoint.x, to.x);
let dy = _quadSlope(t, self.y, controlPoint.y, to.y);
return atan2(dx, dy)
}
func cubicCurveLength(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint) -> CGFloat {
let iterations = lengthCalculationPrecision.rawValue;
var length: CGFloat = 0;
for idx in 0..<iterations {
let t = CGFloat(idx) * (1 / CGFloat(iterations))
let tt = t + (1 / CGFloat(iterations))
let p = self.cubicBezierPoint(to: to, controlPoint1: c1, controlPoint2: c2, t: t)
let pp = self.cubicBezierPoint(to: to, controlPoint1: c1, controlPoint2: c2, t: tt)
length += p.linearLineLength(to: pp)
}
return length
}
func cubicBezierPoint(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGPoint {
let x = _cubicBezier(t, self.x, c1.x, c2.x, to.x);
let y = _cubicBezier(t, self.y, c1.y, c2.y, to.y);
return CGPoint(x: x, y: y);
}
func cubicSlope(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGFloat {
let dx = _cubicSlope(t, self.x, c1.x, c2.x, to.x);
let dy = _cubicSlope(t, self.y, c1.y, c2.y, to.y);
return dy / dx
}
func cubicTangentAngle(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGFloat {
let dx = _cubicSlope(t, self.x, c1.x, c2.x, to.x);
let dy = _cubicSlope(t, self.y, c1.y, c2.y, to.y);
return atan2(dx, dy)
}
}
/// See https://en.wikipedia.org/wiki/Bézier_curve
///
/// [Quad equation](https://wikimedia.org/api/rest_v1/media/math/render/svg/05aa724a6da0e00bcce53ec6510c8ae479aea5c3)
fileprivate func _quadBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t;
let _t² = _t * _t;
let t² = t * t;
return _t² * start +
2 * _t * t * c1 +
t² * end;
}
/// See https://en.wikipedia.org/wiki/Bézier_curve
///
/// [Quad equation dt](https://wikimedia.org/api/rest_v1/media/math/render/svg/698bc1454fe7abf7c01ff47ef9b26665446eb67c)
fileprivate func _quadSlope(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t
return 2 * _t * (c1 - start) +
2 * t * (end - c1)
}
/// See https://en.wikipedia.org/wiki/Bézier_curve
///
/// [Cubic equation](https://wikimedia.org/api/rest_v1/media/math/render/svg/504c44ca5c5f1da2b6cb1702ad9d1afa27cc1ee0)
fileprivate func _cubicBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t;
let _t² = _t * _t;
let _t³ = _t * _t * _t ;
let t² = t * t;
let t³ = t * t * t;
return _t³ * start +
3.0 * _t² * t * c1 +
3.0 * _t * t² * c2 +
t³ * end;
}
/// See https://en.wikipedia.org/wiki/Bézier_curve
///
/// [Cubic equation dt](https://wikimedia.org/api/rest_v1/media/math/render/svg/bda9197c2e77c17d90839b951cb0035d79c8d417)
fileprivate func _cubicSlope(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
let _t = 1 - t
let _t² = _t * _t
let t² = t * t
return 3 * _t² * (c1 - start) +
6 * _t * t * (c2 - c1) +
3 * t² * (end - c2)
}
// -