mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Lottie tests [skip ci]
This commit is contained in:
parent
9c017f9f03
commit
9fcef12d55
2
.bazelrc
2
.bazelrc
@ -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
@ -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
2
Tests/LottieMetalTest/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
TestData/*.json
|
||||
|
@ -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"
|
||||
)
|
14
Tests/LottieMetalTest/LottieSwift/BUILD
Normal file
14
Tests/LottieMetalTest/LottieSwift/BUILD
Normal 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"],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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]>
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 */
|
@ -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)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#ifndef CompositionLayerDelegate_hpp
|
||||
#define CompositionLayerDelegate_hpp
|
||||
|
||||
namespace lottie {
|
||||
|
||||
class CompositionLayerDelegate {
|
||||
public:
|
||||
virtual void frameUpdated(double frame) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* CompositionLayerDelegate_hpp */
|
@ -0,0 +1,5 @@
|
||||
#include "ImageCompositionLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,50 @@
|
||||
//
|
||||
// ImageCompositionLayer.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/25/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
|
||||
final class ImageCompositionLayer: CompositionLayer {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(imageLayer: ImageLayerModel, size: CGSize) {
|
||||
imageReferenceID = imageLayer.referenceID
|
||||
super.init(layer: imageLayer, size: size)
|
||||
contentsLayer.masksToBounds = true
|
||||
contentsLayer.contentsGravity = CALayerContentsGravity.resize
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
|
||||
guard let layer = layer as? ImageCompositionLayer else {
|
||||
fatalError("init(layer:) Wrong Layer Class")
|
||||
}
|
||||
imageReferenceID = layer.imageReferenceID
|
||||
image = nil
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let imageReferenceID: String
|
||||
|
||||
var image: CGImage? = nil {
|
||||
didSet {
|
||||
if let image = image {
|
||||
contentsLayer.contents = image
|
||||
} else {
|
||||
contentsLayer.contents = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "MaskContainerLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -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>
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "NullCompositionLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,28 @@
|
||||
//
|
||||
// NullCompositionLayer.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/25/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class NullCompositionLayer: CompositionLayer {
|
||||
|
||||
init(layer: LayerModel) {
|
||||
super.init(layer: layer, size: .zero)
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
|
||||
guard let layer = layer as? NullCompositionLayer else {
|
||||
fatalError("init(layer:) Wrong Layer Class")
|
||||
}
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "PreCompositionLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -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]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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 */
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,487 @@
|
||||
#include "BezierPathUtils.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
BezierPath makeEllipseBezierPath(
|
||||
Vector2D const &size,
|
||||
Vector2D const ¢er,
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -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 ¢er,
|
||||
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 */
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "TextCompositionLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,149 @@
|
||||
//
|
||||
// TextCompositionLayer.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/25/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
|
||||
/// Needed for NSMutableParagraphStyle...
|
||||
#if os(OSX)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension TextJustification {
|
||||
var textAlignment: NSTextAlignment {
|
||||
switch self {
|
||||
case .left:
|
||||
return .left
|
||||
case .right:
|
||||
return .right
|
||||
case .center:
|
||||
return .center
|
||||
}
|
||||
}
|
||||
|
||||
var caTextAlignement: CATextLayerAlignmentMode {
|
||||
switch self {
|
||||
case .left:
|
||||
return .left
|
||||
case .right:
|
||||
return .right
|
||||
case .center:
|
||||
return .center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TextCompositionLayer
|
||||
|
||||
final class TextCompositionLayer: CompositionLayer {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(textLayer: TextLayerModel, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) {
|
||||
var rootNode: TextAnimatorNode?
|
||||
for animator in textLayer.animators {
|
||||
rootNode = TextAnimatorNode(parentNode: rootNode, textAnimator: animator)
|
||||
}
|
||||
self.rootNode = rootNode
|
||||
textDocument = KeyframeInterpolator(keyframes: textLayer.text.keyframes)
|
||||
|
||||
self.textProvider = textProvider
|
||||
self.fontProvider = fontProvider
|
||||
|
||||
super.init(layer: textLayer, size: .zero)
|
||||
contentsLayer.addSublayer(self.textLayer)
|
||||
self.textLayer.masksToBounds = false
|
||||
self.textLayer.isGeometryFlipped = true
|
||||
|
||||
if let rootNode = rootNode {
|
||||
childKeypaths.append(rootNode)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
/// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
|
||||
guard let layer = layer as? TextCompositionLayer else {
|
||||
fatalError("init(layer:) Wrong Layer Class")
|
||||
}
|
||||
rootNode = nil
|
||||
textDocument = nil
|
||||
|
||||
textProvider = DefaultTextProvider()
|
||||
fontProvider = DefaultFontProvider()
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let rootNode: TextAnimatorNode?
|
||||
let textDocument: KeyframeInterpolator<TextDocument>?
|
||||
|
||||
let textLayer = CoreTextRenderLayer()
|
||||
var textProvider: AnimationTextProvider
|
||||
var fontProvider: AnimationFontProvider
|
||||
|
||||
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
||||
guard let textDocument = textDocument else { return }
|
||||
|
||||
textLayer.contentsScale = renderScale
|
||||
|
||||
let documentUpdate = textDocument.hasUpdate(frame: frame)
|
||||
let animatorUpdate = rootNode?.updateContents(frame, forceLocalUpdate: forceUpdates) ?? false
|
||||
guard documentUpdate == true || animatorUpdate == true else { return }
|
||||
|
||||
rootNode?.rebuildOutputs(frame: frame)
|
||||
|
||||
// Get Text Attributes
|
||||
let text = textDocument.value(frame: frame) as! TextDocument
|
||||
let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
|
||||
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
|
||||
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
|
||||
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
|
||||
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text)
|
||||
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
|
||||
|
||||
// Set all of the text layer options
|
||||
textLayer.text = textString
|
||||
textLayer.font = ctFont
|
||||
textLayer.alignment = text.justification.textAlignment
|
||||
textLayer.lineHeight = CGFloat(text.lineHeight)
|
||||
textLayer.tracking = tracking
|
||||
|
||||
if let fillColor = rootNode?.textOutputNode.fillColor {
|
||||
textLayer.fillColor = fillColor
|
||||
} else if let fillColor = text.fillColorData?.cgColorValue {
|
||||
textLayer.fillColor = fillColor
|
||||
} else {
|
||||
textLayer.fillColor = nil
|
||||
}
|
||||
|
||||
textLayer.preferredSize = text.textFrameSize?.sizeValue
|
||||
textLayer.strokeOnTop = text.strokeOverFill ?? false
|
||||
textLayer.strokeWidth = strokeWidth
|
||||
textLayer.strokeColor = strokeColor
|
||||
textLayer.sizeToFit()
|
||||
|
||||
textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1)
|
||||
textLayer.transform = CATransform3DIdentity
|
||||
textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero
|
||||
textLayer.transform = matrix
|
||||
}
|
||||
|
||||
override func updateRenderScale() {
|
||||
super.updateRenderScale()
|
||||
textLayer.contentsScale = renderScale
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "MainThreadAnimationLayer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
// Created by Jianjun Wu on 2022/5/12.
|
||||
// Copyright © 2022 Airbnb Inc. All rights reserved.
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
// MARK: - CachedImageProvider
|
||||
|
||||
private final class CachedImageProvider: AnimationImageProvider {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
/// Initializes an image provider with an image provider
|
||||
///
|
||||
/// - Parameter imageProvider: The provider to load image from asset
|
||||
///
|
||||
public init(imageProvider: AnimationImageProvider) {
|
||||
self.imageProvider = imageProvider
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
public func imageForAsset(asset: ImageAsset) -> CGImage? {
|
||||
if let image = imageCache.object(forKey: asset.id as NSString) {
|
||||
return image
|
||||
}
|
||||
if let image = imageProvider.imageForAsset(asset: asset) {
|
||||
imageCache.setObject(image, forKey: asset.id as NSString)
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let imageCache: NSCache<NSString, CGImage> = .init()
|
||||
let imageProvider: AnimationImageProvider
|
||||
}
|
||||
|
||||
extension AnimationImageProvider {
|
||||
/// Create a cache enabled image provider which will reuse the asset image with the same asset id
|
||||
/// It wraps the current provider as image loader, and uses `NSCache` to cache the images for resue.
|
||||
/// The cache will be reset when the `animation` is reset.
|
||||
var cachedImageProvider: AnimationImageProvider {
|
||||
CachedImageProvider(imageProvider: self)
|
||||
}
|
||||
}
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,90 @@
|
||||
//
|
||||
// CompositionLayersInitializer.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/25/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
extension Array where Element == LayerModel {
|
||||
|
||||
func initializeCompositionLayers(
|
||||
assetLibrary: AssetLibrary?,
|
||||
layerImageProvider: LayerImageProvider,
|
||||
textProvider: AnimationTextProvider,
|
||||
fontProvider: AnimationFontProvider,
|
||||
frameRate: CGFloat) -> [CompositionLayer]
|
||||
{
|
||||
var compositionLayers = [CompositionLayer]()
|
||||
var layerMap = [Int : CompositionLayer]()
|
||||
|
||||
/// Organize the assets into a dictionary of [ID : ImageAsset]
|
||||
var childLayers = [LayerModel]()
|
||||
|
||||
for layer in self {
|
||||
if layer.hidden == true {
|
||||
let genericLayer = NullCompositionLayer(layer: layer)
|
||||
compositionLayers.append(genericLayer)
|
||||
layerMap[layer.index] = genericLayer
|
||||
} else if let shapeLayer = layer as? ShapeLayerModel {
|
||||
let shapeContainer = ShapeCompositionLayer(shapeLayer: shapeLayer)
|
||||
compositionLayers.append(shapeContainer)
|
||||
layerMap[layer.index] = shapeContainer
|
||||
} else if let solidLayer = layer as? SolidLayerModel {
|
||||
let solidContainer = SolidCompositionLayer(solid: solidLayer)
|
||||
compositionLayers.append(solidContainer)
|
||||
layerMap[layer.index] = solidContainer
|
||||
} else if
|
||||
let precompLayer = layer as? PreCompLayerModel,
|
||||
let assetLibrary = assetLibrary,
|
||||
let precompAsset = assetLibrary.precompAssets[precompLayer.referenceID]
|
||||
{
|
||||
let precompContainer = PreCompositionLayer(
|
||||
precomp: precompLayer,
|
||||
asset: precompAsset,
|
||||
layerImageProvider: layerImageProvider,
|
||||
textProvider: textProvider,
|
||||
fontProvider: fontProvider,
|
||||
assetLibrary: assetLibrary,
|
||||
frameRate: frameRate)
|
||||
compositionLayers.append(precompContainer)
|
||||
layerMap[layer.index] = precompContainer
|
||||
} else if
|
||||
let imageLayer = layer as? ImageLayerModel,
|
||||
let assetLibrary = assetLibrary,
|
||||
let imageAsset = assetLibrary.imageAssets[imageLayer.referenceID]
|
||||
{
|
||||
let imageContainer = ImageCompositionLayer(
|
||||
imageLayer: imageLayer,
|
||||
size: CGSize(width: imageAsset.width, height: imageAsset.height))
|
||||
compositionLayers.append(imageContainer)
|
||||
layerMap[layer.index] = imageContainer
|
||||
} else if let textLayer = layer as? TextLayerModel {
|
||||
let textContainer = TextCompositionLayer(textLayer: textLayer, textProvider: textProvider, fontProvider: fontProvider)
|
||||
compositionLayers.append(textContainer)
|
||||
layerMap[layer.index] = textContainer
|
||||
} else {
|
||||
let genericLayer = NullCompositionLayer(layer: layer)
|
||||
compositionLayers.append(genericLayer)
|
||||
layerMap[layer.index] = genericLayer
|
||||
}
|
||||
if layer.parent != nil {
|
||||
childLayers.append(layer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Now link children with their parents
|
||||
for layerModel in childLayers {
|
||||
if let parentID = layerModel.parent {
|
||||
let childLayer = layerMap[layerModel.index]
|
||||
let parentLayer = layerMap[parentID]
|
||||
childLayer?.transformNode.parentNode = parentLayer?.transformNode
|
||||
}
|
||||
}
|
||||
|
||||
return compositionLayers
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
//
|
||||
// TextLayer.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Brandon Withrow on 8/3/20.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
/// Needed for NSMutableParagraphStyle...
|
||||
#if os(OSX)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
// MARK: - CoreTextRenderLayer
|
||||
|
||||
/// A CALayer subclass that renders text content using CoreText
|
||||
final class CoreTextRenderLayer: CALayer, 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "LayerFontProvider.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,41 @@
|
||||
//
|
||||
// LayerFontProvider.swift
|
||||
// Lottie
|
||||
//
|
||||
// Created by Brandon Withrow on 8/5/20.
|
||||
// Copyright © 2020 YurtvilleProds. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Connects a LottieFontProvider to a group of text layers
|
||||
final class LayerFontProvider {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(fontProvider: AnimationFontProvider) {
|
||||
self.fontProvider = fontProvider
|
||||
textLayers = []
|
||||
reloadTexts()
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var textLayers: [TextCompositionLayer]
|
||||
|
||||
var fontProvider: AnimationFontProvider {
|
||||
didSet {
|
||||
reloadTexts()
|
||||
}
|
||||
}
|
||||
|
||||
func addTextLayers(_ layers: [TextCompositionLayer]) {
|
||||
textLayers += layers
|
||||
}
|
||||
|
||||
func reloadTexts() {
|
||||
textLayers.forEach {
|
||||
$0.fontProvider = fontProvider
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "LayerImageProvider.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,53 @@
|
||||
//
|
||||
// LayerImageProvider.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/25/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Connects a LottieImageProvider to a group of image layers
|
||||
final class LayerImageProvider {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(imageProvider: AnimationImageProvider, assets: [String: ImageAsset]?) {
|
||||
self.imageProvider = imageProvider
|
||||
imageLayers = [ImageCompositionLayer]()
|
||||
if let assets = assets {
|
||||
imageAssets = assets
|
||||
} else {
|
||||
imageAssets = [:]
|
||||
}
|
||||
reloadImages()
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var imageLayers: [ImageCompositionLayer]
|
||||
let imageAssets: [String: ImageAsset]
|
||||
|
||||
var imageProvider: AnimationImageProvider {
|
||||
didSet {
|
||||
reloadImages()
|
||||
}
|
||||
}
|
||||
|
||||
func addImageLayers(_ layers: [ImageCompositionLayer]) {
|
||||
for layer in layers {
|
||||
if imageAssets[layer.imageReferenceID] != nil {
|
||||
/// Found a linking asset in our asset library. Add layer
|
||||
imageLayers.append(layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadImages() {
|
||||
for imageLayer in imageLayers {
|
||||
if let asset = imageAssets[imageLayer.imageReferenceID] {
|
||||
imageLayer.image = imageProvider.imageForAsset(asset: asset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "LayerTextProvider.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,40 @@
|
||||
//
|
||||
// LayerTextProvider.swift
|
||||
// lottie-ios-iOS
|
||||
//
|
||||
// Created by Alexandr Goncharov on 07/06/2019.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Connects a LottieTextProvider to a group of text layers
|
||||
final class LayerTextProvider {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(textProvider: AnimationTextProvider) {
|
||||
self.textProvider = textProvider
|
||||
textLayers = []
|
||||
reloadTexts()
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
private(set) var textLayers: [TextCompositionLayer]
|
||||
|
||||
var textProvider: AnimationTextProvider {
|
||||
didSet {
|
||||
reloadTexts()
|
||||
}
|
||||
}
|
||||
|
||||
func addTextLayers(_ layers: [TextCompositionLayer]) {
|
||||
textLayers += layers
|
||||
}
|
||||
|
||||
func reloadTexts() {
|
||||
textLayers.forEach {
|
||||
$0.textProvider = textProvider
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "LayerTransformNode.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,144 @@
|
||||
//
|
||||
// LayerTransformPropertyMap.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 2/4/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
|
||||
// MARK: - LayerTransformProperties
|
||||
|
||||
final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(transform: Transform) {
|
||||
|
||||
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
|
||||
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
|
||||
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
|
||||
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
|
||||
|
||||
var propertyMap: [String: AnyNodeProperty] = [
|
||||
"Anchor Point" : anchor,
|
||||
"Scale" : scale,
|
||||
"Rotation" : rotation,
|
||||
"Opacity" : opacity,
|
||||
]
|
||||
|
||||
if
|
||||
let positionKeyframesX = transform.positionX?.keyframes,
|
||||
let positionKeyframesY = transform.positionY?.keyframes
|
||||
{
|
||||
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
|
||||
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
|
||||
propertyMap["X Position"] = xPosition
|
||||
propertyMap["Y Position"] = yPosition
|
||||
positionX = xPosition
|
||||
positionY = yPosition
|
||||
position = nil
|
||||
} else if let positionKeyframes = transform.position?.keyframes {
|
||||
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
|
||||
propertyMap["Position"] = position
|
||||
self.position = position
|
||||
positionX = nil
|
||||
positionY = nil
|
||||
} else {
|
||||
position = nil
|
||||
positionY = nil
|
||||
positionX = nil
|
||||
}
|
||||
|
||||
keypathProperties = propertyMap
|
||||
properties = Array(propertyMap.values)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let keypathProperties: [String: AnyNodeProperty]
|
||||
var keypathName = "Transform"
|
||||
|
||||
let properties: [AnyNodeProperty]
|
||||
|
||||
let anchor: NodeProperty<Vector3D>
|
||||
let scale: NodeProperty<Vector3D>
|
||||
let rotation: NodeProperty<Vector1D>
|
||||
let position: NodeProperty<Vector3D>?
|
||||
let positionX: NodeProperty<Vector1D>?
|
||||
let positionY: NodeProperty<Vector1D>?
|
||||
let opacity: NodeProperty<Vector1D>
|
||||
|
||||
var childKeypaths: [KeypathSearchable] {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LayerTransformNode
|
||||
|
||||
class LayerTransformNode: AnimatorNode {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(transform: Transform) {
|
||||
transformProperties = LayerTransformProperties(transform: transform)
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
|
||||
|
||||
let transformProperties: LayerTransformProperties
|
||||
|
||||
var parentNode: AnimatorNode?
|
||||
var hasLocalUpdates = false
|
||||
var hasUpstreamUpdates = false
|
||||
var lastUpdateFrame: CGFloat? = nil
|
||||
var isEnabled = true
|
||||
|
||||
var opacity: Float = 1
|
||||
var localTransform: CATransform3D = CATransform3DIdentity
|
||||
var globalTransform: CATransform3D = CATransform3DIdentity
|
||||
|
||||
// MARK: Animator Node Protocol
|
||||
|
||||
var propertyMap: NodePropertyMap & KeypathSearchable {
|
||||
transformProperties
|
||||
}
|
||||
|
||||
func shouldRebuildOutputs(frame _: CGFloat) -> Bool {
|
||||
hasLocalUpdates || hasUpstreamUpdates
|
||||
}
|
||||
|
||||
func rebuildOutputs(frame _: CGFloat) {
|
||||
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
|
||||
|
||||
let position: CGPoint
|
||||
if let point = transformProperties.position?.value.pointValue {
|
||||
position = point
|
||||
} else if
|
||||
let xPos = transformProperties.positionX?.value.cgFloatValue,
|
||||
let yPos = transformProperties.positionY?.value.cgFloatValue
|
||||
{
|
||||
position = CGPoint(x: xPos, y: yPos)
|
||||
} else {
|
||||
position = .zero
|
||||
}
|
||||
|
||||
localTransform = CATransform3D.makeTransform(
|
||||
anchor: transformProperties.anchor.value.pointValue,
|
||||
position: position,
|
||||
scale: transformProperties.scale.value.sizeValue,
|
||||
rotation: transformProperties.rotation.value.cgFloatValue,
|
||||
skew: nil,
|
||||
skewAxis: nil)
|
||||
|
||||
if let parentNode = parentNode as? LayerTransformNode {
|
||||
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
|
||||
} else {
|
||||
globalTransform = localTransform
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
//
|
||||
// ItemsExtension.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/18/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - NodeTree
|
||||
|
||||
final class NodeTree {
|
||||
var rootNode: AnimatorNode? = nil
|
||||
var transform: ShapeTransform? = nil
|
||||
var renderContainers: [ShapeContainerLayer] = []
|
||||
var paths: [PathOutputNode] = []
|
||||
var childrenNodes: [AnimatorNode] = []
|
||||
}
|
||||
|
||||
extension Array where Element == ShapeItem {
|
||||
func initializeNodeTree() -> NodeTree {
|
||||
|
||||
let nodeTree = NodeTree()
|
||||
|
||||
for item in self {
|
||||
guard item.hidden == false, item.type != .unknown else { continue }
|
||||
if let fill = item as? Fill {
|
||||
let node = FillNode(parentNode: nodeTree.rootNode, fill: fill)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let stroke = item as? Stroke {
|
||||
let node = StrokeNode(parentNode: nodeTree.rootNode, stroke: stroke)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let gradientFill = item as? GradientFill {
|
||||
let node = GradientFillNode(parentNode: nodeTree.rootNode, gradientFill: gradientFill)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let gradientStroke = item as? GradientStroke {
|
||||
let node = GradientStrokeNode(parentNode: nodeTree.rootNode, gradientStroke: gradientStroke)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let ellipse = item as? Ellipse {
|
||||
let node = EllipseNode(parentNode: nodeTree.rootNode, ellipse: ellipse)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let rect = item as? Rectangle {
|
||||
let node = RectangleNode(parentNode: nodeTree.rootNode, rectangle: rect)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let star = item as? Star {
|
||||
switch star.starType {
|
||||
case .none:
|
||||
continue
|
||||
case .polygon:
|
||||
let node = PolygonNode(parentNode: nodeTree.rootNode, star: star)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
case .star:
|
||||
let node = StarNode(parentNode: nodeTree.rootNode, star: star)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
}
|
||||
} else if let shape = item as? Shape {
|
||||
let node = ShapeNode(parentNode: nodeTree.rootNode, shape: shape)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let trim = item as? Trim {
|
||||
let node = TrimPathNode(parentNode: nodeTree.rootNode, trim: trim, upstreamPaths: nodeTree.paths)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
} else if let xform = item as? ShapeTransform {
|
||||
nodeTree.transform = xform
|
||||
continue
|
||||
} else if let group = item as? Group {
|
||||
|
||||
let tree = group.items.initializeNodeTree()
|
||||
let node = GroupNode(name: group.name, parentNode: nodeTree.rootNode, tree: tree)
|
||||
nodeTree.rootNode = node
|
||||
nodeTree.childrenNodes.append(node)
|
||||
/// Now add all child paths to current tree
|
||||
nodeTree.paths.append(contentsOf: tree.paths)
|
||||
nodeTree.renderContainers.append(node.container)
|
||||
}
|
||||
|
||||
if let pathNode = nodeTree.rootNode as? PathNode {
|
||||
//// Add path container to the node tree
|
||||
nodeTree.paths.append(pathNode.pathOutput)
|
||||
}
|
||||
|
||||
if let renderNode = nodeTree.rootNode as? RenderNode {
|
||||
nodeTree.renderContainers.append(ShapeRenderLayer(renderer: renderNode.renderer))
|
||||
}
|
||||
}
|
||||
return nodeTree
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "NodeProperty.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,55 @@
|
||||
//
|
||||
// NodeProperty.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/30/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// A node property that holds a reference to a T ValueProvider and a T ValueContainer.
|
||||
class NodeProperty<T>: AnyNodeProperty {
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(provider: AnyValueProvider) {
|
||||
valueProvider = provider
|
||||
originalValueProvider = valueProvider
|
||||
typedContainer = ValueContainer<T>(provider.value(frame: 0) as! T)
|
||||
typedContainer.setNeedsUpdate()
|
||||
}
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
var valueProvider: AnyValueProvider
|
||||
var originalValueProvider: AnyValueProvider
|
||||
|
||||
var valueType: Any.Type { T.self }
|
||||
|
||||
var value: T {
|
||||
typedContainer.outputValue
|
||||
}
|
||||
|
||||
var valueContainer: AnyValueContainer {
|
||||
typedContainer
|
||||
}
|
||||
|
||||
func needsUpdate(frame: CGFloat) -> Bool {
|
||||
valueContainer.needsUpdate || valueProvider.hasUpdate(frame: frame)
|
||||
}
|
||||
|
||||
func setProvider(provider: AnyValueProvider) {
|
||||
guard provider.valueType == valueType else { return }
|
||||
valueProvider = provider
|
||||
valueContainer.setNeedsUpdate()
|
||||
}
|
||||
|
||||
func update(frame: CGFloat) {
|
||||
typedContainer.setValue(valueProvider.value(frame: frame), forFrame: frame)
|
||||
}
|
||||
|
||||
// MARK: Fileprivate
|
||||
|
||||
fileprivate var typedContainer: ValueContainer<T>
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "AnyNodeProperty.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,50 @@
|
||||
//
|
||||
// AnyNodeProperty.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/30/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
// MARK: - AnyNodeProperty
|
||||
|
||||
/// A property of a node. The node property holds a provider and a container
|
||||
protocol AnyNodeProperty {
|
||||
|
||||
/// Returns true if the property needs to recompute its stored value
|
||||
func needsUpdate(frame: CGFloat) -> Bool
|
||||
|
||||
/// Updates the property for the frame
|
||||
func update(frame: CGFloat)
|
||||
|
||||
/// The stored value container for the property
|
||||
var valueContainer: AnyValueContainer { get }
|
||||
|
||||
/// The value provider for the property
|
||||
var valueProvider: AnyValueProvider { get }
|
||||
|
||||
/// The original value provider for the property
|
||||
var originalValueProvider: AnyValueProvider { get }
|
||||
|
||||
/// The Type of the value provider
|
||||
var valueType: Any.Type { get }
|
||||
|
||||
/// Sets the value provider for the property.
|
||||
func setProvider(provider: AnyValueProvider)
|
||||
}
|
||||
|
||||
extension AnyNodeProperty {
|
||||
|
||||
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
|
||||
func getValueOfType<T>() -> T? {
|
||||
valueContainer.value as? T
|
||||
}
|
||||
|
||||
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
|
||||
func getValue() -> Any? {
|
||||
valueContainer.value
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#include "AnyValueContainer.hpp"
|
||||
|
||||
namespace lottie {
|
||||
|
||||
}
|
@ -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 */
|
@ -0,0 +1,26 @@
|
||||
//
|
||||
// AnyValueContainer.swift
|
||||
// lottie-swift
|
||||
//
|
||||
// Created by Brandon Withrow on 1/30/19.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
/// The container for the value of a property.
|
||||
protocol AnyValueContainer: AnyObject {
|
||||
|
||||
/// The stored value of the container
|
||||
var value: Any { get }
|
||||
|
||||
/// Notifies the provider that it should update its container
|
||||
func setNeedsUpdate()
|
||||
|
||||
/// When true the container needs to have its value updated by its provider
|
||||
var needsUpdate: Bool { get }
|
||||
|
||||
/// The frame time of the last provided update
|
||||
var lastUpdateFrame: CGFloat { get }
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#ifndef HasRenderUpdates_hpp
|
||||
#define HasRenderUpdates_hpp
|
||||
|
||||
namespace lottie {
|
||||
|
||||
class HasRenderUpdates {
|
||||
public:
|
||||
virtual bool hasRenderUpdates(double forFrame) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* HasRenderUpdates_hpp */
|
@ -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
Loading…
x
Reference in New Issue
Block a user