// // CurveVertex.swift // lottie-swift // // Created by Brandon Withrow on 1/11/19. // import CoreGraphics import Foundation /// A single vertex with an in and out tangent struct CurveVertex { // MARK: Lifecycle /// Initializes a curve point with absolute values init(_ inTangent: CGPoint, _ point: CGPoint, _ outTangent: CGPoint) { self.point = point self.inTangent = inTangent self.outTangent = outTangent } /// Initializes a curve point with relative values init(point: CGPoint, inTangentRelative: CGPoint, outTangentRelative: CGPoint) { self.point = point inTangent = point.add(inTangentRelative) outTangent = point.add(outTangentRelative) } /// Initializes a curve point with absolute values init(point: CGPoint, inTangent: CGPoint, outTangent: CGPoint) { self.point = point self.inTangent = inTangent self.outTangent = outTangent } // MARK: Internal let point: CGPoint let inTangent: CGPoint let outTangent: CGPoint var inTangentRelative: CGPoint { inTangent.subtract(point) } var outTangentRelative: CGPoint { outTangent.subtract(point) } func reversed() -> CurveVertex { CurveVertex(point: point, inTangent: outTangent, outTangent: inTangent) } func translated(_ translation: CGPoint) -> CurveVertex { CurveVertex(point: point + translation, inTangent: inTangent + translation, outTangent: outTangent + translation) } /// Trims a path defined by two Vertices at a specific position, from 0 to 1 /// /// The path can be visualized below. /// /// F is fromVertex. /// V is the vertex of the receiver. /// P is the position from 0-1. /// O is the outTangent of fromVertex. /// F====O=========P=======I====V /// /// After trimming the curve can be visualized below. /// /// S is the returned Start vertex. /// E is the returned End vertex. /// T is the trim point. /// TI and TO are the new tangents for the trimPoint /// NO and NI are the new tangents for the startPoint and endPoints /// S==NO=========TI==T==TO=======NI==E func splitCurve(toVertex: CurveVertex, position: CGFloat) -> (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) { /// If position is less than or equal to 0, trim at start. if position <= 0 { return ( start: CurveVertex(point: point, inTangentRelative: inTangentRelative, outTangentRelative: .zero), trimPoint: CurveVertex(point: point, inTangentRelative: .zero, outTangentRelative: outTangentRelative), end: toVertex) } /// If position is greater than or equal to 1, trim at end. if position >= 1 { return ( start: self, trimPoint: CurveVertex( point: toVertex.point, inTangentRelative: toVertex.inTangentRelative, outTangentRelative: .zero), end: CurveVertex( point: toVertex.point, inTangentRelative: .zero, outTangentRelative: toVertex.outTangentRelative)) } if outTangentRelative.isZero, toVertex.inTangentRelative.isZero { /// If both tangents are zero, then span to be trimmed is a straight line. let trimPoint = point.interpolate(to: toVertex.point, amount: position) return ( start: self, trimPoint: CurveVertex(point: trimPoint, inTangentRelative: .zero, outTangentRelative: .zero), end: toVertex) } /// Cutting by amount gives incorrect length.... /// One option is to cut by a stride until it gets close then edge it down. /// Measuring a percentage of the spans does not equal the same as measuring a percentage of length. /// This is where the historical trim path bugs come from. let a = point.interpolate(to: outTangent, amount: position) let b = outTangent.interpolate(to: toVertex.inTangent, amount: position) let c = toVertex.inTangent.interpolate(to: toVertex.point, amount: position) let d = a.interpolate(to: b, amount: position) let e = b.interpolate(to: c, amount: position) let f = d.interpolate(to: e, amount: position) return ( start: CurveVertex(point: point, inTangent: inTangent, outTangent: a), trimPoint: CurveVertex(point: f, inTangent: d, outTangent: e), end: CurveVertex(point: toVertex.point, inTangent: c, outTangent: toVertex.outTangent)) } /// Trims a curve of a known length to a specific length and returns the points. /// /// There is not a performant yet accurate way to cut a curve to a specific length. /// This calls splitCurve(toVertex: position:) to split the curve and then measures /// the length of the new curve. The function then iterates through the samples, /// adjusting the position of the cut for a more precise cut. /// Usually a single iteration is enough to get within 0.5 points of the desired /// length. /// /// This function should probably live in PathElement, since it deals with curve /// lengths. func trimCurve(toVertex: CurveVertex, atLength: CGFloat, curveLength: CGFloat, maxSamples: Int, accuracy: CGFloat = 1) -> (start: CurveVertex, trimPoint: CurveVertex, end: CurveVertex) { var currentPosition = atLength / curveLength var results = splitCurve(toVertex: toVertex, position: currentPosition) if maxSamples == 0 { return results } for _ in 1...maxSamples { let length = results.start.distanceTo(results.trimPoint) let lengthDiff = atLength - length /// Check if length is correct. if lengthDiff < accuracy { return results } let diffPosition = max(min((currentPosition / length) * lengthDiff, currentPosition * 0.5), currentPosition * -0.5) currentPosition = diffPosition + currentPosition results = splitCurve(toVertex: toVertex, position: currentPosition) } return results } /// The distance from the receiver to the provided vertex. /// /// For lines (zeroed tangents) the distance between the two points is measured. /// For curves the curve is iterated over by sample count and the points are measured. /// This is ~99% accurate at a sample count of 30 func distanceTo(_ toVertex: CurveVertex, sampleCount: Int = 25) -> CGFloat { if outTangentRelative.isZero, toVertex.inTangentRelative.isZero { /// Return a linear distance. return point.distanceTo(toVertex.point) } var distance: CGFloat = 0 var previousPoint = point for i in 0..