Lottie tests [skip ci]

This commit is contained in:
Isaac 2024-05-08 22:43:27 +04:00
parent 9c017f9f03
commit 9fcef12d55
426 changed files with 39240 additions and 428 deletions

View File

@ -13,6 +13,8 @@ build --per_file_copt="third-party/webrtc/.*\.mm$","@-std=c++17"
build --per_file_copt="submodules/LottieMeshSwift/LottieMeshBinding/Sources/.*\.mm$","@-std=c++17"
build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.mm$","@-std=c++17"
build --per_file_copt="submodules/TelegramUI/Components/LottieCpp/Sources/.*\.cpp$","@-std=c++17"
build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.cpp$","@-std=c++17"
build --per_file_copt="Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/.*\.mm$","@-std=c++17"
build --swiftcopt=-whole-module-optimization

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,37 +0,0 @@
import Foundation
import UIKit
import LottieMeshSwift
import Postbox
public final class ViewController: UIViewController {
override public func viewDidLoad() {
super.viewDidLoad()
TempBox.initializeShared(basePath: NSTemporaryDirectory(), processType: "test", launchSpecificId: Int64.random(in: Int64.min ..< Int64.max))
self.view.backgroundColor = .black
//let path = Bundle.main.path(forResource: "SUPER Fire", ofType: "json")!
let path = Bundle.main.path(forResource: "Fireworks", ofType: "json")!
//let path = Bundle.main.path(forResource: "Cat", ofType: "json")!
/*for _ in 0 ..< 100 {
let _ = generateMeshAnimation(data: try! Data(contentsOf: URL(fileURLWithPath: path)))!
}*/
if #available(iOS 13.0, *) {
let startTime = CFAbsoluteTimeGetCurrent()
let animationFile = generateMeshAnimation(data: try! Data(contentsOf: URL(fileURLWithPath: path)))!
print("Time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0)")
let buffer = MeshReadBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: animationFile.path)))
let animation = MeshAnimation.read(buffer: buffer)
let renderer = MeshRenderer(wireframe: false)!
renderer.frame = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: CGSize(width: 300.0, height: 300.0))
self.view.addSubview(renderer)
renderer.add(mesh: animation, offset: CGPoint(), loop: true)
}
}
}

2
Tests/LottieMetalTest/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
TestData/*.json

View File

@ -10,6 +10,27 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load(
"@rules_xcodeproj//xcodeproj:defs.bzl",
"top_level_target",
"top_level_targets",
"xcodeproj",
"xcode_provisioning_profile",
)
load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
load(
"@build_configuration//:variables.bzl",
"telegram_bazel_path",
)
filegroup(
name = "AppResources",
srcs = glob([
@ -26,7 +47,14 @@ swift_library(
":AppResources",
],
deps = [
"//submodules/LottieMeshSwift:LottieMeshSwift",
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/LottieCpp",
"//submodules/TelegramUI/Components/LottieMetal",
"//submodules/rlottie:RLottieBinding",
"//Tests/LottieMetalTest/QOILoader",
"//Tests/LottieMetalTest/SoftwareLottieRenderer",
"//Tests/LottieMetalTest/LottieSwift",
],
)
@ -72,7 +100,7 @@ plist_fragment(
<key>CFBundleDisplayName</key>
<string>Test</string>
<key>CFBundleIdentifier</key>
<string>org.telegram.LottieMesh</string>
<string>ph.telegra.Telegraph</string>
<key>CFBundleName</key>
<string>Telegram</string>
<key>CFBundlePackageType</key>
@ -127,12 +155,44 @@ plist_fragment(
"""
)
filegroup(
name = "TestDataBundleFiles",
srcs = glob([
"TestData/*.json",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "TestDataBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.TestDataBundle</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>TestDataBundle</string>
"""
)
apple_resource_bundle(
name = "TestDataBundle",
infoplists = [
":TestDataBundleInfoPlist",
],
resources = [
":TestDataBundleFiles",
],
)
ios_application(
name = "LottieMesh",
bundle_id = "org.telegram.LottieMesh",
name = "LottieMetalTest",
bundle_id = "ph.telegra.Telegraph",
families = ["iphone", "ipad"],
minimum_os_version = "9.0",
provisioning_profile = "@build_configuration//provisioning:Wildcard.mobileprovision",
minimum_os_version = "12.0",
provisioning_profile = "@build_configuration//provisioning:Telegram.mobileprovision",
infoplists = [
":AppInfoPlist",
":BuildNumberInfoPlist",
@ -140,6 +200,7 @@ ios_application(
],
resources = [
"//Tests/Common:LaunchScreen",
":TestDataBundle",
],
frameworks = [
],
@ -147,4 +208,28 @@ ios_application(
"//Tests/Common:Main",
":Lib",
],
visibility = ["//visibility:public"],
)
xcodeproj(
name = "LottieMetalTest_xcodeproj",
build_mode = "bazel",
bazel_path = telegram_bazel_path,
project_name = "LottieMetalTest",
tags = ["manual"],
top_level_targets = top_level_targets(
labels = [
":LottieMetalTest",
],
target_environments = ["device", "simulator"],
),
xcode_configurations = {
"Debug": {
"//command_line_option:compilation_mode": "dbg",
},
"Release": {
"//command_line_option:compilation_mode": "opt",
},
},
default_xcode_configuration = "Debug"
)

View File

@ -0,0 +1,14 @@
load("@build_bazel_rules_swift//swift:swift.bzl",
"swift_library",
)
swift_library(
name = "LottieSwift",
module_name = "LottieSwift",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,76 @@
// Created by Cal Stephens on 1/6/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAAnimation {
/// Creates a `CAAnimation` that wraps this animation,
/// applying timing-related configuration from the given `LayerAnimationContext`
@nonobjc
func timed(with context: LayerAnimationContext, for layer: CALayer) -> CAAnimation {
// The base animation always has the duration of the full animation,
// since that's the time space where keyframing and interpolating happens.
// So we start with a simple animation timeline from 0% to 100%:
//
//
// baseAnimation
//
// 0% 100%
//
let baseAnimation = self
baseAnimation.duration = context.animation.duration
baseAnimation.speed = (context.endFrame < context.startFrame) ? -1 : 1
// To select the subrange of the `baseAnimation` that should be played,
// we create a parent animation with the duration of that subrange
// to clip the `baseAnimation`. This parent animation can then loop
// and/or autoreverse over the clipped subrange.
//
//
//  clippingParent ...
//
// 25% 75%
//
// baseAnimation
//
// 0% 100%
//
let clippingParent = CAAnimationGroup()
clippingParent.animations = [baseAnimation]
clippingParent.duration = Double(abs(context.endFrame - context.startFrame)) / context.animation.framerate
baseAnimation.timeOffset = context.animation.time(forFrame: context.startFrame)
clippingParent.autoreverses = context.timingConfiguration.autoreverses
clippingParent.repeatCount = context.timingConfiguration.repeatCount
clippingParent.timeOffset = context.timingConfiguration.timeOffset
// Once the animation ends, it should pause on the final frame
clippingParent.fillMode = .both
clippingParent.isRemovedOnCompletion = false
// We can pause the animation on a specific frame by setting the root layer's
// `speed` to 0, and then setting the `timeOffset` for the given frame.
// - For that setup to work properly, we have to set the `beginTime`
// of this animation to a time slightly before the current time.
// - It's not really clear why this is necessary, but `timeOffset`
// is not applied correctly without this configuration.
// - We can't do this when playing the animation in real time,
// because it can cause keyframe timings to be incorrect.
if context.timingConfiguration.speed == 0 {
let currentTime = layer.convertTime(CACurrentMediaTime(), from: nil)
clippingParent.beginTime = currentTime - .leastNonzeroMagnitude
}
return clippingParent
}
}
extension CALayer {
/// Adds the given animation to this layer, timed with the given timing configuration
@nonobjc
func add(_ animation: CAPropertyAnimation, timedWith context: LayerAnimationContext) {
add(animation.timed(with: context, for: self), forKey: animation.keyPath)
}
}

View File

@ -0,0 +1,315 @@
// Created by Cal Stephens on 12/14/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
extension CALayer {
// MARK: Internal
/// Constructs a `CAKeyframeAnimation` that reflects the given keyframes,
/// and adds it to this `CALayer`.
@nonobjc
func addAnimation<KeyframeValue, ValueRepresentation: Equatable>(
for property: LayerProperty<ValueRepresentation>,
keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
context: LayerAnimationContext)
throws
{
if let customAnimation = try customizedAnimation(for: property, context: context) {
add(customAnimation, timedWith: context)
}
else if
let defaultAnimation = try defaultAnimation(
for: property,
keyframes: keyframes,
value: keyframeValueMapping,
context: context)
{
add(defaultAnimation, timedWith: context)
}
}
// MARK: Private
/// Constructs a `CAAnimation` that reflects the given keyframes
/// - If the value can be applied directly to the CALayer using KVC,
/// then no `CAAnimation` will be created and the value will be applied directly.
@nonobjc
private func defaultAnimation<KeyframeValue, ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
context: LayerAnimationContext)
throws
-> CAPropertyAnimation?
{
guard !keyframes.isEmpty else { return nil }
// If there is exactly one keyframe value, we can improve performance
// by applying that value directly to the layer instead of creating
// a relatively expensive `CAKeyframeAnimation`.
if keyframes.count == 1 {
let keyframeValue = try keyframeValueMapping(keyframes[0].value)
// If the keyframe value is the same as the layer's default value for this property,
// then we can just ignore this set of keyframes.
if keyframeValue == property.defaultValue {
return nil
}
// If the property on the CALayer being animated hasn't been modified from the default yet,
// then we can apply the keyframe value directly to the layer using KVC instead
// of creating a `CAAnimation`.
if
let defaultValue = property.defaultValue,
defaultValue == value(forKey: property.caLayerKeypath) as? ValueRepresentation
{
setValue(keyframeValue, forKeyPath: property.caLayerKeypath)
return nil
}
// Otherwise, we still need to create a `CAAnimation`, but we can
// create a simple `CABasicAnimation` that is still less expensive
// than computing a `CAKeyframeAnimation`.
let animation = CABasicAnimation(keyPath: property.caLayerKeypath)
animation.fromValue = keyframeValue
animation.toValue = keyframeValue
return animation
}
return try keyframeAnimation(
for: property,
keyframes: keyframes,
value: keyframeValueMapping,
context: context)
}
/// A `CAAnimation` that applies the custom value from the `AnyValueProvider`
/// registered for this specific property's `AnimationKeypath`,
/// if one has been registered using `AnimationView.setValueProvider(_:keypath:)`.
@nonobjc
private func customizedAnimation<ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
context: LayerAnimationContext)
throws
-> CAPropertyAnimation?
{
guard
let customizableProperty = property.customizableProperty,
let customKeyframes = try context.valueProviderStore.customKeyframes(
of: customizableProperty,
for: AnimationKeypath(keys: context.currentKeypath.keys + customizableProperty.name.map { $0.rawValue }),
context: context)
else { return nil }
// Since custom animations are overriding an existing animation,
// we always have to create a CAKeyframeAnimation for these instead of
// letting `defaultAnimation(...)` try to apply the value using KVC.
return try keyframeAnimation(
for: property,
keyframes: customKeyframes.keyframes,
value: { $0 },
context: context)
}
/// Creates a `CAKeyframeAnimation` for the given keyframes
private func keyframeAnimation<KeyframeValue, ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
context: LayerAnimationContext)
throws
-> CAKeyframeAnimation
{
// Convert the list of `Keyframe<T>` into
// the representation used by `CAKeyframeAnimation`
var keyTimes = keyframes.map { keyframeModel -> NSNumber in
NSNumber(value: Float(context.progressTime(for: keyframeModel.time)))
}
var timingFunctions = self.timingFunctions(for: keyframes)
let calculationMode = try self.calculationMode(for: keyframes, context: context)
let animation = CAKeyframeAnimation(keyPath: property.caLayerKeypath)
// Position animations define a `CGPath` curve that should be followed,
// instead of animating directly between keyframe point values.
if property.caLayerKeypath == LayerProperty<CGPoint>.position.caLayerKeypath {
animation.path = try path(keyframes: keyframes, value: { value in
guard let point = try keyframeValueMapping(value) as? CGPoint else {
LottieLogger.shared.assertionFailure("Cannot create point from keyframe with value \(value)")
return .zero
}
return point
})
}
// All other types of keyframes provide individual values that are interpolated by Core Animation
else {
var values = try keyframes.map { keyframeModel in
try keyframeValueMapping(keyframeModel.value)
}
validate(values: &values, keyTimes: &keyTimes, timingFunctions: &timingFunctions, for: calculationMode)
animation.values = values
}
animation.calculationMode = calculationMode
animation.keyTimes = keyTimes
animation.timingFunctions = timingFunctions
return animation
}
/// The `CAAnimationCalculationMode` that should be used for a `CAKeyframeAnimation`
/// animating the given keyframes
private func calculationMode<KeyframeValue>(
for keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
context: LayerAnimationContext)
throws
-> CAAnimationCalculationMode
{
// Animations using `isHold` should use `CAAnimationCalculationMode.discrete`
//
// - Since we currently only create a single `CAKeyframeAnimation`,
// we can currently only correctly support animations where
// `isHold` is either always `true` or always `false`
// (this requirement doesn't apply to the first/last keyframes).
//
// - We should be able to support this in the future by creating multiple
// `CAKeyframeAnimation`s with different `calculationMode`s and
// playing them sequentially.
//
let intermediateKeyframes = keyframes.dropFirst().dropLast()
if intermediateKeyframes.contains(where: \.isHold) {
if intermediateKeyframes.allSatisfy(\.isHold) {
return .discrete
} else {
try context.logCompatibilityIssue("Mixed `isHold` / `!isHold` keyframes are currently unsupported")
}
}
return .linear
}
/// `timingFunctions` to apply to a `CAKeyframeAnimation` animating the given keyframes
private func timingFunctions<KeyframeValue>(
for keyframes: ContiguousArray<Keyframe<KeyframeValue>>)
-> [CAMediaTimingFunction]
{
// Compute the timing function between each keyframe and the subsequent keyframe
var timingFunctions: [CAMediaTimingFunction] = []
for (index, keyframe) in keyframes.enumerated()
where index != keyframes.indices.last
{
let nextKeyframe = keyframes[index + 1]
let controlPoint1 = keyframe.outTangent?.pointValue ?? .zero
let controlPoint2 = nextKeyframe.inTangent?.pointValue ?? CGPoint(x: 1, y: 1)
timingFunctions.append(CAMediaTimingFunction(
controlPoints:
Float(controlPoint1.x),
Float(controlPoint1.y),
Float(controlPoint2.x),
Float(controlPoint2.y)))
}
return timingFunctions
}
/// Creates a `CGPath` for the given `position` keyframes,
/// which accounts for `spatialInTangent`s and `spatialOutTangents`
private func path<KeyframeValue>(
keyframes positionKeyframes: ContiguousArray<Keyframe<KeyframeValue>>,
value keyframeValueMapping: (KeyframeValue) throws -> CGPoint) rethrows
-> CGPath {
let path = CGMutablePath()
for (index, keyframe) in positionKeyframes.enumerated() {
if index == positionKeyframes.indices.first {
path.move(to: try keyframeValueMapping(keyframe.value))
}
if index != positionKeyframes.indices.last {
let nextKeyframe = positionKeyframes[index + 1]
if
let controlPoint1 = keyframe.spatialOutTangent?.pointValue,
let controlPoint2 = nextKeyframe.spatialInTangent?.pointValue,
controlPoint1 != .zero,
controlPoint2 != .zero
{
path.addCurve(
to: try keyframeValueMapping(nextKeyframe.value),
control1: try keyframeValueMapping(keyframe.value) + controlPoint1,
control2: try keyframeValueMapping(nextKeyframe.value) + controlPoint2)
}
else {
path.addLine(to: try keyframeValueMapping(nextKeyframe.value))
}
}
}
path.closeSubpath()
return path
}
/// Validates that the requirements of the `CAKeyframeAnimation` API are met correctly
private func validate<ValueRepresentation>(
values: inout [ValueRepresentation],
keyTimes: inout [NSNumber],
timingFunctions: inout [CAMediaTimingFunction],
for calculationMode: CAAnimationCalculationMode)
{
// Validate that we have correct start (0.0) and end (1.0) keyframes.
// From the documentation of `CAKeyframeAnimation.keyTimes`:
// - The first value in the `keyTimes` array must be 0.0 and the last value must be 1.0.
if keyTimes.first != 0.0 {
keyTimes.insert(0.0, at: 0)
values.insert(values[0], at: 0)
timingFunctions.insert(CAMediaTimingFunction(name: .linear), at: 0)
}
if keyTimes.last != 1.0 {
keyTimes.append(1.0)
values.append(values.last!)
timingFunctions.append(CAMediaTimingFunction(name: .linear))
}
switch calculationMode {
case .linear, .cubic:
// From the documentation of `CAKeyframeAnimation.keyTimes`:
// - The number of elements in the keyTimes array
// should match the number of elements in the values property
LottieLogger.shared.assert(
values.count == keyTimes.count,
"`values.count` must exactly equal `keyTimes.count`")
LottieLogger.shared.assert(
timingFunctions.count == (values.count - 1),
"`timingFunctions.count` must exactly equal `values.count - 1`")
case .discrete:
// From the documentation of `CAKeyframeAnimation.keyTimes`:
// - If the calculationMode is set to discrete... the keyTimes array
// should have one more entry than appears in the values array.
values.removeLast()
LottieLogger.shared.assert(
keyTimes.count == values.count + 1,
"`keyTimes.count` must exactly equal `values.count + 1`")
default:
LottieLogger.shared.assertionFailure("""
Unexpected keyframe calculation mode \(calculationMode)
""")
}
}
}

View File

@ -0,0 +1,52 @@
// Created by Cal Stephens on 1/28/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
/// Adds animations for the given `CombinedShapeItem` to this `CALayer`
@nonobjc
func addAnimations(
for combinedShapes: CombinedShapeItem,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: combinedShapes.shapes.keyframes,
value: { paths in
let combinedPath = CGMutablePath()
for path in paths {
combinedPath.addPath(path.cgPath())
}
return combinedPath
},
context: context)
}
}
// MARK: - CombinedShapeItem
/// A custom `ShapeItem` subclass that combines multiple `Shape`s into a single `KeyframeGroup`
final class CombinedShapeItem: ShapeItem {
// MARK: Lifecycle
init(shapes: KeyframeGroup<[BezierPath]>, name: String) {
self.shapes = shapes
super.init(name: name, type: .shape, hidden: false)
}
required init(from _: Decoder) throws {
fatalError("init(from:) has not been implemented")
}
required init(dictionary _: [String: Any]) throws {
fatalError("init(dictionary:) has not been implemented")
}
// MARK: Internal
let shapes: KeyframeGroup<[BezierPath]>
}

View File

@ -0,0 +1,22 @@
// Created by Cal Stephens on 12/21/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
/// Adds animations for the given `BezierPath` keyframes to this `CALayer`
@nonobjc
func addAnimations(
for customPath: KeyframeGroup<BezierPath>,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: customPath.keyframes,
value: { pathKeyframe in
pathKeyframe.cgPath()
},
context: context)
}
}

View File

@ -0,0 +1,26 @@
// Created by Cal Stephens on 12/21/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
/// Adds animations for the given `Ellipse` to this `CALayer`
@nonobjc
func addAnimations(
for ellipse: Ellipse,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: ellipse.size.keyframes,
value: { sizeKeyframe in
BezierPath.ellipse(
size: sizeKeyframe.sizeValue,
center: try ellipse.position.exactlyOneKeyframe(context: context, description: "ellipse position").value.pointValue,
direction: ellipse.direction)
.cgPath()
},
context: context)
}
}

View File

@ -0,0 +1,146 @@
// Created by Cal Stephens on 1/7/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - GradientShapeItem
/// A `ShapeItem` that represents a gradient
protocol GradientShapeItem: OpacityAnimationModel {
var startPoint: KeyframeGroup<Vector3D> { get }
var endPoint: KeyframeGroup<Vector3D> { get }
var gradientType: GradientType { get }
var numberOfColors: Int { get }
var colors: KeyframeGroup<[Double]> { get }
}
// MARK: - GradientFill + GradientShapeItem
extension GradientFill: GradientShapeItem { }
// MARK: - GradientStroke + GradientShapeItem
extension GradientStroke: GradientShapeItem { }
// MARK: - GradientRenderLayer + GradientShapeItem
extension GradientRenderLayer {
// MARK: Internal
/// Adds gradient-related animations to this layer, from the given `GradientFill`
func addGradientAnimations(for gradient: GradientShapeItem, context: LayerAnimationContext) throws {
// We have to set `colors` to a non-nil value with some valid number of colors
// for the color animation below to have any effect
colors = .init(
repeating: CGColor.rgb(0, 0, 0),
count: gradient.numberOfColors)
try addAnimation(
for: .colors,
keyframes: gradient.colors.keyframes,
value: { colorComponents in
gradient.colorConfiguration(from: colorComponents).map { $0.color }
},
context: context)
try addAnimation(
for: .locations,
keyframes: gradient.colors.keyframes,
value: { colorComponents in
gradient.colorConfiguration(from: colorComponents).map { $0.location }
},
context: context)
try addOpacityAnimation(for: gradient, context: context)
switch gradient.gradientType {
case .linear:
try addLinearGradientAnimations(for: gradient, context: context)
case .radial:
try addRadialGradientAnimations(for: gradient, context: context)
case .none:
break
}
}
// MARK: Private
private func addLinearGradientAnimations(
for gradient: GradientShapeItem,
context: LayerAnimationContext)
throws
{
type = .axial
try addAnimation(
for: .startPoint,
keyframes: gradient.startPoint.keyframes,
value: { absoluteStartPoint in
percentBasedPointInBounds(from: absoluteStartPoint.pointValue)
},
context: context)
try addAnimation(
for: .endPoint,
keyframes: gradient.endPoint.keyframes,
value: { absoluteEndPoint in
percentBasedPointInBounds(from: absoluteEndPoint.pointValue)
},
context: context)
}
private func addRadialGradientAnimations(for gradient: GradientShapeItem, context: LayerAnimationContext) throws {
type = .radial
// To draw the correct gradients, we have to derive a custom `endPoint`
// relative to the `startPoint` value. Since calculating the `endPoint`
// at any given time requires knowing the current `startPoint`,
// we can't allow them to animate separately.
let absoluteStartPoint = try gradient.startPoint
.exactlyOneKeyframe(context: context, description: "gradient startPoint").value.pointValue
let absoluteEndPoint = try gradient.endPoint
.exactlyOneKeyframe(context: context, description: "gradient endPoint").value.pointValue
startPoint = percentBasedPointInBounds(from: absoluteStartPoint)
let radius = absoluteStartPoint.distanceTo(absoluteEndPoint)
endPoint = percentBasedPointInBounds(
from: CGPoint(
x: absoluteStartPoint.x + radius,
y: absoluteStartPoint.y + radius))
}
}
extension GradientShapeItem {
/// Converts the compact `[Double]` color components representation
/// into an array of `CGColor`s and the location of those colors within the gradient
fileprivate func colorConfiguration(
from colorComponents: [Double])
-> [(color: CGColor, location: CGFloat)]
{
precondition(
colorComponents.count >= numberOfColors * 4,
"Each color must have RGB components and a location component")
var cgColors = [(color: CGColor, location: CGFloat)]()
// Each group of four `Double` values represents a single `CGColor`,
// and its relative location within the gradient.
for colorIndex in 0..<numberOfColors {
let colorStartIndex = colorIndex * 4
let location = CGFloat(colorComponents[colorStartIndex])
let color = CGColor.rgb(
CGFloat(colorComponents[colorStartIndex + 1]),
CGFloat(colorComponents[colorStartIndex + 2]),
CGFloat(colorComponents[colorStartIndex + 3]))
cgColors.append((color, location))
}
return cgColors
}
}

View File

@ -0,0 +1,229 @@
// Created by Cal Stephens on 1/11/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - LayerProperty
/// A strongly typed value that can be used as the `keyPath` of a `CAAnimation`
///
/// Supported key paths and their expected value types are described
/// at https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html#//apple_ref/doc/uid/TP40004514-CH11-SW1
/// and https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html
struct LayerProperty<ValueRepresentation: Equatable> {
/// The `CALayer` KVC key path that this value should be assigned to
let caLayerKeypath: String
/// The default value of this property on a `CALayer`
/// - If the keyframe values are just equal to the default value,
/// then we can improve performance a bit by just not creating
/// a CAAnimation (since it would be redundant).
let defaultValue: ValueRepresentation?
/// A description of how this property can be customized dynamically
/// at runtime using `AnimationView.setValueProvider(_:keypath:)`
let customizableProperty: CustomizableProperty<ValueRepresentation>?
}
// MARK: - CustomizableProperty
/// A description of how a `CALayer` property can be customized dynamically
/// at runtime using `AnimationView.setValueProvider(_:keypath:)`
struct CustomizableProperty<ValueRepresentation> {
/// The name that `AnimationKeypath`s can use to refer to this property
/// - When building an animation for this property that will be applied
/// to a specific layer, this `name` is appended to the end of that
/// layer's `AnimationKeypath`. The combined keypath is used to query
/// the `ValueProviderStore`.
let name: [PropertyName]
/// A closure that coverts the type-erased value of an `AnyValueProvider`
/// to the strongly-typed representation used by this property, if possible.
let conversion: (Any) -> ValueRepresentation?
}
// MARK: - PropertyName
/// The name of a customizable property that can be used in an `AnimationKeypath`
/// - These values should be shared between the two rendering engines,
/// since they form the public API of the `AnimationKeypath` system.
enum PropertyName: String {
case color = "Color"
}
// MARK: CALayer properties
extension LayerProperty {
static var position: LayerProperty<CGPoint> {
.init(
caLayerKeypath: "transform.translation",
defaultValue: CGPoint(x: 0, y: 0),
customizableProperty: nil /* currently unsupported */)
}
static var positionX: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.translation.x",
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var positionY: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.translation.y",
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var scale: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.scale",
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
static var scaleX: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.scale.x",
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
static var scaleY: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.scale.y",
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
static var rotation: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.rotation",
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var rotationY: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.rotation.y",
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var anchorPoint: LayerProperty<CGPoint> {
.init(
caLayerKeypath: #keyPath(CALayer.anchorPoint),
// This is intentionally not `GGPoint(x: 0.5, y: 0.5)` (the actual default)
// to opt `anchorPoint` out of the KVC `setValue` flow, which causes issues.
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
static var opacity: LayerProperty<CGFloat> {
.init(
caLayerKeypath: #keyPath(CALayer.opacity),
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
}
// MARK: CAShapeLayer properties
extension LayerProperty {
static var path: LayerProperty<CGPath> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.path),
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
static var fillColor: LayerProperty<CGColor> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.fillColor),
defaultValue: nil,
customizableProperty: .color)
}
static var lineWidth: LayerProperty<CGFloat> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.lineWidth),
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
static var lineDashPhase: LayerProperty<CGFloat> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.lineDashPhase),
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var strokeColor: LayerProperty<CGColor> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.strokeColor),
defaultValue: nil,
customizableProperty: .color)
}
static var strokeStart: LayerProperty<CGFloat> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.strokeStart),
defaultValue: 0,
customizableProperty: nil /* currently unsupported */)
}
static var strokeEnd: LayerProperty<CGFloat> {
.init(
caLayerKeypath: #keyPath(CAShapeLayer.strokeEnd),
defaultValue: 1,
customizableProperty: nil /* currently unsupported */)
}
}
// MARK: CAGradientLayer properties
extension LayerProperty {
static var colors: LayerProperty<[CGColor]> {
.init(
caLayerKeypath: #keyPath(CAGradientLayer.colors),
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
static var locations: LayerProperty<[CGFloat]> {
.init(
caLayerKeypath: #keyPath(CAGradientLayer.locations),
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
static var startPoint: LayerProperty<CGPoint> {
.init(
caLayerKeypath: #keyPath(CAGradientLayer.startPoint),
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
static var endPoint: LayerProperty<CGPoint> {
.init(
caLayerKeypath: #keyPath(CAGradientLayer.endPoint),
defaultValue: nil,
customizableProperty: nil /* currently unsupported */)
}
}
// MARK: - CustomizableProperty types
extension CustomizableProperty {
static var color: CustomizableProperty<CGColor> {
.init(
name: [.color],
conversion: { typeErasedValue in
guard let color = typeErasedValue as? Color else {
return nil
}
return .rgba(CGFloat(color.r), CGFloat(color.g), CGFloat(color.b), CGFloat(color.a))
})
}
}

View File

@ -0,0 +1,52 @@
// Created by Cal Stephens on 5/17/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - OpacityAnimationModel
protocol OpacityAnimationModel {
/// The opacity animation to apply to a `CALayer`
var opacity: KeyframeGroup<Vector1D> { get }
}
// MARK: - Transform + OpacityAnimationModel
extension Transform: OpacityAnimationModel { }
// MARK: - ShapeTransform + OpacityAnimationModel
extension ShapeTransform: OpacityAnimationModel { }
// MARK: - Fill + OpacityAnimationModel
extension Fill: OpacityAnimationModel { }
// MARK: - GradientFill + OpacityAnimationModel
extension GradientFill: OpacityAnimationModel { }
// MARK: - Stroke + OpacityAnimationModel
extension Stroke: OpacityAnimationModel { }
// MARK: - GradientStroke + OpacityAnimationModel
extension GradientStroke: OpacityAnimationModel { }
extension CALayer {
/// Adds the opacity animation from the given `OpacityAnimationModel` to this layer
@nonobjc
func addOpacityAnimation(for opacity: OpacityAnimationModel, context: LayerAnimationContext) throws {
try addAnimation(
for: .opacity,
keyframes: opacity.opacity.keyframes,
value: {
// Lottie animation files express opacity as a numerical percentage value
// (e.g. 0%, 50%, 100%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.0, 0.5, 1.0).
$0.cgFloatValue / 100
},
context: context)
}
}

View File

@ -0,0 +1,29 @@
// Created by Cal Stephens on 12/21/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
/// Adds animations for the given `Rectangle` to this `CALayer`
@nonobjc
func addAnimations(
for rectangle: Rectangle,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: rectangle.size.keyframes,
value: { sizeKeyframe in
BezierPath.rectangle(
position: try rectangle.position
.exactlyOneKeyframe(context: context, description: "rectangle position").value.pointValue,
size: sizeKeyframe.sizeValue,
cornerRadius: try rectangle.cornerRadius
.exactlyOneKeyframe(context: context, description: "rectangle cornerRadius").value.cgFloatValue,
direction: rectangle.direction)
.cgPath()
},
context: context)
}
}

View File

@ -0,0 +1,123 @@
// Created by Cal Stephens on 1/7/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
/// Adds a `path` animation for the given `ShapeItem`
@nonobjc
func addAnimations(for shape: ShapeItem, context: LayerAnimationContext) throws {
switch shape {
case let customShape as Shape:
try addAnimations(for: customShape.path, context: context)
case let combinedShape as CombinedShapeItem:
try addAnimations(for: combinedShape, context: context)
case let ellipse as Ellipse:
try addAnimations(for: ellipse, context: context)
case let rectangle as Rectangle:
try addAnimations(for: rectangle, context: context)
case let star as Star:
try addAnimations(for: star, context: context)
default:
// None of the other `ShapeItem` subclasses draw a `path`
try context.logCompatibilityIssue("Unexpected shape type \(type(of: shape))")
return
}
}
/// Adds a `fillColor` animation for the given `Fill` object
@nonobjc
func addAnimations(for fill: Fill, context: LayerAnimationContext) throws {
fillRule = fill.fillRule.caFillRule
try addAnimation(
for: .fillColor,
keyframes: fill.color.keyframes,
value: \.cgColorValue,
context: context)
try addOpacityAnimation(for: fill, context: context)
}
/// Adds animations for `strokeStart` and `strokeEnd` from the given `Trim` object
@nonobjc
func addAnimations(for trim: Trim, context: LayerAnimationContext) throws {
let (strokeStartKeyframes, strokeEndKeyframes) = trim.caShapeLayerKeyframes()
if trim.offset.keyframes.contains(where: { $0.value.cgFloatValue != 0 }) {
try context.logCompatibilityIssue("""
The CoreAnimation rendering engine doesn't support Trim offsets
""")
}
try addAnimation(
for: .strokeStart,
keyframes: strokeStartKeyframes.keyframes,
value: { strokeStart in
// Lottie animation files express stoke trims as a numerical percentage value
// (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.25, 0.5, 1.0).
CGFloat(strokeStart.cgFloatValue) / 100
}, context: context)
try addAnimation(
for: .strokeEnd,
keyframes: strokeEndKeyframes.keyframes,
value: { strokeEnd in
// Lottie animation files express stoke trims as a numerical percentage value
// (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.25, 0.5, 1.0).
CGFloat(strokeEnd.cgFloatValue) / 100
}, context: context)
}
}
extension Trim {
// MARK: Fileprivate
/// The `strokeStart` and `strokeEnd` keyframes to apply to a `CAShapeLayer`
/// - `CAShapeLayer` requires that `strokeStart` be less than `strokeEnd`.
/// - Since this isn't a requirement in the Lottie schema, there are
/// some animations that have `strokeStart` and `strokeEnd` flipped.
/// - If we detect that this is the case for this specific `Trim`, then
/// we swap the start/end keyframes to match what `CAShapeLayer` expects.
fileprivate func caShapeLayerKeyframes()
-> (strokeStart: KeyframeGroup<Vector1D>, strokeEnd: KeyframeGroup<Vector1D>)
{
if startValueIsAlwaysGreaterThanEndValue() {
return (strokeStart: end, strokeEnd: start)
} else {
return (strokeStart: start, strokeEnd: end)
}
}
// MARK: Private
/// Checks whether or not the value for `trim.start` is greater
/// than the value for every `trim.end` at every keyframe.
private func startValueIsAlwaysGreaterThanEndValue() -> Bool {
let keyframeTimes = Set(start.keyframes.map { $0.time } + end.keyframes.map { $0.time })
let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes)
let endInterpolator = KeyframeInterpolator(keyframes: end.keyframes)
for keyframeTime in keyframeTimes {
guard
let startAtTime = startInterpolator.value(frame: keyframeTime) as? Vector1D,
let endAtTime = endInterpolator.value(frame: keyframeTime) as? Vector1D
else { continue }
if startAtTime.cgFloatValue < endAtTime.cgFloatValue {
return false
}
}
return true
}
}

View File

@ -0,0 +1,90 @@
// Created by Cal Stephens on 1/10/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CAShapeLayer {
// MARK: Internal
/// Adds animations for the given `Rectangle` to this `CALayer`
@nonobjc
func addAnimations(
for star: Star,
context: LayerAnimationContext)
throws
{
switch star.starType {
case .star:
try addStarAnimation(for: star, context: context)
case .polygon:
try addPolygonAnimation(for: star, context: context)
case .none:
break
}
}
// MARK: Private
@nonobjc
private func addStarAnimation(
for star: Star,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: star.position.keyframes,
value: { position in
// We can only use one set of keyframes to animate a given CALayer keypath,
// so we currently animate `position` and ignore any other keyframes.
// TODO: Is there a way to support this properly?
BezierPath.star(
position: position.pointValue,
outerRadius: try star.outerRadius
.exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
innerRadius: try star.innerRadius?
.exactlyOneKeyframe(context: context, description: "innerRadius").value.cgFloatValue ?? 0,
outerRoundedness: try star.outerRoundness
.exactlyOneKeyframe(context: context, description: "outerRoundness").value.cgFloatValue,
innerRoundedness: try star.innerRoundness?
.exactlyOneKeyframe(context: context, description: "innerRoundness").value.cgFloatValue ?? 0,
numberOfPoints: try star.points
.exactlyOneKeyframe(context: context, description: "points").value.cgFloatValue,
rotation: try star.rotation
.exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
direction: star.direction)
.cgPath()
},
context: context)
}
@nonobjc
private func addPolygonAnimation(
for star: Star,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .path,
keyframes: star.position.keyframes,
value: { position in
// We can only use one set of keyframes to animate a given CALayer keypath,
// so we currently animate `position` and ignore any other keyframes.
// TODO: Is there a way to support this properly?
BezierPath.polygon(
position: position.pointValue,
numberOfPoints: try star.points
.exactlyOneKeyframe(context: context, description: "numberOfPoints").value.cgFloatValue,
outerRadius: try star.outerRadius
.exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
outerRoundedness: try star.outerRoundness
.exactlyOneKeyframe(context: context, description: "outerRoundedness").value.cgFloatValue,
rotation: try star.rotation
.exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
direction: star.direction)
.cgPath()
},
context: context)
}
}

