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
}
}
}