// // CompoundBezierPath.swift // lottie-swift // // Created by Brandon Withrow on 1/14/19. // import CoreGraphics import Foundation /// A collection of BezierPath objects that can be trimmed and added. /// struct CompoundBezierPath { // MARK: Lifecycle init() { paths = [] length = 0 } init(path: BezierPath) { paths = [path] length = path.length } init(paths: [BezierPath], length: CGFloat) { self.paths = paths self.length = length } init(paths: [BezierPath]) { self.paths = paths var l: CGFloat = 0 for path in paths { l = l + path.length } length = l } // MARK: Internal let paths: [BezierPath] let length: CGFloat func addPath(path: BezierPath) -> CompoundBezierPath { var newPaths = paths newPaths.append(path) return CompoundBezierPath(paths: newPaths, length: length + path.length) } func combine(_ compoundBezier: CompoundBezierPath) -> CompoundBezierPath { var newPaths = paths newPaths.append(contentsOf: compoundBezier.paths) return CompoundBezierPath(paths: newPaths, length: length + compoundBezier.length) } func trim(fromPosition: CGFloat, toPosition: CGFloat, offset: CGFloat, trimSimultaneously: Bool) -> CompoundBezierPath { if fromPosition == toPosition { return CompoundBezierPath() } if trimSimultaneously { /// Trim each path individually. var newPaths = [BezierPath]() for path in paths { newPaths.append(contentsOf: path.trim( fromLength: fromPosition * path.length, toLength: toPosition * path.length, offsetLength: offset * path.length)) } return CompoundBezierPath(paths: newPaths) } /// Normalize lengths to the curve length. var startPosition = (fromPosition + offset).truncatingRemainder(dividingBy: 1) var endPosition = (toPosition + offset).truncatingRemainder(dividingBy: 1) if startPosition < 0 { startPosition = 1 + startPosition } if endPosition < 0 { endPosition = 1 + endPosition } if startPosition == 1 { startPosition = 0 } if endPosition == 0 { endPosition = 1 } if startPosition == 0 && endPosition == 1 || startPosition == endPosition || startPosition == 1 && endPosition == 0 { /// The trim encompasses the entire path. Return. return self } var positions: [(start: CGFloat, end: CGFloat)] if endPosition < startPosition { positions = [ (start: 0, end: endPosition * length), (start: startPosition * length, end: length), ] } else { positions = [(start: startPosition * length, end: endPosition * length)] } var compoundPath = CompoundBezierPath() var trim = positions.remove(at: 0) var pathStartPosition: CGFloat = 0 var finishedTrimming = false var i = 0 while !finishedTrimming { if paths.count <= i { /// Rounding errors finishedTrimming = true continue } let path = paths[i] let pathEndPosition = pathStartPosition + path.length if pathEndPosition < trim.start { /// Path is not included in the trim, continue. pathStartPosition = pathEndPosition i = i + 1 continue } else if trim.start <= pathStartPosition, pathEndPosition <= trim.end { /// Full Path is inside of trim. Add full path. compoundPath = compoundPath.addPath(path: path) } else { if let trimPath = path.trim( fromLength: trim.start > pathStartPosition ? (trim.start - pathStartPosition) : 0, toLength: trim.end < pathEndPosition ? (trim.end - pathStartPosition) : path.length, offsetLength: 0).first { compoundPath = compoundPath.addPath(path: trimPath) } } if trim.end <= pathEndPosition { /// We are done with the current trim. /// Advance trim but remain on the same path in case the next trim overlaps it. if positions.count > 0 { trim = positions.remove(at: 0) } else { finishedTrimming = true } } else { pathStartPosition = pathEndPosition i = i + 1 } } return compoundPath } }