View File

@ -0,0 +1,70 @@
// Created by Cal Stephens on 2/10/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import Foundation
import QuartzCore
// MARK: - StrokeShapeItem
/// A `ShapeItem` that represents a stroke
protocol StrokeShapeItem: OpacityAnimationModel {
var strokeColor: KeyframeGroup<Color>? { get }
var width: KeyframeGroup<Vector1D> { get }
var lineCap: LineCap { get }
var lineJoin: LineJoin { get }
var miterLimit: Double { get }
var dashPattern: [DashElement]? { get }
}
// MARK: - Stroke + StrokeShapeItem
extension Stroke: StrokeShapeItem {
var strokeColor: KeyframeGroup<Color>? { color }
}
// MARK: - GradientStroke + StrokeShapeItem
extension GradientStroke: StrokeShapeItem {
var strokeColor: KeyframeGroup<Color>? { nil }
}
// MARK: - CAShapeLayer + StrokeShapeItem
extension CAShapeLayer {
/// Adds animations for properties related to the given `Stroke` object (`strokeColor`, `lineWidth`, etc)
@nonobjc
func addStrokeAnimations(for stroke: StrokeShapeItem, context: LayerAnimationContext) throws {
lineJoin = stroke.lineJoin.caLineJoin
lineCap = stroke.lineCap.caLineCap
miterLimit = CGFloat(stroke.miterLimit)
if let strokeColor = stroke.strokeColor {
try addAnimation(
for: .strokeColor,
keyframes: strokeColor.keyframes,
value: \.cgColorValue,
context: context)
}
try addAnimation(
for: .lineWidth,
keyframes: stroke.width.keyframes,
value: \.cgFloatValue,
context: context)
try addOpacityAnimation(for: stroke, context: context)
if let (dashPattern, dashPhase) = stroke.dashPattern?.shapeLayerConfiguration {
lineDashPattern = try dashPattern.map {
try KeyframeGroup(keyframes: $0)
.exactlyOneKeyframe(context: context, description: "stroke dashPattern").value.cgFloatValue as NSNumber
}
try addAnimation(
for: .lineDashPhase,
keyframes: dashPhase,
value: \.cgFloatValue,
context: context)
}
}
}

View File

@ -0,0 +1,205 @@
// Created by Cal Stephens on 12/17/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - TransformModel
/// This protocol mirrors the interface of `Transform`,
/// but it also implemented by `ShapeTransform` to allow
/// both transform types to share the same animation implementation.
protocol TransformModel {
/// The anchor point of the transform.
var anchorPoint: KeyframeGroup<Vector3D> { get }
/// The position of the transform. This is nil if the position data was split.
var _position: KeyframeGroup<Vector3D>? { get }
/// The positionX of the transform. This is nil if the position property is set.
var _positionX: KeyframeGroup<Vector1D>? { get }
/// The positionY of the transform. This is nil if the position property is set.
var _positionY: KeyframeGroup<Vector1D>? { get }
/// The scale of the transform
var scale: KeyframeGroup<Vector3D> { get }
/// The rotation of the transform. Note: This is single dimensional rotation.
var rotation: KeyframeGroup<Vector1D> { get }
}
// MARK: - Transform + TransformModel
extension Transform: TransformModel {
var _position: KeyframeGroup<Vector3D>? { position }
var _positionX: KeyframeGroup<Vector1D>? { positionX }
var _positionY: KeyframeGroup<Vector1D>? { positionY }
}
// MARK: - ShapeTransform + TransformModel
extension ShapeTransform: TransformModel {
var anchorPoint: KeyframeGroup<Vector3D> { anchor }
var _position: KeyframeGroup<Vector3D>? { position }
var _positionX: KeyframeGroup<Vector1D>? { nil }
var _positionY: KeyframeGroup<Vector1D>? { nil }
}
// MARK: - CALayer + TransformModel
extension CALayer {
// MARK: Internal
/// Adds transform-related animations from the given `TransformModel` to this layer
/// - This _doesn't_ apply `transform.opacity`, which has to be handled separately
/// since child layers don't inherit the `opacity` of their parent.
@nonobjc
func addTransformAnimations(for transformModel: TransformModel, context: LayerAnimationContext) throws {
try addPositionAnimations(from: transformModel, context: context)
try addAnchorPointAnimation(from: transformModel, context: context)
try addScaleAnimations(from: transformModel, context: context)
try addRotationAnimation(from: transformModel, context: context)
}
// MARK: Private
@nonobjc
private func addPositionAnimations(
from transformModel: TransformModel,
context: LayerAnimationContext)
throws
{
if let positionKeyframes = transformModel._position?.keyframes {
try addAnimation(
for: .position,
keyframes: positionKeyframes,
value: \.pointValue,
context: context)
} else if
let xKeyframes = transformModel._positionX?.keyframes,
let yKeyframes = transformModel._positionY?.keyframes
{
try addAnimation(
for: .positionX,
keyframes: xKeyframes,
value: \.cgFloatValue,
context: context)
try addAnimation(
for: .positionY,
keyframes: yKeyframes,
value: \.cgFloatValue,
context: context)
} else {
try context.logCompatibilityIssue("""
`Transform` values must provide either `position` or `positionX` / `positionY` keyframes
""")
}
}
@nonobjc
private func addAnchorPointAnimation(
from transformModel: TransformModel,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .anchorPoint,
keyframes: transformModel.anchorPoint.keyframes,
value: { absoluteAnchorPoint in
guard bounds.width > 0, bounds.height > 0 else {
LottieLogger.shared.assertionFailure("Size must be non-zero before an animation can be played")
return .zero
}
// Lottie animation files express anchorPoint as an absolute point value,
// so we have to divide by the width/height of this layer to get the
// relative decimal values expected by Core Animation.
return CGPoint(
x: CGFloat(absoluteAnchorPoint.x) / bounds.width,
y: CGFloat(absoluteAnchorPoint.y) / bounds.height)
},
context: context)
}
@nonobjc
private func addScaleAnimations(
from transformModel: TransformModel,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .scaleX,
keyframes: transformModel.scale.keyframes,
value: { scale in
// Lottie animation files express scale as a numerical percentage value
// (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.5, 1.0, 2.0).
// - Negative `scale.x` values aren't applied correctly by Core Animation.
// This appears to be because we animate `transform.scale.x` and `transform.scale.y`
// as separate `CAKeyframeAnimation`s instead of using a single animation of `transform` itself.
// https://openradar.appspot.com/FB9862872
// - To work around this, we set up a `rotationY` animation below
// to flip the view horizontally, which gives us the desired effect.
abs(CGFloat(scale.x) / 100)
},
context: context)
// When `scale.x` is negative, we have to rotate the view
// half way around the y axis to flip it horizontally.
// - We don't do this in snapshot tests because it breaks the tests
// in surprising ways that don't happen at runtime. Definitely not ideal.
if TestHelpers.snapshotTestsAreRunning {
if transformModel.scale.keyframes.contains(where: { $0.value.x < 0 }) {
LottieLogger.shared.warn("""
Negative `scale.x` values are not displayed correctly in snapshot tests
""")
}
} else {
try addAnimation(
for: .rotationY,
keyframes: transformModel.scale.keyframes,
value: { scale in
if scale.x < 0 {
return .pi
} else {
return 0
}
},
context: context)
}
try addAnimation(
for: .scaleY,
keyframes: transformModel.scale.keyframes,
value: { scale in
// Lottie animation files express scale as a numerical percentage value
// (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
// expected by Core Animation (e.g. 0.5, 1.0, 2.0).
// - Negative `scaleY` values are correctly applied (they flip the view
// vertically), so we don't have to apply an additional rotation animation
// like we do for `scaleX`.
CGFloat(scale.y) / 100
},
context: context)
}
private func addRotationAnimation(
from transformModel: TransformModel,
context: LayerAnimationContext)
throws
{
try addAnimation(
for: .rotation,
keyframes: transformModel.rotation.keyframes,
value: { rotationDegrees in
// Lottie animation files express rotation in degrees
// (e.g. 90º, 180º, 360º) so we covert to radians to get the
// values expected by Core Animation (e.g. π/2, π, 2π)
rotationDegrees.cgFloatValue * .pi / 180
},
context: context)
}
}

View File

