Created
August 20, 2023 00:09
-
-
Save arturdev/240bb3977cdb0408706679a57adaf310 to your computer and use it in GitHub Desktop.
UIBezierPath+Superpowers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CGPoint+Math.swift | |
// | |
// Created by Artur Mkrtchyan on 10/24/20. | |
// | |
import UIKit | |
public extension CGPoint { | |
func symmetryTo(point: CGPoint) -> CGPoint { | |
let x = self.x + 2 * (point.x - self.x) | |
let y = self.y + 2 * (point.y - self.y) | |
let result = CGPoint(x: x, y: y) | |
return result | |
} | |
func nearlyEqual(toPoint point: CGPoint, epsilon: CGFloat) -> Bool { | |
let difference = self - point | |
return abs(difference.x) < epsilon && abs(difference.y) < epsilon | |
} | |
var length: CGFloat { | |
return sqrt(squareLength) | |
} | |
var squareLength: CGFloat { | |
return x * x + y * y | |
} | |
var unit: CGPoint { | |
return self * (1.0 / length) | |
} | |
var phase: CGFloat { | |
return atan2(y, x) | |
} | |
func distance(fromPoint point: CGPoint) -> CGFloat { | |
return (self - point).length | |
} | |
func squareDistance(fromPoint point: CGPoint) -> CGFloat { | |
return (self - point).squareLength | |
} | |
func angle(fromPoint point: CGPoint) -> CGFloat { | |
return acos(cosOfAngle(fromPoint: point)) | |
} | |
func cosOfAngle(fromPoint point: CGPoint) -> CGFloat { | |
return fmin(fmax(self * point / sqrt(self.squareLength * point.squareLength), -1.0), 1.0) | |
} | |
} | |
extension CGPoint { | |
mutating func lerp(to point: CGPoint, t: Double) { | |
if self == point { | |
return | |
} | |
else if distance(fromPoint: point) < 0.001 { | |
self = point | |
return | |
} | |
let difference = point - self | |
self += (difference * CGFloat(t)) | |
} | |
} | |
public extension CGRect { | |
func center() -> CGPoint { | |
return CGPoint(x: midX, y: midY) | |
} | |
} | |
public prefix func + (value: CGPoint) -> CGPoint { | |
return value | |
} | |
public prefix func - (value: CGPoint) -> CGPoint { | |
return CGPoint(x: -value.x, y: -value.y) | |
} | |
public func + (left: CGPoint, right: CGPoint) -> CGPoint { | |
return CGPoint(x: left.x + right.x, y: left.y + right.y) | |
} | |
public func - (left: CGPoint, right: CGPoint) -> CGPoint { | |
return CGPoint(x: left.x - right.x, y: left.y - right.y) | |
} | |
public func * (left: CGPoint, right: CGPoint) -> CGFloat { | |
return left.x * right.x + left.y * right.y | |
} | |
public func * (left: CGPoint, right: CGFloat) -> CGPoint { | |
return CGPoint(x: left.x * right, y: left.y * right) | |
} | |
public func * (left: CGFloat, right: CGPoint) -> CGPoint { | |
return CGPoint(x: right.x * left, y: right.y * left) | |
} | |
public func / (left: CGPoint, right: CGFloat) -> CGPoint { | |
return CGPoint(x: left.x / right, y: left.y / right) | |
} | |
public func += ( left: inout CGPoint, right: CGPoint) { | |
left = left + right | |
} | |
public func -= ( left: inout CGPoint, right: CGPoint) { | |
left = left - right | |
} | |
public func *= ( left: inout CGPoint, right: CGFloat) { | |
left = left * right | |
} | |
public func /= ( left: inout CGPoint, right: CGFloat) { | |
left = left / right | |
} | |
public func + (left: CGPoint, right: CGSize) -> CGPoint { | |
return CGPoint(x: left.x + right.width, y: left.y + right.height) | |
} | |
public func - (left: CGPoint, right: CGSize) -> CGPoint { | |
return CGPoint(x: left.x - right.width, y: left.y - right.height) | |
} | |
public func / (left: CGSize, right: CGFloat) -> CGPoint { | |
return CGPoint(x: left.width / right, y: left.height / right) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// UIBezierPath+Superpowers.swift | |
// | |
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 = 1000 | |
} | |
fileprivate let lengthCalculationPrecision: LengthCalculationPrecision = .high | |
/// 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 = 1 | |
} | |
fileprivate let perpendicularCalculationPrecision: PerpendicularCalculationPrecision = .high | |
fileprivate enum TimeCalculationPrecision: CGFloat { | |
case low = 50 | |
case normal = 100 | |
case high = 1000 | |
} | |
fileprivate let timeCalculationPrecision: TimeCalculationPrecision = .high | |
// - | |
//MARK: - Public API | |
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 sp_* 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. | |
static func sp_prepare() { | |
if swizzled { return } | |
let swizzlingPairs: [(Selector, Selector)] = [ | |
(#selector(UIBezierPath.addLine), #selector(UIBezierPath.sp_addLine)), | |
(#selector(UIBezierPath.addCurve), #selector(UIBezierPath.sp_addCurve)), | |
(#selector(UIBezierPath.addQuadCurve ), #selector(UIBezierPath.sp_addQuadCurve)), | |
(#selector(UIBezierPath.addArc), #selector(UIBezierPath.sp_addArc)), | |
(#selector(UIBezierPath.close), #selector(UIBezierPath.sp_close)), | |
(#selector(UIBezierPath.removeAllPoints), #selector(UIBezierPath.sp_removeAllPoints)), | |
(#selector(UIBezierPath.append), #selector(UIBezierPath.sp_append)), | |
(#selector(UIBezierPath.apply), #selector(UIBezierPath.sp_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 `sp_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 sp_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 sp_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 sp_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 sp_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 sp_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 `sp_perpendicularPoint(for:)`. | |
/// | |
/// - Parameters: | |
/// - point: The point for which to calculate the distance. | |
func sp_perpendicularDistance(from point: CGPoint) -> CGFloat { | |
let closestPathPoint = sp_perpendicularPoint(for: point) | |
return closestPathPoint.linearLineLength(to: point) | |
} | |
func sp_time(at point: CGPoint) -> CGFloat { | |
var t: CGFloat = 0 | |
var closestPoint: (p: CGPoint, distance: CGFloat) = (.zero, .greatestFiniteMagnitude) | |
let step = 1.0 / timeCalculationPrecision.rawValue | |
for i in stride(from: CGFloat(0.0), to: CGFloat(1.0), by: step) { | |
let p = sp_point(atFractionOfLength: i) | |
let distance = p.linearLineLength(to: point) | |
if distance < closestPoint.distance { | |
closestPoint = (p, distance) | |
t = i | |
} | |
} | |
return t | |
} | |
func sp_splitInfo(atFractionOfLength t: CGFloat) -> BezierSplitInfo { | |
var result = BezierSplitInfo() | |
findPathElement(at: t) { (el, t, elementIndex) in | |
if el.type == .addLineToPoint { | |
let center = el.startPoint.linearBezierPoint(to: el.endPoint, t: t) | |
result = BezierSplitInfo(leftControlPoints: nil, | |
rightControlPoints: nil, | |
centerPoint: center, | |
elementIndex: elementIndex, | |
element: el) | |
return | |
} | |
let P0 = el.startPoint | |
let P3 = el.endPoint | |
let P1 = el.controlPoints.first ?? P0 | |
let P2 = el.controlPoints.last ?? P3 | |
let Q0 = (1-t)*P0 + t*P1 | |
let Q1 = (1-t)*P1 + t*P2 | |
let Q2 = (1-t)*P2 + t*P3 | |
let R0 = (1-t)*Q0 + t*Q1 | |
let R1 = (1-t)*Q1 + t*Q2 | |
let S0 = (1-t)*R0 + t*R1 | |
result = BezierSplitInfo(leftControlPoints: BezierSplitInfo.ControlPoint(controlPoint1: Q0, controlPoint2: R0), | |
rightControlPoints: BezierSplitInfo.ControlPoint(controlPoint1: R1, controlPoint2: Q2), | |
centerPoint: S0, | |
elementIndex: elementIndex, | |
element: el) | |
} | |
return result | |
} | |
} | |
// - | |
public struct BezierSplitInfo { | |
public struct ControlPoint { | |
public let controlPoint1: CGPoint | |
public let controlPoint2: CGPoint | |
} | |
public let leftControlPoints: ControlPoint? | |
public let rightControlPoints: ControlPoint? | |
public let centerPoint: CGPoint | |
public let elementIndex: Int | |
public let element: BezierPathElement | |
} | |
fileprivate extension BezierSplitInfo { | |
init() { | |
self.init(leftControlPoints: nil, | |
rightControlPoints: nil, | |
centerPoint: .zero, | |
elementIndex: 0, | |
element: BezierPathElement(type: .moveToPoint, startPoint: .zero, endPoint: .zero)) | |
} | |
} | |
//MARK: - Internal | |
public struct BezierPathElement { | |
public let type: CGPathElementType | |
public var startPoint: CGPoint | |
public var endPoint: CGPoint | |
public 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) } | |
} | |
} | |
public typealias CGPathApplierClosure = @convention(block) (CGPathElement) -> Void | |
public 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 | |
} | |
} | |
public extension CGPathElement { | |
var sp_points: [CGPoint] { | |
return Array(UnsafeBufferPointer(start: points, count: type.numberOfPoints)).map({ | |
CGPoint(x: $0.x.fixed, y: $0.y.fixed) | |
}) | |
} | |
} | |
private extension CGFloat { | |
var fixed: CGFloat { | |
if self == 0 { | |
return 0 | |
} | |
if !self.isNormal { | |
return 0 | |
} | |
return self | |
} | |
} | |
public 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: | |
return 0 | |
} | |
} | |
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: | |
return 0 | |
} | |
} | |
} | |
extension UIBezierPath { | |
func extractPathElements() -> [BezierPathElement] { | |
if let pathElements = self.sp_pathElements { | |
return pathElements | |
} | |
var pathElements: [BezierPathElement] = [] | |
var currentPoint: CGPoint = .zero | |
var index = 0 | |
var firstPoint = CGPoint.zero | |
cgPath.apply { element in | |
defer { | |
index += 1 | |
} | |
let type = element.type | |
let points = element.sp_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] | |
if index == 0 { | |
firstPoint = endPoint | |
} | |
case .addQuadCurveToPoint: | |
endPoint = points[1] | |
controlPoints.append(points[0]) | |
case .addCurveToPoint: | |
endPoint = points[2] | |
controlPoints.append(contentsOf: points[0...1]) | |
case .closeSubpath: | |
endPoint = firstPoint | |
@unknown default: | |
break | |
} | |
// if type != .closeSubpath && type != .moveToPoint { | |
let pathElement = BezierPathElement(type: type, startPoint: currentPoint, endPoint: endPoint, controlPoints: controlPoints) | |
pathElements.append(pathElement) | |
// } | |
currentPoint = endPoint | |
} | |
self.sp_pathElements = pathElements | |
return pathElements | |
} | |
func findPathElement(at t: CGFloat, callback: (_ e: BezierPathElement, _ t: CGFloat, _ elementIndex: Int) -> Void) { | |
// Clamp between 0 and 1.0 | |
let t = min(max(0, t), 1) | |
calculateLengthRanges() | |
for (index, element) in extractPathElements().enumerated() { | |
if let lengthRange = element.lengthRange, lengthRange.contains(t) { | |
let tInElement = (t - lengthRange.lowerBound) / (lengthRange.upperBound - lengthRange.lowerBound) | |
callback(element, tInElement, index) | |
break | |
} | |
} | |
} | |
func pathElement(at index: Int) -> BezierPathElement? { | |
let pathElements = extractPathElements() | |
guard pathElements.indices.contains(index) else { | |
return nil | |
} | |
return pathElements[index] | |
} | |
func calculateLength() -> CGFloat { | |
if let length = self.sp_pathLength { | |
return length | |
} | |
let pathElements = extractPathElements() | |
let length = pathElements.reduce(0) { $0 + $1.length } | |
self.sp_pathLength = length | |
return length | |
} | |
func calculateLengthRanges() { | |
if sp_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 | |
} | |
sp_pathElements = pathElements | |
sp_lengthRangesCalculated = true | |
} | |
func calculatePointLookupTable() { | |
if sp_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 | |
} | |
sp_pathElements = pathElements | |
sp_pointLookupTableCalculated = true | |
} | |
} | |
// - | |
//MARK: - Black magic | |
fileprivate var pathElementsKey = "sp_pathElements_key" | |
fileprivate var pathLengthKey = "sp_pathLength_key" | |
fileprivate var pathElementsLengthRangesCalculated = "sp_pathElementsLengthRangesCalculated_key" | |
fileprivate var pathElementsPointLookupTableCalculated = "sp_pathElementsPointLookupTableCalculated_key" | |
fileprivate extension UIBezierPath { | |
var sp_pathElements: [BezierPathElement]? { | |
get { | |
return objc_getAssociatedObject(self, &pathElementsKey) as? [BezierPathElement] | |
} | |
set { | |
objc_setAssociatedObject(self, &pathElementsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
} | |
var sp_pathLength: CGFloat? { | |
get { | |
return objc_getAssociatedObject(self, &pathLengthKey) as? CGFloat | |
} | |
set { | |
objc_setAssociatedObject(self, &pathLengthKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
} | |
var sp_lengthRangesCalculated: Bool { | |
get { | |
return objc_getAssociatedObject(self, &pathElementsLengthRangesCalculated) as? Bool ?? false | |
} | |
set { | |
objc_setAssociatedObject(self, &pathElementsLengthRangesCalculated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
} | |
var sp_pointLookupTableCalculated: Bool { | |
get { | |
return objc_getAssociatedObject(self, &pathElementsPointLookupTableCalculated) as? Bool ?? false | |
} | |
set { | |
objc_setAssociatedObject(self, &pathElementsPointLookupTableCalculated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
} | |
func invalidatePathCalculations() { | |
sp_pathElements = nil | |
sp_pathLength = nil | |
sp_lengthRangesCalculated = false | |
sp_pointLookupTableCalculated = false | |
} | |
} | |
// - | |
//MARK: - Swizzled selectors | |
// dispatch_once is no longer available in Swift -.- | |
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) | |
} | |
fileprivate extension UIBezierPath { | |
@objc func sp_addLine(to point: CGPoint) { | |
sp_addLine(to: point) | |
invalidatePathCalculations() | |
} | |
@objc func sp_addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) { | |
sp_addCurve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) | |
invalidatePathCalculations() | |
} | |
@objc func sp_addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint) { | |
sp_addQuadCurve(to: endPoint, controlPoint: controlPoint) | |
invalidatePathCalculations() | |
} | |
@objc func sp_addArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { | |
sp_addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) | |
invalidatePathCalculations() | |
} | |
@objc func sp_close() { | |
sp_close() | |
invalidatePathCalculations() | |
} | |
@objc func sp_removeAllPoints() { | |
sp_removeAllPoints() | |
invalidatePathCalculations() | |
} | |
@objc func sp_append(path: UIBezierPath) { | |
sp_append(path: path) | |
invalidatePathCalculations() | |
} | |
@objc func sp_apply(t: CGAffineTransform) { | |
sp_apply(t: t) | |
if t.isTranslationOnly { | |
sp_pathElements?.indices.forEach { sp_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) | |
} | |
// - | |
extension UIBezierPath { | |
public func split(atFractionLength fractionLength: CGFloat) -> (UIBezierPath, UIBezierPath) { | |
let splitInfo = sp_splitInfo(atFractionOfLength: fractionLength) | |
let splitIndex = splitInfo.elementIndex | |
let splitPath1 = UIBezierPath() | |
let splitPath2 = UIBezierPath() | |
let elementCount = extractPathElements().count | |
for i in 0..<elementCount { | |
guard let element = pathElement(at: i) else { continue } | |
if i == splitIndex { | |
appendElement(element, to: splitPath1, tillPoint: splitInfo.centerPoint, splitInfo: splitInfo) | |
splitPath2.move(to: splitInfo.centerPoint) | |
appendElement(element, to: splitPath2, splitInfo: splitInfo) | |
} else if i < splitIndex { | |
appendElement(element, to: splitPath1) | |
} else { | |
appendElement(element, to: splitPath2) | |
if element.type == .closeSubpath, i > 0, let startPoint = splitPath1.pathElement(at: 0)?.endPoint { | |
splitPath2.addLine(to: startPoint) | |
} | |
} | |
} | |
return (splitPath1, splitPath2) | |
} | |
public func split(segments: [CGFloat]) -> [UIBezierPath] { | |
guard segments.count > 1, segments.allSatisfy({ $0 > 0 && $0 < 1 }) else { return [self] } | |
var result = [UIBezierPath]() | |
var pathToSplit = self | |
var totalFractionSplit: CGFloat = 0 | |
for (index, segment) in segments.enumerated() { | |
let fraction = segment - totalFractionSplit | |
let paths = pathToSplit.split(atFractionLength: fraction) | |
result.append(paths.0) | |
if index == segments.count - 1 { | |
result.append(paths.1) | |
} | |
pathToSplit = paths.1 | |
totalFractionSplit = segment | |
} | |
return result | |
} | |
private func appendElement(_ element: BezierPathElement, to path: UIBezierPath, tillPoint: CGPoint? = nil, splitInfo: BezierSplitInfo? = nil) { | |
switch element.type { | |
case .moveToPoint: | |
let point = element.endPoint | |
path.move(to: point) | |
case .addLineToPoint: | |
path.addLine(to: tillPoint ?? element.endPoint) | |
case .addQuadCurveToPoint: | |
//Not fully supported.. perhaps need to adjust the splitInfo implementation to better support quadCurve. | |
if let tillPoint { | |
if let splitInfo { | |
path.addQuadCurve(to: splitInfo.centerPoint, | |
controlPoint: splitInfo.leftControlPoints?.controlPoint1 ?? splitInfo.leftControlPoints?.controlPoint2 ?? element.controlPoints[0]) | |
} else { | |
path.addQuadCurve(to: tillPoint, | |
controlPoint: element.controlPoints[0]) | |
} | |
} else { | |
if let splitInfo { | |
path.addQuadCurve(to: element.endPoint, | |
controlPoint: splitInfo.rightControlPoints?.controlPoint1 ?? splitInfo.rightControlPoints?.controlPoint2 ?? element.controlPoints[0]) | |
} else { | |
path.addQuadCurve(to: element.endPoint, controlPoint: element.controlPoints[0]) | |
} | |
} | |
case .addCurveToPoint: | |
if let tillPoint { | |
if let splitInfo { | |
path.addCurve(to: splitInfo.centerPoint, | |
controlPoint1: splitInfo.leftControlPoints?.controlPoint1 ?? element.controlPoints[0], | |
controlPoint2: splitInfo.leftControlPoints?.controlPoint2 ?? element.controlPoints[1]) | |
} else { | |
path.addCurve(to: tillPoint, | |
controlPoint1: element.controlPoints[0], | |
controlPoint2: element.controlPoints[1]) | |
} | |
} else { | |
if let splitInfo { | |
path.addCurve(to: element.endPoint, | |
controlPoint1: splitInfo.rightControlPoints?.controlPoint1 ?? element.controlPoints[0], | |
controlPoint2: splitInfo.rightControlPoints?.controlPoint2 ?? element.controlPoints[1]) | |
} else { | |
path.addCurve(to: element.endPoint, | |
controlPoint1: element.controlPoints[0], | |
controlPoint2: element.controlPoints[1]) | |
} | |
} | |
case .closeSubpath: | |
break | |
default: | |
break | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment