// // EllipseNode.swift // lottie-swift // // Created by Brandon Withrow on 1/17/19. // import Foundation import QuartzCore // MARK: - EllipseNodeProperties final class EllipseNodeProperties: NodePropertyMap, KeypathSearchable { // MARK: Lifecycle init(ellipse: Ellipse) { keypathName = ellipse.name direction = ellipse.direction position = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.position.keyframes)) size = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.size.keyframes)) keypathProperties = [ "Position" : position, "Size" : size, ] properties = Array(keypathProperties.values) } // MARK: Internal var keypathName: String let direction: PathDirection let position: NodeProperty let size: NodeProperty let keypathProperties: [String: AnyNodeProperty] let properties: [AnyNodeProperty] } // MARK: - EllipseNode final class EllipseNode: AnimatorNode, PathNode { // MARK: Lifecycle init(parentNode: AnimatorNode?, ellipse: Ellipse) { pathOutput = PathOutputNode(parent: parentNode?.outputNode) properties = EllipseNodeProperties(ellipse: ellipse) self.parentNode = parentNode } // MARK: Internal static let ControlPointConstant: CGFloat = 0.55228 let pathOutput: PathOutputNode let properties: EllipseNodeProperties let parentNode: AnimatorNode? var hasLocalUpdates = false var hasUpstreamUpdates = false var lastUpdateFrame: CGFloat? = nil // MARK: Animator Node var propertyMap: NodePropertyMap & KeypathSearchable { properties } var isEnabled = true { didSet { pathOutput.isEnabled = isEnabled } } func rebuildOutputs(frame: CGFloat) { pathOutput.setPath( .ellipse( size: properties.size.value.sizeValue, center: properties.position.value.pointValue, direction: properties.direction), updateFrame: frame) } } extension BezierPath { /// Constructs a `BezierPath` in the shape of an ellipse static func ellipse( size: CGSize, center: CGPoint, direction: PathDirection) -> BezierPath { // Unfortunately we HAVE to manually build out the ellipse. // Every Apple method constructs an ellipse from the 3 o-clock position // After effects constructs from the Noon position. // After effects does clockwise, but also has a flag for reversed. var half = size * 0.5 if direction == .counterClockwise { half.width = half.width * -1 } let q1 = CGPoint(x: center.x, y: center.y - half.height) let q2 = CGPoint(x: center.x + half.width, y: center.y) let q3 = CGPoint(x: center.x, y: center.y + half.height) let q4 = CGPoint(x: center.x - half.width, y: center.y) let cp = half * EllipseNode.ControlPointConstant var path = BezierPath(startPoint: CurveVertex( point: q1, inTangentRelative: CGPoint(x: -cp.width, y: 0), outTangentRelative: CGPoint(x: cp.width, y: 0))) path.addVertex(CurveVertex( point: q2, inTangentRelative: CGPoint(x: 0, y: -cp.height), outTangentRelative: CGPoint(x: 0, y: cp.height))) path.addVertex(CurveVertex( point: q3, inTangentRelative: CGPoint(x: cp.width, y: 0), outTangentRelative: CGPoint(x: -cp.width, y: 0))) path.addVertex(CurveVertex( point: q4, inTangentRelative: CGPoint(x: 0, y: cp.height), outTangentRelative: CGPoint(x: 0, y: -cp.height))) path.addVertex(CurveVertex( point: q1, inTangentRelative: CGPoint(x: -cp.width, y: 0), outTangentRelative: CGPoint(x: cp.width, y: 0))) path.close() return path } }