@ -0,0 +1,37 @@
// Created by Cal Stephens on 12/21/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
extension CALayer {
/// Adds an animation for the given `inTime` and `outTime` to this `CALayer`
@nonobjc
func addVisibilityAnimation(
inFrame: AnimationFrameTime,
outFrame: AnimationFrameTime,
context: LayerAnimationContext)
{
let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden))
animation.calculationMode = .discrete
animation.values = [
true, // hidden, before `inFrame`
false, // visible
true, // hidden, after `outFrame`
]
// From the documentation of `keyTimes`:
// - If the calculationMode is set to discrete, the first value in the array
// must be 0.0 and the last value must be 1.0. The array should have one more
// entry than appears in the values array. For example, if there are two values,
// there should be three key times.
animation.keyTimes = [
NSNumber(value: 0.0),
NSNumber(value: max(Double(context.progressTime(for: inFrame)), 0)),
NSNumber(value: min(Double(context.progressTime(for: outFrame)), 1)),
NSNumber(value: 1.0),
]
add(animation, timedWith: context)
}
}

View File

@ -0,0 +1,128 @@
// Created by Cal Stephens on 5/4/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
// MARK: - CompatibilityIssue
/// A compatibility issue that was encountered while setting up an animation with the Core Animation engine
struct CompatibilityIssue: CustomStringConvertible {
let message: String
let context: String
var description: String {
"[\(context)] \(message)"
}
}
// MARK: - CompatibilityTracker
/// A type that tracks whether or not an animation is compatible with the Core Animation engine
final class CompatibilityTracker {
// MARK: Lifecycle
init(mode: Mode) {
self.mode = mode
}
// MARK: Internal
/// How compatibility issues should be handled
enum Mode {
/// When a compatibility issue is encountered, an error will be thrown immediately,
/// aborting the animation setup process as soon as possible.
case abort
/// When a compatibility issue is encountered, it is stored in `CompatibilityTracker.issues`
case track
}
enum Error: Swift.Error {
case encounteredCompatibilityIssue(CompatibilityIssue)
}
/// Records a compatibility issue that will be reported according to `CompatibilityTracker.Mode`
func logIssue(message: String, context: String) throws {
LottieLogger.shared.assert(!context.isEmpty, "Compatibility issue context is unexpectedly empty")
let issue = CompatibilityIssue(
// Compatibility messages are usually written in source files using multi-line strings,
// but converting them to be a single line makes it easier to read the ultimate log output.
message: message.replacingOccurrences(of: "\n", with: " "),
context: context)
switch mode {
case .abort:
throw CompatibilityTracker.Error.encounteredCompatibilityIssue(issue)
case .track:
issues.append(issue)
}
}
/// Asserts that a condition is true, otherwise logs a compatibility issue that will be reported
/// according to `CompatibilityTracker.Mode`
func assert(
_ condition: Bool,
_ message: @autoclosure () -> String,
context: @autoclosure () -> String)
throws
{
if !condition {
try logIssue(message: message(), context: context())
}
}
/// Reports the compatibility issues that were recorded when setting up the animation,
/// and clears the set of tracked issues.
func reportCompatibilityIssues(_ handler: ([CompatibilityIssue]) -> Void) {
handler(issues)
issues = []
}
// MARK: Private
private let mode: Mode
/// Compatibility issues encountered while setting up the animation
private var issues = [CompatibilityIssue]()
}
// MARK: - CompatibilityTrackerProviding
protocol CompatibilityTrackerProviding {
var compatibilityTracker: CompatibilityTracker { get }
var compatibilityIssueContext: String { get }
}
extension CompatibilityTrackerProviding {
/// Records a compatibility issue that will be reported according to `CompatibilityTracker.Mode`
func logCompatibilityIssue(_ message: String) throws {
try compatibilityTracker.logIssue(message: message, context: compatibilityIssueContext)
}
/// Asserts that a condition is true, otherwise logs a compatibility issue that will be reported
/// according to `CompatibilityTracker.Mode`
func compatibilityAssert(
_ condition: Bool,
_ message: @autoclosure () -> String)
throws
{
try compatibilityTracker.assert(condition, message(), context: compatibilityIssueContext)
}
}
// MARK: - LayerContext + CompatibilityTrackerProviding
extension LayerContext: CompatibilityTrackerProviding {
var compatibilityIssueContext: String {
layerName
}
}
// MARK: - LayerAnimationContext + CompatibilityTrackerProviding
extension LayerAnimationContext: CompatibilityTrackerProviding {
var compatibilityIssueContext: String {
currentKeypath.fullPath
}
}

View File

