unioil-loyalty-rn-app/ios/Pods/lottie-ios/Sources/Private/Utility/Primitives/BezierPath.swift

488 lines
16 KiB
Swift

//
// Shape.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import CoreGraphics
import Foundation
// MARK: - BezierPath
/// A container that holds instructions for creating a single, unbroken Bezier Path.
struct BezierPath {
// MARK: Lifecycle
/// Initializes a new Bezier Path.
init(startPoint: CurveVertex) {
elements = [PathElement(vertex: startPoint)]
length = 0
closed = false
}
init() {
elements = []
length = 0
closed = false
}
// MARK: Internal
/// The elements of the path
private(set) var elements: [PathElement]
/// If the path is closed or not.
private(set) var closed: Bool
/// The total length of the path.
private(set) var length: CGFloat
mutating func moveToStartPoint(_ vertex: CurveVertex) {
elements = [PathElement(vertex: vertex)]
length = 0
}
mutating func addVertex(_ vertex: CurveVertex) {
guard let previous = elements.last else {
addElement(PathElement(vertex: vertex))
return
}
addElement(previous.pathElementTo(vertex))
}
mutating func addCurve(toPoint: CGPoint, outTangent: CGPoint, inTangent: CGPoint) {
guard let previous = elements.last else { return }
let newVertex = CurveVertex(inTangent, toPoint, toPoint)
updateVertex(
CurveVertex(previous.vertex.inTangent, previous.vertex.point, outTangent),
atIndex: elements.endIndex - 1,
remeasure: false)
addVertex(newVertex)
}
mutating func addLine(toPoint: CGPoint) {
guard let previous = elements.last else { return }
let newVertex = CurveVertex(point: toPoint, inTangentRelative: .zero, outTangentRelative: .zero)
updateVertex(
CurveVertex(previous.vertex.inTangent, previous.vertex.point, previous.vertex.point),
atIndex: elements.endIndex - 1,
remeasure: false)
addVertex(newVertex)
}
mutating func close() {
closed = true
}
mutating func addElement(_ pathElement: PathElement) {
elements.append(pathElement)
length = length + pathElement.length
}
mutating func updateVertex(_ vertex: CurveVertex, atIndex: Int, remeasure: Bool) {
if remeasure {
var newElement: PathElement
if atIndex > 0 {
let previousElement = elements[atIndex - 1]
newElement = previousElement.pathElementTo(vertex)
} else {
newElement = PathElement(vertex: vertex)
}
elements[atIndex] = newElement
if atIndex + 1 < elements.count {
let nextElement = elements[atIndex + 1]
elements[atIndex + 1] = newElement.pathElementTo(nextElement.vertex)
}
} else {
let oldElement = elements[atIndex]
elements[atIndex] = oldElement.updateVertex(newVertex: vertex)
}
}
/// Trims a path fromLength toLength with an offset.
///
/// Length and offset are defined in the length coordinate space.
/// If any argument is outside the range of this path, then it will be looped over the path from finish to start.
///
/// Cutting the curve when fromLength is less than toLength
/// x x x x
/// ~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooooooooooooooooooooooo-------------------
/// |Offset |fromLength toLength| |
///
/// Cutting the curve when from Length is greater than toLength
/// x x x x x
/// oooooooooooooooooo--------------------~~~~~~~~~~~~~~~~ooooooooooooooooooooooooooooo
/// | toLength| |Offset |fromLength |
///
func trim(fromLength: CGFloat, toLength: CGFloat, offsetLength: CGFloat) -> [BezierPath] {
guard elements.count > 1 else {
return []
}
if fromLength == toLength {
return []
}
/// Normalize lengths to the curve length.
var start = (fromLength + offsetLength).truncatingRemainder(dividingBy: length)
var end = (toLength + offsetLength).truncatingRemainder(dividingBy: length)
if start < 0 {
start = length + start
}
if end < 0 {
end = length + end
}
if start == length {
start = 0
}
if end == 0 {
end = length
}
if
start == 0 && end == length ||
start == end ||
start == length && end == 0
{
/// The trim encompasses the entire path. Return.
return [self]
}
if start > end {
// Start is greater than end. Two paths are returned.
return trimPathAtLengths(positions: [(start: 0, end: end), (start: start, end: length)])
}
return trimPathAtLengths(positions: [(start: start, end: end)])
}
// MARK: Private
private func trimPathAtLengths(positions: [(start: CGFloat, end: CGFloat)]) -> [BezierPath] {
guard positions.count > 0 else {
return []
}
var remainingPositions = positions
var trim = remainingPositions.remove(at: 0)
var paths = [BezierPath]()
var runningLength: CGFloat = 0
var finishedTrimming = false
var pathElements = elements
var currentPath = BezierPath()
var i = 0
while !finishedTrimming {
if pathElements.count <= i {
/// Do this for rounding errors
paths.append(currentPath)
finishedTrimming = true
continue
}
/// Loop through and add elements within start->end range.
/// Get current element
let element = pathElements[i]
/// Calculate new running length.
let newLength = runningLength + element.length
if newLength < trim.start {
/// Element is not included in the trim, continue.
runningLength = newLength
i = i + 1
/// Increment index, we are done with this element.
continue
}
if newLength == trim.start {
/// Current element IS the start element.
/// For start we want to add a zero length element.
currentPath.moveToStartPoint(element.vertex)
runningLength = newLength
i = i + 1
/// Increment index, we are done with this element.
continue
}
if runningLength < trim.start, trim.start < newLength, currentPath.elements.count == 0 {
/// The start of the trim is between this element and the previous, trim.
/// Get previous element.
let previousElement = pathElements[i - 1]
/// Trim it
let trimLength = trim.start - runningLength
let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength)
/// Add the right span start.
currentPath.moveToStartPoint(trimResults.rightSpan.start.vertex)
pathElements[i] = trimResults.rightSpan.end
pathElements[i - 1] = trimResults.rightSpan.start
runningLength = runningLength + trimResults.leftSpan.end.length
/// Dont increment index or the current length, the end of this path can be within this span.
continue
}
if trim.start < newLength, newLength < trim.end {
/// Element lies within the trim span.
currentPath.addElement(element)
runningLength = newLength
i = i + 1
continue
}
if newLength == trim.end {
/// Element is the end element.
/// The element could have a new length if it's added right after the start node.
currentPath.addElement(element)
/// We are done with this span.
runningLength = newLength
i = i + 1
/// Allow the path to be finalized.
/// Fall through to finalize path and move to next position
}
if runningLength < trim.end, trim.end < newLength {
/// New element must be cut for end.
/// Get previous element.
let previousElement = pathElements[i - 1]
/// Trim it
let trimLength = trim.end - runningLength
let trimResults = element.splitElementAtPosition(fromElement: previousElement, atLength: trimLength)
/// Add the left span end.
currentPath.updateVertex(trimResults.leftSpan.start.vertex, atIndex: currentPath.elements.count - 1, remeasure: false)
currentPath.addElement(trimResults.leftSpan.end)
pathElements[i] = trimResults.rightSpan.end
pathElements[i - 1] = trimResults.rightSpan.start
runningLength = runningLength + trimResults.leftSpan.end.length
/// Dont increment index or the current length, the start of the next path can be within this span.
/// We are done with this span.
/// Allow the path to be finalized.
/// Fall through to finalize path and move to next position
}
paths.append(currentPath)
currentPath = BezierPath()
if remainingPositions.count > 0 {
trim = remainingPositions.remove(at: 0)
} else {
finishedTrimming = true
}
}
return paths
}
}
// MARK: Codable
extension BezierPath: Codable {
// MARK: Lifecycle
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<BezierPath.CodingKeys>
if let keyedContainer = try? decoder.container(keyedBy: BezierPath.CodingKeys.self) {
container = keyedContainer
} else {
var unkeyedContainer = try decoder.unkeyedContainer()
container = try unkeyedContainer.nestedContainer(keyedBy: BezierPath.CodingKeys.self)
}
closed = try container.decodeIfPresent(Bool.self, forKey: .closed) ?? true
var vertexContainer = try container.nestedUnkeyedContainer(forKey: .vertices)
var inPointsContainer = try container.nestedUnkeyedContainer(forKey: .inPoints)
var outPointsContainer = try container.nestedUnkeyedContainer(forKey: .outPoints)
guard vertexContainer.count == inPointsContainer.count, inPointsContainer.count == outPointsContainer.count else {
/// Will throw an error if vertex, inpoints, and outpoints are not the same length.
/// This error is to be expected.
throw DecodingError.dataCorruptedError(
forKey: CodingKeys.vertices,
in: container,
debugDescription: "Vertex data does not match In Tangents and Out Tangents")
}
guard let count = vertexContainer.count, count > 0 else {
length = 0
elements = []
return
}
var decodedElements = [PathElement]()
/// Create first point
let firstVertex = CurveVertex(
point: try vertexContainer.decode(CGPoint.self),
inTangentRelative: try inPointsContainer.decode(CGPoint.self),
outTangentRelative: try outPointsContainer.decode(CGPoint.self))
var previousElement = PathElement(vertex: firstVertex)
decodedElements.append(previousElement)
var totalLength: CGFloat = 0
while !vertexContainer.isAtEnd {
/// Get the next vertex data.
let vertex = CurveVertex(
point: try vertexContainer.decode(CGPoint.self),
inTangentRelative: try inPointsContainer.decode(CGPoint.self),
outTangentRelative: try outPointsContainer.decode(CGPoint.self))
let pathElement = previousElement.pathElementTo(vertex)
decodedElements.append(pathElement)
previousElement = pathElement
totalLength = totalLength + pathElement.length
}
if closed {
let closeElement = previousElement.pathElementTo(firstVertex)
decodedElements.append(closeElement)
totalLength = totalLength + closeElement.length
}
length = totalLength
elements = decodedElements
}
// MARK: Internal
/// The BezierPath container is encoded and decoded from the JSON format
/// that defines points for a lottie animation.
///
/// {
/// "c" = Bool
/// "i" = [[Double]],
/// "o" = [[Double]],
/// "v" = [[Double]]
/// }
///
enum CodingKeys: String, CodingKey {
case closed = "c"
case inPoints = "i"
case outPoints = "o"
case vertices = "v"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: BezierPath.CodingKeys.self)
try container.encode(closed, forKey: .closed)
var vertexContainer = container.nestedUnkeyedContainer(forKey: .vertices)
var inPointsContainer = container.nestedUnkeyedContainer(forKey: .inPoints)
var outPointsContainer = container.nestedUnkeyedContainer(forKey: .outPoints)
/// If closed path, ignore the final element.
let finalIndex = closed ? elements.endIndex - 1 : elements.endIndex
for i in 0..<finalIndex {
let element = elements[i]
try vertexContainer.encode(element.vertex.point)
try inPointsContainer.encode(element.vertex.inTangentRelative)
try outPointsContainer.encode(element.vertex.outTangentRelative)
}
}
}
// MARK: AnyInitializable
extension BezierPath: AnyInitializable {
init(value: Any) throws {
var pathDictionary: [String: Any]
if
let array = value as? [[String: Any]],
let firstDictionary = array.first
{
pathDictionary = firstDictionary
} else if let dictionary = value as? [String: Any] {
pathDictionary = dictionary
} else {
throw InitializableError.invalidInput
}
closed = (try? pathDictionary.value(for: CodingKeys.closed)) ?? true
var vertexDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.vertices)
var inPointsDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.inPoints)
var outPointsDictionaries: [Any] = try pathDictionary.value(for: CodingKeys.outPoints)
guard
vertexDictionaries.count == inPointsDictionaries.count,
inPointsDictionaries.count == outPointsDictionaries.count else
{
throw InitializableError.invalidInput
}
guard vertexDictionaries.count > 0 else {
length = 0
elements = []
return
}
var decodedElements = [PathElement]()
let firstVertexDictionary = vertexDictionaries.removeFirst()
let firstInPointsDictionary = inPointsDictionaries.removeFirst()
let firstOutPointsDictionary = outPointsDictionaries.removeFirst()
let firstVertex = CurveVertex(
point: try CGPoint(value: firstVertexDictionary),
inTangentRelative: try CGPoint(value: firstInPointsDictionary),
outTangentRelative: try CGPoint(value: firstOutPointsDictionary))
var previousElement = PathElement(vertex: firstVertex)
decodedElements.append(previousElement)
var totalLength: CGFloat = 0
while vertexDictionaries.count > 0 {
let vertexDictionary = vertexDictionaries.removeFirst()
let inPointsDictionary = inPointsDictionaries.removeFirst()
let outPointsDictionary = outPointsDictionaries.removeFirst()
let vertex = CurveVertex(
point: try CGPoint(value: vertexDictionary),
inTangentRelative: try CGPoint(value: inPointsDictionary),
outTangentRelative: try CGPoint(value: outPointsDictionary))
let pathElement = previousElement.pathElementTo(vertex)
decodedElements.append(pathElement)
previousElement = pathElement
totalLength = totalLength + pathElement.length
}
if closed {
let closeElement = previousElement.pathElementTo(firstVertex)
decodedElements.append(closeElement)
totalLength = totalLength + closeElement.length
}
length = totalLength
elements = decodedElements
}
}
extension BezierPath {
func cgPath() -> CGPath {
let cgPath = CGMutablePath()
var previousElement: PathElement?
for element in elements {
if let previous = previousElement {
if previous.vertex.outTangentRelative.isZero, element.vertex.inTangentRelative.isZero {
cgPath.addLine(to: element.vertex.point)
} else {
cgPath.addCurve(to: element.vertex.point, control1: previous.vertex.outTangent, control2: element.vertex.inTangent)
}
} else {
cgPath.move(to: element.vertex.point)
}
previousElement = element
}
if closed {
cgPath.closeSubpath()
}
return cgPath
}
}