import Foundation import UIKit public typealias TouchEventIdentifier = String public typealias PointIdentifier = String public typealias EstimationUpdateIndex = NSNumber class Touch: Equatable, Hashable { public let touchIdentifier: UITouchIdentifier lazy public var pointIdentifier: PointIdentifier = { if let estimationUpdateIndex = estimationUpdateIndex { return touchIdentifier + ":\(estimationUpdateIndex)" } else { return touchIdentifier + ":" + identifier } }() public let identifier: String public let timestamp: TimeInterval public let type: UITouch.TouchType public let phase: UITouch.Phase public let force: CGFloat public let maximumPossibleForce: CGFloat public let altitudeAngle: CGFloat public let azimuthUnitVector: CGVector public let azimuth: CGFloat public let location: CGPoint public let estimationUpdateIndex: EstimationUpdateIndex? public let estimatedProperties: UITouch.Properties public let estimatedPropertiesExpectingUpdates: UITouch.Properties public let isUpdate: Bool public let isPrediction: Bool public let view: UIView? public var expectsLocationUpdate: Bool { return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.location) } public var expectsForceUpdate: Bool { return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.force) } public var expectsAzimuthUpdate: Bool { return estimatedPropertiesExpectingUpdates.contains(UITouch.Properties.azimuth) } public var expectsUpdate: Bool { return expectsForceUpdate || expectsAzimuthUpdate || expectsLocationUpdate } public convenience init( coalescedTouch: UITouch, touch: UITouch, in view: UIView, isUpdate: Bool, isPrediction: Bool, phase: UITouch.Phase? = nil, transform: CGAffineTransform = .identity ) { let originalLocation = coalescedTouch.location(in: view) let location = !transform.isIdentity ? originalLocation.applying(transform) : originalLocation self.init( identifier: UUID.init().uuidString, touchIdentifier: touch.identifer, timestamp: coalescedTouch.timestamp, type: coalescedTouch.type, phase: phase ?? coalescedTouch.phase, force: coalescedTouch.force, maximumPossibleForce: coalescedTouch.maximumPossibleForce, altitudeAngle: coalescedTouch.altitudeAngle, azimuthUnitVector: coalescedTouch.azimuthUnitVector(in: view), azimuth: coalescedTouch.azimuthAngle(in: view), location: location, estimationUpdateIndex: coalescedTouch.estimationUpdateIndex, estimatedProperties: coalescedTouch.estimatedProperties, estimatedPropertiesExpectingUpdates: coalescedTouch.estimatedPropertiesExpectingUpdates, isUpdate: isUpdate, isPrediction: isPrediction, in: view ) } public init( identifier: TouchEventIdentifier, touchIdentifier: UITouchIdentifier, timestamp: TimeInterval, type: UITouch.TouchType, phase: UITouch.Phase, force: CGFloat, maximumPossibleForce: CGFloat, altitudeAngle: CGFloat, azimuthUnitVector: CGVector, azimuth: CGFloat, location: CGPoint, estimationUpdateIndex: EstimationUpdateIndex?, estimatedProperties: UITouch.Properties, estimatedPropertiesExpectingUpdates: UITouch.Properties, isUpdate: Bool, isPrediction: Bool, in view: UIView? ) { self.identifier = identifier self.touchIdentifier = touchIdentifier self.timestamp = timestamp self.type = type self.phase = phase self.force = force self.maximumPossibleForce = maximumPossibleForce self.altitudeAngle = altitudeAngle self.azimuthUnitVector = azimuthUnitVector self.azimuth = azimuth self.location = location self.estimationUpdateIndex = estimationUpdateIndex self.estimatedProperties = estimatedProperties self.estimatedPropertiesExpectingUpdates = estimatedPropertiesExpectingUpdates self.isUpdate = isUpdate self.isPrediction = isPrediction self.view = view } public static func == (lhs: Touch, rhs: Touch) -> Bool { return lhs.identifier == rhs.identifier } public func hash(into hasher: inout Hasher) { hasher.combine(identifier) } } class DrawingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { var shouldBegin: (CGPoint) -> Bool = { _ in return true } var onTouches: ([Touch]) -> Void = { _ in } var transform: CGAffineTransform = .identity var usePredictedTouches = false private var currentTouches = Set() override init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.delegate = self self.cancelsTouchesInView = false self.delaysTouchesBegan = false self.delaysTouchesEnded = false self.allowedTouchTypes = [ NSNumber(value: UITouch.TouchType.direct.rawValue), NSNumber(value: UITouch.TouchType.stylus.rawValue) ] } func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { let location = gestureRecognizer.location(in: self.view) if self.shouldBegin(location) { return true } else { return false } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer is UIPinchGestureRecognizer { return true } return true } override func touchesEstimatedPropertiesUpdated(_ touches: Set) { self.process(touches: touches, with: nil, isUpdate: true) } override func touchesBegan(_ touches: Set, with event: UIEvent) { if let location = touches.first?.location(in: self.view), touches.count == 1 && self.shouldBegin(location) { super.touchesBegan(touches, with: event) self.process(touches: touches, with: event) self.state = .began } else { self.state = .cancelled } } override func touchesMoved(_ touches: Set, with event: UIEvent) { if touches.count > 1 { self.state = .cancelled } else { super.touchesMoved(touches, with: event) self.process(touches: touches, with: event) self.state = .changed } } override func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) self.process(touches: touches, with: event) self.state = .ended } override func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) self.state = .cancelled } func process(touches: Set, with event: UIEvent?, isUpdate: Bool = false) { guard let view = self.view else { return } var allTouches: [Touch] = [] for touch in touches { var coalesced = event?.coalescedTouches(for: touch) ?? [touch] if "".isEmpty || coalesced.isEmpty { coalesced = [touch] } for coalescedTouch in coalesced { allTouches.append( Touch( coalescedTouch: coalescedTouch, touch: touch, in: view, isUpdate: isUpdate, isPrediction: false, transform: self.transform ) ) } if self.usePredictedTouches { let predicted = event?.predictedTouches(for: touch) ?? [] for predictedTouch in predicted { allTouches.append( Touch( coalescedTouch: predictedTouch, touch: touch, in: view, isUpdate: isUpdate, isPrediction: true, transform: self.transform ) ) } } } self.onTouches(allTouches) } } class DrawingGesturePipeline { enum Mode { case location case smoothCurve case polyline } enum DrawingGestureState { case began case changed case ended case cancelled } enum DrawingResult { case location(Polyline.Point) case smoothCurve(BezierPath) case polyline(Polyline) } private var pendingTouches: [Touch] = [] var onDrawing: (DrawingGestureState, DrawingResult) -> Void = { _, _ in } var gestureRecognizer: DrawingGestureRecognizer? var transform: CGAffineTransform = .identity { didSet { self.gestureRecognizer?.transform = transform } } var mode: Mode = .location { didSet { if [.location, .polyline].contains(self.mode) { self.gestureRecognizer?.usePredictedTouches = false } else { self.gestureRecognizer?.usePredictedTouches = true } } } init(view: DrawingView) { let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:))) gestureRecognizer.onTouches = { [weak self] touches in self?.pendingTouches.append(contentsOf: touches) } self.gestureRecognizer = gestureRecognizer view.addGestureRecognizer(gestureRecognizer) } @objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) { let state: DrawingGestureState switch gestureRecognizer.state { case .began: state = .began case .changed: state = .changed case .ended: state = .ended case .cancelled: state = .cancelled case .failed: state = .cancelled case .possible: state = .cancelled @unknown default: state = .cancelled } let touchDeltas = self.processTouchEvents(self.pendingTouches) let polylineDeltas = self.processTouchPaths(inputDeltas: touchDeltas) let simplifiedPolylineDeltas = self.simplifyPolylines(inputDeltas: polylineDeltas) switch self.mode { case .location: if let touchPath = self.touchPaths.last, let point = touchPath.points.last { self.onDrawing(state, .location(Polyline.Point(touchPoint: point))) } case .smoothCurve: if let path = self.processPolylines(inputDeltas: simplifiedPolylineDeltas) { self.onDrawing(state, .smoothCurve(path)) } case .polyline: if let polyline = self.simplifiedPolylines.last { self.onDrawing(state, .polyline(polyline)) } } self.pendingTouches.removeAll() } enum TouchPathDelta: Equatable { case addedTouchPath(index: Int) case updatedTouchPath(index: Int, updatedIndexes: MinMaxIndex) case completedTouchPath(index: Int) } private var touchPaths: [TouchPath] = [] private var touchToIndex: [UITouchIdentifier: Int] = [:] private func processTouchEvents(_ touches: [Touch]) -> [TouchPathDelta] { var deltas: [TouchPathDelta] = [] var processedTouchIdentifiers: [UITouchIdentifier] = [] let updatedEventsPerTouch = touches.reduce(into: [String: [Touch]](), { (result, event) in if result[event.touchIdentifier] != nil { result[event.touchIdentifier]?.append(event) } else { result[event.touchIdentifier] = [event] } }) for touchToProcess in touches { let touchIdentifier = touchToProcess.touchIdentifier guard !processedTouchIdentifiers.contains(touchIdentifier), let events = updatedEventsPerTouch[touchIdentifier] else { continue } processedTouchIdentifiers.append(touchIdentifier) if let index = self.touchToIndex[touchIdentifier] { let path = self.touchPaths[index] let updatedIndexes = path.add(touchEvents: events) deltas.append(.updatedTouchPath(index: index, updatedIndexes: updatedIndexes)) if path.isComplete { deltas.append(.completedTouchPath(index: index)) } } else if let touchIdentifier = events.first?.touchIdentifier, let path = TouchPath(touchEvents: events) { let index = self.touchPaths.count self.touchToIndex[touchIdentifier] = index self.touchPaths.append(path) deltas.append(.addedTouchPath(index: index)) if path.isComplete { deltas.append(.completedTouchPath(index: index)) } } } return deltas } enum PolylineDelta: Equatable { case addedPolyline(index: Int) case updatedPolyline(index: Int, updatedIndexes: MinMaxIndex) case completedPolyline(index: Int) } private var indexToIndex: [Int: Int] = [:] private var polylines: [Polyline] = [] func processTouchPaths(inputDeltas: [TouchPathDelta]) -> [PolylineDelta] { var deltas: [PolylineDelta] = [] for delta in inputDeltas { switch delta { case .addedTouchPath(let pathIndex): let line = self.touchPaths[pathIndex] let smoothStroke = Polyline(touchPath: line) let index = polylines.count indexToIndex[pathIndex] = index polylines.append(smoothStroke) deltas.append(.addedPolyline(index: index)) case .updatedTouchPath(let pathIndex, let indexSet): let line = self.touchPaths[pathIndex] if let index = indexToIndex[pathIndex] { let updates = polylines[index].update(with: line, indexSet: indexSet) deltas.append(.updatedPolyline(index: index, updatedIndexes: updates)) } case .completedTouchPath(let pointCollectionIndex): if let index = indexToIndex[pointCollectionIndex] { deltas.append(.completedPolyline(index: index)) } } } return deltas } var simplifiedPolylines: [Polyline] = [] func simplifyPolylines(inputDeltas: [PolylineDelta]) -> [PolylineDelta] { var outDeltas: [PolylineDelta] = [] for delta in inputDeltas { switch delta { case .addedPolyline(let strokeIndex): assert(strokeIndex == self.simplifiedPolylines.count) let line = self.polylines[strokeIndex] self.simplifiedPolylines.append(line) let indexes = MinMaxIndex(0.. CGFloat { guard abs(windowLoc) <= windowSize else { fatalError("Invalid coefficient") } if let cached = cache { return cached[abs(windowLoc)] } var coeffs: [CGFloat] = [] for windowLoc in 0...windowSize { coeffs.append(Self.calcWeight(index, windowLoc, windowSize, order, derivative)) } cache = coeffs return coeffs[abs(windowLoc)] } // MARK: - Coefficients /// calculates the generalised factorial (a)(a-1)...(a-b+1) private static func genFact(_ a: Int, _ b: Int) -> CGFloat { var gf: CGFloat = 1.0 for jj in (a - b + 1) ..< (a + 1) { gf *= CGFloat(jj) } return gf } /// Calculates the Gram Polynomial ( s = 0 ), or its s'th /// derivative evaluated at i, order k, over 2m + 1 points private static func gramPoly(_ index: Int, _ window: Int, _ order: Int, _ derivative: Int) -> CGFloat { var gp_val: CGFloat if order > 0 { let g1 = gramPoly(index, window, order - 1, derivative) let g2 = gramPoly(index, window, order - 1, derivative - 1) let g3 = gramPoly(index, window, order - 2, derivative) let i: CGFloat = CGFloat(index) let m: CGFloat = CGFloat(window) let k: CGFloat = CGFloat(order) let s: CGFloat = CGFloat(derivative) gp_val = (4.0 * k - 2.0) / (k * (2.0 * m - k + 1.0)) * (i * g1 + s * g2) - ((k - 1.0) * (2.0 * m + k)) / (k * (2.0 * m - k + 1.0)) * g3 } else if order == 0 && derivative == 0 { gp_val = 1.0 } else { gp_val = 0.0 } return gp_val } /// calculates the weight of the i'th data point for the t'th Least-square /// point of the s'th derivative, over 2m + 1 points, order n private static func calcWeight(_ index: Int, _ windowLoc: Int, _ windowSize: Int, _ order: Int, _ derivative: Int) -> CGFloat { var sum: CGFloat = 0.0 for k in 0 ..< order + 1 { sum += CGFloat(2 * k + 1) * CGFloat(genFact(2 * windowSize, k) / genFact(2 * windowSize + k + 1, k + 1)) * gramPoly(index, windowSize, k, 0) * gramPoly(windowLoc, windowSize, k, derivative) } return sum } } private var window: Int = 2 private var strength: CGFloat = 1 var deriv: Int = 0 var order: Int = 3 var coeffs: [Coeffs] = [] private func smoothStroke(stroke: inout Polyline, at indexes: MinMaxIndex?, input: Polyline) -> MinMaxIndex { if input.points.count > stroke.points.count { stroke.points.append(contentsOf: input.points[stroke.points.count...]) } else if input.points.count < stroke.points.count { stroke.points.removeSubrange(input.points.count...) } let outIndexes = { () -> MinMaxIndex in if let indexes = indexes, let minIndex = indexes.first, let maxIndex = indexes.last { var outIndexes = MinMaxIndex() let start = max(0, minIndex - window) let end = min(stroke.points.count - 1, maxIndex + window) outIndexes.insert(integersIn: start...end) return outIndexes } return MinMaxIndex(stroke.points.indices) }() for pIndex in outIndexes { let minWin = min(min(window, pIndex), stroke.points.count - 1 - pIndex) // copy over the point in question so that not only our location will be smoothed below, // but also the azimuth/altitude/etc will be the same stroke.points[pIndex] = input.points[pIndex] while coeffs.count < minWin + 1 { coeffs.append(Coeffs(index: 0, windowSize: coeffs.count)) } if minWin > 1 { var outPoint = CGPoint.zero for windowPos in -minWin ... minWin { let wght = coeffs[minWin].weight(windowPos, order, deriv) outPoint.x += wght * input.points[pIndex + windowPos].location.x outPoint.y += wght * input.points[pIndex + windowPos].location.y } let origPoint = stroke.points[pIndex].location stroke.points[pIndex].location = origPoint * CGFloat(1 - strength) + outPoint * strength } } return outIndexes } private var builders: [BezierBuilder] = [] private var bezierIndexToIndex: [Int: Int] = [:] func processPolylines(inputDeltas: [PolylineDelta]) -> BezierPath? { for delta in inputDeltas { switch delta { case .addedPolyline(let lineIndex): assert(bezierIndexToIndex[lineIndex] == nil, "Cannot add existing line") let line = self.simplifiedPolylines[lineIndex] let builder = BezierBuilder(smoother: Smoother()) builder.update(with: line, at: MinMaxIndex(0 ..< line.points.count)) let builderIndex = builders.count bezierIndexToIndex[lineIndex] = builderIndex builders.append(builder) case .updatedPolyline(let lineIndex, let updatedIndexes): let line = self.simplifiedPolylines[lineIndex] guard let builderIndex = bezierIndexToIndex[lineIndex] else { continue } let builder = builders[builderIndex] let _ = builder.update(with: line, at: updatedIndexes) case .completedPolyline: break } } return builders.last?.path } } private var TOUCH_IDENTIFIER: UInt8 = 0 public typealias UITouchIdentifier = String extension UITouch { var identifer: UITouchIdentifier { if let identifier = objc_getAssociatedObject(self, &TOUCH_IDENTIFIER) as? String { return identifier } else { let identifier = UUID().uuidString objc_setAssociatedObject(self, &TOUCH_IDENTIFIER, identifier, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) return identifier } } } public struct MinMaxIndex: Sequence, Equatable { private var start: Int private var end: Int public static let null = MinMaxIndex() public init() { start = .max end = .max } public init(_ indexes: ClosedRange) { guard let first = indexes.first, let last = indexes.last else { self = .null; return } start = first end = last } public init(_ indexes: Range) { guard let first = indexes.first, let last = indexes.last else { self = .null; return } start = first end = last } public init(_ integers: [Int]) { guard !integers.isEmpty else { self = .null; return } start = integers.min()! end = integers.max()! } public init(_ integer: Int) { start = integer end = integer } public init(_ indexSet: IndexSet) { guard !indexSet.isEmpty else { self = .null; return } start = indexSet.min()! end = indexSet.max()! } public var count: Int { guard self != .null else { return 0 } return end - start + 1 } public var first: Int? { guard self != .null else { return nil } return start } public var last: Int? { guard self != .null else { return nil } return end } @inlinable @inline(__always) public func asIndexSet() -> IndexSet { guard let first = first, let last = last else { return IndexSet() } return IndexSet(integersIn: first...last) } public mutating func insert(_ index: Int) { if self == Self.null { start = index end = index } else { start = Swift.min(start, index) end = Swift.max(end, index) } } @inlinable @inline(__always) public mutating func insert(integersIn indexes: ClosedRange) { guard let first = indexes.first, let last = indexes.last else { return } insert(first) insert(last) } public mutating func remove(_ index: Int) { if start == index { start += 1 } if end == index { end -= 1 } if start > end { start = .max end = .max } } @inlinable @inline(__always) public func contains(_ index: Int) -> Bool { guard let first = first, let last = last else { return false } return index >= first && index <= last } public func makeIterator() -> Iterator { return Iterator(min: start, max: end) } public struct Iterator: IteratorProtocol { public typealias Element = Int var min: Int let max: Int init(min: Int, max: Int) { self.min = min self.max = max } public mutating func next() -> Int? { if min == max, min == .max { return nil } else if min > max { return nil } else { let ret = min min += 1 return ret } } } } extension Array { @inline(__always) @inlinable mutating func pop() -> Element? { guard !isEmpty else { return nil } return removeLast() } @inline(__always) @inlinable mutating func dequeue() -> Element? { guard !isEmpty else { return nil } return removeFirst() } } class TouchPath: Hashable { class Point: Hashable { public private(set) var events: [Touch] public var event: Touch { return events.last! } public var expectsUpdate: Bool { return self.event.isPrediction || self.event.expectsUpdate } public var isPrediction: Bool { return events.allSatisfy({ $0.isPrediction }) } public init(event: Touch) { events = [event] events.reserveCapacity(10) } func add(event: Touch) { events.append(event) } static func == (lhs: Point, rhs: Point) -> Bool { return lhs.expectsUpdate == rhs.expectsUpdate && lhs.events == rhs.events } func hash(into hasher: inout Hasher) { hasher.combine(events) } } public private(set) var touchIdentifier: String private var _points: [Point]? public var points: [Point] { if let _points = _points { return _points } let ret = confirmedPoints + predictedPoints _points = ret return ret } public var bounds: CGRect { return points.reduce(.null) { partialResult, point -> CGRect in return CGRect(x: min(partialResult.origin.x, point.event.location.x), y: min(partialResult.origin.y, point.event.location.y), width: max(partialResult.origin.x, point.event.location.x), height: max(partialResult.origin.y, point.event.location.y)) } } public var isComplete: Bool { let phase = confirmedPoints.last?.event.phase return (phase == .ended || phase == .cancelled) && predictedPoints.isEmpty } private var confirmedPoints: [Point] { didSet { _points = nil } } private var predictedPoints: [Point] { didSet { _points = nil } } private var consumable: [Point] private var expectingUpdate: Set private var eventToPoint: [PointIdentifier: Point] private var eventToIndex: [PointIdentifier: Int] init?(touchEvents: [Touch]) { guard !touchEvents.isEmpty else { return nil } self.confirmedPoints = [] self.predictedPoints = [] self.consumable = [] self.eventToPoint = [:] self.eventToIndex = [:] self.expectingUpdate = Set() self.touchIdentifier = touchEvents.first!.touchIdentifier add(touchEvents: touchEvents) } @discardableResult func add(touchEvents: [Touch]) -> MinMaxIndex { var indexSet = MinMaxIndex() let startingCount = points.count for event in touchEvents { assert(touchIdentifier == event.touchIdentifier) if event.isPrediction { if let prediction = consumable.dequeue() { prediction.add(event: event) predictedPoints.append(prediction) let index = confirmedPoints.count + predictedPoints.count - 1 indexSet.insert(index) } else { let prediction = Point(event: event) predictedPoints.append(prediction) let index = confirmedPoints.count + predictedPoints.count - 1 indexSet.insert(index) } } else if eventToPoint[event.pointIdentifier] != nil, let index = eventToIndex[event.pointIdentifier] { eventToPoint[event.pointIdentifier]?.add(event: event) if !event.expectsUpdate { self.expectingUpdate.remove(event.pointIdentifier) } indexSet.insert(index) if event.phase == .ended || event.phase == .cancelled { consumable.append(contentsOf: predictedPoints) predictedPoints.removeAll() } } else if isComplete { } else { consumable.append(contentsOf: predictedPoints) predictedPoints.removeAll() if let point = consumable.dequeue() ?? predictedPoints.dequeue() { if event.expectsUpdate { self.expectingUpdate.insert(event.pointIdentifier) } point.add(event: event) eventToPoint[event.pointIdentifier] = point confirmedPoints.append(point) let index = confirmedPoints.count - 1 eventToIndex[event.pointIdentifier] = index indexSet.insert(index) } else { if event.expectsUpdate { self.expectingUpdate.insert(event.pointIdentifier) } let point = Point(event: event) eventToPoint[event.pointIdentifier] = point confirmedPoints.append(point) let index = confirmedPoints.count - 1 eventToIndex[event.pointIdentifier] = index indexSet.insert(index) } } } for index in consumable.indices { let possiblyRemovedIndex = confirmedPoints.count + predictedPoints.count + index if possiblyRemovedIndex < startingCount { indexSet.insert(possiblyRemovedIndex) } else { indexSet.remove(possiblyRemovedIndex) } } return indexSet } public static func == (lhs: TouchPath, rhs: TouchPath) -> Bool { return lhs.touchIdentifier == rhs.touchIdentifier && lhs.points == rhs.points } public func hash(into hasher: inout Hasher) { hasher.combine(touchIdentifier) } } struct Polyline { struct Point: Equatable { var location: CGPoint var force: CGFloat var altitudeAngle: CGFloat var azimuth: CGFloat var velocity: CGFloat = 0.0 let touchPoint: TouchPath.Point var event: Touch { return touchPoint.event } var expectsUpdate: Bool { return touchPoint.expectsUpdate } var x: CGFloat { return self.location.x } var y: CGFloat { return self.location.y } init( location: CGPoint, force: CGFloat, altitudeAngle: CGFloat, azimuth: CGFloat, velocity: CGFloat, touchPoint: TouchPath.Point ) { self.location = location self.force = force self.altitudeAngle = altitudeAngle self.azimuth = azimuth self.touchPoint = touchPoint } init(touchPoint: TouchPath.Point) { self.location = touchPoint.event.location self.force = touchPoint.event.force self.altitudeAngle = touchPoint.event.altitudeAngle self.azimuth = touchPoint.event.azimuth self.touchPoint = touchPoint } func offsetBy(_ point: CGPoint) -> Polyline.Point { return Point( location: self.location.offsetBy(dx: point.x, dy: point.y), force: self.force, altitudeAngle: self.altitudeAngle, azimuth: self.azimuth, velocity: self.velocity, touchPoint: self.touchPoint ) } func withLocation(_ point: CGPoint) -> Polyline.Point { return Point( location: point, force: self.force, altitudeAngle: self.altitudeAngle, azimuth: self.azimuth, velocity: self.velocity, touchPoint: self.touchPoint ) } } public internal(set) var isComplete: Bool public let touchIdentifier: String public var points: [Point] public var bounds: CGRect { return self.points.reduce(.null) { partialResult, point -> CGRect in return CGRect(x: min(partialResult.origin.x, point.x), y: min(partialResult.origin.y, point.y), width: max(partialResult.size.width, point.x), height: max(partialResult.size.height, point.y)) } } init(touchPath: TouchPath) { isComplete = touchPath.isComplete touchIdentifier = touchPath.touchIdentifier var points: [Point] = [] var previousTouchPoint: TouchPath.Point? for touchPoint in touchPath.points { var point = Point(touchPoint: touchPoint) if let previousTouchPoint = previousTouchPoint { let distance = touchPoint.event.location.distance(to: previousTouchPoint.event.location) let elapsed = max(0.0, touchPoint.event.timestamp - previousTouchPoint.event.timestamp) let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 point.velocity = velocity } points.append(point) previousTouchPoint = touchPoint } self.points = points } init(points: [Point]) { self.isComplete = true self.touchIdentifier = points.first?.event.touchIdentifier ?? "" self.points = points } mutating func update(with path: TouchPath, indexSet: MinMaxIndex) -> MinMaxIndex { var indexesToRemove = MinMaxIndex() for index in indexSet { if index < path.points.count { if index < points.count { points[index].location = path.points[index].event.location points[index].force = path.points[index].event.force points[index].azimuth = path.points[index].event.azimuth points[index].altitudeAngle = path.points[index].event.altitudeAngle if index > 0 { let previousTouchPoint = points[index - 1] let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location) let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp) let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 points[index].velocity = velocity } } else if index == points.count { points.append(Point(touchPoint: path.points[index])) if index > 0 { let previousTouchPoint = points[index - 1] let distance = path.points[index].event.location.distance(to: previousTouchPoint.event.location) let elapsed = max(0.0, path.points[index].event.timestamp - previousTouchPoint.event.timestamp) let velocity = elapsed > 0.0 ? distance / elapsed : 0.0 points[index].velocity = velocity } } else { assertionFailure("Attempting to modify a point that doesn't yet exist. maybe an update is out of order?") } } else { indexesToRemove.insert(index) } } for index in indexesToRemove.reversed() { guard index < points.count else { print("Error: unknown polyline index \(index)") continue } points.remove(at: index) } isComplete = path.isComplete return indexSet } } private class BezierBuilder { private var elements: [BezierPath.Element] = [] private let smoother: Smoother private(set) var path = BezierPath() init(smoother: Smoother) { self.smoother = smoother } @discardableResult func update(with line: Polyline, at lineIndexes: MinMaxIndex) -> MinMaxIndex { let updatedPathIndexes = smoother.elementIndexes(for: line, at: lineIndexes, with: path) guard let min = updatedPathIndexes.first, let max = updatedPathIndexes.last else { return updatedPathIndexes } let updatedPath: BezierPath if min - 1 < path.elementCount, min - 1 >= 0 { updatedPath = path.trimming(to: min - 1) } else { updatedPath = BezierPath() } for elementIndex in min ... max { assert(elementIndex <= elements.count, "Invalid element index") if updatedPathIndexes.contains(elementIndex) { if elementIndex > smoother.maxIndex(for: line) { } else { let element = smoother.element(for: line, at: elementIndex) if elementIndex == elements.count { elements.append(element) } else { elements[elementIndex] = element } updatedPath.append(element) } } else { // use the existing element let element = elements[elementIndex] updatedPath.append(element) } } for elementIndex in max + 1 ..< elements.count { let element = elements[elementIndex] updatedPath.append(element) } path = updatedPath return updatedPathIndexes } } private class Smoother { let smoothFactor: CGFloat init(smoothFactor: CGFloat = 0.7) { self.smoothFactor = smoothFactor } func element(for line: Polyline, at elementIndex: Int) -> BezierPath.Element { assert(elementIndex >= 0 && elementIndex <= maxIndex(for: line)) if elementIndex == 0 { return BezierPath.Element(type: .moveTo, startPoint: line.points[0], endPoint: line.points[0], controlPoints: []) } if elementIndex == 1 { return Self.newCurve(smoothFactor: smoothFactor, startPoint: line.points[0], p1: line.points[0].location, p2: line.points[1], p3: line.points[2].location) } if line.isComplete && elementIndex == maxIndex(for: line) { return Self.newCurve(smoothFactor: smoothFactor, startPoint: line.points[elementIndex - 1], p0: line.points[elementIndex - 2].location, p1: line.points[elementIndex - 1].location, p2: line.points[elementIndex], p3: line.points[elementIndex].location) } return Self.newCurve(smoothFactor: smoothFactor, startPoint: line.points[elementIndex - 1], p0: line.points[elementIndex - 2].location, p1: line.points[elementIndex - 1].location, p2: line.points[elementIndex], p3: line.points[elementIndex + 1].location) } func maxIndex(for line: Polyline) -> Int { let lastIndex = line.points.count - 1 return Swift.max(0, lastIndex - 1) + (line.points.count > 2 && line.isComplete ? 1 : 0) } func elementIndexes(for line: Polyline, at lineIndexes: MinMaxIndex, with bezier: BezierPath) -> MinMaxIndex { var curveIndexes = MinMaxIndex() for index in lineIndexes { elementIndexes(for: line, at: index, with: bezier, into: &curveIndexes) } return curveIndexes } func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath) -> MinMaxIndex { var ret = MinMaxIndex() elementIndexes(for: line, at: lineIndex, with: bezier, into: &ret) return ret } // Below are the examples of input indexes, and which smoothed elements that point index affects // 0 => 2, 1, 0 // 1 => 3, 2, 1, 0 // 2 => 4, 3, 2, 1 // 3 => 5, 4, 3, 2 // 4 => 6, 5, 4, 3 // 5 => 7, 6, 5, 4 // 6 => 8, 7, 6, 5 // 7 => 9, 8, 7, 6 private func elementIndexes(for line: Polyline, at lineIndex: Int, with bezier: BezierPath, into indexes: inout MinMaxIndex) { guard lineIndex >= 0 else { return } let max = maxIndex(for: line) if lineIndex > 1, (lineIndex - 1 <= max) || (lineIndex - 1 < bezier.elementCount) { indexes.insert(lineIndex - 1) } if (lineIndex <= max) || (lineIndex < bezier.elementCount) { indexes.insert(lineIndex) } if (lineIndex + 1 <= max) || (lineIndex + 1 < bezier.elementCount) { indexes.insert(lineIndex + 1) } if (lineIndex + 2 <= max) || (lineIndex + 2 < bezier.elementCount) { indexes.insert(lineIndex + 2) } } // MARK: - Helper private static func newCurve( smoothFactor: CGFloat, startPoint: Polyline.Point, p0: CGPoint? = nil, p1: CGPoint, p2: Polyline.Point, p3: CGPoint ) -> BezierPath.Element { let p0 = p0 ?? p1 let c1 = CGPoint(x: (p0.x + p1.x) / 2.0, y: (p0.y + p1.y) / 2.0) let c2 = CGPoint(x: (p1.x + p2.x) / 2.0, y: (p1.y + p2.y) / 2.0) let c3 = CGPoint(x: (p2.x + p3.x) / 2.0, y: (p2.y + p3.y) / 2.0) let len1 = sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)) let len2 = sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)) let len3 = sqrt((p3.x - p2.x) * (p3.x - p2.x) + (p3.y - p2.y) * (p3.y - p2.y)) let k1 = len1 / (len1 + len2) let k2 = len2 / (len2 + len3) let m1 = CGPoint(x: c1.x + (c2.x - c1.x) * k1, y: c1.y + (c2.y - c1.y) * k1) let m2 = CGPoint(x: c2.x + (c3.x - c2.x) * k2, y: c2.y + (c3.y - c2.y) * k2) // Resulting control points. Here smooth_value is mentioned // above coefficient K whose value should be in range [0...1]. var ctrl1 = CGPoint(x: m1.x + (c2.x - m1.x) * smoothFactor + p1.x - m1.x, y: m1.y + (c2.y - m1.y) * smoothFactor + p1.y - m1.y) var ctrl2 = CGPoint(x: m2.x + (c2.x - m2.x) * smoothFactor + p2.x - m2.x, y: m2.y + (c2.y - m2.y) * smoothFactor + p2.y - m2.y) if ctrl1.x.isNaN || ctrl1.y.isNaN { ctrl1 = p1 } if ctrl2.x.isNaN || ctrl2.y.isNaN { ctrl2 = p2.location } return BezierPath.Element(type: .cubicCurve, startPoint: startPoint, endPoint: p2, controlPoints: [ctrl1, ctrl2]) } }