@ -0,0 +1,456 @@
// Created by Cal Stephens on 12/13/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import Foundation
import QuartzCore
// MARK: - CoreAnimationLayer
/// The root `CALayer` of the Core Animation rendering engine
final class CoreAnimationLayer: BaseAnimationLayer {
// MARK: Lifecycle
/// Initializes a `CALayer` that renders the given animation using `CAAnimation`s.
/// - This initializer is throwing, but will only throw when using
/// `CompatibilityTracker.Mode.abort`.
init(
animation: Animation,
imageProvider: AnimationImageProvider,
fontProvider: AnimationFontProvider,
compatibilityTrackerMode: CompatibilityTracker.Mode)
throws
{
self.animation = animation
self.imageProvider = imageProvider
self.fontProvider = fontProvider
compatibilityTracker = CompatibilityTracker(mode: compatibilityTrackerMode)
super.init()
setup()
try setupChildLayers()
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("init(layer:) incorrectly called with \(type(of: layer))")
}
animation = typedLayer.animation
currentAnimationConfiguration = typedLayer.currentAnimationConfiguration
imageProvider = typedLayer.imageProvider
fontProvider = typedLayer.fontProvider
didSetUpAnimation = typedLayer.didSetUpAnimation
compatibilityTracker = typedLayer.compatibilityTracker
super.init(layer: typedLayer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
/// Timing-related configuration to apply to this layer's child `CAAnimation`s
/// - This is effectively a configurable subset of `CAMediaTiming`
struct CAMediaTimingConfiguration: Equatable {
var autoreverses = false
var repeatCount: Float = 0
var speed: Float = 1
var timeOffset: TimeInterval = 0
}
enum PlaybackState: Equatable {
/// The animation is playing in real-time
case playing
/// The animation is statically displaying a specific frame
case paused(frame: AnimationFrameTime)
}
/// A closure that is called after this layer sets up its animation.
/// If the animation setup was unsuccessful and encountered compatibility issues,
/// those issues are included in this call.
var didSetUpAnimation: (([CompatibilityIssue]) -> Void)?
/// The `AnimationImageProvider` that `ImageLayer`s use to retrieve images,
/// referenced by name in the animation json.
var imageProvider: AnimationImageProvider {
didSet { reloadImages() }
}
/// The `FontProvider` that `TextLayer`s use to retrieve the `CTFont`
/// that they should use to render their text content
var fontProvider: AnimationFontProvider {
didSet { reloadFonts() }
}
/// Queues the animation with the given timing configuration
/// to begin playing at the next `display()` call.
/// - This batches together animations so that even if `playAnimation`
/// is called multiple times in the same run loop cycle, the animation
/// will only be set up a single time.
func playAnimation(
context: AnimationContext,
timingConfiguration: CAMediaTimingConfiguration,
playbackState: PlaybackState = .playing)
{
pendingAnimationConfiguration = (
animationConfiguration: .init(animationContext: context, timingConfiguration: timingConfiguration),
playbackState: playbackState)
setNeedsDisplay()
}
override func layoutSublayers() {
super.layoutSublayers()
// If no animation has been set up yet, display the first frame
// now that the layer hierarchy has been setup and laid out
if
pendingAnimationConfiguration == nil,
currentAnimationConfiguration == nil,
bounds.size != .zero
{
currentFrame = animation.frameTime(forProgress: animationProgress)
}
}
override func display() {
// We intentionally don't call `super.display()`, since this layer
// doesn't directly render any content.
// - This fixes an issue where certain animations would unexpectedly
// allocate a very large amount of memory (400mb+).
// - Alternatively this layer could subclass `CATransformLayer`,
// but this causes Core Animation to emit unnecessary logs.
if let pendingAnimationConfiguration = pendingAnimationConfiguration {
self.pendingAnimationConfiguration = nil
do {
try setupAnimation(for: pendingAnimationConfiguration.animationConfiguration)
} catch {
if case CompatibilityTracker.Error.encounteredCompatibilityIssue(let compatibilityIssue) = error {
// Even though the animation setup failed, we still update the layer's playback state
// so it can be read by the parent `AnimationView` when handling this error
currentPlaybackState = pendingAnimationConfiguration.playbackState
didSetUpAnimation?([compatibilityIssue])
return
}
}
currentPlaybackState = pendingAnimationConfiguration.playbackState
compatibilityTracker.reportCompatibilityIssues { compatibilityIssues in
didSetUpAnimation?(compatibilityIssues)
}
}
}
// MARK: Private
private struct AnimationConfiguration: Equatable {
let animationContext: AnimationContext
let timingConfiguration: CAMediaTimingConfiguration
}
/// The configuration for the most recent animation which has been
/// queued by calling `playAnimation` but not yet actually set up
private var pendingAnimationConfiguration: (
animationConfiguration: AnimationConfiguration,
playbackState: PlaybackState)?
/// Configuration for the animation that is currently setup in this layer
private var currentAnimationConfiguration: AnimationConfiguration?
/// The current progress of the placeholder `CAAnimation`,
/// which is also the realtime animation progress of this layer's animation
@objc private var animationProgress: CGFloat = 0
private let animation: Animation
private let valueProviderStore = ValueProviderStore()
private let compatibilityTracker: CompatibilityTracker
/// The current playback state of the animation that is displayed in this layer
private var currentPlaybackState: PlaybackState? {
didSet {
guard playbackState != oldValue else { return }
switch playbackState {
case .playing, nil:
timeOffset = 0
case .paused(let frame):
timeOffset = animation.time(forFrame: frame)
}
}
}
/// The current or pending playback state of the animation displayed in this layer
private var playbackState: PlaybackState? {
pendingAnimationConfiguration?.playbackState ?? currentPlaybackState
}
/// Context used when setting up and configuring sublayers
private var layerContext: LayerContext {
LayerContext(
animation: animation,
imageProvider: imageProvider,
fontProvider: fontProvider,
compatibilityTracker: compatibilityTracker,
layerName: "root layer")
}
private func setup() {
bounds = animation.bounds
}
private func setupChildLayers() throws {
try setupLayerHierarchy(
for: animation.layers,
context: layerContext)
}
/// Immediately builds and begins playing `CAAnimation`s for each sublayer
private func setupAnimation(for configuration: AnimationConfiguration) throws {
// Remove any existing animations from the layer hierarchy
removeAnimations()
currentAnimationConfiguration = configuration
let layerContext = LayerAnimationContext(
animation: animation,
timingConfiguration: configuration.timingConfiguration,
startFrame: configuration.animationContext.playFrom,
endFrame: configuration.animationContext.playTo,
valueProviderStore: valueProviderStore,
compatibilityTracker: compatibilityTracker,
currentKeypath: AnimationKeypath(keys: []))
// Perform a layout pass if necessary so all of the sublayers
// have the most up-to-date sizing information
layoutIfNeeded()
// Set the speed of this layer, which will be inherited
// by all sublayers and their animations.
// - This is required to support scrubbing with a speed of 0
speed = configuration.timingConfiguration.speed
// Setup a placeholder animation to let us track the realtime animation progress
setupPlaceholderAnimation(context: layerContext)
// Set up the new animations with the current `TimingConfiguration`
for animationLayer in sublayers ?? [] {
try (animationLayer as? AnimationLayer)?.setupAnimations(context: layerContext)
}
}
/// Sets up a placeholder `CABasicAnimation` that tracks the current
/// progress of this animation (between 0 and 1). This lets us provide
/// realtime animation progress via `self.currentFrame`.
private func setupPlaceholderAnimation(context: LayerAnimationContext) {
let animationProgressTracker = CABasicAnimation(keyPath: #keyPath(animationProgress))
animationProgressTracker.fromValue = 0
animationProgressTracker.toValue = 1
let timedProgressAnimation = animationProgressTracker.timed(with: context, for: self)
timedProgressAnimation.delegate = currentAnimationConfiguration?.animationContext.closure
add(timedProgressAnimation, forKey: #keyPath(animationProgress))
}
// Removes the current `CAAnimation`s, and rebuilds new animations
// using the same configuration as the previous animations.
private func rebuildCurrentAnimation() {
guard
let currentConfiguration = currentAnimationConfiguration,
let playbackState = playbackState,
// Don't replace any pending animations that are queued to begin
// on the next run loop cycle, since an existing pending animation
// will cause the animation to be rebuilt anyway.
pendingAnimationConfiguration == nil
else { return }
removeAnimations()
switch playbackState {
case .paused(let frame):
currentFrame = frame
case .playing:
playAnimation(
context: currentConfiguration.animationContext,
timingConfiguration: currentConfiguration.timingConfiguration)
}
}
}
// MARK: RootAnimationLayer
extension CoreAnimationLayer: RootAnimationLayer {
var primaryAnimationKey: AnimationKey {
.specific(#keyPath(animationProgress))
}
var isAnimationPlaying: Bool? {
switch playbackState {
case .playing:
return true
case nil, .paused:
return false
}
}
var currentFrame: AnimationFrameTime {
get {
switch playbackState {
case .playing, nil:
return animation.frameTime(forProgress: (presentation() ?? self).animationProgress)
case .paused(let frame):
return frame
}
}
set {
// We can display a specific frame of the animation by setting
// `timeOffset` of this layer. This requires setting up the layer hierarchy
// with a specific configuration (speed=0, etc) at least once. But if
// the layer hierarchy is already set up correctly, we can update the
// `timeOffset` very cheaply.
let requiredAnimationConfiguration = AnimationConfiguration(
animationContext: AnimationContext(
playFrom: animation.startFrame,
playTo: animation.endFrame,
closure: nil),
timingConfiguration: CAMediaTimingConfiguration(speed: 0))
if
pendingAnimationConfiguration == nil,
currentAnimationConfiguration == requiredAnimationConfiguration
{
currentPlaybackState = .paused(frame: newValue)
}
else {
playAnimation(
context: requiredAnimationConfiguration.animationContext,
timingConfiguration: requiredAnimationConfiguration.timingConfiguration,
playbackState: .paused(frame: newValue))
}
}
}
var renderScale: CGFloat {
get { contentsScale }
set {
contentsScale = newValue
for sublayer in allSublayers {
sublayer.contentsScale = newValue
}
}
}
var respectAnimationFrameRate: Bool {
get { false }
set { LottieLogger.shared.assertionFailure("`respectAnimationFrameRate` is currently unsupported") }
}
var _animationLayers: [CALayer] {
(sublayers ?? []).filter { $0 is AnimationLayer }
}
var textProvider: AnimationTextProvider {
get { DictionaryTextProvider([:]) }
set { LottieLogger.shared.assertionFailure("`textProvider` is currently unsupported") }
}
func reloadImages() {
// When the image provider changes, we have to update all `ImageLayer`s
// so they can query the most up-to-date image from the new image provider.
for sublayer in allSublayers {
if let imageLayer = sublayer as? ImageLayer {
imageLayer.setupImage(context: layerContext)
}
}
}
func reloadFonts() {
// When the text provider changes, we have to update all `TextLayer`s
// so they can query the most up-to-date font from the new font provider.
for sublayer in allSublayers {
if let textLayer = sublayer as? TextLayer {
try? textLayer.configureRenderLayer(with: layerContext)
}
}
}
func forceDisplayUpdate() {
// Unimplemented / unused
}
func logHierarchyKeypaths() {
// Unimplemented / unused
}
func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
valueProviderStore.setValueProvider(valueProvider, keypath: keypath)
// We need to rebuild the current animation after registering a value provider,
// since any existing `CAAnimation`s could now be out of date.
rebuildCurrentAnimation()
}
func getValue(for _: AnimationKeypath, atFrame _: AnimationFrameTime?) -> Any? {
LottieLogger.shared.assertionFailure("""
The Core Animation rendering engine doesn't support querying values for individual frames
""")
return nil
}
func getOriginalValue(for _: AnimationKeypath, atFrame _: AnimationFrameTime?) -> Any? {
LottieLogger.shared.assertionFailure("""
The Core Animation rendering engine doesn't support querying values for individual frames
""")
return nil
}
func layer(for _: AnimationKeypath) -> CALayer? {
LottieLogger.shared.assertionFailure("`AnimationKeypath`s are currently unsupported")
return nil
}
func animatorNodes(for _: AnimationKeypath) -> [AnimatorNode]? {
LottieLogger.shared.assertionFailure("`AnimatorNode`s are not used in this rendering implementation")
return nil
}
func removeAnimations() {
currentAnimationConfiguration = nil
currentPlaybackState = nil
removeAllAnimations()
for sublayer in allSublayers {
sublayer.removeAllAnimations()
}
}
}
// MARK: - CALayer + allSublayers
extension CALayer {
/// All of the layers in the layer tree that are descendants from this later
@nonobjc
var allSublayers: [CALayer] {
var allSublayers: [CALayer] = []
for sublayer in sublayers ?? [] {
allSublayers.append(sublayer)
allSublayers.append(contentsOf: sublayer.allSublayers)
}
return allSublayers
}
}

View File

@ -0,0 +1,35 @@
// Created by Cal Stephens on 12/15/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - CALayer + fillBoundsOfSuperlayer
extension CALayer {
/// Updates the `bounds` of this layer to fill the bounds of its `superlayer`
/// without setting `frame` (which is not permitted if the layer can rotate)
@nonobjc
func fillBoundsOfSuperlayer() {
guard let superlayer = superlayer else { return }
if let customLayerLayer = self as? CustomLayoutLayer {
customLayerLayer.layout(superlayerBounds: superlayer.bounds)
}
else {
// By default the `anchorPoint` of a layer is `CGPoint(x: 0.5, y: 0.5)`.
// Setting it to `.zero` makes the layer have the same coordinate space
// as its superlayer, which lets use use `superlayer.bounds` directly.
anchorPoint = .zero
bounds = superlayer.bounds
}
}
}
// MARK: - CustomLayoutLayer
/// A `CALayer` that sets a custom `bounds` and `anchorPoint` relative to its superlayer
protocol CustomLayoutLayer: CALayer {
func layout(superlayerBounds: CGRect)
}

View File

@ -0,0 +1,37 @@
// Created by Cal Stephens on 1/11/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
// MARK: - KeyframeGroup + exactlyOneKeyframe
extension KeyframeGroup {
/// Retrieves the first `Keyframe` from this group,
/// and asserts that there are not any extra keyframes that would be ignored
///
/// - There are several places in Lottie animation definitions where multiple
/// sets of keyframe timings can be provided for properties that have to
/// be applied to a single `CALayer` property (for example, the definition for a
/// `Rectangle` technically lets you animate `size`, `position`, and `cornerRadius`
/// separately, but these all have to be combined into a single `CAKeyframeAnimation`
/// on the `CAShapeLayer.path` property.
///
/// - In those sorts of cases, we currently choose one one `KeyframeGroup` to provide the
/// timing information, and disallow simultaneous animations on the other properties.
///
func exactlyOneKeyframe(
context: CompatibilityTrackerProviding,
description: String,
fileID _: StaticString = #fileID,
line _: UInt = #line)
throws
-> Keyframe<T>
{
try context.compatibilityAssert(
keyframes.count == 1,
"""
The Core Animation rendering engine does not support animating multiple keyframes
for \(description) values (due to limitations of Core Animation `CAKeyframeAnimation`s).
""")
return keyframes[0]
}
}

View File

@ -0,0 +1,61 @@
// Created by Cal Stephens on 1/28/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
// MARK: - Keyframes
enum Keyframes {
/// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s
/// into a single `KeyframeGroup` of `Keyframe<[T]>`s
/// if all of the `KeyframeGroup`s have the exact same animation timing
static func combinedIfPossible<T>(_ groups: [KeyframeGroup<T>]) -> KeyframeGroup<[T]>? {
guard
!groups.isEmpty,
groups.allSatisfy({ $0.hasSameTimingParameters(as: groups[0]) })
else { return nil }
var combinedKeyframes = ContiguousArray<Keyframe<[T]>>()
for index in groups[0].keyframes.indices {
let baseKeyframe = groups[0].keyframes[index]
let combinedValues = groups.map { $0.keyframes[index].value }
combinedKeyframes.append(baseKeyframe.withValue(combinedValues))
}
return KeyframeGroup(keyframes: combinedKeyframes)
}
/// Combines the given `[KeyframeGroup?]` of `Keyframe<T>`s
/// into a single `KeyframeGroup` of `Keyframe<[T]>`s
/// if all of the `KeyframeGroup`s have the exact same animation timing
static func combinedIfPossible<T>(_ groups: [KeyframeGroup<T>?]) -> KeyframeGroup<[T]>? {
let nonOptionalGroups = groups.compactMap { $0 }
guard nonOptionalGroups.count == groups.count else { return nil }
return combinedIfPossible(nonOptionalGroups)
}
}
extension KeyframeGroup {
/// Whether or not all of the keyframes in this `KeyframeGroup` have the same
/// timing parameters as the corresponding keyframe in the other given `KeyframeGroup`
func hasSameTimingParameters<T>(as other: KeyframeGroup<T>) -> Bool {
guard keyframes.count == other.keyframes.count else {
return false
}
return zip(keyframes, other.keyframes).allSatisfy {
$0.hasSameTimingParameters(as: $1)
}
}
}
extension Keyframe {
/// Whether or not this keyframe has the same timing parameters as the given keyframe
func hasSameTimingParameters<T>(as other: Keyframe<T>) -> Bool {
time == other.time
&& isHold == other.isHold
&& inTangent == other.inTangent
&& outTangent == other.outTangent
&& spatialInTangent == other.spatialInTangent
&& spatialOutTangent == other.spatialOutTangent
}
}

View File

@ -0,0 +1,70 @@
// Created by Cal Stephens on 12/14/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - AnimationLayer
/// A type of `CALayer` that can be used in a Lottie animation
/// - Layers backed by a `LayerModel` subclass should subclass `BaseCompositionLayer`
protocol AnimationLayer: CALayer {
/// Instructs this layer to setup its `CAAnimation`s
/// using the given `LayerAnimationContext`
func setupAnimations(context: LayerAnimationContext) throws
}
// MARK: - LayerAnimationContext
// Context describing the timing parameters of the current animation
struct LayerAnimationContext {
/// The animation being played
let animation: Animation
/// The timing configuration that should be applied to `CAAnimation`s
let timingConfiguration: CoreAnimationLayer.CAMediaTimingConfiguration
/// The absolute frame number that this animation begins at
let startFrame: AnimationFrameTime
/// The absolute frame number that this animation ends at
let endFrame: AnimationFrameTime
/// The set of custom Value Providers applied to this animation
let valueProviderStore: ValueProviderStore
/// Information about whether or not an animation is compatible with the Core Animation engine
let compatibilityTracker: CompatibilityTracker
/// The AnimationKeypath represented by the current layer
var currentKeypath: AnimationKeypath
/// A closure that remaps the given frame in the child layer's local time to a frame
/// in the animation's overall global time
private(set) var timeRemapping: ((AnimationFrameTime) -> AnimationFrameTime) = { $0 }
/// Adds the given component string to the `AnimationKeypath` stored
/// that describes the current path being configured by this context value
func addingKeypathComponent(_ component: String) -> LayerAnimationContext {
var context = self
context.currentKeypath.keys.append(component)
return context
}
/// The `AnimationProgressTime` for the given `AnimationFrameTime` within this layer,
/// accounting for the `timeRemapping` applied to this layer
func progressTime(for frame: AnimationFrameTime) -> AnimationProgressTime {
animation.progressTime(forFrame: timeRemapping(frame), clamped: false)
}
/// Chains an additional `timeRemapping` closure onto this layer context
func withTimeRemapping(
_ additionalTimeRemapping: @escaping (AnimationFrameTime) -> AnimationFrameTime)
-> LayerAnimationContext
{
var copy = self
copy.timeRemapping = { [existingTimeRemapping = timeRemapping] time in
existingTimeRemapping(additionalTimeRemapping(time))
}
return copy
}
}

View File

@ -0,0 +1,33 @@
// Created by Cal Stephens on 1/27/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
/// A base `CALayer` that manages the frame and animations
/// of its `sublayers` and `mask`
class BaseAnimationLayer: CALayer, AnimationLayer {
// MARK: Internal
override func layoutSublayers() {
super.layoutSublayers()
for sublayer in managedSublayers {
sublayer.fillBoundsOfSuperlayer()
}
}
func setupAnimations(context: LayerAnimationContext) throws {
for childAnimationLayer in managedSublayers {
try (childAnimationLayer as? AnimationLayer)?.setupAnimations(context: context)
}
}
// MARK: Private
/// All of the sublayers managed by this container
private var managedSublayers: [CALayer] {
(sublayers ?? []) + [mask].compactMap { $0 }
}
}

View File

@ -0,0 +1,87 @@
// Created by Cal Stephens on 12/20/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - BaseCompositionLayer
/// The base type of `AnimationLayer` that can contain other `AnimationLayer`s
class BaseCompositionLayer: BaseAnimationLayer {
// MARK: Lifecycle
init(layerModel: LayerModel) {
baseLayerModel = layerModel
super.init()
setupSublayers()
compositingFilter = layerModel.blendMode.filterName
name = layerModel.name
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
baseLayerModel = typedLayer.baseLayerModel
super.init(layer: typedLayer)
}
// MARK: Internal
/// Whether or not this layer render should render any visible content
var renderLayerContents: Bool { true }
/// Sets up the base `LayerModel` animations for this layer,
/// and all child `AnimationLayer`s.
/// - Can be overridden by subclasses, which much call `super`.
override func setupAnimations(context: LayerAnimationContext) throws {
var context = context
if renderLayerContents {
context = context.addingKeypathComponent(baseLayerModel.name)
}
try setupLayerAnimations(context: context)
try setupChildAnimations(context: context)
}
func setupLayerAnimations(context: LayerAnimationContext) throws {
let context = context.addingKeypathComponent(baseLayerModel.name)
try addTransformAnimations(for: baseLayerModel.transform, context: context)
if renderLayerContents {
try addOpacityAnimation(for: baseLayerModel.transform, context: context)
addVisibilityAnimation(
inFrame: CGFloat(baseLayerModel.inFrame),
outFrame: CGFloat(baseLayerModel.outFrame),
context: context)
}
}
func setupChildAnimations(context: LayerAnimationContext) throws {
try super.setupAnimations(context: context)
}
// MARK: Private
private let baseLayerModel: LayerModel
private func setupSublayers() {
if
renderLayerContents,
let masks = baseLayerModel.masks
{
mask = MaskCompositionLayer(masks: masks)
}
}
}

View File

@ -0,0 +1,131 @@
// Created by Cal Stephens on 1/11/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CALayer {
/// Sets up an `AnimationLayer` / `CALayer` hierarchy in this layer,
/// using the given list of layers.
@nonobjc
func setupLayerHierarchy(
for layers: [LayerModel],
context: LayerContext)
throws
{
// An `Animation`'s `LayerModel`s are listed from front to back,
// but `CALayer.sublayers` are listed from back to front.
// We reverse the layer ordering to match what Core Animation expects.
// The final view hierarchy must display the layers in this exact order.
let layersInZAxisOrder = layers.reversed()
let layersByIndex = Dictionary(grouping: layersInZAxisOrder, by: \.index)
.compactMapValues(\.first)
/// Layers specify a `parent` layer. Child layers inherit the `transform` of their parent.
/// - We can't add the child as a sublayer of the parent `CALayer`, since that would
/// break the ordering specified in `layersInZAxisOrder`.
/// - Instead, we create an invisible `TransformLayer` to handle the parent
/// transform animations, and add the child layer to that `TransformLayer`.
func makeParentTransformLayer(
childLayerModel: LayerModel,
childLayer: CALayer,
name: (LayerModel) -> String)
-> CALayer
{
guard
let parentIndex = childLayerModel.parent,
let parentLayerModel = layersByIndex[parentIndex]
else { return childLayer }
let parentLayer = TransformLayer(layerModel: parentLayerModel)
parentLayer.name = name(parentLayerModel)
parentLayer.addSublayer(childLayer)
return makeParentTransformLayer(
childLayerModel: parentLayerModel,
childLayer: parentLayer,
name: name)
}
// Create an `AnimationLayer` for each `LayerModel`
for (layerModel, maskLayerModel) in try layersInZAxisOrder.pairedLayersAndMasks(context: context) {
guard let layer = try layerModel.makeAnimationLayer(context: context) else {
continue
}
// If this layer has a `parent`, we create an invisible `TransformLayer`
// to handle displaying / animating the parent transform.
let parentTransformLayer = makeParentTransformLayer(
childLayerModel: layerModel,
childLayer: layer,
name: { parentLayerModel in
"\(layerModel.name) (parent, \(parentLayerModel.name))"
})
// Create the `mask` layer for this layer, if it has a `MatteType`
if
let maskLayerModel = maskLayerModel,
let maskLayer = try maskLayerModel.makeAnimationLayer(context: context)
{
let maskParentTransformLayer = makeParentTransformLayer(
childLayerModel: maskLayerModel,
childLayer: maskLayer,
name: { parentLayerModel in
"\(maskLayerModel.name) (mask of \(layerModel.name)) (parent, \(parentLayerModel.name))"
})
// Set up a parent container to host both the layer
// and its mask in the same coordinate space
let maskContainer = BaseAnimationLayer()
maskContainer.name = "\(layerModel.name) (parent, masked)"
maskContainer.addSublayer(parentTransformLayer)
// Core Animation will silently fail to apply a mask if a `mask` layer
// itself _also_ has a `mask`. As a workaround, we can wrap this layer's
// mask in an additional container layer which never has its own `mask`.
let additionalMaskParent = BaseAnimationLayer()
additionalMaskParent.addSublayer(maskParentTransformLayer)
maskContainer.mask = additionalMaskParent
addSublayer(maskContainer)
}
else {
addSublayer(parentTransformLayer)
}
}
}
}
extension Collection where Element == LayerModel {
/// Pairs each `LayerModel` within this array with
/// a `LayerModel` to use as its mask, if applicable
/// based on the layer's `MatteType` configuration.
/// - Assumes the layers are sorted in z-axis order.
fileprivate func pairedLayersAndMasks(context: LayerContext) throws -> [(layer: LayerModel, mask: LayerModel?)] {
var layersAndMasks = [(layer: LayerModel, mask: LayerModel?)]()
var unprocessedLayers = reversed()
while let layer = unprocessedLayers.popLast() {
/// If a layer has a `MatteType`, then the next layer will be used as its `mask`
if
let matteType = layer.matte,
matteType != .none,
let maskLayer = unprocessedLayers.popLast()
{
try context.compatibilityAssert(
matteType == .add,
"The Core Animation rendering engine currently only supports `MatteMode.add`.")
layersAndMasks.append((layer: layer, mask: maskLayer))
}
else {
layersAndMasks.append((layer: layer, mask: nil))
}
}
return layersAndMasks
}
}

View File

@ -0,0 +1,87 @@
// Created by Cal Stephens on 1/10/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - GradientRenderLayer
/// A `CAGradientLayer` subclass used to render a gradient _outside_ the normal layer bounds
///
/// - `GradientFill.startPoint` and `GradientFill.endPoint` are expressed
/// with respect to the `bounds` of the `ShapeItemLayer`.
///
/// - The gradient itself is supposed to be rendered infinitely in all directions
/// (e.g. including outside of `bounds`). This is because `ShapeItemLayer` paths
/// don't necessarily sit within the layer's `bounds`.
///
/// - To support this, `GradientRenderLayer` tracks a `gradientReferenceBounds`
/// that `startPoint` / `endPoint` are calculated relative to.
/// The _actual_ `bounds` of this layer is padded by a large amount so that
/// the gradient can be drawn outside of the `gradientReferenceBounds`.
///
final class GradientRenderLayer: CAGradientLayer {
// MARK: Internal
/// The reference bounds within this layer that the gradient's
/// `startPoint` and `endPoint` should be calculated relative to
var gradientReferenceBounds: CGRect = .zero {
didSet {
if oldValue != gradientReferenceBounds {
updateLayout()
}
}
}
/// Converts the given `CGPoint` within `gradientReferenceBounds`
/// to a percentage value relative to the full `bounds` of this layer
/// - This converts absolute `startPoint` and `endPoint` values into
/// the percent-based values expected by Core Animation,
/// with respect to the custom bounds geometry used by this layer type.
func percentBasedPointInBounds(from referencePoint: CGPoint) -> CGPoint {
guard bounds.width > 0, bounds.height > 0 else {
LottieLogger.shared.assertionFailure("Size must be non-zero before an animation can be played")
return .zero
}
let pointInBounds = CGPoint(
x: referencePoint.x + gradientPadding,
y: referencePoint.y + gradientPadding)
return CGPoint(
x: CGFloat(pointInBounds.x) / bounds.width,
y: CGFloat(pointInBounds.y) / bounds.height)
}
// MARK: Private
/// Extra padding around the `gradientReferenceBounds` where the gradient is also rendered
/// - This specific value is arbitrary and can be increased if necessary.
/// Theoretically this should be "infinite", to match the behavior of
/// `CGContext.drawLinearGradient` with `[.drawsAfterEndLocation, .drawsBeforeStartLocation]`.
private let gradientPadding: CGFloat = 2_000
private func updateLayout() {
anchorPoint = .zero
bounds = CGRect(
x: gradientReferenceBounds.origin.x,
y: gradientReferenceBounds.origin.y,
width: gradientPadding + gradientReferenceBounds.width + gradientPadding,
height: gradientPadding + gradientReferenceBounds.height + gradientPadding)
transform = CATransform3DMakeTranslation(
-gradientPadding,
-gradientPadding,
0)
}
}
// MARK: CustomLayoutLayer
extension GradientRenderLayer: CustomLayoutLayer {
func layout(superlayerBounds: CGRect) {
gradientReferenceBounds = superlayerBounds
}
}

View File

@ -0,0 +1,79 @@
// Created by Cal Stephens on 1/10/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - ImageLayer
/// The `CALayer` type responsible for rendering `ImageLayerModel`s
final class ImageLayer: BaseCompositionLayer {
// MARK: Lifecycle
init(
imageLayer: ImageLayerModel,
context: LayerContext)
{
self.imageLayer = imageLayer
super.init(layerModel: imageLayer)
setupImage(context: context)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
imageLayer = typedLayer.imageLayer
super.init(layer: typedLayer)
}
// MARK: Internal
func setupImage(context: LayerContext) {
guard
let imageAsset = context.animation.assetLibrary?.imageAssets[imageLayer.referenceID],
let image = context.imageProvider.imageForAsset(asset: imageAsset)
else {
self.imageAsset = nil
contents = nil
return
}
self.imageAsset = imageAsset
contents = image
setNeedsLayout()
}
// MARK: Private
private let imageLayer: ImageLayerModel
private var imageAsset: ImageAsset?
}
// MARK: CustomLayoutLayer
extension ImageLayer: CustomLayoutLayer {
func layout(superlayerBounds: CGRect) {
anchorPoint = .zero
guard let imageAsset = imageAsset else {
bounds = superlayerBounds
return
}
// Image layers specifically need to use the size of the image itself
bounds = CGRect(
x: superlayerBounds.origin.x,
y: superlayerBounds.origin.y,
width: CGFloat(imageAsset.width),
height: CGFloat(imageAsset.height))
}
}

View File

@ -0,0 +1,60 @@
// Created by Cal Stephens on 12/20/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - LayerContext
/// Context available when constructing an `AnimationLayer`
struct LayerContext {
let animation: Animation
let imageProvider: AnimationImageProvider
let fontProvider: AnimationFontProvider
let compatibilityTracker: CompatibilityTracker
var layerName: String
func forLayer(_ layer: LayerModel) -> LayerContext {
var context = self
context.layerName = layer.name
return context
}
}
// MARK: - LayerModel + makeAnimationLayer
extension LayerModel {
/// Constructs an `AnimationLayer` / `CALayer` that represents this `LayerModel`
func makeAnimationLayer(context: LayerContext) throws -> BaseCompositionLayer? {
let context = context.forLayer(self)
switch (type, self) {
case (.precomp, let preCompLayerModel as PreCompLayerModel):
let preCompLayer = PreCompLayer(preCompLayer: preCompLayerModel)
try preCompLayer.setup(context: context)
return preCompLayer
case (.solid, let solidLayerModel as SolidLayerModel):
return SolidLayer(solidLayerModel)
case (.shape, let shapeLayerModel as ShapeLayerModel):
return try ShapeLayer(shapeLayer: shapeLayerModel, context: context)
case (.image, let imageLayerModel as ImageLayerModel):
return ImageLayer(imageLayer: imageLayerModel, context: context)
case (.text, let textLayerModel as TextLayerModel):
return try TextLayer(textLayerModel: textLayerModel, context: context)
case (.null, _):
return TransformLayer(layerModel: self)
default:
try context.logCompatibilityIssue("""
Unexpected layer type combination ("\(type)" and "\(Swift.type(of: self))")
""")
return nil
}
}
}

View File

@ -0,0 +1,104 @@
// Created by Cal Stephens on 1/6/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - MaskCompositionLayer
/// The CALayer type responsible for rendering the `Mask` of a `BaseCompositionLayer`
final class MaskCompositionLayer: CALayer {
// MARK: Lifecycle
init(masks: [Mask]) {
maskLayers = masks.map(MaskLayer.init(mask:))
super.init()
for maskLayer in maskLayers {
addSublayer(maskLayer)
}
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
maskLayers = typedLayer.maskLayers
super.init(layer: typedLayer)
}
// MARK: Internal
override func layoutSublayers() {
super.layoutSublayers()
for sublayer in sublayers ?? [] {
sublayer.fillBoundsOfSuperlayer()
}
}
// MARK: Private
private let maskLayers: [MaskLayer]
}
// MARK: AnimationLayer
extension MaskCompositionLayer: AnimationLayer {
func setupAnimations(context: LayerAnimationContext) throws {
for maskLayer in maskLayers {
try maskLayer.setupAnimations(context: context)
}
}
}
// MARK: - MaskLayer
extension MaskCompositionLayer {
final class MaskLayer: CAShapeLayer {
// MARK: Lifecycle
init(mask: Mask) {
maskModel = mask
super.init()
fillColor = .rgb(0, 0, 0)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
maskModel = typedLayer.maskModel
super.init(layer: typedLayer)
}
// MARK: Private
private let maskModel: Mask
}
}
// MARK: - MaskCompositionLayer.MaskLayer + AnimationLayer
extension MaskCompositionLayer.MaskLayer: AnimationLayer {
func setupAnimations(context: LayerAnimationContext) throws {
try addAnimations(for: maskModel.shape, context: context)
}
}

View File

@ -0,0 +1,140 @@
// Created by Cal Stephens on 12/14/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - PreCompLayer
/// The `CALayer` type responsible for rendering `PreCompLayerModel`s
final class PreCompLayer: BaseCompositionLayer {
// MARK: Lifecycle
init(preCompLayer: PreCompLayerModel) {
self.preCompLayer = preCompLayer
super.init(layerModel: preCompLayer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
preCompLayer = typedLayer.preCompLayer
timeRemappingInterpolator = typedLayer.timeRemappingInterpolator
super.init(layer: typedLayer)
}
// MARK: Internal
/// Post-init setup for `PreCompLayer`s.
/// Should always be called after `PreCompLayer.init(preCompLayer:)`.
///
/// This is a workaround for a hard-to-reproduce crash that was
/// triggered when `PreCompLayer.init` was called reentantly. We didn't
/// have any consistent repro steps for this crash (it happened 100% of
/// the time for some testers, and 0% of the time for other testers),
/// but moving this code out of `PreCompLayer.init` does seem to fix it.
///
/// The stack trace looked like:
/// - `_os_unfair_lock_recursive_abort`
/// - `-[CALayerAccessibility__UIKit__QuartzCore dealloc]`
/// - `PreCompLayer.__allocating_init(preCompLayer:context:)` <- reentrant init call
/// - ...
/// - `CALayer.setupLayerHierarchy(for:context:)`
/// - `PreCompLayer.init(preCompLayer:context:)`
///
func setup(context: LayerContext) throws {
if let timeRemappingKeyframes = preCompLayer.timeRemapping {
timeRemappingInterpolator = try .timeRemapping(keyframes: timeRemappingKeyframes, context: context)
} else {
timeRemappingInterpolator = nil
}
try setupLayerHierarchy(
for: context.animation.assetLibrary?.precompAssets[preCompLayer.referenceID]?.layers ?? [],
context: context)
}
override func setupAnimations(context: LayerAnimationContext) throws {
var context = context
context = context.addingKeypathComponent(preCompLayer.name)
try setupLayerAnimations(context: context)
// Precomp layers can adjust the local time of their child layers (relative to the
// animation's global time) via `timeRemapping` or a custom `startTime`
let contextForChildren = context.withTimeRemapping { [preCompLayer, timeRemappingInterpolator] layerLocalFrame in
if let timeRemappingInterpolator = timeRemappingInterpolator {
return timeRemappingInterpolator.value(frame: layerLocalFrame) as? AnimationFrameTime ?? layerLocalFrame
} else {
return layerLocalFrame + AnimationFrameTime(preCompLayer.startTime)
}
}
try setupChildAnimations(context: contextForChildren)
}
// MARK: Private
private let preCompLayer: PreCompLayerModel
private var timeRemappingInterpolator: KeyframeInterpolator<AnimationFrameTime>?
}
// MARK: CustomLayoutLayer
extension PreCompLayer: CustomLayoutLayer {
func layout(superlayerBounds: CGRect) {
anchorPoint = .zero
// Pre-comp layers use a size specified in the layer model,
// and clip the composition to that bounds
bounds = CGRect(
x: superlayerBounds.origin.x,
y: superlayerBounds.origin.y,
width: CGFloat(preCompLayer.width),
height: CGFloat(preCompLayer.height))
masksToBounds = true
}
}
extension KeyframeInterpolator where ValueType == AnimationFrameTime {
/// A `KeyframeInterpolator` for the given `timeRemapping` keyframes
static func timeRemapping(
keyframes timeRemappingKeyframes: KeyframeGroup<Vector1D>,
context: LayerContext)
throws
-> KeyframeInterpolator<AnimationFrameTime>
{
try context.logCompatibilityIssue("""
The Core Animation rendering engine partially supports time remapping keyframes,
but this is somewhat experimental and has some known issues. Since it doesn't work
in all cases, we have to fall back to using the main thread engine when using
`RenderingEngineOption.automatic`.
""")
// `timeRemapping` is a mapping from the animation's global time to the layer's local time.
// In the Core Animation engine, we need to perform the opposite calculation -- convert
// the layer's local time into the animation's global time. We can get this by inverting
// the time remapping, swapping the x axis (global time) and the y axis (local time).
let localTimeToGlobalTimeMapping = timeRemappingKeyframes.keyframes.map { keyframe in
Keyframe(
value: keyframe.time,
time: keyframe.value.cgFloatValue * CGFloat(context.animation.framerate),
isHold: keyframe.isHold,
inTangent: keyframe.inTangent,
outTangent: keyframe.outTangent,
spatialInTangent: keyframe.spatialInTangent,
spatialOutTangent: keyframe.spatialOutTangent)
}
return KeyframeInterpolator(keyframes: .init(localTimeToGlobalTimeMapping))
}
}

View File

@ -0,0 +1,257 @@
// Created by Cal Stephens on 12/13/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - ShapeItemLayer
/// A CALayer type that renders an array of `[ShapeItem]`s,
/// from a `Group` in a `ShapeLayerModel`.
final class ShapeItemLayer: BaseAnimationLayer {
// MARK: Lifecycle
/// Initializes a `ShapeItemLayer` that renders a `Group` from a `ShapeLayerModel`
/// - Parameters:
/// - shape: The `ShapeItem` in this group that renders a `GGPath`
/// - otherItems: Other items in this group that affect the appearance of the shape
init(shape: Item, otherItems: [Item], context: LayerContext) throws {
self.shape = shape
self.otherItems = otherItems
try context.compatibilityAssert(
shape.item.drawsCGPath,
"`ShapeItemLayer` must contain exactly one `ShapeItem` that draws a `GPPath`")
try context.compatibilityAssert(
!otherItems.contains(where: { $0.item.drawsCGPath }),
"`ShapeItemLayer` must contain exactly one `ShapeItem` that draws a `GPPath`")
super.init()
setupLayerHierarchy()
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
shape = typedLayer.shape
otherItems = typedLayer.otherItems
super.init(layer: typedLayer)
}
// MARK: Internal
/// An item that can be displayed by this layer
struct Item {
/// A `ShapeItem` that should be rendered by this layer
let item: ShapeItem
/// The group that contains this `ShapeItem`, if applicable
let parentGroup: Group?
}
override func setupAnimations(context: LayerAnimationContext) throws {
try super.setupAnimations(context: context)
guard let sublayerConfiguration = sublayerConfiguration else { return }
switch sublayerConfiguration.fill {
case .solidFill(let shapeLayer):
try setupSolidFillAnimations(shapeLayer: shapeLayer, context: context)
case .gradientFill(let gradientLayers):
try setupGradientFillAnimations(
gradientLayer: gradientLayers.gradientLayer,
maskLayer: gradientLayers.maskLayer,
context: context)
}
if let gradientStrokeConfiguration = sublayerConfiguration.gradientStroke {
try setupGradientStrokeAnimations(
gradientLayer: gradientStrokeConfiguration.gradientLayer,
maskLayer: gradientStrokeConfiguration.maskLayer,
context: context)
}
}
// MARK: Private
private struct GradientLayers {
/// The `CALayer` that renders the actual gradient
let gradientLayer: GradientRenderLayer
/// The `CAShapeLayer` that clips the gradient layer to the expected shape
let maskLayer: CAShapeLayer
}
/// The configuration of this layer's `fill` sublayers
private enum FillLayerConfiguration {
/// This layer displays a single `CAShapeLayer`
case solidFill(CAShapeLayer)
/// This layer displays a `GradientRenderLayer` masked by a `CAShapeLayer`.
case gradientFill(GradientLayers)
}
/// The `ShapeItem` in this group that renders a `GGPath`
private let shape: Item
/// Other items in this group that affect the appearance of the shape
private let otherItems: [Item]
/// The current configuration of this layer's sublayer(s)
private var sublayerConfiguration: (fill: FillLayerConfiguration, gradientStroke: GradientLayers?)?
private func setupLayerHierarchy() {
// We have to build a different layer hierarchy depending on if
// we're rendering a gradient (a `CAGradientLayer` masked by a `CAShapeLayer`)
// or a solid shape (a simple `CAShapeLayer`).
let fillLayerConfiguration: FillLayerConfiguration
if otherItems.contains(where: { $0.item is GradientFill }) {
fillLayerConfiguration = setupGradientFillLayerHierarchy()
} else {
fillLayerConfiguration = setupSolidFillLayerHierarchy()
}
let gradientStrokeConfiguration: GradientLayers?
if otherItems.contains(where: { $0.item is GradientStroke }) {
gradientStrokeConfiguration = setupGradientStrokeLayerHierarchy()
} else {
gradientStrokeConfiguration = nil
}
sublayerConfiguration = (fillLayerConfiguration, gradientStrokeConfiguration)
}
private func setupSolidFillLayerHierarchy() -> FillLayerConfiguration {
let shapeLayer = LottieCAShapeLayer()
addSublayer(shapeLayer)
// `CAShapeLayer.fillColor` defaults to black, so we have to
// nil out the background color if there isn't an expected fill color
if !otherItems.contains(where: { $0.item is Fill }) {
shapeLayer.fillColor = nil
}
return .solidFill(shapeLayer)
}
private func setupGradientFillLayerHierarchy() -> FillLayerConfiguration {
let pathMask = LottieCAShapeLayer()
pathMask.fillColor = .rgb(0, 0, 0)
mask = pathMask
let gradientLayer = GradientRenderLayer()
addSublayer(gradientLayer)
return .gradientFill(.init(gradientLayer: gradientLayer, maskLayer: pathMask))
}
private func setupGradientStrokeLayerHierarchy() -> GradientLayers {
let container = BaseAnimationLayer()
let pathMask = LottieCAShapeLayer()
pathMask.fillColor = nil
pathMask.strokeColor = .rgb(0, 0, 0)
container.mask = pathMask
let gradientLayer = GradientRenderLayer()
container.addSublayer(gradientLayer)
addSublayer(container)
return .init(gradientLayer: gradientLayer, maskLayer: pathMask)
}
private func setupSolidFillAnimations(
shapeLayer: CAShapeLayer,
context: LayerAnimationContext)
throws
{
try shapeLayer.addAnimations(for: shape.item, context: context.for(shape))
if let (fill, context) = otherItems.first(Fill.self, context: context) {
try shapeLayer.addAnimations(for: fill, context: context)
}
if let (stroke, context) = otherItems.first(Stroke.self, context: context) {
try shapeLayer.addStrokeAnimations(for: stroke, context: context)
}
if let (trim, context) = otherItems.first(Trim.self, context: context) {
try shapeLayer.addAnimations(for: trim, context: context)
}
}
private func setupGradientFillAnimations(
gradientLayer: GradientRenderLayer,
maskLayer: CAShapeLayer,
context: LayerAnimationContext)
throws
{
try maskLayer.addAnimations(for: shape.item, context: context.for(shape))
if let (gradientFill, context) = otherItems.first(GradientFill.self, context: context) {
try gradientLayer.addGradientAnimations(for: gradientFill, context: context)
}
}
private func setupGradientStrokeAnimations(
gradientLayer: GradientRenderLayer,
maskLayer: CAShapeLayer,
context: LayerAnimationContext)
throws
{
try maskLayer.addAnimations(for: shape.item, context: context.for(shape))
if let (gradientStroke, context) = otherItems.first(GradientStroke.self, context: context) {
try gradientLayer.addGradientAnimations(for: gradientStroke, context: context)
try maskLayer.addStrokeAnimations(for: gradientStroke, context: context)
}
if let (trim, context) = otherItems.first(Trim.self, context: context) {
try maskLayer.addAnimations(for: trim, context: context)
}
}
}
// MARK: - [ShapeItem] helpers
extension Array where Element == ShapeItemLayer.Item {
/// The first `ShapeItem` in this array of the given type
func first<ItemType: ShapeItem>(
_: ItemType.Type, context: LayerAnimationContext)
-> (item: ItemType, context: LayerAnimationContext)?
{
for item in self {
if let match = item.item as? ItemType {
return (match, context.for(item))
}
}
return nil
}
}
extension LayerAnimationContext {
/// An updated `LayerAnimationContext` with the`AnimationKeypath`
/// that refers to this specific `ShapeItem`.
func `for`(_ item: ShapeItemLayer.Item) -> LayerAnimationContext {
var context = self
if let group = item.parentGroup {
context.currentKeypath.keys.append(group.name)
}
context.currentKeypath.keys.append(item.item.name)
return context
}
}

View File

@ -0,0 +1,305 @@
// Created by Cal Stephens on 12/14/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - ShapeLayer
/// The CALayer type responsible for rendering `ShapeLayerModel`s
final class ShapeLayer: BaseCompositionLayer {
// MARK: Lifecycle
init(shapeLayer: ShapeLayerModel, context: LayerContext) throws {
self.shapeLayer = shapeLayer
super.init(layerModel: shapeLayer)
try setupGroups(from: shapeLayer.items, parentGroup: nil, context: context)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
shapeLayer = typedLayer.shapeLayer
super.init(layer: typedLayer)
}
// MARK: Private
private let shapeLayer: ShapeLayerModel
}
// MARK: - GroupLayer
/// The CALayer type responsible for rendering `Group`s
final class GroupLayer: BaseAnimationLayer {
// MARK: Lifecycle
init(group: Group, inheritedItems: [ShapeItemLayer.Item], context: LayerContext) throws {
self.group = group
self.inheritedItems = inheritedItems
super.init()
try setupLayerHierarchy(context: context)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
group = typedLayer.group
inheritedItems = typedLayer.inheritedItems
super.init(layer: typedLayer)
}
// MARK: Internal
override func setupAnimations(context: LayerAnimationContext) throws {
try super.setupAnimations(context: context)
if let (shapeTransform, context) = nonGroupItems.first(ShapeTransform.self, context: context) {
try addTransformAnimations(for: shapeTransform, context: context)
try addOpacityAnimation(for: shapeTransform, context: context)
}
}
// MARK: Private
private let group: Group
/// `ShapeItem`s that were listed in the parent's `items: [ShapeItem]` array
/// - This layer's parent is either the root `ShapeLayerModel` or some other `Group`
private let inheritedItems: [ShapeItemLayer.Item]
/// `ShapeItem`s (other than nested `Group`s) that are included in this group
private lazy var nonGroupItems = group.items
.filter { !($0 is Group) }
.map { ShapeItemLayer.Item(item: $0, parentGroup: group) }
+ inheritedItems
private func setupLayerHierarchy(context: LayerContext) throws {
// Groups can contain other groups, so we may have to continue
// recursively creating more `GroupLayer`s
try setupGroups(from: group.items, parentGroup: group, context: context)
// Create `ShapeItemLayer`s for each subgroup of shapes that should be rendered as a single unit
// - These groups are listed from front-to-back, so we have to add the sublayers in reverse order
for shapeRenderGroup in nonGroupItems.shapeRenderGroups.reversed() {
// If all of the path-drawing `ShapeItem`s have keyframes with the same timing information,
// we can combine the `[KeyframeGroup<BezierPath>]` (which have to animate in separate layers)
// into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation.
//
// This is how Groups with multiple path-drawing items are supposed to be rendered,
// because combining multiple paths into a single `CGPath` (instead of rendering them in separate layers)
// allows `CAShapeLayerFillRule.evenOdd` to be applied if the paths overlap. We just can't do this
// in all cases, due to limitations of Core Animation.
if
shapeRenderGroup.pathItems.count > 1,
let combinedShapeKeyframes = Keyframes.combinedIfPossible(
shapeRenderGroup.pathItems.map { ($0.item as? Shape)?.path }),
// `Trim`s are currently only applied correctly using individual `ShapeItemLayer`s,
// because each path has to be trimmed separately.
!shapeRenderGroup.otherItems.contains(where: { $0.item is Trim })
{
let combinedShape = CombinedShapeItem(
shapes: combinedShapeKeyframes,
name: group.name)
let sublayer = try ShapeItemLayer(
shape: ShapeItemLayer.Item(item: combinedShape, parentGroup: group),
otherItems: shapeRenderGroup.otherItems,
context: context)
addSublayer(sublayer)
}
// Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently,
// we have to create a separate `ShapeItemLayer` for each one.
else {
for pathDrawingItem in shapeRenderGroup.pathItems {
let sublayer = try ShapeItemLayer(
shape: pathDrawingItem,
otherItems: shapeRenderGroup.otherItems,
context: context)
addSublayer(sublayer)
}
}
}
}
}
extension CALayer {
/// Sets up `GroupLayer`s for each `Group` in the given list of `ShapeItem`s
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
/// - Other `ShapeItem` are applied to all sublayers
fileprivate func setupGroups(from items: [ShapeItem], parentGroup: Group?, context: LayerContext) throws {
let (groupItems, otherItems) = items.grouped(by: { $0 is Group })
// Groups are listed from front to back,
// but `CALayer.sublayers` are listed from back to front.
let groupsInZAxisOrder = groupItems.reversed()
for group in groupsInZAxisOrder {
guard let group = group as? Group else { continue }
// `ShapeItem`s either draw a path, or modify how a path is rendered.
// - If this group doesn't have any items that draw a path, then its
// items are applied to all of this groups children.
let inheritedItems: [ShapeItemLayer.Item]
if !otherItems.contains(where: { $0.drawsCGPath }) {
inheritedItems = otherItems.map {
ShapeItemLayer.Item(item: $0, parentGroup: parentGroup)
}
} else {
inheritedItems = []
}
let groupLayer = try GroupLayer(
group: group,
inheritedItems: inheritedItems,
context: context)
addSublayer(groupLayer)
}
}
}
extension ShapeItem {
/// Whether or not this `ShapeItem` is responsible for rendering a `CGPath`
var drawsCGPath: Bool {
switch type {
case .ellipse, .rectangle, .shape, .star:
return true
case .fill, .gradientFill, .group, .gradientStroke, .merge,
.repeater, .round, .stroke, .trim, .transform, .unknown:
return false
}
}
/// Whether or not this `ShapeItem` provides a fill for a set of shapes
var isFill: Bool {
switch type {
case .fill, .gradientFill:
return true
case .ellipse, .rectangle, .shape, .star, .group, .gradientStroke,
.merge, .repeater, .round, .stroke, .trim, .transform, .unknown:
return false
}
}
/// Whether or not this `ShapeItem` provides a stroke for a set of shapes
var isStroke: Bool {
switch type {
case .stroke, .gradientStroke:
return true
case .ellipse, .rectangle, .shape, .star, .group, .gradientFill,
.merge, .repeater, .round, .fill, .trim, .transform, .unknown:
return false
}
}
}
extension Collection {
/// Splits this collection into two groups, based on the given predicate
func grouped(by predicate: (Element) -> Bool) -> (trueElements: [Element], falseElements: [Element]) {
var trueElements = [Element]()
var falseElements = [Element]()
for element in self {
if predicate(element) {
trueElements.append(element)
} else {
falseElements.append(element)
}
}
return (trueElements, falseElements)
}
}
// MARK: - ShapeRenderGroup
/// A group of `ShapeItem`s that should be rendered together as a single unit
struct ShapeRenderGroup {
/// The items in this group that render `CGPath`s
var pathItems: [ShapeItemLayer.Item] = []
/// Shape items that modify the appearance of the shapes rendered by this group
var otherItems: [ShapeItemLayer.Item] = []
}
extension Array where Element == ShapeItemLayer.Item {
/// Splits this list of `ShapeItem`s into groups that should be rendered together as individual units
var shapeRenderGroups: [ShapeRenderGroup] {
var renderGroups = [ShapeRenderGroup()]
for item in self {
// `renderGroups` is non-empty, so is guaranteed to have a valid end index
let lastIndex = renderGroups.indices.last!
if item.item.drawsCGPath {
renderGroups[lastIndex].pathItems.append(item)
}
// `Fill` items are unique, because they specifically only apply to _previous_ shapes in a `Group`
// - For example, with [Rectangle, Fill(Red), Circle, Fill(Blue)], the Rectangle should be Red
// but the Circle should be Blue.
// - To handle this, we create a new `ShapeRenderGroup` when we encounter a `Fill` item
else if item.item.isFill {
renderGroups[lastIndex].otherItems.append(item)
renderGroups.append(ShapeRenderGroup())
}
// Other items in the list are applied to all subgroups
else {
for index in renderGroups.indices {
renderGroups[index].otherItems.append(item)
}
}
}
// `Fill` and `Stroke` items have an `alpha` property that can be animated separately,
// but each layer only has a single `opacity` property, so we have to create
// separate layers / render groups for each of these if necessary.
return renderGroups.flatMap { group -> [ShapeRenderGroup] in
let (strokesAndFills, otherItems) = group.otherItems.grouped(by: { $0.item.isFill || $0.item.isStroke })
// However, if all of the strokes / fills have the exact same opacity animation configuration,
// then we can continue using a single layer / render group.
let allAlphaAnimationsAreIdentical = strokesAndFills.allSatisfy { item in
(item.item as? OpacityAnimationModel)?.opacity
== (strokesAndFills.first?.item as? OpacityAnimationModel)?.opacity
}
if allAlphaAnimationsAreIdentical {
return [group]
}
// Create a new group for each stroke / fill
return strokesAndFills.map { strokeOrFill in
ShapeRenderGroup(
pathItems: group.pathItems,
otherItems: [strokeOrFill] + otherItems)
}
}
}
}

View File

@ -0,0 +1,47 @@
// Created by Cal Stephens on 12/13/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - SolidLayer
final class SolidLayer: BaseCompositionLayer {
// MARK: Lifecycle
init(_ solidLayer: SolidLayerModel) {
self.solidLayer = solidLayer
super.init(layerModel: solidLayer)
setupContentLayer()
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
solidLayer = typedLayer.solidLayer
super.init(layer: typedLayer)
}
// MARK: Private
private let solidLayer: SolidLayerModel
private func setupContentLayer() {
// Render the fill color in a child `CAShapeLayer`
// - Using a `CAShapeLayer` specifically, instead of a `CALayer` with a `backgroundColor`,
// allows the size of the fill shape to be different from `contentsLayer.size`.
let shapeLayer = LottieCAShapeLayer()
shapeLayer.fillColor = solidLayer.colorHex.cgColor
shapeLayer.path = CGPath(rect: .init(x: 0, y: 0, width: solidLayer.width, height: solidLayer.height), transform: nil)
addSublayer(shapeLayer)
}
}

View File

@ -0,0 +1,91 @@
// Created by Cal Stephens on 2/9/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
/// The `CALayer` type responsible for rendering `TextLayer`s
final class TextLayer: BaseCompositionLayer {
// MARK: Lifecycle
init(
textLayerModel: TextLayerModel,
context: LayerContext)
throws
{
self.textLayerModel = textLayerModel
super.init(layerModel: textLayerModel)
setupSublayers()
try configureRenderLayer(with: context)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}
textLayerModel = typedLayer.textLayerModel
super.init(layer: typedLayer)
}
// MARK: Internal
func configureRenderLayer(with context: LayerContext) throws {
// We can't use `CATextLayer`, because it doesn't support enough features we use.
// Instead, we use the same `CoreTextRenderLayer` (with a custom `draw` implementation)
// used by the Main Thread rendering engine. This means the Core Animation engine can't
// _animate_ text properties, but it can display static text without any issues.
let text = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text").value
// The Core Animation engine doesn't currently support `TextAnimator`s.
// - We could add support for animating the transform-related properties without much trouble.
// - We may be able to support animating `fillColor` by getting clever with layer blend modes
// or masks (e.g. use `CoreTextRenderLayer` to draw black glyphs, and then fill them in
// using a `CAShapeLayer`).
if !textLayerModel.animators.isEmpty {
try context.logCompatibilityIssue("""
The Core Animation rendering engine currently doesn't support text animators.
""")
}
renderLayer.text = text.text
renderLayer.font = context.fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
renderLayer.alignment = text.justification.textAlignment
renderLayer.lineHeight = CGFloat(text.lineHeight)
renderLayer.tracking = (CGFloat(text.fontSize) * CGFloat(text.tracking)) / 1000
renderLayer.fillColor = text.fillColorData?.cgColorValue
renderLayer.strokeColor = text.strokeColorData?.cgColorValue
renderLayer.strokeWidth = CGFloat(text.strokeWidth ?? 0)
renderLayer.strokeOnTop = text.strokeOverFill ?? false
renderLayer.preferredSize = text.textFrameSize?.sizeValue
renderLayer.sizeToFit()
renderLayer.transform = CATransform3DIdentity
renderLayer.position = text.textFramePosition?.pointValue ?? .zero
}
// MARK: Private
private let textLayerModel: TextLayerModel
private let renderLayer = CoreTextRenderLayer()
private func setupSublayers() {
// Place the text render layer in an additional container
// - Direct sublayers of a `BaseCompositionLayer` always fill the bounds
// of their superlayer -- so this container will be the bounds of self,
// and the text render layer can be positioned anywhere.
let textContainerLayer = CALayer()
textContainerLayer.addSublayer(renderLayer)
addSublayer(textContainerLayer)
}
}

View File

@ -0,0 +1,11 @@
// Created by Cal Stephens on 12/21/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.
/// The CALayer type responsible for only rendering the `transform` of a `LayerModel`
final class TransformLayer: BaseCompositionLayer {
/// `TransformLayer`s don't render any visible content,
/// they just `transform` their sublayers
override var renderLayerContents: Bool { false }
}

View File

@ -0,0 +1,127 @@
// Created by Cal Stephens on 1/13/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
// MARK: - ValueProviderStore
/// Registration and storage for `AnyValueProvider`s that can dynamically
/// provide custom values for `AnimationKeypath`s within an `Animation`.
final class ValueProviderStore {
// MARK: Internal
/// Registers an `AnyValueProvider` for the given `AnimationKeypath`
func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
LottieLogger.shared.assert(
valueProvider.typeErasedStorage.isSupportedByCoreAnimationRenderingEngine,
"""
The Core Animation rendering engine doesn't support Value Providers that vend a closure,
because that would require calling the closure on the main thread once per frame.
""")
// TODO: Support more value types
LottieLogger.shared.assert(
keypath.keys.last == PropertyName.color.rawValue,
"The Core Animation rendering engine currently only supports customizing color values")
valueProviders.append((keypath: keypath, valueProvider: valueProvider))
}
// Retrieves the custom value keyframes for the given property,
// if an `AnyValueProvider` was registered for the given keypath.
func customKeyframes<Value>(
of customizableProperty: CustomizableProperty<Value>,
for keypath: AnimationKeypath,
context: LayerAnimationContext)
throws
-> KeyframeGroup<Value>?
{
guard let anyValueProvider = valueProvider(for: keypath) else {
return nil
}
// Retrieve the type-erased keyframes from the custom `ValueProvider`
let typeErasedKeyframes: [Keyframe<Any>]
switch anyValueProvider.typeErasedStorage {
case .singleValue(let typeErasedValue):
typeErasedKeyframes = [Keyframe(typeErasedValue)]
case .keyframes(let keyframes, _):
typeErasedKeyframes = keyframes
case .closure:
try context.logCompatibilityIssue("""
The Core Animation rendering engine doesn't support Value Providers that vend a closure,
because that would require calling the closure on the main thread once per frame.
""")
return nil
}
// Convert the type-erased keyframe values using this `CustomizableProperty`'s conversion closure
let typedKeyframes = typeErasedKeyframes.compactMap { typeErasedKeyframe -> Keyframe<Value>? in
guard let convertedValue = customizableProperty.conversion(typeErasedKeyframe.value) else {
LottieLogger.shared.assertionFailure("""
Could not convert value of type \(type(of: typeErasedKeyframe.value)) to expected type \(Value.self)
""")
return nil
}
return typeErasedKeyframe.withValue(convertedValue)
}
// Verify that all of the keyframes were successfully converted to the expected type
guard typedKeyframes.count == typeErasedKeyframes.count else {
return nil
}
return KeyframeGroup(keyframes: ContiguousArray(typedKeyframes))
}
// MARK: Private
private var valueProviders = [(keypath: AnimationKeypath, valueProvider: AnyValueProvider)]()
/// Retrieves the most-recently-registered Value Provider that matches the given keypat
private func valueProvider(for keypath: AnimationKeypath) -> AnyValueProvider? {
// Find the last keypath matching the given keypath,
// so we return the value provider that was registered most-recently
valueProviders.last(where: { registeredKeypath, _ in
keypath.matches(registeredKeypath)
})?.valueProvider
}
}
extension AnyValueProviderStorage {
/// Whether or not this type of value provider is supported
/// by the new Core Animation rendering engine
var isSupportedByCoreAnimationRenderingEngine: Bool {
switch self {
case .singleValue, .keyframes:
return true
case .closure:
return false
}
}
}
extension AnimationKeypath {
/// Whether or not this keypath from the animation hierarchy
/// matches the given keypath (which may contain wildcards)
func matches(_ keypath: AnimationKeypath) -> Bool {
var regex = "^" // match the start of the string
+ keypath.keys.joined(separator: "\\.") // match this keypath, escaping "." characters
+ "$" // match the end of the string
// ** wildcards match anything
// - "**.Color" matches both "Layer 1.Color" and "Layer 1.Layer 2.Color"
regex = regex.replacingOccurrences(of: "**", with: ".+")
// * wildcards match any individual path component
// - "*.Color" matches "Layer 1.Color" but not "Layer 1.Layer 2.Color"
regex = regex.replacingOccurrences(of: "*", with: "[^.]+")
return fullPath.range(of: regex, options: .regularExpression) != nil
}
}

View File

@ -0,0 +1,29 @@
#include "CompositionLayer.hpp"
#include "Lottie/Public/Primitives/RenderTree.hpp"
namespace lottie {
InvertedMatteLayer::InvertedMatteLayer(std::shared_ptr<CompositionLayer> inputMatte) :
_inputMatte(inputMatte) {
setBounds(inputMatte->bounds());
setNeedsDisplay(true);
addSublayer(_inputMatte);
}
void InvertedMatteLayer::setup() {
_inputMatte->setLayerDelegate(shared_from_base<InvertedMatteLayer>());
}
void InvertedMatteLayer::frameUpdated(double frame) {
setNeedsDisplay(true);
}
std::shared_ptr<InvertedMatteLayer> makeInvertedMatteLayer(std::shared_ptr<CompositionLayer> compositionLayer) {
auto result = std::make_shared<InvertedMatteLayer>(compositionLayer);
result->setup();
return result;
}
}

View File

@ -0,0 +1,217 @@
#ifndef CompositionLayer_hpp
#define CompositionLayer_hpp
#include "Lottie/Public/Primitives/Vectors.hpp"
#include "Lottie/Public/Primitives/CALayer.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp"
#include "Lottie/Private/Model/Layers/LayerModel.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTransformNode.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayerDelegate.hpp"
#include <memory>
namespace lottie {
class CompositionLayer;
class InvertedMatteLayer;
/// A layer that inverses the alpha output of its input layer.
class InvertedMatteLayer: public CALayer, public CompositionLayerDelegate {
public:
InvertedMatteLayer(std::shared_ptr<CompositionLayer> inputMatte);
void setup();
std::shared_ptr<CompositionLayer> _inputMatte;
//let wrapperLayer = CALayer()
virtual void frameUpdated(double frame) override;
/*virtual bool implementsDraw() const override;
virtual void draw(std::shared_ptr<CGContext> const &context) override;*/
//virtual std::shared_ptr<RenderableItem> renderableItem() override;
virtual bool isInvertedMatte() const override {
return true;
}
};
std::shared_ptr<InvertedMatteLayer> makeInvertedMatteLayer(std::shared_ptr<CompositionLayer> compositionLayer);
/// The base class for a child layer of CompositionContainer
class CompositionLayer: public CALayer, public KeypathSearchable {
public:
CompositionLayer(std::shared_ptr<LayerModel> const &layer, Vector2D size) {
_contentsLayer = std::make_shared<CALayer>();
_transformNode = std::make_shared<LayerTransformNode>(layer->transform);
if (layer->masks.has_value()) {
_maskLayer = std::make_shared<MaskContainerLayer>(layer->masks.value());
} else {
_maskLayer = nullptr;
}
_matteType = layer->matte;
_inFrame = layer->inFrame;
_outFrame = layer->outFrame;
_timeStretch = layer->timeStretch();
_startFrame = layer->startTime;
if (layer->name.has_value()) {
_keypathName = layer->name.value();
} else {
_keypathName = "Layer";
}
_childKeypaths.push_back(_transformNode->transformProperties());
_contentsLayer->setBounds(CGRect(0.0, 0.0, size.x, size.y));
if (layer->blendMode.has_value() && layer->blendMode.value() != BlendMode::Normal) {
setCompositingFilter(layer->blendMode);
}
addSublayer(_contentsLayer);
if (_maskLayer) {
_contentsLayer->setMask(_maskLayer);
}
}
virtual std::string keypathName() const override {
return _keypathName;
}
virtual std::map<std::string, std::shared_ptr<AnyNodeProperty>> keypathProperties() const override {
return {};
}
virtual std::shared_ptr<CALayer> keypathLayer() const override {
return _contentsLayer;
}
void displayWithFrame(double frame, bool forceUpdates) {
_transformNode->updateTree(frame, forceUpdates);
bool layerVisible = isInRangeOrEqual(frame, _inFrame, _outFrame);
/// Only update contents if current time is within the layers time bounds.
if (layerVisible) {
displayContentsWithFrame(frame, forceUpdates);
if (_maskLayer) {
_maskLayer->updateWithFrame(frame, forceUpdates);
}
}
_contentsLayer->setTransform(_transformNode->globalTransform());
_contentsLayer->setOpacity(_transformNode->opacity());
_contentsLayer->setIsHidden(!layerVisible);
if (const auto delegate = _layerDelegate.lock()) {
delegate->frameUpdated(frame);
}
}
virtual void displayContentsWithFrame(double frame, bool forceUpdates) {
/// To be overridden by subclass
}
virtual std::vector<std::shared_ptr<KeypathSearchable>> const &childKeypaths() const override {
return _childKeypaths;
}
std::shared_ptr<CompositionLayer> _matteLayer;
void setMatteLayer(std::shared_ptr<CompositionLayer> matteLayer) {
_matteLayer = matteLayer;
if (matteLayer) {
if (_matteType.has_value() && _matteType.value() == MatteType::Invert) {
setMask(makeInvertedMatteLayer(matteLayer));
} else {
setMask(matteLayer);
}
} else {
setMask(nullptr);
}
}
std::weak_ptr<CompositionLayerDelegate> const &layerDelegate() const {
return _layerDelegate;
}
void setLayerDelegate(std::weak_ptr<CompositionLayerDelegate> const &layerDelegate) {
_layerDelegate = layerDelegate;
}
std::shared_ptr<CALayer> const &contentsLayer() const {
return _contentsLayer;
}
std::shared_ptr<MaskContainerLayer> const &maskLayer() const {
return _maskLayer;
}
void setMaskLayer(std::shared_ptr<MaskContainerLayer> const &maskLayer) {
_maskLayer = maskLayer;
}
std::optional<MatteType> const &matteType() const {
return _matteType;
}
double inFrame() const {
return _inFrame;
}
double outFrame() const {
return _outFrame;
}
double startFrame() const {
return _startFrame;
}
double timeStretch() const {
return _timeStretch;
}
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() {
return nullptr;
}
public:
std::shared_ptr<LayerTransformNode> const transformNode() const {
return _transformNode;
}
protected:
std::shared_ptr<CALayer> _contentsLayer;
std::optional<MatteType> _matteType;
private:
std::weak_ptr<CompositionLayerDelegate> _layerDelegate;
std::shared_ptr<LayerTransformNode> _transformNode;
std::shared_ptr<MaskContainerLayer> _maskLayer;
double _inFrame = 0.0;
double _outFrame = 0.0;
double _startFrame = 0.0;
double _timeStretch = 0.0;
// MARK: Keypath Searchable
std::string _keypathName;
//std::shared_ptr<RenderTreeNode> _renderTree;
public:
virtual bool isImageCompositionLayer() const {
return false;
}
virtual bool isTextCompositionLayer() const {
return false;
}
protected:
std::vector<std::shared_ptr<KeypathSearchable>> _childKeypaths;
};
}
#endif /* CompositionLayer_hpp */

View File

@ -0,0 +1,164 @@
//
// LayerContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
protocol LottieDrawingLayer: CALayer {
}
// 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)
}

View File

@ -0,0 +1,13 @@
#ifndef CompositionLayerDelegate_hpp
#define CompositionLayerDelegate_hpp
namespace lottie {
class CompositionLayerDelegate {
public:
virtual void frameUpdated(double frame) = 0;
};
}
#endif /* CompositionLayerDelegate_hpp */

View File

@ -0,0 +1,5 @@
#include "ImageCompositionLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,42 @@
#ifndef ImageCompositionLayer_hpp
#define ImageCompositionLayer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
#include "Lottie/Private/Model/Layers/ImageLayerModel.hpp"
namespace lottie {
class ImageCompositionLayer: public CompositionLayer {
public:
ImageCompositionLayer(std::shared_ptr<ImageLayerModel> const &imageLayer, Vector2D const &size) :
CompositionLayer(imageLayer, size) {
_imageReferenceID = imageLayer->referenceID;
contentsLayer()->setMasksToBounds(true);
}
std::shared_ptr<CGImage> image() {
return _image;
}
void setImage(std::shared_ptr<CGImage> image) {
_image = image;
contentsLayer()->setContents(image);
}
std::string const &imageReferenceID() {
return _imageReferenceID;
}
public:
virtual bool isImageCompositionLayer() const override {
return true;
}
private:
std::string _imageReferenceID;
std::shared_ptr<CGImage> _image;
};
}
#endif /* ImageCompositionLayer_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "MaskContainerLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,176 @@
#ifndef MaskContainerLayer_hpp
#define MaskContainerLayer_hpp
#include "Lottie/Private/Model/Objects/Mask.hpp"
#include "Lottie/Public/Primitives/CALayer.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp"
namespace lottie {
inline MaskMode usableMaskMode(MaskMode mode) {
switch (mode) {
case MaskMode::Add:
return MaskMode::Add;
case MaskMode::Subtract:
return MaskMode::Subtract;
case MaskMode::Intersect:
return MaskMode::Intersect;
case MaskMode::Lighten:
return MaskMode::Add;
case MaskMode::Darken:
return MaskMode::Darken;
case MaskMode::Difference:
return MaskMode::Intersect;
case MaskMode::None:
return MaskMode::None;
}
}
class MaskNodeProperties: public NodePropertyMap {
public:
MaskNodeProperties(std::shared_ptr<Mask> const &mask) :
_mode(mask->mode()),
_inverted(mask->inverted) {
_opacity = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(mask->opacity->keyframes));
_shape = std::make_shared<NodeProperty<BezierPath>>(std::make_shared<KeyframeInterpolator<BezierPath>>(mask->shape.keyframes));
_expansion = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(mask->expansion->keyframes));
_propertyMap.insert(std::make_pair("Opacity", _opacity));
_propertyMap.insert(std::make_pair("Shape", _shape));
_propertyMap.insert(std::make_pair("Expansion", _expansion));
for (const auto &it : _propertyMap) {
_properties.push_back(it.second);
}
}
virtual std::vector<std::shared_ptr<AnyNodeProperty>> &properties() override {
return _properties;
}
virtual std::vector<std::shared_ptr<KeypathSearchable>> const &childKeypaths() const override {
return _childKeypaths;
}
std::shared_ptr<NodeProperty<Vector1D>> const &opacity() const {
return _opacity;
}
std::shared_ptr<NodeProperty<BezierPath>> const &shape() const {
return _shape;
}
std::shared_ptr<NodeProperty<Vector1D>> const &expansion() const {
return _expansion;
}
MaskMode mode() const {
return _mode;
}
bool inverted() const {
return _inverted;
}
private:
std::map<std::string, std::shared_ptr<AnyNodeProperty>> _propertyMap;
std::vector<std::shared_ptr<KeypathSearchable>> _childKeypaths;
std::vector<std::shared_ptr<AnyNodeProperty>> _properties;
MaskMode _mode = MaskMode::Add;
bool _inverted = false;
std::shared_ptr<NodeProperty<Vector1D>> _opacity;
std::shared_ptr<NodeProperty<BezierPath>> _shape;
std::shared_ptr<NodeProperty<Vector1D>> _expansion;
};
class MaskLayer: public CALayer {
public:
MaskLayer(std::shared_ptr<Mask> const &mask) :
_properties(mask) {
_maskLayer = std::make_shared<CAShapeLayer>();
addSublayer(_maskLayer);
if (mask->mode() == MaskMode::Add) {
_maskLayer->setFillColor(Color(1.0, 0.0, 0.0, 1.0));
} else {
_maskLayer->setFillColor(Color(0.0, 1.0, 0.0, 1.0));
}
_maskLayer->setFillRule(FillRule::EvenOdd);
}
void updateWithFrame(double frame, bool forceUpdates) {
if (_properties.opacity()->needsUpdate(frame) || forceUpdates) {
_properties.opacity()->update(frame);
setOpacity(_properties.opacity()->value().value);
}
if (_properties.shape()->needsUpdate(frame) || forceUpdates) {
_properties.shape()->update(frame);
_properties.expansion()->update(frame);
auto path = _properties.shape()->value().cgPath();
auto usableMode = usableMaskMode(_properties.mode());
if ((usableMode == MaskMode::Subtract && !_properties.inverted()) ||
(usableMode == MaskMode::Add && _properties.inverted())) {
/// Add a bounds rect to invert the mask
auto newPath = CGPath::makePath();
newPath->addRect(CGRect::veryLarge());
newPath->addPath(path);
path = std::static_pointer_cast<CGPath>(newPath);
}
_maskLayer->setPath(path);
}
}
private:
MaskNodeProperties _properties;
std::shared_ptr<CAShapeLayer> _maskLayer;
};
class MaskContainerLayer: public CALayer {
public:
MaskContainerLayer(std::vector<std::shared_ptr<Mask>> const &masks) {
auto containerLayer = std::make_shared<CALayer>();
bool firstObject = true;
for (const auto &mask : masks) {
auto maskLayer = std::make_shared<MaskLayer>(mask);
_maskLayers.push_back(maskLayer);
auto usableMode = usableMaskMode(mask->mode());
if (usableMode == MaskMode::None) {
continue;
} else if (usableMode == MaskMode::Add || firstObject) {
firstObject = false;
containerLayer->addSublayer(maskLayer);
} else {
containerLayer->setMask(maskLayer);
auto newContainer = std::make_shared<CALayer>();
newContainer->addSublayer(containerLayer);
containerLayer = newContainer;
}
}
addSublayer(containerLayer);
}
// MARK: Internal
void updateWithFrame(double frame, bool forceUpdates) {
for (const auto &maskLayer : _maskLayers) {
maskLayer->updateWithFrame(frame, forceUpdates);
}
}
private:
std::vector<std::shared_ptr<MaskLayer>> _maskLayers;
};
}
#endif /* MaskContainerLayer_hpp */

