// // AnimatorNode.swift // lottie-swift // // Created by Brandon Withrow on 1/15/19. // import Foundation import QuartzCore // MARK: - NodeOutput /// Defines the basic outputs of an animator node. /// protocol NodeOutput { /// The parent node. var parent: NodeOutput? { get } /// Returns true if there are any updates upstream. OutputPath must be built before returning. func hasOutputUpdates(_ forFrame: CGFloat) -> Bool var outputPath: CGPath? { get } var isEnabled: Bool { get set } } // MARK: - AnimatorNode /// The Animator Node is the base node in the render system tree. /// /// It defines a single node that has an output path and option input node. /// At animation time the root animation node is asked to update its contents for /// the current frame. /// The node reaches up its chain of nodes until the first node that does not need /// updating is found. Then each node updates its contents down the render pipeline. /// Each node adds its local path to its input path and passes it forward. /// /// An animator node holds a group of interpolators. These interpolators determine /// if the node needs an update for the current frame. /// protocol AnimatorNode: AnyObject, KeypathSearchable { /// The available properties of the Node. /// /// These properties are automatically updated each frame. /// These properties are also settable and gettable through the dynamic /// property system. /// var propertyMap: NodePropertyMap & KeypathSearchable { get } /// The upstream input node var parentNode: AnimatorNode? { get } /// The output of the node. var outputNode: NodeOutput { get } /// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true. func rebuildOutputs(frame: CGFloat) /// Setters for marking current node state. var isEnabled: Bool { get set } var hasLocalUpdates: Bool { get set } var hasUpstreamUpdates: Bool { get set } var lastUpdateFrame: CGFloat? { get set } // MARK: Optional /// Marks if updates to this node affect nodes downstream. func localUpdatesPermeateDownstream() -> Bool func forceUpstreamOutputUpdates() -> Bool /// Called at the end of this nodes update cycle. Always called. Optional. func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool) /// The default simply returns `hasLocalUpdates` func shouldRebuildOutputs(frame: CGFloat) -> Bool } /// Basic Node Logic extension AnimatorNode { func shouldRebuildOutputs(frame _: CGFloat) -> Bool { hasLocalUpdates } func localUpdatesPermeateDownstream() -> Bool { /// Optional override true } func forceUpstreamOutputUpdates() -> Bool { /// Optional false } func performAdditionalLocalUpdates(frame _: CGFloat, forceLocalUpdate: Bool) -> Bool { /// Optional forceLocalUpdate } func performAdditionalOutputUpdates(_: CGFloat, forceOutputUpdate _: Bool) { /// Optional } @discardableResult func updateOutputs(_ frame: CGFloat, forceOutputUpdate: Bool) -> Bool { guard isEnabled else { // Disabled node, pass through. lastUpdateFrame = frame return parentNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate) ?? false } if forceOutputUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame { /// This node has already updated for this frame. Go ahead and return the results. return hasUpstreamUpdates || hasLocalUpdates } /// Ask if this node should force output updates upstream. let forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates() /// Perform upstream output updates. Optionally mark upstream updates if any. hasUpstreamUpdates = ( parentNode? .updateOutputs(frame, forceOutputUpdate: forceUpstreamUpdates) ?? false || hasUpstreamUpdates) /// Perform additional local output updates performAdditionalOutputUpdates(frame, forceOutputUpdate: forceUpstreamUpdates) /// If there are local updates, or if updates have been force, rebuild outputs if forceUpstreamUpdates || shouldRebuildOutputs(frame: frame) { lastUpdateFrame = frame rebuildOutputs(frame: frame) } return hasUpstreamUpdates || hasLocalUpdates } /// Rebuilds the content of this node, and upstream nodes if necessary. @discardableResult func updateContents(_ frame: CGFloat, forceLocalUpdate: Bool) -> Bool { guard isEnabled else { // Disabled node, pass through. return parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false } if forceLocalUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame { /// This node has already updated for this frame. Go ahead and return the results. return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates } /// Are there local updates? If so mark the node. hasLocalUpdates = forceLocalUpdate ? forceLocalUpdate : propertyMap.needsLocalUpdate(frame: frame) /// Were there upstream updates? If so mark the node hasUpstreamUpdates = parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false /// Perform property updates if necessary. if hasLocalUpdates { /// Rebuild local properties propertyMap.updateNodeProperties(frame: frame) } /// Ask the node to perform any other updates it might have. hasUpstreamUpdates = performAdditionalLocalUpdates(frame: frame, forceLocalUpdate: forceLocalUpdate) || hasUpstreamUpdates /// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream. return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates } func updateTree(_ frame: CGFloat, forceUpdates: Bool = false) { updateContents(frame, forceLocalUpdate: forceUpdates) updateOutputs(frame, forceOutputUpdate: forceUpdates) } } extension AnimatorNode { /// Default implementation for Keypath searchable. /// Forward all calls to the propertyMap. var keypathName: String { propertyMap.keypathName } var keypathProperties: [String: AnyNodeProperty] { propertyMap.keypathProperties } var childKeypaths: [KeypathSearchable] { propertyMap.childKeypaths } var keypathLayer: CALayer? { nil } }