Skip to content

Instantly share code, notes, and snippets.

@arturdev
Created August 20, 2023 00:09
Show Gist options
  • Save arturdev/240bb3977cdb0408706679a57adaf310 to your computer and use it in GitHub Desktop.
Save arturdev/240bb3977cdb0408706679a57adaf310 to your computer and use it in GitHub Desktop.
UIBezierPath+Superpowers
//
// 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)
}
//
// 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