View File

@ -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 = LottieCAShapeLayer()
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>
}

View File

@ -0,0 +1,5 @@
#include "NullCompositionLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,17 @@
#ifndef NullCompositionLayer_hpp
#define NullCompositionLayer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
namespace lottie {
class NullCompositionLayer: public CompositionLayer {
public:
NullCompositionLayer(std::shared_ptr<LayerModel> const &layer) :
CompositionLayer(layer, Vector2D::Zero()) {
}
};
}
#endif /* NullCompositionLayer_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "PreCompositionLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,200 @@
#ifndef PreCompositionLayer_hpp
#define PreCompositionLayer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
#include "Lottie/Private/Model/Layers/PreCompLayerModel.hpp"
#include "Lottie/Private/Model/Assets/PrecompAsset.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp"
#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp"
#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp"
#include "Lottie/Private/Model/Assets/AssetLibrary.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp"
namespace lottie {
class PreCompositionLayer: public CompositionLayer {
public:
PreCompositionLayer(
std::shared_ptr<PreCompLayerModel> const &precomp,
PrecompAsset const &asset,
std::shared_ptr<LayerImageProvider> const &layerImageProvider,
std::shared_ptr<AnimationTextProvider> const &textProvider,
std::shared_ptr<AnimationFontProvider> const &fontProvider,
std::shared_ptr<AssetLibrary> const &assetLibrary,
double frameRate
) : CompositionLayer(precomp, Vector2D(precomp->width, precomp->height)) {
if (precomp->timeRemapping) {
_remappingNode = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(precomp->timeRemapping->keyframes));
}
_frameRate = frameRate;
setBounds(CGRect(0.0, 0.0, precomp->width, precomp->height));
contentsLayer()->setMasksToBounds(true);
contentsLayer()->setBounds(bounds());
auto layers = initializeCompositionLayers(
asset.layers,
assetLibrary,
layerImageProvider,
textProvider,
fontProvider,
frameRate
);
std::vector<std::shared_ptr<ImageCompositionLayer>> imageLayers;
std::shared_ptr<CompositionLayer> mattedLayer;
for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) {
std::shared_ptr<CompositionLayer> layer = *layerIt;
layer->setBounds(bounds());
_animationLayers.push_back(layer);
if (layer->isImageCompositionLayer()) {
imageLayers.push_back(std::static_pointer_cast<ImageCompositionLayer>(layer));
}
if (mattedLayer) {
/// The previous layer requires this layer to be its matte
mattedLayer->setMatteLayer(layer);
mattedLayer = nullptr;
continue;
}
if (layer->matteType().has_value() && (layer->matteType().value() == MatteType::Add || layer->matteType().value() == MatteType::Invert)) {
/// We have a layer that requires a matte.
mattedLayer = layer;
}
contentsLayer()->addSublayer(layer);
}
for (const auto &layer : layers) {
_childKeypaths.push_back(layer);
}
layerImageProvider->addImageLayers(imageLayers);
}
virtual std::map<std::string, std::shared_ptr<AnyNodeProperty>> keypathProperties() const override {
if (!_remappingNode) {
return {};
}
std::map<std::string, std::shared_ptr<AnyNodeProperty>> result;
result.insert(std::make_pair("Time Remap", _remappingNode));
return result;
}
virtual void displayContentsWithFrame(double frame, bool forceUpdates) override {
double localFrame = 0.0;
if (_remappingNode) {
_remappingNode->update(frame);
localFrame = _remappingNode->value().value * _frameRate;
} else {
localFrame = (frame - startFrame()) / timeStretch();
}
for (const auto &animationLayer : _animationLayers) {
animationLayer->displayWithFrame(localFrame, forceUpdates);
}
}
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override {
if (_contentsLayer->isHidden()) {
return nullptr;
}
std::shared_ptr<RenderTreeNode> maskNode;
bool invertMask = false;
if (_matteLayer) {
maskNode = _matteLayer->renderTreeNode();
if (maskNode && _matteType.has_value() && _matteType.value() == MatteType::Invert) {
invertMask = true;
}
}
std::vector<std::shared_ptr<RenderTreeNode>> renderTreeValue;
auto renderTreeContentItem = renderTree();
if (renderTreeContentItem) {
renderTreeValue.push_back(renderTreeContentItem);
}
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
subnodes.push_back(std::make_shared<RenderTreeNode>(
_contentsLayer->bounds(),
_contentsLayer->position(),
_contentsLayer->transform(),
_contentsLayer->opacity(),
_contentsLayer->masksToBounds(),
_contentsLayer->isHidden(),
nullptr,
renderTreeValue,
nullptr,
false
));
assert(opacity() == 1.0);
assert(!isHidden());
assert(!masksToBounds());
assert(transform().isIdentity());
assert(position() == Vector2D::Zero());
return std::make_shared<RenderTreeNode>(
bounds(),
position(),
transform(),
opacity(),
masksToBounds(),
isHidden(),
nullptr,
subnodes,
maskNode,
invertMask
);
}
std::shared_ptr<RenderTreeNode> renderTree() {
std::vector<std::shared_ptr<RenderTreeNode>> result;
for (const auto &animationLayer : _animationLayers) {
bool found = false;
for (const auto &sublayer : contentsLayer()->sublayers()) {
if (animationLayer == sublayer) {
found = true;
break;
}
}
if (found) {
auto node = animationLayer->renderTreeNode();
if (node) {
result.push_back(node);
}
}
}
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
return std::make_shared<RenderTreeNode>(
CGRect(0.0, 0.0, 0.0, 0.0),
Vector2D(0.0, 0.0),
CATransform3D::identity(),
1.0,
false,
false,
nullptr,
result,
nullptr,
false
);
}
private:
double _frameRate = 0.0;
std::shared_ptr<NodeProperty<Vector1D>> _remappingNode;
std::vector<std::shared_ptr<CompositionLayer>> _animationLayers;
};
}
#endif /* PreCompositionLayer_hpp */

