185 lines
6.6 KiB
Swift
185 lines
6.6 KiB
Swift
//
|
|
// 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..<sampleCount {
|
|
let pointOnCurve = splitCurve(toVertex: toVertex, position: CGFloat(i) / CGFloat(sampleCount)).trimPoint
|
|
distance = distance + previousPoint.distanceTo(pointOnCurve.point)
|
|
previousPoint = pointOnCurve.point
|
|
}
|
|
distance = distance + previousPoint.distanceTo(toVertex.point)
|
|
return distance
|
|
}
|
|
}
|