Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,161 @@
//
// LayerContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
// MARK: - CompositionLayer
/// The base class for a child layer of CompositionContainer
class CompositionLayer: CALayer, KeypathSearchable {
// MARK: Lifecycle
init(layer: LayerModel, size: CGSize) {
transformNode = LayerTransformNode(transform: layer.transform)
if let masks = layer.masks {
maskLayer = MaskContainerLayer(masks: masks)
} else {
maskLayer = nil
}
matteType = layer.matte
inFrame = layer.inFrame.cgFloat
outFrame = layer.outFrame.cgFloat
timeStretch = layer.timeStretch.cgFloat
startFrame = layer.startTime.cgFloat
keypathName = layer.name
childKeypaths = [transformNode.transformProperties]
super.init()
anchorPoint = .zero
actions = [
"opacity" : NSNull(),
"transform" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"sublayerTransform" : NSNull(),
]
contentsLayer.anchorPoint = .zero
contentsLayer.bounds = CGRect(origin: .zero, size: size)
contentsLayer.actions = [
"opacity" : NSNull(),
"transform" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"sublayerTransform" : NSNull(),
"hidden" : NSNull(),
]
compositingFilter = layer.blendMode.filterName
addSublayer(contentsLayer)
if let maskLayer = maskLayer {
contentsLayer.mask = maskLayer
}
name = layer.name
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? CompositionLayer else {
fatalError("Wrong Layer Class")
}
transformNode = layer.transformNode
matteType = layer.matteType
inFrame = layer.inFrame
outFrame = layer.outFrame
timeStretch = layer.timeStretch
startFrame = layer.startFrame
keypathName = layer.keypathName
childKeypaths = [transformNode.transformProperties]
maskLayer = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
weak var layerDelegate: CompositionLayerDelegate?
let transformNode: LayerTransformNode
let contentsLayer = CALayer()
let maskLayer: MaskContainerLayer?
let matteType: MatteType?
let inFrame: CGFloat
let outFrame: CGFloat
let startFrame: CGFloat
let timeStretch: CGFloat
// MARK: Keypath Searchable
let keypathName: String
final var childKeypaths: [KeypathSearchable]
var renderScale: CGFloat = 1 {
didSet {
updateRenderScale()
}
}
var matteLayer: CompositionLayer? {
didSet {
if let matte = matteLayer {
if let type = matteType, type == .invert {
mask = InvertedMatteLayer(inputMatte: matte)
} else {
mask = matte
}
} else {
mask = nil
}
}
}
var keypathProperties: [String: AnyNodeProperty] {
[:]
}
var keypathLayer: CALayer? {
contentsLayer
}
final func displayWithFrame(frame: CGFloat, forceUpdates: Bool) {
transformNode.updateTree(frame, forceUpdates: forceUpdates)
let layerVisible = frame.isInRangeOrEqual(inFrame, outFrame)
/// Only update contents if current time is within the layers time bounds.
if layerVisible {
displayContentsWithFrame(frame: frame, forceUpdates: forceUpdates)
maskLayer?.updateWithFrame(frame: frame, forceUpdates: forceUpdates)
}
contentsLayer.transform = transformNode.globalTransform
contentsLayer.opacity = transformNode.opacity
contentsLayer.isHidden = !layerVisible
layerDelegate?.frameUpdated(frame: frame)
}
func displayContentsWithFrame(frame _: CGFloat, forceUpdates _: Bool) {
/// To be overridden by subclass
}
func updateRenderScale() {
contentsScale = renderScale
}
}
// MARK: - CompositionLayerDelegate
protocol CompositionLayerDelegate: AnyObject {
func frameUpdated(frame: CGFloat)
}
@@ -0,0 +1,50 @@
//
// ImageCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import CoreGraphics
import Foundation
import QuartzCore
final class ImageCompositionLayer: CompositionLayer {
// MARK: Lifecycle
init(imageLayer: ImageLayerModel, size: CGSize) {
imageReferenceID = imageLayer.referenceID
super.init(layer: imageLayer, size: size)
contentsLayer.masksToBounds = true
contentsLayer.contentsGravity = CALayerContentsGravity.resize
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? ImageCompositionLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
imageReferenceID = layer.imageReferenceID
image = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let imageReferenceID: String
var image: CGImage? = nil {
didSet {
if let image = image {
contentsLayer.contents = image
} else {
contentsLayer.contents = nil
}
}
}
}
@@ -0,0 +1,191 @@
//
// MaskContainerLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
import QuartzCore
extension MaskMode {
var usableMode: MaskMode {
switch self {
case .add:
return .add
case .subtract:
return .subtract
case .intersect:
return .intersect
case .lighten:
return .add
case .darken:
return .darken
case .difference:
return .intersect
case .none:
return .none
}
}
}
// MARK: - MaskContainerLayer
final class MaskContainerLayer: CALayer {
// MARK: Lifecycle
init(masks: [Mask]) {
super.init()
anchorPoint = .zero
var containerLayer = CALayer()
var firstObject = true
for mask in masks {
let maskLayer = MaskLayer(mask: mask)
maskLayers.append(maskLayer)
if mask.mode.usableMode == .none {
continue
} else if mask.mode.usableMode == .add || firstObject {
firstObject = false
containerLayer.addSublayer(maskLayer)
} else {
containerLayer.mask = maskLayer
let newContainer = CALayer()
newContainer.addSublayer(containerLayer)
containerLayer = newContainer
}
}
addSublayer(containerLayer)
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? MaskContainerLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
maskLayers.forEach({ $0.updateWithFrame(frame: frame, forceUpdates: forceUpdates) })
}
// MARK: Fileprivate
fileprivate var maskLayers: [MaskLayer] = []
}
extension CGRect {
static var veryLargeRect: CGRect {
CGRect(
x: -100_000_000,
y: -100_000_000,
width: 200_000_000,
height: 200_000_000)
}
}
// MARK: - MaskLayer
private class MaskLayer: CALayer {
// MARK: Lifecycle
init(mask: Mask) {
properties = MaskNodeProperties(mask: mask)
super.init()
addSublayer(maskLayer)
anchorPoint = .zero
maskLayer.fillColor = mask.mode == .add
? CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1])
: CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
actions = [
"opacity" : NSNull(),
]
}
override init(layer: Any) {
properties = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let properties: MaskNodeProperties?
let maskLayer = CAShapeLayer()
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let properties = properties else { return }
if properties.opacity.needsUpdate(frame: frame) || forceUpdates {
properties.opacity.update(frame: frame)
opacity = Float(properties.opacity.value.cgFloatValue)
}
if properties.shape.needsUpdate(frame: frame) || forceUpdates {
properties.shape.update(frame: frame)
properties.expansion.update(frame: frame)
let shapePath = properties.shape.value.cgPath()
var path = shapePath
if
properties.mode.usableMode == .subtract && !properties.inverted ||
(properties.mode.usableMode == .add && properties.inverted)
{
/// Add a bounds rect to invert the mask
let newPath = CGMutablePath()
newPath.addRect(CGRect.veryLargeRect)
newPath.addPath(shapePath)
path = newPath
}
maskLayer.path = path
}
}
}
// MARK: - MaskNodeProperties
private class MaskNodeProperties: NodePropertyMap {
// MARK: Lifecycle
init(mask: Mask) {
mode = mask.mode
inverted = mask.inverted
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.opacity.keyframes))
shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
propertyMap = [
"Opacity" : opacity,
"Shape" : shape,
"Expansion" : expansion,
]
properties = Array(propertyMap.values)
}
// MARK: Internal
var propertyMap: [String: AnyNodeProperty]
var properties: [AnyNodeProperty]
let mode: MaskMode
let inverted: Bool
let opacity: NodeProperty<Vector1D>
let shape: NodeProperty<BezierPath>
let expansion: NodeProperty<Vector1D>
}
@@ -0,0 +1,28 @@
//
// NullCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
final class NullCompositionLayer: CompositionLayer {
init(layer: LayerModel) {
super.init(layer: layer, size: .zero)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? NullCompositionLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
super.init(layer: layer)
}
}
@@ -0,0 +1,121 @@
//
// PreCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
import QuartzCore
final class PreCompositionLayer: CompositionLayer {
// MARK: Lifecycle
init(
precomp: PreCompLayerModel,
asset: PrecompAsset,
layerImageProvider: LayerImageProvider,
textProvider: AnimationTextProvider,
fontProvider: AnimationFontProvider,
assetLibrary: AssetLibrary?,
frameRate: CGFloat)
{
animationLayers = []
if let keyframes = precomp.timeRemapping?.keyframes {
remappingNode = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframes))
} else {
remappingNode = nil
}
self.frameRate = frameRate
super.init(layer: precomp, size: CGSize(width: precomp.width, height: precomp.height))
bounds = CGRect(origin: .zero, size: CGSize(width: precomp.width, height: precomp.height))
contentsLayer.masksToBounds = true
contentsLayer.bounds = bounds
let layers = asset.layers.initializeCompositionLayers(
assetLibrary: assetLibrary,
layerImageProvider: layerImageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: frameRate)
var imageLayers = [ImageCompositionLayer]()
var mattedLayer: CompositionLayer? = nil
for layer in layers.reversed() {
layer.bounds = bounds
animationLayers.append(layer)
if let imageLayer = layer as? ImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
mattedLayer = nil
continue
}
if
let matte = layer.matteType,
matte == .add || matte == .invert
{
/// We have a layer that requires a matte.
mattedLayer = layer
}
contentsLayer.addSublayer(layer)
}
childKeypaths.append(contentsOf: layers)
layerImageProvider.addImageLayers(imageLayers)
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? PreCompositionLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
frameRate = layer.frameRate
remappingNode = nil
animationLayers = []
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let frameRate: CGFloat
let remappingNode: NodeProperty<Vector1D>?
override var keypathProperties: [String: AnyNodeProperty] {
guard let remappingNode = remappingNode else {
return super.keypathProperties
}
return ["Time Remap" : remappingNode]
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
let localFrame: CGFloat
if let remappingNode = remappingNode {
remappingNode.update(frame: frame)
localFrame = remappingNode.value.cgFloatValue * frameRate
} else {
localFrame = (frame - startFrame) / timeStretch
}
animationLayers.forEach( { $0.displayWithFrame(frame: localFrame, forceUpdates: forceUpdates) })
}
override func updateRenderScale() {
super.updateRenderScale()
animationLayers.forEach( { $0.renderScale = renderScale } )
}
// MARK: Fileprivate
fileprivate var animationLayers: [CompositionLayer]
}
@@ -0,0 +1,58 @@
//
// ShapeLayerContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import CoreGraphics
import Foundation
/// A CompositionLayer responsible for initializing and rendering shapes
final class ShapeCompositionLayer: CompositionLayer {
// MARK: Lifecycle
init(shapeLayer: ShapeLayerModel) {
let results = shapeLayer.items.initializeNodeTree()
let renderContainer = ShapeContainerLayer()
self.renderContainer = renderContainer
rootNode = results.rootNode
super.init(layer: shapeLayer, size: .zero)
contentsLayer.addSublayer(renderContainer)
for container in results.renderContainers {
renderContainer.insertRenderLayer(container)
}
rootNode?.updateTree(0, forceUpdates: true)
childKeypaths.append(contentsOf: results.childrenNodes)
}
override init(layer: Any) {
guard let layer = layer as? ShapeCompositionLayer else {
fatalError("init(layer:) wrong class.")
}
rootNode = nil
renderContainer = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let rootNode: AnimatorNode?
let renderContainer: ShapeContainerLayer?
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
rootNode?.updateTree(frame, forceUpdates: forceUpdates)
renderContainer?.markRenderUpdates(forFrame: frame)
}
override func updateRenderScale() {
super.updateRenderScale()
renderContainer?.renderScale = renderScale
}
}
@@ -0,0 +1,57 @@
//
// SolidCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
import QuartzCore
final class SolidCompositionLayer: CompositionLayer {
// MARK: Lifecycle
init(solid: SolidLayerModel) {
let components = solid.colorHex.hexColorComponents()
colorProperty =
NodeProperty(provider: SingleValueProvider(Color(
r: Double(components.red),
g: Double(components.green),
b: Double(components.blue),
a: 1)))
super.init(layer: solid, size: .zero)
solidShape.path = CGPath(rect: CGRect(x: 0, y: 0, width: solid.width, height: solid.height), transform: nil)
contentsLayer.addSublayer(solidShape)
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? SolidCompositionLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
colorProperty = layer.colorProperty
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let colorProperty: NodeProperty<Color>?
let solidShape = CAShapeLayer()
override var keypathProperties: [String: AnyNodeProperty] {
guard let colorProperty = colorProperty else { return super.keypathProperties }
return [PropertyName.color.rawValue : colorProperty]
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates _: Bool) {
guard let colorProperty = colorProperty else { return }
colorProperty.update(frame: frame)
solidShape.fillColor = colorProperty.value.cgColorValue
}
}
@@ -0,0 +1,149 @@
//
// TextCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import CoreGraphics
import CoreText
import Foundation
import QuartzCore
/// Needed for NSMutableParagraphStyle...
#if os(OSX)
import AppKit
#else
import UIKit
#endif
extension TextJustification {
var textAlignment: NSTextAlignment {
switch self {
case .left:
return .left
case .right:
return .right
case .center:
return .center
}
}
var caTextAlignement: CATextLayerAlignmentMode {
switch self {
case .left:
return .left
case .right:
return .right
case .center:
return .center
}
}
}
// MARK: - TextCompositionLayer
final class TextCompositionLayer: CompositionLayer {
// MARK: Lifecycle
init(textLayer: TextLayerModel, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) {
var rootNode: TextAnimatorNode?
for animator in textLayer.animators {
rootNode = TextAnimatorNode(parentNode: rootNode, textAnimator: animator)
}
self.rootNode = rootNode
textDocument = KeyframeInterpolator(keyframes: textLayer.text.keyframes)
self.textProvider = textProvider
self.fontProvider = fontProvider
super.init(layer: textLayer, size: .zero)
contentsLayer.addSublayer(self.textLayer)
self.textLayer.masksToBounds = false
self.textLayer.isGeometryFlipped = true
if let rootNode = rootNode {
childKeypaths.append(rootNode)
}
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(layer: Any) {
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
guard let layer = layer as? TextCompositionLayer else {
fatalError("init(layer:) Wrong Layer Class")
}
rootNode = nil
textDocument = nil
textProvider = DefaultTextProvider()
fontProvider = DefaultFontProvider()
super.init(layer: layer)
}
// MARK: Internal
let rootNode: TextAnimatorNode?
let textDocument: KeyframeInterpolator<TextDocument>?
let textLayer = CoreTextRenderLayer()
var textProvider: AnimationTextProvider
var fontProvider: AnimationFontProvider
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let textDocument = textDocument else { return }
textLayer.contentsScale = renderScale
let documentUpdate = textDocument.hasUpdate(frame: frame)
let animatorUpdate = rootNode?.updateContents(frame, forceLocalUpdate: forceUpdates) ?? false
guard documentUpdate == true || animatorUpdate == true else { return }
rootNode?.rebuildOutputs(frame: frame)
// Get Text Attributes
let text = textDocument.value(frame: frame) as! TextDocument
let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text)
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
// Set all of the text layer options
textLayer.text = textString
textLayer.font = ctFont
textLayer.alignment = text.justification.textAlignment
textLayer.lineHeight = CGFloat(text.lineHeight)
textLayer.tracking = tracking
if let fillColor = rootNode?.textOutputNode.fillColor {
textLayer.fillColor = fillColor
} else if let fillColor = text.fillColorData?.cgColorValue {
textLayer.fillColor = fillColor
} else {
textLayer.fillColor = nil
}
textLayer.preferredSize = text.textFrameSize?.sizeValue
textLayer.strokeOnTop = text.strokeOverFill ?? false
textLayer.strokeWidth = strokeWidth
textLayer.strokeColor = strokeColor
textLayer.sizeToFit()
textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1)
textLayer.transform = CATransform3DIdentity
textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero
textLayer.transform = matrix
}
override func updateRenderScale() {
super.updateRenderScale()
textLayer.contentsScale = renderScale
}
}
@@ -0,0 +1,287 @@
//
// MainThreadAnimationLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/24/19.
//
import Foundation
import QuartzCore
import UIKit
// MARK: - MainThreadAnimationLayer
/// The base `CALayer` for the Main Thread rendering engine
///
/// This layer holds a single composition container and allows for animation of
/// the currentFrame property.
final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
// MARK: Lifecycle
init(
animation: Animation,
imageProvider: AnimationImageProvider,
textProvider: AnimationTextProvider,
fontProvider: AnimationFontProvider)
{
layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)
layerTextProvider = LayerTextProvider(textProvider: textProvider)
layerFontProvider = LayerFontProvider(fontProvider: fontProvider)
animationLayers = []
super.init()
bounds = animation.bounds
let layers = animation.layers.initializeCompositionLayers(
assetLibrary: animation.assetLibrary,
layerImageProvider: layerImageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: CGFloat(animation.framerate))
var imageLayers = [ImageCompositionLayer]()
var textLayers = [TextCompositionLayer]()
var mattedLayer: CompositionLayer? = nil
for layer in layers.reversed() {
layer.bounds = bounds
animationLayers.append(layer)
if let imageLayer = layer as? ImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let textLayer = layer as? TextCompositionLayer {
textLayers.append(textLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
mattedLayer = nil
continue
}
if
let matte = layer.matteType,
matte == .add || matte == .invert
{
/// We have a layer that requires a matte.
mattedLayer = layer
}
addSublayer(layer)
}
layerImageProvider.addImageLayers(imageLayers)
layerImageProvider.reloadImages()
layerTextProvider.addTextLayers(textLayers)
layerTextProvider.reloadTexts()
layerFontProvider.addTextLayers(textLayers)
layerFontProvider.reloadTexts()
setNeedsDisplay()
}
/// For CAAnimation Use
public override init(layer: Any) {
animationLayers = []
layerImageProvider = LayerImageProvider(imageProvider: BlankImageProvider(), assets: nil)
layerTextProvider = LayerTextProvider(textProvider: DefaultTextProvider())
layerFontProvider = LayerFontProvider(fontProvider: DefaultFontProvider())
super.init(layer: layer)
guard let animationLayer = layer as? MainThreadAnimationLayer else { return }
currentFrame = animationLayer.currentFrame
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Public
public var respectAnimationFrameRate = false
// MARK: CALayer Animations
override public class func needsDisplay(forKey key: String) -> Bool {
if key == "currentFrame" {
return true
}
return super.needsDisplay(forKey: key)
}
override public func action(forKey event: String) -> CAAction? {
if event == "currentFrame" {
let animation = CABasicAnimation(keyPath: event)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.fromValue = presentation()?.currentFrame
return animation
}
return super.action(forKey: event)
}
public override func display() {
guard Thread.isMainThread else { return }
var newFrame: CGFloat
if
let animationKeys = animationKeys(),
!animationKeys.isEmpty
{
newFrame = presentation()?.currentFrame ?? currentFrame
} else {
// We ignore the presentation's frame if there's no animation in the layer.
newFrame = currentFrame
}
if respectAnimationFrameRate {
newFrame = floor(newFrame)
}
animationLayers.forEach { $0.displayWithFrame(frame: newFrame, forceUpdates: false) }
}
// MARK: Internal
/// The animatable Current Frame Property
@NSManaged var currentFrame: CGFloat
var animationLayers: ContiguousArray<CompositionLayer>
var primaryAnimationKey: AnimationKey {
.managed
}
var isAnimationPlaying: Bool? {
nil // this state is managed by `AnimationView`
}
var _animationLayers: [CALayer] {
Array(animationLayers)
}
var imageProvider: AnimationImageProvider {
get {
layerImageProvider.imageProvider
}
set {
layerImageProvider.imageProvider = newValue
}
}
var renderScale: CGFloat = 1 {
didSet {
animationLayers.forEach({ $0.renderScale = renderScale })
}
}
var textProvider: AnimationTextProvider {
get { layerTextProvider.textProvider }
set { layerTextProvider.textProvider = newValue }
}
var fontProvider: AnimationFontProvider {
get { layerFontProvider.fontProvider }
set { layerFontProvider.fontProvider = newValue }
}
func reloadImages() {
layerImageProvider.reloadImages()
}
func removeAnimations() {
// no-op, since the primary animation is managed by the `AnimationView`.
}
/// Forces the view to update its drawing.
func forceDisplayUpdate() {
animationLayers.forEach( { $0.displayWithFrame(frame: currentFrame, forceUpdates: true) })
}
func logHierarchyKeypaths() {
print("Lottie: Logging Animation Keypaths")
animationLayers.forEach({ $0.logKeypaths(for: nil) })
}
func allKeypaths(predicate: (AnimationKeypath) -> Bool) -> [String] {
var result: [String] = []
for animationLayer in animationLayers {
result.append(contentsOf: animationLayer.allKeypaths(for: nil, predicate: predicate))
}
return result
}
func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
for layer in animationLayers {
if let foundProperties = layer.nodeProperties(for: keypath) {
for property in foundProperties {
property.setProvider(provider: valueProvider)
}
layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true)
}
}
}
func getValue(for keypath: AnimationKeypath, atFrame: CGFloat?) -> Any? {
for layer in animationLayers {
if
let foundProperties = layer.nodeProperties(for: keypath),
let first = foundProperties.first
{
return first.valueProvider.value(frame: atFrame ?? currentFrame)
}
}
return nil
}
func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? {
for layer in animationLayers {
if
let foundProperties = layer.nodeProperties(for: keypath),
let first = foundProperties.first
{
return first.originalValueProvider.value(frame: atFrame ?? currentFrame)
}
}
return nil
}
func layer(for keypath: AnimationKeypath) -> CALayer? {
for layer in animationLayers {
if let foundLayer = layer.layer(for: keypath) {
return foundLayer
}
}
return nil
}
func allLayers(for keypath: AnimationKeypath) -> [CALayer] {
var result: [CALayer] = []
for layer in animationLayers {
result.append(contentsOf: layer.allLayers(for: keypath))
}
return result
}
func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? {
var results = [AnimatorNode]()
for layer in animationLayers {
if let nodes = layer.animatorNodes(for: keypath) {
results.append(contentsOf: nodes)
}
}
if results.count == 0 {
return nil
}
return results
}
// MARK: Fileprivate
fileprivate let layerImageProvider: LayerImageProvider
fileprivate let layerTextProvider: LayerTextProvider
fileprivate let layerFontProvider: LayerFontProvider
}
// MARK: - BlankImageProvider
private class BlankImageProvider: AnimationImageProvider {
func imageForAsset(asset _: ImageAsset) -> CGImage? {
nil
}
}
@@ -0,0 +1,47 @@
// Created by Jianjun Wu on 2022/5/12.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import CoreGraphics
import Foundation
// MARK: - CachedImageProvider
private final class CachedImageProvider: AnimationImageProvider {
// MARK: Lifecycle
/// Initializes an image provider with an image provider
///
/// - Parameter imageProvider: The provider to load image from asset
///
public init(imageProvider: AnimationImageProvider) {
self.imageProvider = imageProvider
}
// MARK: Public
public func imageForAsset(asset: ImageAsset) -> CGImage? {
if let image = imageCache.object(forKey: asset.id as NSString) {
return image
}
if let image = imageProvider.imageForAsset(asset: asset) {
imageCache.setObject(image, forKey: asset.id as NSString)
return image
}
return nil
}
// MARK: Internal
let imageCache: NSCache<NSString, CGImage> = .init()
let imageProvider: AnimationImageProvider
}
extension AnimationImageProvider {
/// Create a cache enabled image provider which will reuse the asset image with the same asset id
/// It wraps the current provider as image loader, and uses `NSCache` to cache the images for resue.
/// The cache will be reset when the `animation` is reset.
var cachedImageProvider: AnimationImageProvider {
CachedImageProvider(imageProvider: self)
}
}
@@ -0,0 +1,90 @@
//
// CompositionLayersInitializer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import CoreGraphics
import Foundation
extension Array where Element == LayerModel {
func initializeCompositionLayers(
assetLibrary: AssetLibrary?,
layerImageProvider: LayerImageProvider,
textProvider: AnimationTextProvider,
fontProvider: AnimationFontProvider,
frameRate: CGFloat) -> [CompositionLayer]
{
var compositionLayers = [CompositionLayer]()
var layerMap = [Int : CompositionLayer]()
/// Organize the assets into a dictionary of [ID : ImageAsset]
var childLayers = [LayerModel]()
for layer in self {
if layer.hidden == true {
let genericLayer = NullCompositionLayer(layer: layer)
compositionLayers.append(genericLayer)
layerMap[layer.index] = genericLayer
} else if let shapeLayer = layer as? ShapeLayerModel {
let shapeContainer = ShapeCompositionLayer(shapeLayer: shapeLayer)
compositionLayers.append(shapeContainer)
layerMap[layer.index] = shapeContainer
} else if let solidLayer = layer as? SolidLayerModel {
let solidContainer = SolidCompositionLayer(solid: solidLayer)
compositionLayers.append(solidContainer)
layerMap[layer.index] = solidContainer
} else if
let precompLayer = layer as? PreCompLayerModel,
let assetLibrary = assetLibrary,
let precompAsset = assetLibrary.precompAssets[precompLayer.referenceID]
{
let precompContainer = PreCompositionLayer(
precomp: precompLayer,
asset: precompAsset,
layerImageProvider: layerImageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
assetLibrary: assetLibrary,
frameRate: frameRate)
compositionLayers.append(precompContainer)
layerMap[layer.index] = precompContainer
} else if
let imageLayer = layer as? ImageLayerModel,
let assetLibrary = assetLibrary,
let imageAsset = assetLibrary.imageAssets[imageLayer.referenceID]
{
let imageContainer = ImageCompositionLayer(
imageLayer: imageLayer,
size: CGSize(width: imageAsset.width, height: imageAsset.height))
compositionLayers.append(imageContainer)
layerMap[layer.index] = imageContainer
} else if let textLayer = layer as? TextLayerModel {
let textContainer = TextCompositionLayer(textLayer: textLayer, textProvider: textProvider, fontProvider: fontProvider)
compositionLayers.append(textContainer)
layerMap[layer.index] = textContainer
} else {
let genericLayer = NullCompositionLayer(layer: layer)
compositionLayers.append(genericLayer)
layerMap[layer.index] = genericLayer
}
if layer.parent != nil {
childLayers.append(layer)
}
}
/// Now link children with their parents
for layerModel in childLayers {
if let parentID = layerModel.parent {
let childLayer = layerMap[layerModel.index]
let parentLayer = layerMap[parentID]
childLayer?.transformNode.parentNode = parentLayer?.transformNode
}
}
return compositionLayers
}
}
@@ -0,0 +1,320 @@
//
// TextLayer.swift
// Pods
//
// Created by Brandon Withrow on 8/3/20.
//
import CoreGraphics
import CoreText
import Foundation
import QuartzCore
/// Needed for NSMutableParagraphStyle...
#if os(OSX)
import AppKit
#else
import UIKit
#endif
// MARK: - CoreTextRenderLayer
/// A CALayer subclass that renders text content using CoreText
final class CoreTextRenderLayer: CALayer {
// MARK: Public
public var text: String? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var font: CTFont? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var alignment: NSTextAlignment = .left {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var lineHeight: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var tracking: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var fillColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeWidth: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeOnTop = false {
didSet {
setNeedsLayout()
setNeedsDisplay()
}
}
public var preferredSize: CGSize? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public func sizeToFit() {
updateTextContent()
bounds = drawingRect
anchorPoint = drawingAnchor
setNeedsLayout()
setNeedsDisplay()
}
// MARK: Internal
override func action(forKey _: String) -> CAAction? {
nil
}
override func draw(in ctx: CGContext) {
guard let attributedString = attributedString else { return }
updateTextContent()
guard fillFrameSetter != nil || strokeFrameSetter != nil else { return }
ctx.textMatrix = .identity
ctx.setAllowsAntialiasing(true)
ctx.setAllowsFontSubpixelPositioning(true)
ctx.setAllowsFontSubpixelQuantization(true)
ctx.setShouldAntialias(true)
ctx.setShouldSubpixelPositionFonts(true)
ctx.setShouldSubpixelQuantizeFonts(true)
if contentsAreFlipped() {
ctx.translateBy(x: 0, y: drawingRect.height)
ctx.scaleBy(x: 1.0, y: -1.0)
}
let drawingPath = CGPath(rect: drawingRect, transform: nil)
let fillFrame: CTFrame?
if let setter = fillFrameSetter {
fillFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
fillFrame = nil
}
let strokeFrame: CTFrame?
if let setter = strokeFrameSetter {
strokeFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
strokeFrame = nil
}
if !strokeOnTop, let strokeFrame = strokeFrame {
CTFrameDraw(strokeFrame, ctx)
}
if let fillFrame = fillFrame {
CTFrameDraw(fillFrame, ctx)
}
if strokeOnTop, let strokeFrame = strokeFrame {
CTFrameDraw(strokeFrame, ctx)
}
}
// MARK: Private
private var drawingRect: CGRect = .zero
private var drawingAnchor: CGPoint = .zero
private var fillFrameSetter: CTFramesetter?
private var attributedString: NSAttributedString?
private var strokeFrameSetter: CTFramesetter?
private var needsContentUpdate = false
// Draws Debug colors for the font alignment.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
private func drawDebug(_ ctx: CGContext) {
if let font = font {
let ascent = CTFontGetAscent(font)
let descent = CTFontGetDescent(font)
let capHeight = CTFontGetCapHeight(font)
let leading = CTFontGetLeading(font)
// Ascent Red
ctx.setFillColor(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: 0, width: drawingRect.width, height: ascent))
// Descent Blue
ctx.setFillColor(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent, width: drawingRect.width, height: descent))
// Leading Yellow
ctx.setFillColor(CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent + descent, width: drawingRect.width, height: leading))
// Cap height Green
ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent - capHeight, width: drawingRect.width, height: capHeight))
if drawingRect.height - ascent + descent + leading > 0 {
// Remainder
ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 1, alpha: 0.5))
ctx
.fill(CGRect(
x: 0,
y: ascent + descent + leading,
width: drawingRect.width,
height: drawingRect.height - ascent + descent + leading))
}
}
}
private func updateTextContent() {
guard needsContentUpdate else { return }
needsContentUpdate = false
guard let font = font, let text = text, text.count > 0, fillColor != nil || strokeColor != nil else {
drawingRect = .zero
drawingAnchor = .zero
attributedString = nil
fillFrameSetter = nil
strokeFrameSetter = nil
return
}
// Get Font properties
let ascent = CTFontGetAscent(font)
let descent = CTFontGetDescent(font)
let capHeight = CTFontGetCapHeight(font)
let leading = CTFontGetLeading(font)
let minLineHeight = -(ascent + descent + leading)
// Calculate line spacing
let lineSpacing = max(CGFloat(minLineHeight) + lineHeight, CGFloat(minLineHeight))
// Build Attributes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.lineHeightMultiple = 1
paragraphStyle.maximumLineHeight = ascent + descent + leading
paragraphStyle.alignment = alignment
paragraphStyle.lineBreakMode = NSLineBreakMode.byWordWrapping
var attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.ligature: 0,
NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: tracking,
NSAttributedString.Key.paragraphStyle: paragraphStyle,
]
if let fillColor = fillColor {
attributes[NSAttributedString.Key.foregroundColor] = fillColor
}
let attrString = NSAttributedString(string: text, attributes: attributes)
attributedString = attrString
if fillColor != nil {
let setter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
fillFrameSetter = setter
} else {
fillFrameSetter = nil
}
if let strokeColor = strokeColor {
attributes[NSAttributedString.Key.foregroundColor] = nil
attributes[NSAttributedString.Key.strokeWidth] = strokeWidth
attributes[NSAttributedString.Key.strokeColor] = strokeColor
let strokeAttributedString = NSAttributedString(string: text, attributes: attributes)
strokeFrameSetter = CTFramesetterCreateWithAttributedString(strokeAttributedString as CFAttributedString)
} else {
strokeFrameSetter = nil
strokeWidth = 0
}
guard let setter = fillFrameSetter ?? strokeFrameSetter else {
return
}
// Calculate drawing size and anchor offset
let textAnchor: CGPoint
if let preferredSize = preferredSize {
drawingRect = CGRect(origin: .zero, size: preferredSize)
drawingRect.size.height += (ascent - capHeight)
drawingRect.size.height += descent
textAnchor = CGPoint(x: 0, y: ascent - capHeight)
} else {
let size = CTFramesetterSuggestFrameSizeWithConstraints(
setter,
CFRange(location: 0, length: attrString.length),
nil,
CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
nil)
switch alignment {
case .left:
textAnchor = CGPoint(x: 0, y: ascent)
case .right:
textAnchor = CGPoint(x: size.width, y: ascent)
case .center:
textAnchor = CGPoint(x: size.width * 0.5, y: ascent)
default:
textAnchor = .zero
}
drawingRect = CGRect(
x: 0,
y: 0,
width: ceil(size.width),
height: ceil(size.height))
}
// Now Calculate Anchor
drawingAnchor = CGPoint(
x: textAnchor.x.remap(fromLow: 0, fromHigh: drawingRect.size.width, toLow: 0, toHigh: 1),
y: textAnchor.y.remap(fromLow: 0, fromHigh: drawingRect.size.height, toLow: 0, toHigh: 1))
if fillFrameSetter != nil && strokeFrameSetter != nil {
drawingRect.size.width += strokeWidth
drawingRect.size.height += strokeWidth
}
}
}
@@ -0,0 +1,58 @@
//
// InvertedMatteLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/28/19.
//
import Foundation
import QuartzCore
/// A layer that inverses the alpha output of its input layer.
///
/// WARNING: This is experimental and probably not very performant.
final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
// MARK: Lifecycle
init(inputMatte: CompositionLayer) {
self.inputMatte = inputMatte
super.init()
inputMatte.layerDelegate = self
anchorPoint = .zero
bounds = inputMatte.bounds
setNeedsDisplay()
}
override init(layer: Any) {
guard let layer = layer as? InvertedMatteLayer else {
fatalError("init(layer:) wrong class.")
}
inputMatte = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let inputMatte: CompositionLayer?
let wrapperLayer = CALayer()
func frameUpdated(frame _: CGFloat) {
displayIfNeeded()
}
override func draw(in ctx: CGContext) {
guard let inputMatte = inputMatte else { return }
guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])
else { return }
ctx.setFillColor(fillColor)
ctx.fill(bounds)
ctx.setBlendMode(.destinationOut)
inputMatte.render(in: ctx)
}
}
@@ -0,0 +1,41 @@
//
// LayerFontProvider.swift
// Lottie
//
// Created by Brandon Withrow on 8/5/20.
// Copyright © 2020 YurtvilleProds. All rights reserved.
//
import Foundation
/// Connects a LottieFontProvider to a group of text layers
final class LayerFontProvider {
// MARK: Lifecycle
init(fontProvider: AnimationFontProvider) {
self.fontProvider = fontProvider
textLayers = []
reloadTexts()
}
// MARK: Internal
private(set) var textLayers: [TextCompositionLayer]
var fontProvider: AnimationFontProvider {
didSet {
reloadTexts()
}
}
func addTextLayers(_ layers: [TextCompositionLayer]) {
textLayers += layers
}
func reloadTexts() {
textLayers.forEach {
$0.fontProvider = fontProvider
}
}
}
@@ -0,0 +1,53 @@
//
// LayerImageProvider.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
/// Connects a LottieImageProvider to a group of image layers
final class LayerImageProvider {
// MARK: Lifecycle
init(imageProvider: AnimationImageProvider, assets: [String: ImageAsset]?) {
self.imageProvider = imageProvider
imageLayers = [ImageCompositionLayer]()
if let assets = assets {
imageAssets = assets
} else {
imageAssets = [:]
}
reloadImages()
}
// MARK: Internal
private(set) var imageLayers: [ImageCompositionLayer]
let imageAssets: [String: ImageAsset]
var imageProvider: AnimationImageProvider {
didSet {
reloadImages()
}
}
func addImageLayers(_ layers: [ImageCompositionLayer]) {
for layer in layers {
if imageAssets[layer.imageReferenceID] != nil {
/// Found a linking asset in our asset library. Add layer
imageLayers.append(layer)
}
}
}
func reloadImages() {
for imageLayer in imageLayers {
if let asset = imageAssets[imageLayer.imageReferenceID] {
imageLayer.image = imageProvider.imageForAsset(asset: asset)
}
}
}
}
@@ -0,0 +1,40 @@
//
// LayerTextProvider.swift
// lottie-ios-iOS
//
// Created by Alexandr Goncharov on 07/06/2019.
//
import Foundation
/// Connects a LottieTextProvider to a group of text layers
final class LayerTextProvider {
// MARK: Lifecycle
init(textProvider: AnimationTextProvider) {
self.textProvider = textProvider
textLayers = []
reloadTexts()
}
// MARK: Internal
private(set) var textLayers: [TextCompositionLayer]
var textProvider: AnimationTextProvider {
didSet {
reloadTexts()
}
}
func addTextLayers(_ layers: [TextCompositionLayer]) {
textLayers += layers
}
func reloadTexts() {
textLayers.forEach {
$0.textProvider = textProvider
}
}
}
@@ -0,0 +1,144 @@
//
// LayerTransformPropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - LayerTransformProperties
final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(transform: Transform) {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
var propertyMap: [String: AnyNodeProperty] = [
"Anchor Point" : anchor,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity,
]
if
let positionKeyframesX = transform.positionX?.keyframes,
let positionKeyframesY = transform.positionY?.keyframes
{
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
propertyMap["X Position"] = xPosition
propertyMap["Y Position"] = yPosition
positionX = xPosition
positionY = yPosition
position = nil
} else if let positionKeyframes = transform.position?.keyframes {
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
propertyMap["Position"] = position
self.position = position
positionX = nil
positionY = nil
} else {
position = nil
positionY = nil
positionX = nil
}
keypathProperties = propertyMap
properties = Array(propertyMap.values)
}
// MARK: Internal
let keypathProperties: [String: AnyNodeProperty]
var keypathName = "Transform"
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let position: NodeProperty<Vector3D>?
let positionX: NodeProperty<Vector1D>?
let positionY: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>
var childKeypaths: [KeypathSearchable] {
[]
}
}
// MARK: - LayerTransformNode
class LayerTransformNode: AnimatorNode {
// MARK: Lifecycle
init(transform: Transform) {
transformProperties = LayerTransformProperties(transform: transform)
}
// MARK: Internal
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
let transformProperties: LayerTransformProperties
var parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
var opacity: Float = 1
var localTransform: CATransform3D = CATransform3DIdentity
var globalTransform: CATransform3D = CATransform3DIdentity
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
transformProperties
}
func shouldRebuildOutputs(frame _: CGFloat) -> Bool {
hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame _: CGFloat) {
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
let position: CGPoint
if let point = transformProperties.position?.value.pointValue {
position = point
} else if
let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue
{
position = CGPoint(x: xPos, y: yPos)
} else {
position = .zero
}
localTransform = CATransform3D.makeTransform(
anchor: transformProperties.anchor.value.pointValue,
position: position,
scale: transformProperties.scale.value.sizeValue,
rotation: transformProperties.rotation.value.cgFloatValue,
skew: nil,
skewAxis: nil)
if let parentNode = parentNode as? LayerTransformNode {
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
} else {
globalTransform = localTransform
}
}
}
@@ -0,0 +1,97 @@
//
// ItemsExtension.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import Foundation
// MARK: - NodeTree
final class NodeTree {
var rootNode: AnimatorNode? = nil
var transform: ShapeTransform? = nil
var renderContainers: [ShapeContainerLayer] = []
var paths: [PathOutputNode] = []
var childrenNodes: [AnimatorNode] = []
}
extension Array where Element == ShapeItem {
func initializeNodeTree() -> NodeTree {
let nodeTree = NodeTree()
for item in self {
guard item.hidden == false, item.type != .unknown else { continue }
if let fill = item as? Fill {
let node = FillNode(parentNode: nodeTree.rootNode, fill: fill)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let stroke = item as? Stroke {
let node = StrokeNode(parentNode: nodeTree.rootNode, stroke: stroke)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let gradientFill = item as? GradientFill {
let node = GradientFillNode(parentNode: nodeTree.rootNode, gradientFill: gradientFill)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let gradientStroke = item as? GradientStroke {
let node = GradientStrokeNode(parentNode: nodeTree.rootNode, gradientStroke: gradientStroke)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let ellipse = item as? Ellipse {
let node = EllipseNode(parentNode: nodeTree.rootNode, ellipse: ellipse)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let rect = item as? Rectangle {
let node = RectangleNode(parentNode: nodeTree.rootNode, rectangle: rect)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let star = item as? Star {
switch star.starType {
case .none:
continue
case .polygon:
let node = PolygonNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
case .star:
let node = StarNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
}
} else if let shape = item as? Shape {
let node = ShapeNode(parentNode: nodeTree.rootNode, shape: shape)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let trim = item as? Trim {
let node = TrimPathNode(parentNode: nodeTree.rootNode, trim: trim, upstreamPaths: nodeTree.paths)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let xform = item as? ShapeTransform {
nodeTree.transform = xform
continue
} else if let group = item as? Group {
let tree = group.items.initializeNodeTree()
let node = GroupNode(name: group.name, parentNode: nodeTree.rootNode, tree: tree)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
/// Now add all child paths to current tree
nodeTree.paths.append(contentsOf: tree.paths)
nodeTree.renderContainers.append(node.container)
}
if let pathNode = nodeTree.rootNode as? PathNode {
//// Add path container to the node tree
nodeTree.paths.append(pathNode.pathOutput)
}
if let renderNode = nodeTree.rootNode as? RenderNode {
nodeTree.renderContainers.append(ShapeRenderLayer(renderer: renderNode.renderer))
}
}
return nodeTree
}
}
@@ -0,0 +1,55 @@
//
// NodeProperty.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A node property that holds a reference to a T ValueProvider and a T ValueContainer.
class NodeProperty<T>: AnyNodeProperty {
// MARK: Lifecycle
init(provider: AnyValueProvider) {
valueProvider = provider
originalValueProvider = valueProvider
typedContainer = ValueContainer<T>(provider.value(frame: 0) as! T)
typedContainer.setNeedsUpdate()
}
// MARK: Internal
var valueProvider: AnyValueProvider
var originalValueProvider: AnyValueProvider
var valueType: Any.Type { T.self }
var value: T {
typedContainer.outputValue
}
var valueContainer: AnyValueContainer {
typedContainer
}
func needsUpdate(frame: CGFloat) -> Bool {
valueContainer.needsUpdate || valueProvider.hasUpdate(frame: frame)
}
func setProvider(provider: AnyValueProvider) {
guard provider.valueType == valueType else { return }
valueProvider = provider
valueContainer.setNeedsUpdate()
}
func update(frame: CGFloat) {
typedContainer.setValue(valueProvider.value(frame: frame), forFrame: frame)
}
// MARK: Fileprivate
fileprivate var typedContainer: ValueContainer<T>
}
@@ -0,0 +1,50 @@
//
// AnyNodeProperty.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
// MARK: - AnyNodeProperty
/// A property of a node. The node property holds a provider and a container
protocol AnyNodeProperty {
/// Returns true if the property needs to recompute its stored value
func needsUpdate(frame: CGFloat) -> Bool
/// Updates the property for the frame
func update(frame: CGFloat)
/// The stored value container for the property
var valueContainer: AnyValueContainer { get }
/// The value provider for the property
var valueProvider: AnyValueProvider { get }
/// The original value provider for the property
var originalValueProvider: AnyValueProvider { get }
/// The Type of the value provider
var valueType: Any.Type { get }
/// Sets the value provider for the property.
func setProvider(provider: AnyValueProvider)
}
extension AnyNodeProperty {
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
func getValueOfType<T>() -> T? {
valueContainer.value as? T
}
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
func getValue() -> Any? {
valueContainer.value
}
}
@@ -0,0 +1,26 @@
//
// AnyValueContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// The container for the value of a property.
protocol AnyValueContainer: AnyObject {
/// The stored value of the container
var value: Any { get }
/// Notifies the provider that it should update its container
func setNeedsUpdate()
/// When true the container needs to have its value updated by its provider
var needsUpdate: Bool { get }
/// The frame time of the last provided update
var lastUpdateFrame: CGFloat { get }
}
@@ -0,0 +1,24 @@
//
// KeypathSettable.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import Foundation
import QuartzCore
/// Protocol that provides keypath search functionality. Returns all node properties associated with a keypath.
protocol KeypathSearchable {
/// The name of the Keypath
var keypathName: String { get }
/// A list of properties belonging to the keypath.
var keypathProperties: [String: AnyNodeProperty] { get }
/// Children Keypaths
var childKeypaths: [KeypathSearchable] { get }
var keypathLayer: CALayer? { get }
}
@@ -0,0 +1,44 @@
//
// NodePropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - NodePropertyMap
protocol NodePropertyMap {
var properties: [AnyNodeProperty] { get }
}
extension NodePropertyMap {
var childKeypaths: [KeypathSearchable] {
[]
}
var keypathLayer: CALayer? {
nil
}
/// Checks if the node's local contents need to be rebuilt.
func needsLocalUpdate(frame: CGFloat) -> Bool {
for property in properties {
if property.needsUpdate(frame: frame) {
return true
}
}
return false
}
/// Rebuilds only the local nodes that have an update for the frame
func updateNodeProperties(frame: CGFloat) {
properties.forEach { property in
property.update(frame: frame)
}
}
}
@@ -0,0 +1,47 @@
//
// ValueContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A container for a node value that is Typed to T.
class ValueContainer<T>: AnyValueContainer {
// MARK: Lifecycle
init(_ value: T) {
outputValue = value
}
// MARK: Internal
private(set) var lastUpdateFrame = CGFloat.infinity
fileprivate(set) var needsUpdate = true
var value: Any {
outputValue as Any
}
var outputValue: T {
didSet {
needsUpdate = false
}
}
func setValue(_ value: Any, forFrame: CGFloat) {
if let typedValue = value as? T {
needsUpdate = false
lastUpdateFrame = forFrame
outputValue = typedValue
}
}
func setNeedsUpdate() {
needsUpdate = true
}
}
@@ -0,0 +1,39 @@
//
// KeyframeGroupInterpolator.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import CoreGraphics
import Foundation
/// A value provider that produces an array of values from an array of Keyframe Interpolators
final class GroupInterpolator<ValueType>: ValueProvider where ValueType: Interpolatable {
// MARK: Lifecycle
/// Initialize with an array of array of keyframes.
init(keyframeGroups: ContiguousArray<ContiguousArray<Keyframe<ValueType>>>) {
keyframeInterpolators = ContiguousArray(keyframeGroups.map({ KeyframeInterpolator(keyframes: $0) }))
}
// MARK: Internal
let keyframeInterpolators: ContiguousArray<KeyframeInterpolator<ValueType>>
var valueType: Any.Type {
[ValueType].self
}
var storage: ValueProviderStorage<[ValueType]> {
.closure { frame in
self.keyframeInterpolators.map({ $0.value(frame: frame) as! ValueType })
}
}
func hasUpdate(frame: CGFloat) -> Bool {
let updated = keyframeInterpolators.first(where: { $0.hasUpdate(frame: frame) })
return updated != nil
}
}
@@ -0,0 +1,253 @@
//
// KeyframeInterpolator.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/15/19.
//
import CoreGraphics
import Foundation
// MARK: - KeyframeInterpolator
/// A value provider that produces a value at Time from a group of keyframes
final class KeyframeInterpolator<ValueType>: ValueProvider where ValueType: AnyInterpolatable {
// MARK: Lifecycle
init(keyframes: ContiguousArray<Keyframe<ValueType>>) {
self.keyframes = keyframes
}
// MARK: Internal
let keyframes: ContiguousArray<Keyframe<ValueType>>
var valueType: Any.Type {
ValueType.self
}
var storage: ValueProviderStorage<ValueType> {
.closure { [self] frame in
// First set the keyframe span for the frame.
updateSpanIndices(frame: frame)
lastUpdatedFrame = frame
// If only one keyframe return its value
let progress: CGFloat
let value: ValueType
if
let leading = leadingKeyframe,
let trailing = trailingKeyframe
{
/// We have leading and trailing keyframe.
progress = leading.interpolatedProgress(trailing, keyTime: frame)
value = leading.interpolate(to: trailing, progress: progress)
} else if let leading = leadingKeyframe {
progress = 0
value = leading.value
} else if let trailing = trailingKeyframe {
progress = 1
value = trailing.value
} else {
/// Satisfy the compiler.
progress = 0
value = keyframes[0].value
}
return value
}
}
/// Returns true to trigger a frame update for this interpolator.
///
/// An interpolator will be asked if it needs to update every frame.
/// If the interpolator needs updating it will be asked to compute its value for
/// the given frame.
///
/// Cases a keyframe should not be updated:
/// - If time is in span and leading keyframe is hold
/// - If time is after the last keyframe.
/// - If time is before the first keyframe
///
/// Cases for updating a keyframe:
/// - If time is in the span, and is not a hold
/// - If time is outside of the span, and there are more keyframes
/// - If a value delegate is set
/// - If leading and trailing are both nil.
func hasUpdate(frame: CGFloat) -> Bool {
if lastUpdatedFrame == nil {
return true
}
if
let leading = leadingKeyframe,
trailingKeyframe == nil,
leading.time < frame
{
/// Frame is after bounds of keyframes
return false
}
if
let trailing = trailingKeyframe,
leadingKeyframe == nil,
frame < trailing.time
{
/// Frame is before bounds of keyframes
return false
}
if
let leading = leadingKeyframe,
let trailing = trailingKeyframe,
leading.isHold,
leading.time < frame,
frame < trailing.time
{
return false
}
return true
}
// MARK: Fileprivate
fileprivate var lastUpdatedFrame: CGFloat?
fileprivate var leadingIndex: Int? = nil
fileprivate var trailingIndex: Int? = nil
fileprivate var leadingKeyframe: Keyframe<ValueType>? = nil
fileprivate var trailingKeyframe: Keyframe<ValueType>? = nil
/// Finds the appropriate Leading and Trailing keyframe index for the given time.
fileprivate func updateSpanIndices(frame: CGFloat) {
guard keyframes.count > 0 else {
leadingIndex = nil
trailingIndex = nil
leadingKeyframe = nil
trailingKeyframe = nil
return
}
// This function searches through the array to find the span of two keyframes
// that contain the current time.
//
// We could use Array.first(where:) but that would search through the entire array
// each frame.
// Instead we track the last used index and search either forwards or
// backwards from there. This reduces the iterations and complexity from
//
// O(n), where n is the length of the sequence to
// O(n), where n is the number of items after or before the last used index.
//
if keyframes.count == 1 {
/// Only one keyframe. Set it as first and move on.
leadingIndex = 0
trailingIndex = nil
leadingKeyframe = keyframes[0]
trailingKeyframe = nil
return
}
/// Sets the initial keyframes. This is often only needed for the first check.
if
leadingIndex == nil &&
trailingIndex == nil
{
if frame < keyframes[0].time {
/// Time is before the first keyframe. Set it as the trailing.
trailingIndex = 0
} else {
/// Time is after the first keyframe. Set the keyframe and the trailing.
leadingIndex = 0
trailingIndex = 1
}
}
if
let currentTrailing = trailingIndex,
keyframes[currentTrailing].time <= frame
{
/// Time is after the current span. Iterate forward.
var newLeading = currentTrailing
var keyframeFound = false
while !keyframeFound {
leadingIndex = newLeading
trailingIndex = keyframes.validIndex(newLeading + 1)
guard let trailing = trailingIndex else {
/// We have reached the end of our keyframes. Time is after the last keyframe.
keyframeFound = true
continue
}
if frame < keyframes[trailing].time {
/// Keyframe in current span.
keyframeFound = true
continue
}
/// Advance the array.
newLeading = trailing
}
} else if
let currentLeading = leadingIndex,
frame < keyframes[currentLeading].time
{
/// Time is before the current span. Iterate backwards
var newTrailing = currentLeading
var keyframeFound = false
while !keyframeFound {
leadingIndex = keyframes.validIndex(newTrailing - 1)
trailingIndex = newTrailing
guard let leading = leadingIndex else {
/// We have reached the end of our keyframes. Time is after the last keyframe.
keyframeFound = true
continue
}
if keyframes[leading].time <= frame {
/// Keyframe in current span.
keyframeFound = true
continue
}
/// Step back
newTrailing = leading
}
}
if let keyFrame = leadingIndex {
leadingKeyframe = keyframes[keyFrame]
} else {
leadingKeyframe = nil
}
if let keyFrame = trailingIndex {
trailingKeyframe = keyframes[keyFrame]
} else {
trailingKeyframe = nil
}
}
}
extension Array {
fileprivate func validIndex(_ index: Int) -> Int? {
if 0 <= index, index < endIndex {
return index
}
return nil
}
}
extension ContiguousArray {
fileprivate func validIndex(_ index: Int) -> Int? {
if 0 <= index, index < endIndex {
return index
}
return nil
}
}
@@ -0,0 +1,43 @@
//
// SingleValueProvider.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// Returns a value for every frame.
final class SingleValueProvider<ValueType: AnyInterpolatable>: ValueProvider {
// MARK: Lifecycle
init(_ value: ValueType) {
self.value = value
}
// MARK: Internal
var value: ValueType {
didSet {
hasUpdate = true
}
}
var storage: ValueProviderStorage<ValueType> {
.singleValue(value)
}
var valueType: Any.Type {
ValueType.self
}
func hasUpdate(frame _: CGFloat) -> Bool {
hasUpdate
}
// MARK: Private
private var hasUpdate = true
}
@@ -0,0 +1,281 @@
//
// TrimPathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/23/19.
//
import Foundation
import QuartzCore
// MARK: - TrimPathProperties
final class TrimPathProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(trim: Trim) {
keypathName = trim.name
start = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.start.keyframes))
end = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.end.keyframes))
offset = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.offset.keyframes))
type = trim.trimType
keypathProperties = [
"Start" : start,
"End" : end,
"Offset" : offset,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let keypathName: String
let start: NodeProperty<Vector1D>
let end: NodeProperty<Vector1D>
let offset: NodeProperty<Vector1D>
let type: TrimType
}
// MARK: - TrimPathNode
final class TrimPathNode: AnimatorNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, trim: Trim, upstreamPaths: [PathOutputNode]) {
outputNode = PassThroughOutputNode(parent: parentNode?.outputNode)
self.parentNode = parentNode
properties = TrimPathProperties(trim: trim)
self.upstreamPaths = upstreamPaths
}
// MARK: Internal
let properties: TrimPathProperties
let parentNode: AnimatorNode?
let outputNode: NodeOutput
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
func forceUpstreamOutputUpdates() -> Bool {
hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame: CGFloat) {
/// Make sure there is a trim.
let startValue = properties.start.value.cgFloatValue * 0.01
let endValue = properties.end.value.cgFloatValue * 0.01
let start = min(startValue, endValue)
let end = max(startValue, endValue)
let offset = properties.offset.value.cgFloatValue.truncatingRemainder(dividingBy: 360) / 360
/// No need to trim, it's a full path
if start == 0, end == 1 {
return
}
/// All paths are empty.
if start == end {
for pathContainer in upstreamPaths {
pathContainer.removePaths(updateFrame: frame)
}
return
}
if properties.type == .simultaneously {
/// Just trim each path
for pathContainer in upstreamPaths {
let pathObjects = pathContainer.removePaths(updateFrame: frame)
for path in pathObjects {
// We are treating each compount path as an individual path. Its subpaths are treated as a whole.
pathContainer.appendPath(
path.trim(fromPosition: start, toPosition: end, offset: offset, trimSimultaneously: false),
updateFrame: frame)
}
}
return
}
/// Individual path trimming.
/// Brace yourself for the below code.
/// Normalize lengths with offset.
var startPosition = (start + offset).truncatingRemainder(dividingBy: 1)
var endPosition = (end + 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
}
/// First get the total length of all paths.
var totalLength: CGFloat = 0
upstreamPaths.forEach({ totalLength = totalLength + $0.totalLength })
/// Now determine the start and end cut lengths
let startLength = startPosition * totalLength
let endLength = endPosition * totalLength
var pathStart: CGFloat = 0
/// Now loop through all path containers
for pathContainer in upstreamPaths {
let pathEnd = pathStart + pathContainer.totalLength
if
!startLength.isInRange(pathStart, pathEnd) &&
endLength.isInRange(pathStart, pathEnd)
{
// pathStart|=======E----------------------|pathEnd
// Cut path components, removing after end.
let pathCutLength = endLength - pathStart
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if pathCutLength < subpathEnd {
/// This is the subpath that needs to be cut.
let cutLength = pathCutLength - subpathStart
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
} else {
/// Add to container and move on
pathContainer.appendPath(path, updateFrame: frame)
}
if pathCutLength == subpathEnd {
/// Right on the end. The next subpath is not included. Break.
break
}
subpathStart = subpathEnd
}
} else if
!endLength.isInRange(pathStart, pathEnd) &&
startLength.isInRange(pathStart, pathEnd)
{
// pathStart|-------S======================|pathEnd
//
// Cut path components, removing before beginning.
let pathCutLength = startLength - pathStart
// Clear paths from container
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if subpathStart < pathCutLength, pathCutLength < subpathEnd {
/// This is the subpath that needs to be cut.
let cutLength = pathCutLength - subpathStart
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
} else if pathCutLength <= subpathStart {
pathContainer.appendPath(path, updateFrame: frame)
}
subpathStart = subpathEnd
}
} else if
endLength.isInRange(pathStart, pathEnd) &&
startLength.isInRange(pathStart, pathEnd)
{
// pathStart|-------S============E---------|endLength
// pathStart|=====E----------------S=======|endLength
// trim from path beginning to endLength.
// Cut path components, removing before beginnings.
let startCutLength = startLength - pathStart
let endCutLength = endLength - pathStart
// Clear paths from container
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if
!startCutLength.isInRange(subpathStart, subpathEnd) &&
!endCutLength.isInRange(subpathStart, subpathEnd)
{
// The whole path is included. Add
// S|==============================|E
pathContainer.appendPath(path, updateFrame: frame)
} else if
startCutLength.isInRange(subpathStart, subpathEnd) &&
!endCutLength.isInRange(subpathStart, subpathEnd)
{
/// The start of the path needs to be trimmed
// |-------S======================|E
let cutLength = startCutLength - subpathStart
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
} else if
!startCutLength.isInRange(subpathStart, subpathEnd) &&
endCutLength.isInRange(subpathStart, subpathEnd)
{
// S|=======E----------------------|
let cutLength = endCutLength - subpathStart
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
} else if
startCutLength.isInRange(subpathStart, subpathEnd) &&
endCutLength.isInRange(subpathStart, subpathEnd)
{
// |-------S============E---------|
let cutFromLength = startCutLength - subpathStart
let cutToLength = endCutLength - subpathStart
let newPath = path.trim(
fromPosition: cutFromLength / path.length,
toPosition: cutToLength / path.length,
offset: 0,
trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
}
subpathStart = subpathEnd
}
} else if
(endLength <= pathStart && pathEnd <= startLength) ||
(startLength <= pathStart && endLength <= pathStart) ||
(pathEnd <= startLength && pathEnd <= endLength)
{
/// The Path needs to be cleared
pathContainer.removePaths(updateFrame: frame)
}
pathStart = pathEnd
}
}
// MARK: Fileprivate
fileprivate let upstreamPaths: [PathOutputNode]
}
@@ -0,0 +1,76 @@
//
// TransformNodeOutput.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
import QuartzCore
class GroupOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?, rootNode: NodeOutput?) {
self.parent = parent
self.rootNode = rootNode
}
// MARK: Internal
let parent: NodeOutput?
let rootNode: NodeOutput?
var isEnabled = true
private(set) var outputPath: CGPath? = nil
private(set) var transform: CATransform3D = CATransform3DIdentity
func setTransform(_ xform: CATransform3D, forFrame _: CGFloat) {
transform = xform
outputPath = nil
}
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
guard isEnabled else {
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
outputPath = parent?.outputPath
return upstreamUpdates
}
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
if upstreamUpdates {
outputPath = nil
}
let rootUpdates = rootNode?.hasOutputUpdates(forFrame) ?? false
if rootUpdates {
outputPath = nil
}
var localUpdates = false
if outputPath == nil {
localUpdates = true
let newPath = CGMutablePath()
if let parentNode = parent, let parentPath = parentNode.outputPath {
/// First add parent path.
newPath.addPath(parentPath)
}
var xform = CATransform3DGetAffineTransform(transform)
if
let rootNode = rootNode,
let rootPath = rootNode.outputPath,
let xformedPath = rootPath.copy(using: &xform)
{
/// Now add root path. Note root path is transformed.
newPath.addPath(xformedPath)
}
outputPath = newPath
}
return upstreamUpdates || localUpdates
}
}
@@ -0,0 +1,47 @@
//
// PassThroughOutputNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
class PassThroughOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?) {
self.parent = parent
}
// MARK: Internal
let parent: NodeOutput?
var hasUpdate = false
var isEnabled = true
var outputPath: CGPath? {
if let parent = parent {
return parent.outputPath
}
return nil
}
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
/// Changes to this node do not affect downstream nodes.
let parentUpdate = parent?.hasOutputUpdates(forFrame) ?? false
/// Changes to upstream nodes do, however, affect this nodes state.
hasUpdate = hasUpdate || parentUpdate
return parentUpdate
}
func hasRenderUpdates(_ forFrame: CGFloat) -> Bool {
/// Return true if there are upstream updates or if this node has updates
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
hasUpdate = hasUpdate || upstreamUpdates
return hasUpdate
}
}
@@ -0,0 +1,90 @@
//
// PathNodeOutput.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A node that has an output of a BezierPath
class PathOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?) {
self.parent = parent
}
// MARK: Internal
let parent: NodeOutput?
fileprivate(set) var outputPath: CGPath? = nil
var lastUpdateFrame: CGFloat? = nil
var lastPathBuildFrame: CGFloat? = nil
var isEnabled = true
fileprivate(set) var totalLength: CGFloat = 0
fileprivate(set) var pathObjects: [CompoundBezierPath] = []
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
guard isEnabled else {
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
outputPath = parent?.outputPath
return upstreamUpdates
}
/// Ask if parent was updated
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
/// If parent was updated and the path hasn't been built for this frame, clear the path.
if upstreamUpdates && lastPathBuildFrame != forFrame {
outputPath = nil
}
if outputPath == nil {
/// If the path is clear, build the new path.
lastPathBuildFrame = forFrame
let newPath = CGMutablePath()
if let parentNode = parent, let parentPath = parentNode.outputPath {
newPath.addPath(parentPath)
}
for path in pathObjects {
for subPath in path.paths {
newPath.addPath(subPath.cgPath())
}
}
outputPath = newPath
}
/// Return true if there were upstream updates or if this node was updated.
return upstreamUpdates || (lastUpdateFrame == forFrame)
}
@discardableResult
func removePaths(updateFrame: CGFloat?) -> [CompoundBezierPath] {
lastUpdateFrame = updateFrame
let returnPaths = pathObjects
outputPath = nil
totalLength = 0
pathObjects = []
return returnPaths
}
func setPath(_ path: BezierPath, updateFrame: CGFloat) {
lastUpdateFrame = updateFrame
outputPath = nil
totalLength = path.length
pathObjects = [CompoundBezierPath(path: path)]
}
func appendPath(_ path: CompoundBezierPath, updateFrame: CGFloat) {
lastUpdateFrame = updateFrame
outputPath = nil
totalLength = totalLength + path.length
pathObjects.append(path)
}
}
@@ -0,0 +1,71 @@
//
// FillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
import QuartzCore
extension FillRule {
var cgFillRule: CGPathFillRule {
switch self {
case .evenOdd:
return .evenOdd
default:
return .winding
}
}
var caFillRule: CAShapeLayerFillRule {
switch self {
case .evenOdd:
return CAShapeLayerFillRule.evenOdd
default:
return CAShapeLayerFillRule.nonZero
}
}
}
// MARK: - FillRenderer
/// A rendered for a Path Fill
final class FillRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = false
var color: CGColor? {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var fillRule: FillRule = .none {
didSet {
hasUpdate = true
}
}
func render(_: CGContext) {
// do nothing
}
func setupSublayers(layer _: CAShapeLayer) {
// do nothing
}
func updateShapeLayer(layer: CAShapeLayer) {
layer.fillColor = color
layer.opacity = Float(opacity)
layer.fillRule = fillRule.caFillRule
hasUpdate = false
}
}
@@ -0,0 +1,242 @@
//
// GradientFillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
// MARK: - GradientFillLayer
private final class GradientFillLayer: CALayer {
var start: CGPoint = .zero {
didSet {
setNeedsDisplay()
}
}
var numberOfColors = 0 {
didSet {
setNeedsDisplay()
}
}
var colors: [CGFloat] = [] {
didSet {
setNeedsDisplay()
}
}
var end: CGPoint = .zero {
didSet {
setNeedsDisplay()
}
}
var type: GradientType = .none {
didSet {
setNeedsDisplay()
}
}
override func draw(in ctx: CGContext) {
var alphaColors = [CGColor]()
var alphaLocations = [CGFloat]()
var gradientColors = [CGColor]()
var colorLocations = [CGFloat]()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let maskColorSpace = CGColorSpaceCreateDeviceGray()
for i in 0..<numberOfColors {
let ix = i * 4
if
colors.count > ix, let color = CGColor(
colorSpace: colorSpace,
components: [colors[ix + 1], colors[ix + 2], colors[ix + 3], 1])
{
gradientColors.append(color)
colorLocations.append(colors[ix])
}
}
var drawMask = false
for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) {
let alpha = colors[i + 1]
if alpha < 1 {
drawMask = true
}
if let color = CGColor(colorSpace: maskColorSpace, components: [alpha, 1]) {
alphaLocations.append(colors[i])
alphaColors.append(color)
}
}
/// First draw a mask is necessary.
if drawMask {
guard
let maskGradient = CGGradient(
colorsSpace: maskColorSpace,
colors: alphaColors as CFArray,
locations: alphaLocations),
let maskContext = CGContext(
data: nil,
width: ctx.width,
height: ctx.height,
bitsPerComponent: 8,
bytesPerRow: ctx.width,
space: maskColorSpace,
bitmapInfo: 0) else { return }
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(maskContext.height))
maskContext.concatenate(flipVertical)
maskContext.concatenate(ctx.ctm)
if type == .linear {
maskContext.drawLinearGradient(
maskGradient,
start: start,
end: end,
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
maskContext.drawRadialGradient(
maskGradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
/// Clips the gradient
if let alphaMask = maskContext.makeImage() {
ctx.clip(to: ctx.boundingBoxOfClipPath, mask: alphaMask)
}
}
/// Now draw the gradient
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: colorLocations)
else { return }
if type == .linear {
ctx.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
ctx.drawRadialGradient(
gradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
}
}
// MARK: - GradientFillRenderer
/// A rendered for a Path Fill
final class GradientFillRenderer: PassThroughOutputNode, Renderable {
// MARK: Lifecycle
override init(parent: NodeOutput?) {
super.init(parent: parent)
maskLayer.fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1])
gradientLayer.mask = maskLayer
maskLayer.actions = [
"startPoint" : NSNull(),
"endPoint" : NSNull(),
"opacity" : NSNull(),
"locations" : NSNull(),
"colors" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"isRadial" : NSNull(),
"path" : NSNull(),
]
gradientLayer.actions = maskLayer.actions
}
// MARK: Internal
var shouldRenderInContext = false
var start: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var numberOfColors = 0 {
didSet {
hasUpdate = true
}
}
var colors: [CGFloat] = [] {
didSet {
hasUpdate = true
}
}
var end: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var type: GradientType = .none {
didSet {
hasUpdate = true
}
}
func render(_: CGContext) {
// do nothing
}
func setupSublayers(layer: CAShapeLayer) {
layer.addSublayer(gradientLayer)
layer.fillColor = nil
}
func updateShapeLayer(layer: CAShapeLayer) {
hasUpdate = false
guard let path = layer.path else {
return
}
let frame = path.boundingBox
let anchor = CGPoint(
x: -frame.origin.x / frame.size.width,
y: -frame.origin.y / frame.size.height)
maskLayer.path = path
maskLayer.bounds = path.boundingBox
maskLayer.anchorPoint = anchor
gradientLayer.bounds = maskLayer.bounds
gradientLayer.anchorPoint = anchor
// setup gradient properties
gradientLayer.start = start
gradientLayer.end = end
gradientLayer.numberOfColors = numberOfColors
gradientLayer.colors = colors
gradientLayer.opacity = Float(opacity)
gradientLayer.type = type
}
// MARK: Private
private let gradientLayer = GradientFillLayer()
private let maskLayer = CAShapeLayer()
}
@@ -0,0 +1,66 @@
//
// GradientStrokeRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
// MARK: - Renderer
final class GradientStrokeRenderer: PassThroughOutputNode, Renderable {
// MARK: Lifecycle
override init(parent: NodeOutput?) {
strokeRender = StrokeRenderer(parent: nil)
gradientRender = LegacyGradientFillRenderer(parent: nil)
strokeRender.color = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1])
super.init(parent: parent)
}
// MARK: Internal
var shouldRenderInContext = true
let strokeRender: StrokeRenderer
let gradientRender: LegacyGradientFillRenderer
override func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
let updates = super.hasOutputUpdates(forFrame)
return updates || strokeRender.hasUpdate || gradientRender.hasUpdate
}
func updateShapeLayer(layer _: CAShapeLayer) {
/// Not Applicable
}
func setupSublayers(layer _: CAShapeLayer) {
/// Not Applicable
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
strokeRender.hasUpdate = false
hasUpdate = false
gradientRender.hasUpdate = false
strokeRender.setupForStroke(inContext)
inContext.replacePathWithStrokedPath()
/// Now draw the gradient.
gradientRender.render(inContext)
}
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
strokeRender.renderBoundsFor(boundingBox)
}
}
@@ -0,0 +1,153 @@
//
// LegacyGradientFillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// A rendered for a Path Fill
final class LegacyGradientFillRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = true
var start: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var numberOfColors = 0 {
didSet {
hasUpdate = true
}
}
var colors: [CGFloat] = [] {
didSet {
hasUpdate = true
}
}
var end: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var type: GradientType = .none {
didSet {
hasUpdate = true
}
}
func updateShapeLayer(layer _: CAShapeLayer) {
// Not applicable
}
func setupSublayers(layer _: CAShapeLayer) {
// Not applicable
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
hasUpdate = false
var alphaColors = [CGColor]()
var alphaLocations = [CGFloat]()
var gradientColors = [CGColor]()
var colorLocations = [CGFloat]()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let maskColorSpace = CGColorSpaceCreateDeviceGray()
for i in 0..<numberOfColors {
let ix = i * 4
if
colors.count > ix, let color = CGColor(
colorSpace: colorSpace,
components: [colors[ix + 1], colors[ix + 2], colors[ix + 3], 1])
{
gradientColors.append(color)
colorLocations.append(colors[ix])
}
}
var drawMask = false
for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) {
let alpha = colors[i + 1]
if alpha < 1 {
drawMask = true
}
if let color = CGColor(colorSpace: maskColorSpace, components: [alpha, 1]) {
alphaLocations.append(colors[i])
alphaColors.append(color)
}
}
inContext.setAlpha(opacity)
inContext.clip()
/// First draw a mask is necessary.
if drawMask {
guard
let maskGradient = CGGradient(
colorsSpace: maskColorSpace,
colors: alphaColors as CFArray,
locations: alphaLocations),
let maskContext = CGContext(
data: nil,
width: inContext.width,
height: inContext.height,
bitsPerComponent: 8,
bytesPerRow: inContext.width,
space: maskColorSpace,
bitmapInfo: 0) else { return }
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(maskContext.height))
maskContext.concatenate(flipVertical)
maskContext.concatenate(inContext.ctm)
if type == .linear {
maskContext.drawLinearGradient(
maskGradient,
start: start,
end: end,
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
maskContext.drawRadialGradient(
maskGradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
/// Clips the gradient
if let alphaMask = maskContext.makeImage() {
inContext.clip(to: inContext.boundingBoxOfClipPath, mask: alphaMask)
}
}
/// Now draw the gradient
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: colorLocations)
else { return }
if type == .linear {
inContext.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
inContext.drawRadialGradient(
gradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
}
}
@@ -0,0 +1,166 @@
//
// StrokeRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
extension LineJoin {
var cgLineJoin: CGLineJoin {
switch self {
case .bevel:
return .bevel
case .none:
return .miter
case .miter:
return .miter
case .round:
return .round
}
}
var caLineJoin: CAShapeLayerLineJoin {
switch self {
case .none:
return CAShapeLayerLineJoin.miter
case .miter:
return CAShapeLayerLineJoin.miter
case .round:
return CAShapeLayerLineJoin.round
case .bevel:
return CAShapeLayerLineJoin.bevel
}
}
}
extension LineCap {
var cgLineCap: CGLineCap {
switch self {
case .none:
return .butt
case .butt:
return .butt
case .round:
return .round
case .square:
return .square
}
}
var caLineCap: CAShapeLayerLineCap {
switch self {
case .none:
return CAShapeLayerLineCap.butt
case .butt:
return CAShapeLayerLineCap.butt
case .round:
return CAShapeLayerLineCap.round
case .square:
return CAShapeLayerLineCap.square
}
}
}
// MARK: - StrokeRenderer
/// A rendered that renders a stroke on a path.
final class StrokeRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = false
var color: CGColor? {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var width: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var miterLimit: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var lineCap: LineCap = .none {
didSet {
hasUpdate = true
}
}
var lineJoin: LineJoin = .none {
didSet {
hasUpdate = true
}
}
var dashPhase: CGFloat? {
didSet {
hasUpdate = true
}
}
var dashLengths: [CGFloat]? {
didSet {
hasUpdate = true
}
}
func setupSublayers(layer _: CAShapeLayer) {
// empty
}
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
boundingBox.insetBy(dx: -width, dy: -width)
}
func setupForStroke(_ inContext: CGContext) {
inContext.setLineWidth(width)
inContext.setMiterLimit(miterLimit)
inContext.setLineCap(lineCap.cgLineCap)
inContext.setLineJoin(lineJoin.cgLineJoin)
if let dashPhase = dashPhase, let lengths = dashLengths {
inContext.setLineDash(phase: dashPhase, lengths: lengths)
} else {
inContext.setLineDash(phase: 0, lengths: [])
}
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
guard let color = color else { return }
hasUpdate = false
setupForStroke(inContext)
inContext.setAlpha(opacity)
inContext.setStrokeColor(color)
inContext.strokePath()
}
func updateShapeLayer(layer: CAShapeLayer) {
layer.strokeColor = color
layer.opacity = Float(opacity)
layer.lineWidth = width
layer.lineJoin = lineJoin.caLineJoin
layer.lineCap = lineCap.caLineCap
layer.lineDashPhase = dashPhase ?? 0
layer.fillColor = nil
if let dashPattern = dashLengths {
layer.lineDashPattern = dashPattern.map({ NSNumber(value: Double($0)) })
}
}
}
@@ -0,0 +1,139 @@
//
// 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<Vector3D>
let size: NodeProperty<Vector3D>
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
}
}
@@ -0,0 +1,170 @@
//
// PolygonNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - PolygonNodeProperties
final class PolygonNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(star: Star) {
keypathName = star.name
direction = star.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes))
outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes))
outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes))
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Rotation" : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
var childKeypaths: [KeypathSearchable] = []
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let outerRadius: NodeProperty<Vector1D>
let outerRoundedness: NodeProperty<Vector1D>
let rotation: NodeProperty<Vector1D>
let points: NodeProperty<Vector1D>
}
// MARK: - PolygonNode
final class PolygonNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, star: Star) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = PolygonNodeProperties(star: star)
self.parentNode = parentNode
}
// MARK: Internal
/// Magic number needed for constructing path.
static let PolygonConstant: CGFloat = 0.25
let properties: PolygonNodeProperties
let pathOutput: PathOutputNode
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) {
let path = BezierPath.polygon(
position: properties.position.value.pointValue,
numberOfPoints: properties.points.value.cgFloatValue,
outerRadius: properties.outerRadius.value.cgFloatValue,
outerRoundedness: properties.outerRoundedness.value.cgFloatValue,
rotation: properties.rotation.value.cgFloatValue,
direction: properties.direction)
pathOutput.setPath(path, updateFrame: frame)
}
}
extension BezierPath {
/// Creates a `BezierPath` in the shape of a polygon
static func polygon(
position: CGPoint,
numberOfPoints: CGFloat,
outerRadius: CGFloat,
outerRoundedness inputOuterRoundedness: CGFloat,
rotation: CGFloat,
direction: PathDirection)
-> BezierPath
{
var currentAngle = (rotation - 90).toRadians()
let anglePerPoint = ((2 * CGFloat.pi) / numberOfPoints)
let outerRoundedness = inputOuterRoundedness * 0.01
var point = CGPoint(
x: outerRadius * cos(currentAngle),
y: outerRadius * sin(currentAngle))
var vertices = [CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero)]
var previousPoint = point
currentAngle += anglePerPoint;
for _ in 0..<Int(ceil(numberOfPoints)) {
previousPoint = point
point = CGPoint(
x: outerRadius * cos(currentAngle),
y: outerRadius * sin(currentAngle))
if outerRoundedness != 0 {
let cp1Theta = (atan2(previousPoint.y, previousPoint.x) - CGFloat.pi / 2)
let cp1Dx = cos(cp1Theta);
let cp1Dy = sin(cp1Theta);
let cp2Theta = (atan2(point.y, point.x) - CGFloat.pi / 2)
let cp2Dx = cos(cp2Theta)
let cp2Dy = sin(cp2Theta)
let cp1 = CGPoint(
x: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp1Dx,
y: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp1Dy)
let cp2 = CGPoint(
x: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp2Dx,
y: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp2Dy)
let previousVertex = vertices[vertices.endIndex - 1]
vertices[vertices.endIndex - 1] = CurveVertex(
previousVertex.inTangent,
previousVertex.point,
previousVertex.point - cp1)
vertices.append(CurveVertex(point: point + position, inTangentRelative: cp2, outTangentRelative: .zero))
} else {
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
}
currentAngle += anglePerPoint;
}
let reverse = direction == .counterClockwise
if reverse {
vertices = vertices.reversed()
}
var path = BezierPath()
for vertex in vertices {
path.addVertex(reverse ? vertex.reversed() : vertex)
}
path.close()
return path
}
}
@@ -0,0 +1,225 @@
//
// RectNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import CoreGraphics
import Foundation
// MARK: - RectNodeProperties
final class RectNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(rectangle: Rectangle) {
keypathName = rectangle.name
direction = rectangle.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.position.keyframes))
size = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.size.keyframes))
cornerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.cornerRadius.keyframes))
keypathProperties = [
"Position" : position,
"Size" : size,
"Roundness" : cornerRadius,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let size: NodeProperty<Vector3D>
let cornerRadius: NodeProperty<Vector1D>
}
// MARK: - RectangleNode
final class RectangleNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, rectangle: Rectangle) {
properties = RectNodeProperties(rectangle: rectangle)
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
self.parentNode = parentNode
}
// MARK: Internal
let properties: RectNodeProperties
let pathOutput: PathOutputNode
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(
.rectangle(
position: properties.position.value.pointValue,
size: properties.size.value.sizeValue,
cornerRadius: properties.cornerRadius.value.cgFloatValue,
direction: properties.direction),
updateFrame: frame)
}
}
// MARK: - BezierPath + rectangle
extension BezierPath {
/// Constructs a `BezierPath` in the shape of a rectangle, optionally with rounded corners
static func rectangle(
position: CGPoint,
size inputSize: CGSize,
cornerRadius: CGFloat,
direction: PathDirection)
-> BezierPath
{
let size = inputSize * 0.5
let radius = min(min(cornerRadius, size.width) , size.height)
var bezierPath = BezierPath()
let points: [CurveVertex]
if radius <= 0 {
/// No Corners
points = [
/// Lead In
CurveVertex(
point: CGPoint(x: size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 1
CurveVertex(
point: CGPoint(x: size.width, y: size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 2
CurveVertex(
point: CGPoint(x: -size.width, y: size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 3
CurveVertex(
point: CGPoint(x: -size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 4
CurveVertex(
point: CGPoint(x: size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
]
} else {
let controlPoint = radius * EllipseNode.ControlPointConstant
points = [
/// Lead In
CurveVertex(
CGPoint(x: radius, y: 0),
CGPoint(x: radius, y: 0),
CGPoint(x: radius, y: 0))
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
/// Corner 1
CurveVertex(
CGPoint(x: radius, y: 0), // In tangent
CGPoint(x: radius, y: 0), // Point
CGPoint(x: radius, y: controlPoint))
.translated(CGPoint(x: -radius, y: -radius))
.translated(CGPoint(x: size.width, y: size.height))
.translated(position),
CurveVertex(
CGPoint(x: controlPoint, y: radius), // In tangent
CGPoint(x: 0, y: radius), // Point
CGPoint(x: 0, y: radius)) // Out Tangent
.translated(CGPoint(x: -radius, y: -radius))
.translated(CGPoint(x: size.width, y: size.height))
.translated(position),
/// Corner 2
CurveVertex(
CGPoint(x: 0, y: radius), // In tangent
CGPoint(x: 0, y: radius), // Point
CGPoint(x: -controlPoint, y: radius))// Out tangent
.translated(CGPoint(x: radius, y: -radius))
.translated(CGPoint(x: -size.width, y: size.height))
.translated(position),
CurveVertex(
CGPoint(x: -radius, y: controlPoint), // In tangent
CGPoint(x: -radius, y: 0), // Point
CGPoint(x: -radius, y: 0)) // Out tangent
.translated(CGPoint(x: radius, y: -radius))
.translated(CGPoint(x: -size.width, y: size.height))
.translated(position),
/// Corner 3
CurveVertex(
CGPoint(x: -radius, y: 0), // In tangent
CGPoint(x: -radius, y: 0), // Point
CGPoint(x: -radius, y: -controlPoint)) // Out tangent
.translated(CGPoint(x: radius, y: radius))
.translated(CGPoint(x: -size.width, y: -size.height))
.translated(position),
CurveVertex(
CGPoint(x: -controlPoint, y: -radius), // In tangent
CGPoint(x: 0, y: -radius), // Point
CGPoint(x: 0, y: -radius)) // Out tangent
.translated(CGPoint(x: radius, y: radius))
.translated(CGPoint(x: -size.width, y: -size.height))
.translated(position),
/// Corner 4
CurveVertex(
CGPoint(x: 0, y: -radius), // In tangent
CGPoint(x: 0, y: -radius), // Point
CGPoint(x: controlPoint, y: -radius)) // Out tangent
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
CurveVertex(
CGPoint(x: radius, y: -controlPoint), // In tangent
CGPoint(x: radius, y: 0), // Point
CGPoint(x: radius, y: 0)) // Out tangent
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
]
}
let reversed = direction == .counterClockwise
let pathPoints = reversed ? points.reversed() : points
for point in pathPoints {
bezierPath.addVertex(reversed ? point.reversed() : point)
}
bezierPath.close()
return bezierPath
}
}
@@ -0,0 +1,74 @@
//
// PathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/16/19.
//
import CoreGraphics
import Foundation
// MARK: - ShapeNodeProperties
final class ShapeNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(shape: Shape) {
keypathName = shape.name
path = NodeProperty(provider: KeyframeInterpolator(keyframes: shape.path.keyframes))
keypathProperties = [
"Path" : path,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let path: NodeProperty<BezierPath>
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - ShapeNode
final class ShapeNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, shape: Shape) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = ShapeNodeProperties(shape: shape)
self.parentNode = parentNode
}
// MARK: Internal
let properties: ShapeNodeProperties
let pathOutput: PathOutputNode
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(properties.path.value, updateFrame: frame)
}
}
@@ -0,0 +1,222 @@
//
// StarNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - StarNodeProperties
final class StarNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(star: Star) {
keypathName = star.name
direction = star.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes))
outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes))
outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes))
if let innerRadiusKeyframes = star.innerRadius?.keyframes {
innerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: innerRadiusKeyframes))
} else {
innerRadius = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
if let innderRoundedness = star.innerRoundness?.keyframes {
innerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: innderRoundedness))
} else {
innerRoundedness = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Inner Radius" : innerRadius,
"Inner Roundedness" : innerRoundedness,
"Rotation" : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let outerRadius: NodeProperty<Vector1D>
let outerRoundedness: NodeProperty<Vector1D>
let innerRadius: NodeProperty<Vector1D>
let innerRoundedness: NodeProperty<Vector1D>
let rotation: NodeProperty<Vector1D>
let points: NodeProperty<Vector1D>
}
// MARK: - StarNode
final class StarNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, star: Star) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = StarNodeProperties(star: star)
self.parentNode = parentNode
}
// MARK: Internal
/// Magic number needed for building path data
static let PolystarConstant: CGFloat = 0.47829
let properties: StarNodeProperties
let pathOutput: PathOutputNode
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) {
let path = BezierPath.star(
position: properties.position.value.pointValue,
outerRadius: properties.outerRadius.value.cgFloatValue,
innerRadius: properties.innerRadius.value.cgFloatValue,
outerRoundedness: properties.outerRoundedness.value.cgFloatValue,
innerRoundedness: properties.innerRoundedness.value.cgFloatValue,
numberOfPoints: properties.points.value.cgFloatValue,
rotation: properties.rotation.value.cgFloatValue,
direction: properties.direction)
pathOutput.setPath(path, updateFrame: frame)
}
}
extension BezierPath {
/// Constructs a `BezierPath` in the shape of a star
static func star(
position: CGPoint,
outerRadius: CGFloat,
innerRadius: CGFloat,
outerRoundedness inoutOuterRoundedness: CGFloat,
innerRoundedness inputInnerRoundedness: CGFloat,
numberOfPoints: CGFloat,
rotation: CGFloat,
direction: PathDirection)
-> BezierPath
{
var currentAngle = (rotation - 90).toRadians()
let anglePerPoint = (2 * CGFloat.pi) / numberOfPoints
let halfAnglePerPoint = anglePerPoint / 2.0
let partialPointAmount = numberOfPoints - floor(numberOfPoints)
let outerRoundedness = inoutOuterRoundedness * 0.01
let innerRoundedness = inputInnerRoundedness * 0.01
var point: CGPoint = .zero
var partialPointRadius: CGFloat = 0
if partialPointAmount != 0 {
currentAngle += halfAnglePerPoint * (1 - partialPointAmount)
partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius)
point.x = (partialPointRadius * cos(currentAngle))
point.y = (partialPointRadius * sin(currentAngle))
currentAngle += anglePerPoint * partialPointAmount / 2
} else {
point.x = (outerRadius * cos(currentAngle))
point.y = (outerRadius * sin(currentAngle))
currentAngle += halfAnglePerPoint
}
var vertices = [CurveVertex]()
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
var previousPoint = point
var longSegment = false
let numPoints = Int(ceil(numberOfPoints) * 2)
for i in 0..<numPoints {
var radius = longSegment ? outerRadius : innerRadius
var dTheta = halfAnglePerPoint
if partialPointRadius != 0 && i == numPoints - 2 {
dTheta = anglePerPoint * partialPointAmount / 2
}
if partialPointRadius != 0 && i == numPoints - 1 {
radius = partialPointRadius
}
previousPoint = point
point.x = (radius * cos(currentAngle))
point.y = (radius * sin(currentAngle))
if innerRoundedness == 0 && outerRoundedness == 0 {
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
} else {
let cp1Theta = (atan2(previousPoint.y, previousPoint.x) - CGFloat.pi / 2)
let cp1Dx = cos(cp1Theta)
let cp1Dy = sin(cp1Theta)
let cp2Theta = (atan2(point.y, point.x) - CGFloat.pi / 2)
let cp2Dx = cos(cp2Theta)
let cp2Dy = sin(cp2Theta)
let cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness
let cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness
let cp1Radius = longSegment ? innerRadius : outerRadius
let cp2Radius = longSegment ? outerRadius : innerRadius
var cp1 = CGPoint(
x: cp1Radius * cp1Roundedness * StarNode.PolystarConstant * cp1Dx,
y: cp1Radius * cp1Roundedness * StarNode.PolystarConstant * cp1Dy)
var cp2 = CGPoint(
x: cp2Radius * cp2Roundedness * StarNode.PolystarConstant * cp2Dx,
y: cp2Radius * cp2Roundedness * StarNode.PolystarConstant * cp2Dy)
if partialPointAmount != 0 {
if i == 0 {
cp1 = cp1 * partialPointAmount
} else if i == numPoints - 1 {
cp2 = cp2 * partialPointAmount
}
}
let previousVertex = vertices[vertices.endIndex - 1]
vertices[vertices.endIndex - 1] = CurveVertex(
previousVertex.inTangent,
previousVertex.point,
previousVertex.point - cp1)
vertices.append(CurveVertex(point: point + position, inTangentRelative: cp2, outTangentRelative: .zero))
}
currentAngle += dTheta
longSegment = !longSegment
}
let reverse = direction == .counterClockwise
if reverse {
vertices = vertices.reversed()
}
var path = BezierPath()
for vertex in vertices {
path.addVertex(reverse ? vertex.reversed() : vertex)
}
path.close()
return path
}
}
@@ -0,0 +1,155 @@
//
// GroupNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - GroupNodeProperties
final class GroupNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(transform: ShapeTransform?) {
if let transform = transform {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchor.keyframes))
position = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.position.keyframes))
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
skew = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.skew.keyframes))
skewAxis = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.skewAxis.keyframes))
} else {
/// Transform node missing. Default to empty transform.
anchor = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(0), y: CGFloat(0), z: CGFloat(0))))
position = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(0), y: CGFloat(0), z: CGFloat(0))))
scale = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(100), y: CGFloat(100), z: CGFloat(100))))
rotation = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
opacity = NodeProperty(provider: SingleValueProvider(Vector1D(100)))
skew = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
skewAxis = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Anchor Point" : anchor,
"Position" : position,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity,
"Skew" : skew,
"Skew Axis" : skewAxis,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName = "Transform"
var childKeypaths: [KeypathSearchable] = []
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let position: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let opacity: NodeProperty<Vector1D>
let skew: NodeProperty<Vector1D>
let skewAxis: NodeProperty<Vector1D>
var caTransform: CATransform3D {
CATransform3D.makeTransform(
anchor: anchor.value.pointValue,
position: position.value.pointValue,
scale: scale.value.sizeValue,
rotation: rotation.value.cgFloatValue,
skew: skew.value.cgFloatValue,
skewAxis: skewAxis.value.cgFloatValue)
}
}
// MARK: - GroupNode
final class GroupNode: AnimatorNode {
// MARK: Lifecycle
// MARK: Initializer
init(name: String, parentNode: AnimatorNode?, tree: NodeTree) {
self.parentNode = parentNode
keypathName = name
rootNode = tree.rootNode
properties = GroupNodeProperties(transform: tree.transform)
groupOutput = GroupOutputNode(parent: parentNode?.outputNode, rootNode: rootNode?.outputNode)
var childKeypaths: [KeypathSearchable] = tree.childrenNodes
childKeypaths.append(properties)
self.childKeypaths = childKeypaths
for childContainer in tree.renderContainers {
container.insertRenderLayer(childContainer)
}
}
// MARK: Internal
// MARK: Properties
let groupOutput: GroupOutputNode
let properties: GroupNodeProperties
let rootNode: AnimatorNode?
var container = ShapeContainerLayer()
// MARK: Keypath Searchable
let keypathName: String
let childKeypaths: [KeypathSearchable]
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var keypathLayer: CALayer? {
container
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var outputNode: NodeOutput {
groupOutput
}
var isEnabled = true {
didSet {
container.isHidden = !isEnabled
}
}
func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool {
rootNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false
}
func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool) {
rootNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate)
}
func rebuildOutputs(frame: CGFloat) {
container.opacity = Float(properties.opacity.value.cgFloatValue) * 0.01
container.transform = properties.caTransform
groupOutput.setTransform(container.transform, forFrame: frame)
}
}
@@ -0,0 +1,90 @@
//
// FillNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import CoreGraphics
import Foundation
// MARK: - FillNodeProperties
final class FillNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(fill: Fill) {
keypathName = fill.name
color = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.color.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.opacity.keyframes))
type = fill.fillRule
keypathProperties = [
"Opacity" : opacity,
PropertyName.color.rawValue : color,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let color: NodeProperty<Color>
let type: FillRule
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - FillNode
final class FillNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, fill: Fill) {
fillRender = FillRenderer(parent: parentNode?.outputNode)
fillProperties = FillNodeProperties(fill: fill)
self.parentNode = parentNode
}
// MARK: Internal
let fillRender: FillRenderer
let fillProperties: FillNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
fillRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
fillProperties
}
var isEnabled = true {
didSet {
fillRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
fillRender.color = fillProperties.color.value.cgColorValue
fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01
fillRender.fillRule = fillProperties.type
}
}
@@ -0,0 +1,102 @@
//
// GradientFillNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
// MARK: - GradientFillProperties
final class GradientFillProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(gradientfill: GradientFill) {
keypathName = gradientfill.name
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.opacity.keyframes))
startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.startPoint.keyframes))
endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.endPoint.keyframes))
colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.colors.keyframes))
gradientType = gradientfill.gradientType
numberOfColors = gradientfill.numberOfColors
keypathProperties = [
"Opacity" : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let startPoint: NodeProperty<Vector3D>
let endPoint: NodeProperty<Vector3D>
let colors: NodeProperty<[Double]>
let gradientType: GradientType
let numberOfColors: Int
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - GradientFillNode
final class GradientFillNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, gradientFill: GradientFill) {
fillRender = GradientFillRenderer(parent: parentNode?.outputNode)
fillProperties = GradientFillProperties(gradientfill: gradientFill)
self.parentNode = parentNode
}
// MARK: Internal
let fillRender: GradientFillRenderer
let fillProperties: GradientFillProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
fillRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
fillProperties
}
var isEnabled = true {
didSet {
fillRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
fillRender.start = fillProperties.startPoint.value.pointValue
fillRender.end = fillProperties.endPoint.value.pointValue
fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01
fillRender.colors = fillProperties.colors.value.map { CGFloat($0) }
fillRender.type = fillProperties.gradientType
fillRender.numberOfColors = fillProperties.numberOfColors
}
}
@@ -0,0 +1,151 @@
//
// GradientStrokeNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/23/19.
//
import CoreGraphics
import Foundation
// MARK: - GradientStrokeProperties
final class GradientStrokeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(gradientStroke: GradientStroke) {
keypathName = gradientStroke.name
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.opacity.keyframes))
startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.startPoint.keyframes))
endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.endPoint.keyframes))
colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.colors.keyframes))
gradientType = gradientStroke.gradientType
numberOfColors = gradientStroke.numberOfColors
width = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.width.keyframes))
miterLimit = CGFloat(gradientStroke.miterLimit)
lineCap = gradientStroke.lineCap
lineJoin = gradientStroke.lineJoin
if let dashes = gradientStroke.dashPattern {
var dashPatterns = ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>()
var dashPhase = ContiguousArray<Keyframe<Vector1D>>()
for dash in dashes {
if dash.type == .offset {
dashPhase = dash.value.keyframes
} else {
dashPatterns.append(dash.value.keyframes)
}
}
dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns))
self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase))
} else {
dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]()))
dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
"Stroke Width" : width,
"Dashes" : dashPattern,
"Dash Phase" : dashPhase,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let startPoint: NodeProperty<Vector3D>
let endPoint: NodeProperty<Vector3D>
let colors: NodeProperty<[Double]>
let width: NodeProperty<Vector1D>
let dashPattern: NodeProperty<[Vector1D]>
let dashPhase: NodeProperty<Vector1D>
let lineCap: LineCap
let lineJoin: LineJoin
let miterLimit: CGFloat
let gradientType: GradientType
let numberOfColors: Int
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - GradientStrokeNode
final class GradientStrokeNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, gradientStroke: GradientStroke) {
strokeRender = GradientStrokeRenderer(parent: parentNode?.outputNode)
strokeProperties = GradientStrokeProperties(gradientStroke: gradientStroke)
self.parentNode = parentNode
}
// MARK: Internal
let strokeRender: GradientStrokeRenderer
let strokeProperties: GradientStrokeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
strokeRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
strokeProperties
}
var isEnabled = true {
didSet {
strokeRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
/// Update gradient properties
strokeRender.gradientRender.start = strokeProperties.startPoint.value.pointValue
strokeRender.gradientRender.end = strokeProperties.endPoint.value.pointValue
strokeRender.gradientRender.opacity = strokeProperties.opacity.value.cgFloatValue
strokeRender.gradientRender.colors = strokeProperties.colors.value.map { CGFloat($0) }
strokeRender.gradientRender.type = strokeProperties.gradientType
strokeRender.gradientRender.numberOfColors = strokeProperties.numberOfColors
/// Now update stroke properties
strokeRender.strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue
strokeRender.strokeRender.width = strokeProperties.width.value.cgFloatValue
strokeRender.strokeRender.miterLimit = strokeProperties.miterLimit
strokeRender.strokeRender.lineCap = strokeProperties.lineCap
strokeRender.strokeRender.lineJoin = strokeProperties.lineJoin
/// Get dash lengths
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
if dashLengths.count > 0 {
strokeRender.strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
strokeRender.strokeRender.dashLengths = dashLengths
} else {
strokeRender.strokeRender.dashLengths = nil
strokeRender.strokeRender.dashPhase = nil
}
}
}
@@ -0,0 +1,153 @@
//
// StrokeNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
// MARK: - StrokeNodeProperties
final class StrokeNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(stroke: Stroke) {
keypathName = stroke.name
color = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.color.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.opacity.keyframes))
width = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.width.keyframes))
miterLimit = CGFloat(stroke.miterLimit)
lineCap = stroke.lineCap
lineJoin = stroke.lineJoin
if let dashes = stroke.dashPattern {
let (dashPatterns, dashPhase) = dashes.shapeLayerConfiguration
dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns))
if dashPhase.count == 0 {
self.dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
} else {
self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase))
}
} else {
dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]()))
dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
PropertyName.color.rawValue : color,
"Stroke Width" : width,
"Dashes" : dashPattern,
"Dash Phase" : dashPhase,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let opacity: NodeProperty<Vector1D>
let color: NodeProperty<Color>
let width: NodeProperty<Vector1D>
let dashPattern: NodeProperty<[Vector1D]>
let dashPhase: NodeProperty<Vector1D>
let lineCap: LineCap
let lineJoin: LineJoin
let miterLimit: CGFloat
}
// MARK: - StrokeNode
/// Node that manages stroking a path
final class StrokeNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, stroke: Stroke) {
strokeRender = StrokeRenderer(parent: parentNode?.outputNode)
strokeProperties = StrokeNodeProperties(stroke: stroke)
self.parentNode = parentNode
}
// MARK: Internal
let strokeRender: StrokeRenderer
let strokeProperties: StrokeNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
strokeRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
strokeProperties
}
var isEnabled = true {
didSet {
strokeRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
strokeRender.color = strokeProperties.color.value.cgColorValue
strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue * 0.01
strokeRender.width = strokeProperties.width.value.cgFloatValue
strokeRender.miterLimit = strokeProperties.miterLimit
strokeRender.lineCap = strokeProperties.lineCap
strokeRender.lineJoin = strokeProperties.lineJoin
/// Get dash lengths
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
if dashLengths.count > 0 {
strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
strokeRender.dashLengths = dashLengths
} else {
strokeRender.dashLengths = nil
strokeRender.dashPhase = nil
}
}
}
// MARK: - [DashElement] + shapeLayerConfiguration
extension Array where Element == DashElement {
typealias ShapeLayerConfiguration = (
dashPatterns: ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>,
dashPhase: ContiguousArray<Keyframe<Vector1D>>)
/// Converts the `[DashElement]` data model into `lineDashPattern` and `lineDashPhase`
/// representations usable in a `CAShapeLayer`
var shapeLayerConfiguration: ShapeLayerConfiguration {
var dashPatterns = ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>()
var dashPhase = ContiguousArray<Keyframe<Vector1D>>()
for dash in self {
if dash.type == .offset {
dashPhase = dash.value.keyframes
} else {
dashPatterns.append(dash.value.keyframes)
}
}
return (dashPatterns, dashPhase)
}
}
@@ -0,0 +1,270 @@
//
// TextAnimatorNode.swift
// lottie-ios-iOS
//
// Created by Brandon Withrow on 2/19/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - TextAnimatorNodeProperties
final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(textAnimator: TextAnimator) {
keypathName = textAnimator.name
var properties = [String : AnyNodeProperty]()
if let keyframeGroup = textAnimator.anchor {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Anchor"] = anchor
} else {
anchor = nil
}
if let keyframeGroup = textAnimator.position {
position = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Position"] = position
} else {
position = nil
}
if let keyframeGroup = textAnimator.scale {
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Scale"] = scale
} else {
scale = nil
}
if let keyframeGroup = textAnimator.skew {
skew = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Skew"] = skew
} else {
skew = nil
}
if let keyframeGroup = textAnimator.skewAxis {
skewAxis = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Skew Axis"] = skewAxis
} else {
skewAxis = nil
}
if let keyframeGroup = textAnimator.rotation {
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Rotation"] = rotation
} else {
rotation = nil
}
if let keyframeGroup = textAnimator.opacity {
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Opacity"] = opacity
} else {
opacity = nil
}
if let keyframeGroup = textAnimator.strokeColor {
strokeColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Stroke Color"] = strokeColor
} else {
strokeColor = nil
}
if let keyframeGroup = textAnimator.fillColor {
fillColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Fill Color"] = fillColor
} else {
fillColor = nil
}
if let keyframeGroup = textAnimator.strokeWidth {
strokeWidth = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Stroke Width"] = strokeWidth
} else {
strokeWidth = nil
}
if let keyframeGroup = textAnimator.tracking {
tracking = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Tracking"] = tracking
} else {
tracking = nil
}
keypathProperties = properties
self.properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathName: String
let anchor: NodeProperty<Vector3D>?
let position: NodeProperty<Vector3D>?
let scale: NodeProperty<Vector3D>?
let skew: NodeProperty<Vector1D>?
let skewAxis: NodeProperty<Vector1D>?
let rotation: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>?
let strokeColor: NodeProperty<Color>?
let fillColor: NodeProperty<Color>?
let strokeWidth: NodeProperty<Vector1D>?
let tracking: NodeProperty<Vector1D>?
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
var caTransform: CATransform3D {
CATransform3D.makeTransform(
anchor: anchor?.value.pointValue ?? .zero,
position: position?.value.pointValue ?? .zero,
scale: scale?.value.sizeValue ?? CGSize(width: 100, height: 100),
rotation: rotation?.value.cgFloatValue ?? 0,
skew: skew?.value.cgFloatValue,
skewAxis: skewAxis?.value.cgFloatValue)
}
}
// MARK: - TextOutputNode
final class TextOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: TextOutputNode?) {
parentTextNode = parent
}
// MARK: Internal
var parentTextNode: TextOutputNode?
var isEnabled = true
var outputPath: CGPath?
var parent: NodeOutput? {
parentTextNode
}
var xform: CATransform3D {
get {
_xform ?? parentTextNode?.xform ?? CATransform3DIdentity
}
set {
_xform = newValue
}
}
var opacity: CGFloat {
get {
_opacity ?? parentTextNode?.opacity ?? 1
}
set {
_opacity = newValue
}
}
var strokeColor: CGColor? {
get {
_strokeColor ?? parentTextNode?.strokeColor
}
set {
_strokeColor = newValue
}
}
var fillColor: CGColor? {
get {
_fillColor ?? parentTextNode?.fillColor
}
set {
_fillColor = newValue
}
}
var tracking: CGFloat {
get {
_tracking ?? parentTextNode?.tracking ?? 0
}
set {
_tracking = newValue
}
}
var strokeWidth: CGFloat {
get {
_strokeWidth ?? parentTextNode?.strokeWidth ?? 0
}
set {
_strokeWidth = newValue
}
}
func hasOutputUpdates(_: CGFloat) -> Bool {
// TODO Fix This
true
}
// MARK: Fileprivate
fileprivate var _xform: CATransform3D?
fileprivate var _opacity: CGFloat?
fileprivate var _strokeColor: CGColor?
fileprivate var _fillColor: CGColor?
fileprivate var _tracking: CGFloat?
fileprivate var _strokeWidth: CGFloat?
}
// MARK: - TextAnimatorNode
class TextAnimatorNode: AnimatorNode {
// MARK: Lifecycle
init(parentNode: TextAnimatorNode?, textAnimator: TextAnimator) {
textOutputNode = TextOutputNode(parent: parentNode?.textOutputNode)
textAnimatorProperties = TextAnimatorNodeProperties(textAnimator: textAnimator)
self.parentNode = parentNode
}
// MARK: Internal
let textOutputNode: TextOutputNode
let textAnimatorProperties: TextAnimatorNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
var outputNode: NodeOutput {
textOutputNode
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
textAnimatorProperties
}
func localUpdatesPermeateDownstream() -> Bool {
true
}
func rebuildOutputs(frame _: CGFloat) {
textOutputNode.xform = textAnimatorProperties.caTransform
textOutputNode.opacity = (textAnimatorProperties.opacity?.value.cgFloatValue ?? 100) * 0.01
textOutputNode.strokeColor = textAnimatorProperties.strokeColor?.value.cgColorValue
textOutputNode.fillColor = textAnimatorProperties.fillColor?.value.cgColorValue
textOutputNode.tracking = textAnimatorProperties.tracking?.value.cgFloatValue ?? 1
textOutputNode.strokeWidth = textAnimatorProperties.strokeWidth?.value.cgFloatValue ?? 0
}
}
@@ -0,0 +1,198 @@
//
// 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
}
}
@@ -0,0 +1,22 @@
//
// PathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import Foundation
// MARK: - PathNode
protocol PathNode {
var pathOutput: PathOutputNode { get }
}
extension PathNode where Self: AnimatorNode {
var outputNode: NodeOutput {
pathOutput
}
}
@@ -0,0 +1,62 @@
//
// RenderNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - RenderNode
/// A protocol that defines a node that holds render instructions
protocol RenderNode {
var renderer: Renderable & NodeOutput { get }
}
// MARK: - Renderable
/// A protocol that defines anything with render instructions
protocol Renderable {
/// The last frame in which this node was updated.
var hasUpdate: Bool { get }
func hasRenderUpdates(_ forFrame: CGFloat) -> Bool
/// Determines if the renderer requires a custom context for drawing.
/// If yes the shape layer will perform a custom drawing pass.
/// If no the shape layer will be a standard CAShapeLayer
var shouldRenderInContext: Bool { get }
/// Passes in the CAShapeLayer to update
func updateShapeLayer(layer: CAShapeLayer)
/// Asks the renderer what the renderable bounds is for the given box.
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect
/// Opportunity for renderers to inject sublayers
func setupSublayers(layer: CAShapeLayer)
/// Renders the shape in a custom context
func render(_ inContext: CGContext)
}
extension RenderNode where Self: AnimatorNode {
var outputNode: NodeOutput {
renderer
}
}
extension Renderable {
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
/// Optional
boundingBox
}
}
@@ -0,0 +1,75 @@
//
// ShapeContainerLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// The base layer that holds Shapes and Shape Renderers
class ShapeContainerLayer: CALayer {
// MARK: Lifecycle
override init() {
super.init()
actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"transform" : NSNull(),
"opacity" : NSNull(),
"hidden" : NSNull(),
]
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(layer: Any) {
guard let layer = layer as? ShapeContainerLayer else {
fatalError("init(layer:) wrong class.")
}
super.init(layer: layer)
}
// MARK: Internal
private(set) var renderLayers: [ShapeContainerLayer] = []
var renderScale: CGFloat = 1 {
didSet {
updateRenderScale()
}
}
func insertRenderLayer(_ layer: ShapeContainerLayer) {
renderLayers.append(layer)
insertSublayer(layer, at: 0)
}
func markRenderUpdates(forFrame: CGFloat) {
if hasRenderUpdate(forFrame: forFrame) {
rebuildContents(forFrame: forFrame)
}
guard isHidden == false else { return }
renderLayers.forEach { $0.markRenderUpdates(forFrame: forFrame) }
}
func hasRenderUpdate(forFrame _: CGFloat) -> Bool {
false
}
func rebuildContents(forFrame _: CGFloat) {
/// Override
}
func updateRenderScale() {
contentsScale = renderScale
renderLayers.forEach( { $0.renderScale = renderScale } )
}
}
@@ -0,0 +1,100 @@
//
// RenderLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import Foundation
import QuartzCore
/// The layer responsible for rendering shape objects
final class ShapeRenderLayer: ShapeContainerLayer {
// MARK: Lifecycle
init(renderer: Renderable & NodeOutput) {
self.renderer = renderer
super.init()
anchorPoint = .zero
actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"path" : NSNull(),
"transform" : NSNull(),
"opacity" : NSNull(),
"hidden" : NSNull(),
]
shapeLayer.actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"path" : NSNull(),
"fillColor" : NSNull(),
"strokeColor" : NSNull(),
"lineWidth" : NSNull(),
"miterLimit" : NSNull(),
"lineDashPhase" : NSNull(),
"opacity": NSNull(),
"hidden" : NSNull(),
]
addSublayer(shapeLayer)
renderer.setupSublayers(layer: shapeLayer)
}
override init(layer: Any) {
guard let layer = layer as? ShapeRenderLayer else {
fatalError("init(layer:) wrong class.")
}
renderer = layer.renderer
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
fileprivate(set) var renderer: Renderable & NodeOutput
let shapeLayer = CAShapeLayer()
override func hasRenderUpdate(forFrame: CGFloat) -> Bool {
isHidden = !renderer.isEnabled
guard isHidden == false else { return false }
return renderer.hasRenderUpdates(forFrame)
}
override func rebuildContents(forFrame _: CGFloat) {
if renderer.shouldRenderInContext {
if let newPath = renderer.outputPath {
bounds = renderer.renderBoundsFor(newPath.boundingBox)
} else {
bounds = .zero
}
position = bounds.origin
setNeedsDisplay()
} else {
shapeLayer.path = renderer.outputPath
renderer.updateShapeLayer(layer: shapeLayer)
}
}
override func draw(in ctx: CGContext) {
if let path = renderer.outputPath {
if !path.isEmpty {
ctx.addPath(path)
}
}
renderer.render(ctx)
}
override func updateRenderScale() {
super.updateRenderScale()
shapeLayer.contentsScale = renderScale
}
}