View File

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

View File

@ -0,0 +1,31 @@
#ifndef ShapeCompositionLayer_hpp
#define ShapeCompositionLayer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
#include "Lottie/Private/Model/Layers/ShapeLayerModel.hpp"
#include "Lottie/Private/Model/Layers/SolidLayerModel.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp"
namespace lottie {
class ShapeLayerPresentationTree;
/// A CompositionLayer responsible for initializing and rendering shapes
class ShapeCompositionLayer: public CompositionLayer {
public:
ShapeCompositionLayer(std::shared_ptr<ShapeLayerModel> const &shapeLayer);
ShapeCompositionLayer(std::shared_ptr<SolidLayerModel> const &solidLayer);
virtual void displayContentsWithFrame(double frame, bool forceUpdates) override;
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() override;
private:
std::shared_ptr<ShapeLayerPresentationTree> _contentTree;
AnimationFrameTime _frameTime = 0.0;
bool _frameTimeInitialized = false;
};
}
#endif /* ShapeCompositionLayer_hpp */

View File

@ -0,0 +1,57 @@
//
// 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
}
}

View File

@ -0,0 +1,487 @@
#include "BezierPathUtils.hpp"
namespace lottie {
BezierPath makeEllipseBezierPath(
Vector2D const &size,
Vector2D const &center,
PathDirection direction
) {
const double ControlPointConstant = 0.55228;
Vector2D half = size * 0.5;
if (direction == PathDirection::CounterClockwise) {
half.x = half.x * -1.0;
}
Vector2D q1(center.x, center.y - half.y);
Vector2D q2(center.x + half.x, center.y);
Vector2D q3(center.x, center.y + half.y);
Vector2D q4(center.x - half.x, center.y);
Vector2D cp = half * ControlPointConstant;
BezierPath path(CurveVertex::relative(
q1,
Vector2D(-cp.x, 0),
Vector2D(cp.x, 0)));
path.addVertex(CurveVertex::relative(
q2,
Vector2D(0, -cp.y),
Vector2D(0, cp.y)));
path.addVertex(CurveVertex::relative(
q3,
Vector2D(cp.x, 0),
Vector2D(-cp.x, 0)));
path.addVertex(CurveVertex::relative(
q4,
Vector2D(0, cp.y),
Vector2D(0, -cp.y)));
path.addVertex(CurveVertex::relative(
q1,
Vector2D(-cp.x, 0),
Vector2D(cp.x, 0)));
path.close();
return path;
}
BezierPath makeRectangleBezierPath(
Vector2D const &position,
Vector2D const &inputSize,
double cornerRadius,
PathDirection direction
) {
const double ControlPointConstant = 0.55228;
Vector2D size = inputSize * 0.5;
double radius = std::min(std::min(cornerRadius, size.x), size.y);
BezierPath bezierPath;
std::vector<CurveVertex> points;
if (radius <= 0.0) {
/// No Corners
points = {
/// Lead In
CurveVertex::relative(
Vector2D(size.x, -size.y),
Vector2D::Zero(),
Vector2D::Zero())
.translated(position),
/// Corner 1
CurveVertex::relative(
Vector2D(size.x, size.y),
Vector2D::Zero(),
Vector2D::Zero())
.translated(position),
/// Corner 2
CurveVertex::relative(
Vector2D(-size.x, size.y),
Vector2D::Zero(),
Vector2D::Zero())
.translated(position),
/// Corner 3
CurveVertex::relative(
Vector2D(-size.x, -size.y),
Vector2D::Zero(),
Vector2D::Zero())
.translated(position),
/// Corner 4
CurveVertex::relative(
Vector2D(size.x, -size.y),
Vector2D::Zero(),
Vector2D::Zero())
.translated(position)
};
} else {
double controlPoint = radius * ControlPointConstant;
points = {
/// Lead In
CurveVertex::absolute(
Vector2D(radius, 0),
Vector2D(radius, 0),
Vector2D(radius, 0))
.translated(Vector2D(-radius, radius))
.translated(Vector2D(size.x, -size.y))
.translated(position),
/// Corner 1
CurveVertex::absolute(
Vector2D(radius, 0), // Point
Vector2D(radius, 0), // In tangent
Vector2D(radius, controlPoint))
.translated(Vector2D(-radius, -radius))
.translated(Vector2D(size.x, size.y))
.translated(position),
CurveVertex::absolute(
Vector2D(0, radius), // Point
Vector2D(controlPoint, radius), // In tangent
Vector2D(0, radius)) // Out Tangent
.translated(Vector2D(-radius, -radius))
.translated(Vector2D(size.x, size.y))
.translated(position),
/// Corner 2
CurveVertex::absolute(
Vector2D(0, radius), // Point
Vector2D(0, radius), // In tangent
Vector2D(-controlPoint, radius))// Out tangent
.translated(Vector2D(radius, -radius))
.translated(Vector2D(-size.x, size.y))
.translated(position),
CurveVertex::absolute(
Vector2D(-radius, 0), // Point
Vector2D(-radius, controlPoint), // In tangent
Vector2D(-radius, 0)) // Out tangent
.translated(Vector2D(radius, -radius))
.translated(Vector2D(-size.x, size.y))
.translated(position),
/// Corner 3
CurveVertex::absolute(
Vector2D(-radius, 0), // Point
Vector2D(-radius, 0), // In tangent
Vector2D(-radius, -controlPoint)) // Out tangent
.translated(Vector2D(radius, radius))
.translated(Vector2D(-size.x, -size.y))
.translated(position),
CurveVertex::absolute(
Vector2D(0, -radius), // Point
Vector2D(-controlPoint, -radius), // In tangent
Vector2D(0, -radius)) // Out tangent
.translated(Vector2D(radius, radius))
.translated(Vector2D(-size.x, -size.y))
.translated(position),
/// Corner 4
CurveVertex::absolute(
Vector2D(0, -radius), // Point
Vector2D(0, -radius), // In tangent
Vector2D(controlPoint, -radius)) // Out tangent
.translated(Vector2D(-radius, radius))
.translated(Vector2D(size.x, -size.y))
.translated(position),
CurveVertex::absolute(
Vector2D(radius, 0), // Point
Vector2D(radius, -controlPoint), // In tangent
Vector2D(radius, 0)) // Out tangent
.translated(Vector2D(-radius, radius))
.translated(Vector2D(size.x, -size.y))
.translated(position)
};
}
bool reversed = direction == PathDirection::CounterClockwise;
if (reversed) {
for (auto vertexIt = points.rbegin(); vertexIt != points.rend(); vertexIt++) {
bezierPath.addVertex((*vertexIt).reversed());
}
} else {
for (auto vertexIt = points.begin(); vertexIt != points.end(); vertexIt++) {
bezierPath.addVertex(*vertexIt);
}
}
bezierPath.close();
return bezierPath;
}
/// Magic number needed for building path data
static constexpr double StarNodePolystarConstant = 0.47829;
BezierPath makeStarBezierPath(
Vector2D const &position,
double outerRadius,
double innerRadius,
double inputOuterRoundedness,
double inputInnerRoundedness,
double numberOfPoints,
double rotation,
PathDirection direction
) {
double currentAngle = degreesToRadians(rotation - 90.0);
double anglePerPoint = (2.0 * M_PI) / numberOfPoints;
double halfAnglePerPoint = anglePerPoint / 2.0;
double partialPointAmount = numberOfPoints - floor(numberOfPoints);
double outerRoundedness = inputOuterRoundedness * 0.01;
double innerRoundedness = inputInnerRoundedness * 0.01;
Vector2D point = Vector2D::Zero();
double partialPointRadius = 0.0;
if (partialPointAmount != 0.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;
}
std::vector<CurveVertex> vertices;
vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero()));
Vector2D previousPoint = point;
bool longSegment = false;
int numPoints = (int)(ceil(numberOfPoints) * 2.0);
for (int i = 0; i < numPoints; i++) {
double radius = longSegment ? outerRadius : innerRadius;
double dTheta = halfAnglePerPoint;
if (partialPointRadius != 0.0 && i == numPoints - 2) {
dTheta = anglePerPoint * partialPointAmount / 2;
}
if (partialPointRadius != 0.0 && i == numPoints - 1) {
radius = partialPointRadius;
}
previousPoint = point;
point.x = (radius * cos(currentAngle));
point.y = (radius * sin(currentAngle));
if (innerRoundedness == 0.0 && outerRoundedness == 0.0) {
vertices.push_back(CurveVertex::relative(point + position, Vector2D::Zero(), Vector2D::Zero()));
} else {
double cp1Theta = (atan2(previousPoint.y, previousPoint.x) - M_PI / 2.0);
double cp1Dx = cos(cp1Theta);
double cp1Dy = sin(cp1Theta);
double cp2Theta = (atan2(point.y, point.x) - M_PI / 2.0);
double cp2Dx = cos(cp2Theta);
double cp2Dy = sin(cp2Theta);
double cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness;
double cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness;
double cp1Radius = longSegment ? innerRadius : outerRadius;
double cp2Radius = longSegment ? outerRadius : innerRadius;
Vector2D cp1(
cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dx,
cp1Radius * cp1Roundedness * StarNodePolystarConstant * cp1Dy
);
Vector2D cp2(
cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dx,
cp2Radius * cp2Roundedness * StarNodePolystarConstant * cp2Dy
);
if (partialPointAmount != 0.0) {
if (i == 0) {
cp1 = cp1 * partialPointAmount;
} else if (i == numPoints - 1) {
cp2 = cp2 * partialPointAmount;
}
}
auto previousVertex = vertices[vertices.size() - 1];
vertices[vertices.size() - 1] = CurveVertex::absolute(
previousVertex.point,
previousVertex.inTangent,
previousVertex.point - cp1
);
vertices.push_back(CurveVertex::relative(point + position, cp2, Vector2D::Zero()));
}
currentAngle += dTheta;
longSegment = !longSegment;
}
bool reverse = direction == PathDirection::CounterClockwise;
BezierPath path;
if (reverse) {
for (auto vertexIt = vertices.rbegin(); vertexIt != vertices.rend(); vertexIt++) {
path.addVertex((*vertexIt).reversed());
}
} else {
for (auto vertexIt = vertices.begin(); vertexIt != vertices.end(); vertexIt++) {
path.addVertex(*vertexIt);
}
}
path.close();
return path;
}
CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, double start, double end, double offset, TrimType type) {
/// No need to trim, it's a full path
if (start == 0.0 && end == 1.0) {
return sourcePath;
}
/// All paths are empty.
if (start == end) {
return CompoundBezierPath();
}
if (type == TrimType::Simultaneously) {
CompoundBezierPath result;
for (BezierPath &path : sourcePath.paths) {
CompoundBezierPath tempPath;
tempPath.appendPath(path);
auto subPaths = tempPath.trim(start, end, offset);
for (const auto &subPath : subPaths->paths) {
result.appendPath(subPath);
}
}
return result;
}
/// Individual path trimming.
/// Brace yourself for the below code.
/// Normalize lengths with offset.
double startPosition = fmod(start + offset, 1.0);
double endPosition = fmod(end + offset, 1.0);
if (startPosition < 0.0) {
startPosition = 1.0 + startPosition;
}
if (endPosition < 0.0) {
endPosition = 1.0 + endPosition;
}
if (startPosition == 1.0) {
startPosition = 0.0;
}
if (endPosition == 0.0) {
endPosition = 1.0;
}
/// First get the total length of all paths.
double totalLength = 0.0;
for (auto &upstreamPath : sourcePath.paths) {
totalLength += upstreamPath.length();
}
/// Now determine the start and end cut lengths
double startLength = startPosition * totalLength;
double endLength = endPosition * totalLength;
double pathStart = 0.0;
CompoundBezierPath result;
/// Now loop through all path containers
for (auto &pathContainer : sourcePath.paths) {
auto pathEnd = pathStart + pathContainer.length();
if (!isInRange(startLength, pathStart, pathEnd) &&
isInRange(endLength, pathStart, pathEnd)) {
// pathStart|=======E----------------------|pathEnd
// Cut path components, removing after end.
double pathCutLength = endLength - pathStart;
double subpathStart = 0.0;
double subpathEnd = subpathStart + pathContainer.length();
if (pathCutLength < subpathEnd) {
/// This is the subpath that needs to be cut.
double cutLength = pathCutLength - subpathStart;
CompoundBezierPath tempPath;
tempPath.appendPath(pathContainer);
auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0);
for (const auto &newPath : newPaths->paths) {
result.appendPath(newPath);
}
} else {
/// Add to container and move on
result.appendPath(pathContainer);
}
/*if (pathCutLength == subpathEnd) {
/// Right on the end. The next subpath is not included. Break.
break;
}
subpathStart = subpathEnd;*/
} else if (!isInRange(endLength, pathStart, pathEnd) &&
isInRange(startLength, pathStart, pathEnd)) {
// pathStart|-------S======================|pathEnd
//
// Cut path components, removing before beginning.
double pathCutLength = startLength - pathStart;
// Clear paths from container
double subpathStart = 0.0;
double subpathEnd = subpathStart + pathContainer.length();
if (subpathStart < pathCutLength && pathCutLength < subpathEnd) {
/// This is the subpath that needs to be cut.
double cutLength = pathCutLength - subpathStart;
CompoundBezierPath tempPath;
tempPath.appendPath(pathContainer);
auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0);
for (const auto &newPath : newPaths->paths) {
result.appendPath(newPath);
}
} else if (pathCutLength <= subpathStart) {
result.appendPath(pathContainer);
}
//subpathStart = subpathEnd;
} else if (isInRange(endLength, pathStart, pathEnd) &&
isInRange(startLength, pathStart, pathEnd)) {
// pathStart|-------S============E---------|endLength
// pathStart|=====E----------------S=======|endLength
// trim from path beginning to endLength.
// Cut path components, removing before beginnings.
double startCutLength = startLength - pathStart;
double endCutLength = endLength - pathStart;
double subpathStart = 0.0;
double subpathEnd = subpathStart + pathContainer.length();
if (!isInRange(startCutLength, subpathStart, subpathEnd) &&
!isInRange(endCutLength, subpathStart, subpathEnd))
{
// The whole path is included. Add
// S|==============================|E
result.appendPath(pathContainer);
} else if (isInRange(startCutLength, subpathStart, subpathEnd) &&
!isInRange(endCutLength, subpathStart, subpathEnd)) {
/// The start of the path needs to be trimmed
// |-------S======================|E
double cutLength = startCutLength - subpathStart;
CompoundBezierPath tempPath;
tempPath.appendPath(pathContainer);
auto newPaths = tempPath.trim(cutLength / pathContainer.length(), 1, 0);
for (const auto &newPath : newPaths->paths) {
result.appendPath(newPath);
}
} else if (!isInRange(startCutLength, subpathStart, subpathEnd) &&
isInRange(endCutLength, subpathStart, subpathEnd)) {
// S|=======E----------------------|
double cutLength = endCutLength - subpathStart;
CompoundBezierPath tempPath;
tempPath.appendPath(pathContainer);
auto newPaths = tempPath.trim(0, cutLength / pathContainer.length(), 0);
for (const auto &newPath : newPaths->paths) {
result.appendPath(newPath);
}
} else if (isInRange(startCutLength, subpathStart, subpathEnd) &&
isInRange(endCutLength, subpathStart, subpathEnd)) {
// |-------S============E---------|
double cutFromLength = startCutLength - subpathStart;
double cutToLength = endCutLength - subpathStart;
CompoundBezierPath tempPath;
tempPath.appendPath(pathContainer);
auto newPaths = tempPath.trim(
cutFromLength / pathContainer.length(),
cutToLength / pathContainer.length(),
0
);
for (const auto &newPath : newPaths->paths) {
result.appendPath(newPath);
}
}
} else if ((endLength <= pathStart && pathEnd <= startLength) ||
(startLength <= pathStart && endLength <= pathStart) ||
(pathEnd <= startLength && pathEnd <= endLength)) {
/// The Path needs to be cleared
} else {
result.appendPath(pathContainer);
}
pathStart = pathEnd;
}
return result;
}
}

View File

@ -0,0 +1,39 @@
#ifndef BezierPaths_h
#define BezierPaths_h
#include "Lottie/Private/Model/ShapeItems/Ellipse.hpp"
#include "Lottie/Private/Utility/Primitives/BezierPath.hpp"
#include "Lottie/Private/Utility/Primitives/CompoundBezierPath.hpp"
#include "Lottie/Private/Model/ShapeItems/Trim.hpp"
namespace lottie {
BezierPath makeEllipseBezierPath(
Vector2D const &size,
Vector2D const &center,
PathDirection direction
);
BezierPath makeRectangleBezierPath(
Vector2D const &position,
Vector2D const &inputSize,
double cornerRadius,
PathDirection direction
);
BezierPath makeStarBezierPath(
Vector2D const &position,
double outerRadius,
double innerRadius,
double inputOuterRoundedness,
double inputInnerRoundedness,
double numberOfPoints,
double rotation,
PathDirection direction
);
CompoundBezierPath trimCompoundPath(CompoundBezierPath sourcePath, double start, double end, double offset, TrimType type);
}
#endif /* BezierPaths_h */

View File

@ -0,0 +1,60 @@
//
// SolidCompositionLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
import QuartzCore
final class LottieCAShapeLayer: CAShapeLayer {
}
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 = LottieCAShapeLayer()
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
}
}

View File

@ -0,0 +1,5 @@
#include "TextCompositionLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,124 @@
#ifndef TextCompositionLayer_hpp
#define TextCompositionLayer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
#include "Lottie/Private/Model/Layers/TextLayerModel.hpp"
#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp"
#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.hpp"
namespace lottie {
class TextCompositionLayer: public CompositionLayer {
public:
TextCompositionLayer(std::shared_ptr<TextLayerModel> const &textLayer, std::shared_ptr<AnimationTextProvider> textProvider, std::shared_ptr<AnimationFontProvider> fontProvider) :
CompositionLayer(textLayer, Vector2D::Zero()) {
std::shared_ptr<TextAnimatorNode> rootNode;
for (const auto &animator : textLayer->animators) {
rootNode = std::make_shared<TextAnimatorNode>(rootNode, animator);
}
_rootNode = rootNode;
_textDocument = std::make_shared<KeyframeInterpolator<TextDocument>>(textLayer->text.keyframes);
_textProvider = textProvider;
_fontProvider = fontProvider;
//_contentsLayer->addSublayer(_textLayer);
assert(false);
//self.textLayer.masksToBounds = false
//self.textLayer.isGeometryFlipped = true
if (_rootNode) {
_childKeypaths.push_back(rootNode);
}
}
std::shared_ptr<AnimationTextProvider> const &textProvider() const {
return _textProvider;
}
void setTextProvider(std::shared_ptr<AnimationTextProvider> const &textProvider) {
_textProvider = textProvider;
}
std::shared_ptr<AnimationFontProvider> const &fontProvider() const {
return _fontProvider;
}
void setFontProvider(std::shared_ptr<AnimationFontProvider> const &fontProvider) {
_fontProvider = fontProvider;
}
virtual void displayContentsWithFrame(double frame, bool forceUpdates) override {
if (!_textDocument) {
return;
}
bool documentUpdate = _textDocument->hasUpdate(frame);
bool animatorUpdate = false;
if (_rootNode) {
animatorUpdate = _rootNode->updateContents(frame, forceUpdates);
}
if (!(documentUpdate || animatorUpdate)) {
return;
}
if (_rootNode) {
_rootNode->rebuildOutputs(frame);
}
assert(false);
/*// 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*/
}
public:
virtual bool isTextCompositionLayer() const override {
return true;
}
private:
std::shared_ptr<TextAnimatorNode> _rootNode;
std::shared_ptr<KeyframeInterpolator<TextDocument>> _textDocument;
//std::shared_ptr<CoreTextRenderLayer> _textLayer;
std::shared_ptr<AnimationTextProvider> _textProvider;
std::shared_ptr<AnimationFontProvider> _fontProvider;
};
}
#endif /* TextCompositionLayer_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "MainThreadAnimationLayer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,272 @@
#ifndef MainThreadAnimationLayer_hpp
#define MainThreadAnimationLayer_hpp
#include "Lottie/Public/Primitives/CALayer.hpp"
#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp"
#include "Lottie/Private/Model/Animation.hpp"
#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp"
#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerTextProvider.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerFontProvider.hpp"
#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp"
#include "Lottie/Public/DynamicProperties/AnimationKeypath.hpp"
namespace lottie {
class BlankImageProvider: public AnimationImageProvider {
public:
std::shared_ptr<CGImage> imageForAsset(ImageAsset const &asset) {
return nullptr;
}
};
class MainThreadAnimationLayer: public CALayer {
public:
MainThreadAnimationLayer(
Animation const &animation,
std::shared_ptr<AnimationImageProvider> const &imageProvider,
std::shared_ptr<AnimationTextProvider> const &textProvider,
std::shared_ptr<AnimationFontProvider> const &fontProvider
) {
if (animation.assetLibrary) {
_layerImageProvider = std::make_shared<LayerImageProvider>(imageProvider, animation.assetLibrary->imageAssets);
} else {
std::map<std::string, std::shared_ptr<ImageAsset>> imageAssets;
_layerImageProvider = std::make_shared<LayerImageProvider>(imageProvider, imageAssets);
}
_layerTextProvider = std::make_shared<LayerTextProvider>(textProvider);
_layerFontProvider = std::make_shared<LayerFontProvider>(fontProvider);
setBounds(CGRect(0.0, 0.0, animation.width, animation.height));
auto layers = initializeCompositionLayers(
animation.layers,
animation.assetLibrary,
_layerImageProvider,
textProvider,
fontProvider,
animation.framerate
);
std::vector<std::shared_ptr<ImageCompositionLayer>> imageLayers;
std::vector<std::shared_ptr<TextCompositionLayer>> textLayers;
std::shared_ptr<CompositionLayer> mattedLayer;
for (auto layerIt = layers.rbegin(); layerIt != layers.rend(); layerIt++) {
std::shared_ptr<CompositionLayer> const &layer = *layerIt;
layer->setBounds(bounds());
_animationLayers.push_back(layer);
if (layer->isImageCompositionLayer()) {
imageLayers.push_back(std::static_pointer_cast<ImageCompositionLayer>(layer));
}
if (layer->isTextCompositionLayer()) {
textLayers.push_back(std::static_pointer_cast<TextCompositionLayer>(layer));
}
if (mattedLayer) {
/// The previous layer requires this layer to be its matte
mattedLayer->setMatteLayer(layer);
mattedLayer = nullptr;
continue;
}
if (layer->matteType().has_value() && (layer->matteType() == MatteType::Add || layer->matteType() == MatteType::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(true);
}
void setRespectAnimationFrameRate(bool respectAnimationFrameRate) {
_respectAnimationFrameRate = respectAnimationFrameRate;
}
void display() {
double newFrame = currentFrame();
if (_respectAnimationFrameRate) {
newFrame = floor(newFrame);
}
for (const auto &layer : _animationLayers) {
layer->displayWithFrame(newFrame, false);
}
}
std::vector<std::shared_ptr<CompositionLayer>> const &animationLayers() const {
return _animationLayers;
}
void reloadImages() {
_layerImageProvider->reloadImages();
}
/// Forces the view to update its drawing.
void forceDisplayUpdate() {
for (const auto &layer : _animationLayers) {
layer->displayWithFrame(currentFrame(), true);
}
}
void logHierarchyKeypaths() {
printf("Lottie: Logging Animation Keypaths\n");
assert(false);
//animationLayers.forEach({ $0.logKeypaths(for: nil) })
}
void setValueProvider(std::shared_ptr<AnyValueProvider> const &valueProvider, AnimationKeypath const &keypath) {
for (const auto &layer : _animationLayers) {
assert(false);
/*if let foundProperties = layer.nodeProperties(for: keypath) {
for property in foundProperties {
property.setProvider(provider: valueProvider)
}
layer.displayWithFrame(frame: presentation()?.currentFrame ?? currentFrame, forceUpdates: true)
}*/
}
}
std::optional<AnyValue> getValue(AnimationKeypath const &keypath, std::optional<double> atFrame) {
for (const auto &layer : _animationLayers) {
assert(false);
/*if
let foundProperties = layer.nodeProperties(for: keypath),
let first = foundProperties.first
{
return first.valueProvider.value(frame: atFrame ?? currentFrame)
}*/
}
return std::nullopt;
}
std::optional<AnyValue> getOriginalValue(AnimationKeypath const &keypath, std::optional<double> atFrame) {
for (const auto &layer : _animationLayers) {
assert(false);
/*if
let foundProperties = layer.nodeProperties(for: keypath),
let first = foundProperties.first
{
return first.originalValueProvider.value(frame: atFrame ?? currentFrame)
}*/
}
return std::nullopt;
}
std::shared_ptr<CALayer> layerForKeypath(AnimationKeypath const &keyPath) {
assert(false);
/*for layer in animationLayers {
if let foundLayer = layer.layer(for: keypath) {
return foundLayer
}
}*/
return nullptr;
}
std::vector<std::shared_ptr<AnimatorNode>> animatorNodesForKeypath(AnimationKeypath const &keypath) {
std::vector<std::shared_ptr<AnimatorNode>> results;
/*for (const auto &layer : _animationLayers) {
if let nodes = layer.animatorNodes(for: keypath) {
results.append(contentsOf: nodes)
}
}*/
return results;
}
double currentFrame() const {
return _currentFrame;
}
void setCurrentFrame(double currentFrame) {
_currentFrame = currentFrame;
for (size_t i = 0; i < _animationLayers.size(); i++) {
_animationLayers[i]->displayWithFrame(_currentFrame, false);
}
}
std::shared_ptr<AnimationImageProvider> imageProvider() const {
return _layerImageProvider->imageProvider();
}
void setImageProvider(std::shared_ptr<AnimationImageProvider> const &imageProvider) {
_layerImageProvider->setImageProvider(imageProvider);
}
std::shared_ptr<AnimationTextProvider> textProvider() const {
return _layerTextProvider->textProvider();
}
void setTextProvider(std::shared_ptr<AnimationTextProvider> const &textProvider) {
_layerTextProvider->setTextProvider(textProvider);
}
std::shared_ptr<AnimationFontProvider> fontProvider() const {
return _layerFontProvider->fontProvider();
}
void setFontProvider(std::shared_ptr<AnimationFontProvider> const &fontProvider) {
_layerFontProvider->setFontProvider(fontProvider);
}
virtual std::shared_ptr<RenderTreeNode> renderTreeNode() {
std::vector<std::shared_ptr<RenderTreeNode>> subnodes;
for (const auto &animationLayer : _animationLayers) {
bool found = false;
for (const auto &sublayer : sublayers()) {
if (animationLayer == sublayer) {
found = true;
break;
}
}
if (found) {
auto node = animationLayer->renderTreeNode();
if (node) {
subnodes.push_back(node);
}
}
}
return std::make_shared<RenderTreeNode>(
bounds(),
position(),
CATransform3D::identity(),
1.0,
false,
false,
nullptr,
subnodes,
nullptr,
false
);
}
private:
// MARK: Internal
/// The animatable Current Frame Property
double _currentFrame = 0.0;
std::shared_ptr<AnimationImageProvider> _imageProvider;
std::shared_ptr<AnimationTextProvider> _textProvider;
std::shared_ptr<AnimationFontProvider> _fontProvider;
bool _respectAnimationFrameRate = true;
std::vector<std::shared_ptr<CompositionLayer>> _animationLayers;
std::shared_ptr<LayerImageProvider> _layerImageProvider;
std::shared_ptr<LayerTextProvider> _layerTextProvider;
std::shared_ptr<LayerFontProvider> _layerFontProvider;
};
}
#endif /* MainThreadAnimationLayer_hpp */

View File

@ -0,0 +1,279 @@
//
// MainThreadAnimationLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/24/19.
//
import Foundation
import QuartzCore
// 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.
public final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
// MARK: Lifecycle
public 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 public 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 public 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) })
}
public func displayUpdate() {
for i in 0 ..< animationLayers.count {
animationLayers[i].displayWithFrame(frame: currentFrame, forceUpdates: false)
}
}
func logHierarchyKeypaths() {
print("Lottie: Logging Animation Keypaths")
animationLayers.forEach({ $0.logKeypaths(for: nil) })
}
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 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
public class BlankImageProvider: AnimationImageProvider {
public init() {
}
public func imageForAsset(asset _: ImageAsset) -> CGImage? {
nil
}
}

View File

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

View File

@ -0,0 +1,112 @@
#include "CompositionLayersInitializer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/NullCompositionLayer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ShapeCompositionLayer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp"
namespace lottie {
std::vector<std::shared_ptr<CompositionLayer>> initializeCompositionLayers(
std::vector<std::shared_ptr<LayerModel>> const &layers,
std::shared_ptr<AssetLibrary> const &assetLibrary,
std::shared_ptr<LayerImageProvider> const &layerImageProvider,
std::shared_ptr<AnimationTextProvider> const &textProvider,
std::shared_ptr<AnimationFontProvider> const &fontProvider,
double frameRate
) {
std::vector<std::shared_ptr<CompositionLayer>> compositionLayers;
std::map<int, std::shared_ptr<CompositionLayer>> layerMap;
/// Organize the assets into a dictionary of [ID : ImageAsset]
std::vector<std::shared_ptr<LayerModel>> childLayers;
for (const auto &layer : layers) {
if (layer->hidden) {
auto genericLayer = std::make_shared<NullCompositionLayer>(layer);
compositionLayers.push_back(genericLayer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), genericLayer));
}
} else if (layer->type == LayerType::Shape) {
auto shapeContainer = std::make_shared<ShapeCompositionLayer>(std::static_pointer_cast<ShapeLayerModel>(layer));
compositionLayers.push_back(shapeContainer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), shapeContainer));
}
} else if (layer->type == LayerType::Solid) {
auto shapeContainer = std::make_shared<ShapeCompositionLayer>(std::static_pointer_cast<SolidLayerModel>(layer));
compositionLayers.push_back(shapeContainer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), shapeContainer));
}
} else if (layer->type == LayerType::Precomp && assetLibrary) {
auto precompLayer = std::static_pointer_cast<PreCompLayerModel>(layer);
auto precompAssetIt = assetLibrary->precompAssets.find(precompLayer->referenceID);
if (precompAssetIt != assetLibrary->precompAssets.end()) {
auto precompContainer = std::make_shared<PreCompositionLayer>(
precompLayer,
*(precompAssetIt->second),
layerImageProvider,
textProvider,
fontProvider,
assetLibrary,
frameRate
);
compositionLayers.push_back(precompContainer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), precompContainer));
}
}
} else if (layer->type == LayerType::Image && assetLibrary) {
auto imageLayer = std::static_pointer_cast<ImageLayerModel>(layer);
auto imageAssetIt = assetLibrary->imageAssets.find(imageLayer->referenceID);
if (imageAssetIt != assetLibrary->imageAssets.end()) {
auto imageContainer = std::make_shared<ImageCompositionLayer>(
imageLayer,
Vector2D((*imageAssetIt->second).width, (*imageAssetIt->second).height)
);
compositionLayers.push_back(imageContainer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), imageContainer));
}
}
} else if (layer->type == LayerType::Text) {
auto textContainer = std::make_shared<TextCompositionLayer>(std::static_pointer_cast<TextLayerModel>(layer), textProvider, fontProvider);
compositionLayers.push_back(textContainer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), textContainer));
}
} else {
auto genericLayer = std::make_shared<NullCompositionLayer>(layer);
compositionLayers.push_back(genericLayer);
if (layer->index) {
layerMap.insert(std::make_pair(layer->index.value(), genericLayer));
}
}
if (layer->parent) {
childLayers.push_back(layer);
}
}
/// Now link children with their parents
for (const auto &layerModel : childLayers) {
if (!layerModel->index.has_value()) {
continue;
}
if (const auto parentID = layerModel->parent) {
auto childLayerIt = layerMap.find(layerModel->index.value());
if (childLayerIt != layerMap.end()) {
auto parentLayerIt = layerMap.find(parentID.value());
if (parentLayerIt != layerMap.end()) {
childLayerIt->second->transformNode()->setParentNode(parentLayerIt->second->transformNode());
}
}
}
}
return compositionLayers;
}
}

View File

@ -0,0 +1,23 @@
#ifndef CompositionLayersInitializer_hpp
#define CompositionLayersInitializer_hpp
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.hpp"
#include "Lottie/Private/Model/Assets/AssetLibrary.hpp"
#include "Lottie/Private/MainThread/LayerContainers/Utility/LayerImageProvider.hpp"
#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp"
#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp"
namespace lottie {
std::vector<std::shared_ptr<CompositionLayer>> initializeCompositionLayers(
std::vector<std::shared_ptr<LayerModel>> const &layers,
std::shared_ptr<AssetLibrary> const &assetLibrary,
std::shared_ptr<LayerImageProvider> const &layerImageProvider,
std::shared_ptr<AnimationTextProvider> const &textProvider,
std::shared_ptr<AnimationFontProvider> const &fontProvider,
double frameRate
);
}
#endif /* CompositionLayersInitializer_hpp */

View File

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

View File

@ -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, LottieDrawingLayer {
// 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
}
}
}

View File

@ -0,0 +1,59 @@
//
// 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, LottieDrawingLayer {
// 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) {
setNeedsDisplay()
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)
}
}

View File

@ -0,0 +1,5 @@
#include "LayerFontProvider.hpp"
namespace lottie {
}

View File

@ -0,0 +1,45 @@
#ifndef LayerFontProvider_hpp
#define LayerFontProvider_hpp
#include "Lottie/Public/FontProvider/AnimationFontProvider.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp"
namespace lottie {
/// Connects a LottieFontProvider to a group of text layers
class LayerFontProvider {
public:
LayerFontProvider(std::shared_ptr<AnimationFontProvider> const &fontProvider) {
_fontProvider = fontProvider;
reloadTexts();
}
std::shared_ptr<AnimationFontProvider> const &fontProvider() const {
return _fontProvider;
}
void setFontProvider(std::shared_ptr<AnimationFontProvider> const &fontProvider) {
_fontProvider = fontProvider;
reloadTexts();
}
void addTextLayers(std::vector<std::shared_ptr<TextCompositionLayer>> const &layers) {
for (const auto &layer : layers) {
_textLayers.push_back(layer);
}
}
void reloadTexts() {
for (const auto &layer : _textLayers) {
layer->setFontProvider(_fontProvider);
}
}
private:
std::vector<std::shared_ptr<TextCompositionLayer>> _textLayers;
std::shared_ptr<AnimationFontProvider> _fontProvider;
};
}
#endif /* LayerFontProvider_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "LayerImageProvider.hpp"
namespace lottie {
}

View File

@ -0,0 +1,58 @@
#ifndef LayerImageProvider_hpp
#define LayerImageProvider_hpp
#include "Lottie/Public/ImageProvider/AnimationImageProvider.hpp"
#include "Lottie/Private/Model/Assets/ImageAsset.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.hpp"
namespace lottie {
/// Connects a LottieImageProvider to a group of image layers
class LayerImageProvider {
public:
LayerImageProvider(std::shared_ptr<AnimationImageProvider> const &imageProvider, std::map<std::string, std::shared_ptr<ImageAsset>> const &assets) :
_imageProvider(imageProvider),
_imageAssets(assets) {
reloadImages();
}
std::shared_ptr<AnimationImageProvider> imageProvider() const {
return _imageProvider;
}
void setImageProvider(std::shared_ptr<AnimationImageProvider> const &imageProvider) {
_imageProvider = imageProvider;
reloadImages();
}
std::vector<std::shared_ptr<ImageCompositionLayer>> const &imageLayers() const {
return _imageLayers;
}
void addImageLayers(std::vector<std::shared_ptr<ImageCompositionLayer>> const &layers) {
for (const auto &layer : layers) {
auto it = _imageAssets.find(layer->imageReferenceID());
if (it != _imageAssets.end()) {
_imageLayers.push_back(layer);
}
}
}
void reloadImages() {
for (const auto &imageLayer : imageLayers()) {
auto it = _imageAssets.find(imageLayer->imageReferenceID());
if (it != _imageAssets.end()) {
imageLayer->setImage(_imageProvider->imageForAsset(*it->second));
}
}
}
private:
std::shared_ptr<AnimationImageProvider> _imageProvider;
std::vector<std::shared_ptr<ImageCompositionLayer>> _imageLayers;
std::map<std::string, std::shared_ptr<ImageAsset>> _imageAssets;
};
}
#endif /* LayerImageProvider_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "LayerTextProvider.hpp"
namespace lottie {
}

View File

@ -0,0 +1,45 @@
#ifndef LayerTextProvider_hpp
#define LayerTextProvider_hpp
#include "Lottie/Public/TextProvider/AnimationTextProvider.hpp"
#include "Lottie/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.hpp"
namespace lottie {
/// Connects a LottieTextProvider to a group of text layers
class LayerTextProvider {
public:
LayerTextProvider(std::shared_ptr<AnimationTextProvider> const &textProvider) {
_textProvider = textProvider;
reloadTexts();
}
std::shared_ptr<AnimationTextProvider> const &textProvider() const {
return _textProvider;
}
void setTextProvider(std::shared_ptr<AnimationTextProvider> const &textProvider) {
_textProvider = textProvider;
reloadTexts();
}
void addTextLayers(std::vector<std::shared_ptr<TextCompositionLayer>> const &layers) {
for (const auto &layer : layers) {
_textLayers.push_back(layer);
}
}
void reloadTexts() {
for (const auto &layer : _textLayers) {
layer->setTextProvider(_textProvider);
}
}
private:
std::vector<std::shared_ptr<TextCompositionLayer>> _textLayers;
std::shared_ptr<AnimationTextProvider> _textProvider;
};
}
#endif /* LayerTextProvider_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "LayerTransformNode.hpp"
namespace lottie {
}

View File

@ -0,0 +1,201 @@
#ifndef LayerTransformNode_hpp
#define LayerTransformNode_hpp
#include "Lottie/Private/Model/Objects/Transform.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/NodeProperty.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueProviders/KeyframeInterpolator.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.hpp"
namespace lottie {
class LayerTransformProperties: public KeypathSearchableNodePropertyMap {
public:
LayerTransformProperties(std::shared_ptr<Transform> transform) {
_anchor = std::make_shared<NodeProperty<Vector3D>>(std::make_shared<KeyframeInterpolator<Vector3D>>(transform->anchorPoint().keyframes));
_scale = std::make_shared<NodeProperty<Vector3D>>(std::make_shared<KeyframeInterpolator<Vector3D>>(transform->scale().keyframes));
_rotation = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(transform->rotation().keyframes));
_opacity = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(transform->opacity().keyframes));
std::map<std::string, std::shared_ptr<AnyNodeProperty>> propertyMap;
_keypathProperties.insert(std::make_pair("Anchor Point", _anchor));
_keypathProperties.insert(std::make_pair("Scale", _scale));
_keypathProperties.insert(std::make_pair("Rotation", _rotation));
_keypathProperties.insert(std::make_pair("Opacity", _opacity));
if (transform->positionX().has_value() && transform->positionY().has_value()) {
auto xPosition = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(transform->positionX()->keyframes));
auto yPosition = std::make_shared<NodeProperty<Vector1D>>(std::make_shared<KeyframeInterpolator<Vector1D>>(transform->positionY()->keyframes));
_keypathProperties.insert(std::make_pair("X Position", xPosition));
_keypathProperties.insert(std::make_pair("Y Position", yPosition));
_positionX = xPosition;
_positionY = yPosition;
_position = nullptr;
} else if (transform->position().has_value()) {
auto position = std::make_shared<NodeProperty<Vector3D>>(std::make_shared<KeyframeInterpolator<Vector3D>>(transform->position()->keyframes));
_keypathProperties.insert(std::make_pair("Position", position));
_position = position;
_positionX = nullptr;
_positionY = nullptr;
} else {
_position = nullptr;
_positionX = nullptr;
_positionY = nullptr;
}
for (const auto &it : _keypathProperties) {
_properties.push_back(it.second);
}
}
virtual std::vector<std::shared_ptr<AnyNodeProperty>> &properties() override {
return _properties;
}
virtual std::vector<std::shared_ptr<KeypathSearchable>> const &childKeypaths() const override {
return _childKeypaths;
}
virtual std::string keypathName() const override {
return "Transform";
}
virtual std::map<std::string, std::shared_ptr<AnyNodeProperty>> keypathProperties() const override {
return _keypathProperties;
}
virtual std::shared_ptr<CALayer> keypathLayer() const override {
return nullptr;
}
std::shared_ptr<NodeProperty<Vector3D>> const &anchor() {
return _anchor;
}
std::shared_ptr<NodeProperty<Vector3D>> const &scale() {
return _scale;
}
std::shared_ptr<NodeProperty<Vector1D>> const &rotation() {
return _rotation;
}
std::shared_ptr<NodeProperty<Vector3D>> const &position() {
return _position;
}
std::shared_ptr<NodeProperty<Vector1D>> const &positionX() {
return _positionX;
}
std::shared_ptr<NodeProperty<Vector1D>> const &positionY() {
return _positionY;
}
std::shared_ptr<NodeProperty<Vector1D>> const &opacity() {
return _opacity;
}
private:
std::map<std::string, std::shared_ptr<AnyNodeProperty>> _keypathProperties;
std::vector<std::shared_ptr<KeypathSearchable>> _childKeypaths;
std::vector<std::shared_ptr<AnyNodeProperty>> _properties;
std::shared_ptr<NodeProperty<Vector3D>> _anchor;
std::shared_ptr<NodeProperty<Vector3D>> _scale;
std::shared_ptr<NodeProperty<Vector1D>> _rotation;
std::shared_ptr<NodeProperty<Vector3D>> _position;
std::shared_ptr<NodeProperty<Vector1D>> _positionX;
std::shared_ptr<NodeProperty<Vector1D>> _positionY;
std::shared_ptr<NodeProperty<Vector1D>> _opacity;
};
class LayerTransformNode: public AnimatorNode {
public:
LayerTransformNode(std::shared_ptr<Transform> transform) :
AnimatorNode(nullptr),
_transformProperties(std::make_shared<LayerTransformProperties>(transform)) {
_outputNode = std::make_shared<PassThroughOutputNode>(nullptr);
}
virtual std::shared_ptr<NodeOutput> outputNode() override {
return _outputNode;
}
virtual std::shared_ptr<KeypathSearchableNodePropertyMap> propertyMap() const override {
return _transformProperties;
}
virtual bool shouldRebuildOutputs(double frame) override {
return hasLocalUpdates() || hasUpstreamUpdates();
}
virtual void rebuildOutputs(double frame) override {
_opacity = ((float)_transformProperties->opacity()->value().value) * 0.01f;
Vector2D position(0.0, 0.0);
if (_transformProperties->position()) {
auto position3d = _transformProperties->position()->value();
position.x = position3d.x;
position.y = position3d.y;
} else if (_transformProperties->positionX() && _transformProperties->positionY()) {
position = Vector2D(
_transformProperties->positionX()->value().value,
_transformProperties->positionY()->value().value
);
}
Vector3D anchor = _transformProperties->anchor()->value();
Vector3D scale = _transformProperties->scale()->value();
_localTransform = CATransform3D::makeTransform(
Vector2D(anchor.x, anchor.y),
position,
Vector2D(scale.x, scale.y),
_transformProperties->rotation()->value().value,
std::nullopt,
std::nullopt
);
if (parentNode() && parentNode()->asLayerTransformNode()) {
_globalTransform = _localTransform * parentNode()->asLayerTransformNode()->_globalTransform;
} else {
_globalTransform = _localTransform;
}
}
std::shared_ptr<LayerTransformProperties> const &transformProperties() {
return _transformProperties;
}
float opacity() {
return _opacity;
}
CATransform3D const &globalTransform() {
return _globalTransform;
}
private:
std::shared_ptr<NodeOutput> _outputNode;
std::shared_ptr<LayerTransformProperties> _transformProperties;
float _opacity = 1.0;
CATransform3D _localTransform = CATransform3D::identity();
CATransform3D _globalTransform = CATransform3D::identity();
public:
virtual LayerTransformNode *asLayerTransformNode() override {
return this;
}
};
}
#endif /* LayerTransformNode_hpp */

View File

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

View File

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

View File

@ -0,0 +1,5 @@
#include "NodeProperty.hpp"
namespace lottie {
}

View File

@ -0,0 +1,55 @@
#ifndef NodeProperty_hpp
#define NodeProperty_hpp
#include "Lottie/Public/Primitives/AnyValue.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyNodeProperty.hpp"
#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/ValueContainer.hpp"
namespace lottie {
/// A node property that holds a reference to a T ValueProvider and a T ValueContainer.
template<typename T>
class NodeProperty: public AnyNodeProperty {
public:
NodeProperty(std::shared_ptr<ValueProvider<T>> provider) :
_valueProvider(provider),
//_originalValueProvider(provider),
_typedContainer(provider->value(0.0)) {
_typedContainer.setNeedsUpdate();
}
public:
virtual AnyValue::Type valueType() const override {
return AnyValueType<T>::type();
}
virtual T value() {
return _typedContainer.outputValue();
}
virtual bool needsUpdate(double frame) const override {
return _typedContainer.needsUpdate() || _valueProvider->hasUpdate(frame);
}
virtual void setProvider(std::shared_ptr<AnyValueProvider> provider) override {
/*if (provider->valueType() != valueType()) {
return;
}
_valueProvider = provider;
_typedContainer.setNeedsUpdate();*/
}
virtual void update(double frame) override {
_typedContainer.setValue(_valueProvider->value(frame), frame);
}
private:
ValueContainer<T> _typedContainer;
std::shared_ptr<ValueProvider<T>> _valueProvider;
//std::shared_ptr<AnyValueProvider> _originalValueProvider;
};
}
#endif /* NodeProperty_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "AnyNodeProperty.hpp"
namespace lottie {
}

View File

@ -0,0 +1,33 @@
#ifndef AnyNodeProperty_hpp
#define AnyNodeProperty_hpp
#include "Lottie/Public/Primitives/AnyValue.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/AnyValueContainer.hpp"
#include "Lottie/Public/DynamicProperties/AnyValueProvider.hpp"
#include <memory>
namespace lottie {
/// A property of a node. The node property holds a provider and a container
class AnyNodeProperty {
public:
virtual ~AnyNodeProperty() = default;
public:
/// Returns true if the property needs to recompute its stored value
virtual bool needsUpdate(double frame) const = 0;
/// Updates the property for the frame
virtual void update(double frame) = 0;
/// The Type of the value provider
virtual AnyValue::Type valueType() const = 0;
/// Sets the value provider for the property.
virtual void setProvider(std::shared_ptr<AnyValueProvider> provider) = 0;
};
}
#endif /* AnyNodeProperty_hpp */

View File

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

View File

@ -0,0 +1,5 @@
#include "AnyValueContainer.hpp"
namespace lottie {
}

View File

@ -0,0 +1,25 @@
#ifndef AnyValueContainer_hpp
#define AnyValueContainer_hpp
#include "Lottie/Public/Primitives/AnyValue.hpp"
namespace lottie {
class AnyValueContainer {
public:
/// The stored value of the container
virtual AnyValue value() const = 0;
/// Notifies the provider that it should update its container
virtual void setNeedsUpdate() = 0;
/// When true the container needs to have its value updated by its provider
virtual bool needsUpdate() const = 0;
/// The frame time of the last provided update
virtual double lastUpdateFrame() const = 0;
};
}
#endif /* AnyValueContainer_hpp */

View File

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

View File

@ -0,0 +1,13 @@
#ifndef HasRenderUpdates_hpp
#define HasRenderUpdates_hpp
namespace lottie {
class HasRenderUpdates {
public:
virtual bool hasRenderUpdates(double forFrame) = 0;
};
}
#endif /* HasRenderUpdates_hpp */

View File

@ -0,0 +1,14 @@
#ifndef HasUpdate_hpp
#define HasUpdate_hpp
namespace lottie {
class HasUpdate {
public:
/// The last frame in which this node was updated.
virtual bool hasUpdate() = 0;
};
}
#endif /* HasUpdate_hpp */

Some files were not shown because too many files have changed in this diff Show More