Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Opacity, Position, Scale, and Rotation value providers #2047

Merged
merged 3 commits into from Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 68 additions & 6 deletions Sources/Private/CoreAnimation/Animations/LayerProperty.swift
Expand Up @@ -60,8 +60,12 @@ struct CustomizableProperty<ValueRepresentation> {
/// 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 {
enum PropertyName: String, CaseIterable {
case color = "Color"
case opacity = "Opacity"
case scale = "Scale"
case position = "Position"
case rotation = "Rotation"
}

// MARK: CALayer properties
Expand All @@ -71,7 +75,7 @@ extension LayerProperty {
.init(
caLayerKeypath: "transform.translation",
defaultValue: CGPoint(x: 0, y: 0),
customizableProperty: nil /* currently unsupported */ )
customizableProperty: .position)
}

static var positionX: LayerProperty<CGFloat> {
Expand Down Expand Up @@ -99,14 +103,14 @@ extension LayerProperty {
.init(
caLayerKeypath: "transform.scale.x",
defaultValue: 1,
customizableProperty: nil /* currently unsupported */ )
customizableProperty: .scaleX)
}

static var scaleY: LayerProperty<CGFloat> {
.init(
caLayerKeypath: "transform.scale.y",
defaultValue: 1,
customizableProperty: nil /* currently unsupported */ )
customizableProperty: .scaleY)
}

static var rotationX: LayerProperty<CGFloat> {
Expand All @@ -127,7 +131,7 @@ extension LayerProperty {
.init(
caLayerKeypath: "transform.rotation.z",
defaultValue: 0,
customizableProperty: nil /* currently unsupported */ )
customizableProperty: .rotation)
}

static var anchorPoint: LayerProperty<CGPoint> {
Expand All @@ -143,7 +147,7 @@ extension LayerProperty {
.init(
caLayerKeypath: #keyPath(CALayer.opacity),
defaultValue: 1,
customizableProperty: nil /* currently unsupported */ )
customizableProperty: .opacity)
}

static var transform: LayerProperty<CATransform3D> {
Expand Down Expand Up @@ -256,4 +260,62 @@ extension CustomizableProperty {
return .rgba(CGFloat(color.r), CGFloat(color.g), CGFloat(color.b), CGFloat(color.a))
})
}

static var opacity: CustomizableProperty<CGFloat> {
.init(
name: [.opacity],
conversion: { typeErasedValue in
guard let vector = typeErasedValue as? LottieVector1D else { return nil }

// Lottie animation files express opacity 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).
return vector.cgFloatValue / 100
})
}

static var scaleX: CustomizableProperty<CGFloat> {
.init(
name: [.scale],
conversion: { typeErasedValue in
guard let vector = typeErasedValue as? LottieVector3D else { return nil }

// 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).
return vector.pointValue.x / 100
})
}

static var scaleY: CustomizableProperty<CGFloat> {
.init(
name: [.scale],
conversion: { typeErasedValue in
guard let vector = typeErasedValue as? LottieVector3D else { return nil }

// 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).
return vector.pointValue.y / 100
})
}

static var rotation: CustomizableProperty<CGFloat> {
.init(
name: [.rotation],
conversion: { typeErasedValue in
guard let vector = typeErasedValue as? LottieVector1D else { return nil }

// 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π)
return vector.cgFloatValue * .pi / 180
})
}

static var position: CustomizableProperty<CGPoint> {
.init(
name: [.position],
conversion: { ($0 as? LottieVector3D)?.pointValue })
}
}
16 changes: 7 additions & 9 deletions Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift
Expand Up @@ -48,22 +48,20 @@ class BaseCompositionLayer: BaseAnimationLayer {
/// 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)
}
let layerContext = context.addingKeypathComponent(baseLayerModel.name)
let childContext = renderLayerContents ? layerContext : context

try setupLayerAnimations(context: context)
try setupChildAnimations(context: context)
try setupLayerAnimations(context: layerContext)
try setupChildAnimations(context: childContext)
}

func setupLayerAnimations(context: LayerAnimationContext) throws {
let context = context.addingKeypathComponent(baseLayerModel.name)
let transformContext = context.addingKeypathComponent("Transform")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed an issue where we were:

  1. incorrectly appending the layer name twice in the path
  2. missing "Transform" from the path

For example, "H1.Transform.Scale" was incorrectly using "H1.H1.Scale" previously.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!


try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: context)
try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: transformContext)

if renderLayerContents {
try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: context)
try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: transformContext)

contentsLayer.addVisibilityAnimation(
inFrame: CGFloat(baseLayerModel.inFrame),
Expand Down
11 changes: 8 additions & 3 deletions Sources/Private/CoreAnimation/ValueProviderStore.swift
Expand Up @@ -26,10 +26,15 @@ final class ValueProviderStore {
because that would require calling the closure on the main thread once per frame.
""")

// TODO: Support more value types
let supportedProperties = PropertyName.allCases.map { $0.rawValue }
let propertyBeingCustomized = keypath.keys.last ?? ""

logger.assert(
keypath.keys.last == PropertyName.color.rawValue,
"The Core Animation rendering engine currently only supports customizing color values")
supportedProperties.contains(propertyBeingCustomized),
"""
The Core Animation rendering engine currently doesn't support customizing "\(propertyBeingCustomized)" \
properties. Supported properties are: \(supportedProperties.joined(separator: ", ")).
""")

valueProviders.append((keypath: keypath, valueProvider: valueProvider))
}
Expand Down
Expand Up @@ -167,7 +167,7 @@ private class MaskNodeProperties: NodePropertyMap {
shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
propertyMap = [
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
"Shape" : shape,
"Expansion" : expansion,
]
Expand Down
Expand Up @@ -25,12 +25,12 @@ final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {

var propertyMap: [String: AnyNodeProperty] = [
"Anchor Point" : anchor,
"Scale" : scale,
"Rotation": rotationZ,
PropertyName.scale.rawValue : scale,
PropertyName.rotation.rawValue: rotationZ,
"Rotation X" : rotationX,
"Rotation Y" : rotationY,
"Rotation Z" : rotationZ,
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
]

if
Expand All @@ -46,7 +46,7 @@ final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
position = nil
} else if let positionKeyframes = transform.position?.keyframes {
let position: NodeProperty<LottieVector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
propertyMap["Position"] = position
propertyMap[PropertyName.position.rawValue] = position
self.position = position
positionX = nil
positionY = nil
Expand Down
Expand Up @@ -20,7 +20,7 @@ final class EllipseNodeProperties: NodePropertyMap, KeypathSearchable {
position = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.position.keyframes))
size = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.size.keyframes))
keypathProperties = [
"Position" : position,
PropertyName.position.rawValue : position,
"Size" : size,
]
properties = Array(keypathProperties.values)
Expand Down
Expand Up @@ -23,10 +23,10 @@ final class PolygonNodeProperties: NodePropertyMap, KeypathSearchable {
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
PropertyName.position.rawValue : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Rotation" : rotation,
PropertyName.rotation.rawValue : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
Expand Down
Expand Up @@ -22,7 +22,7 @@ final class RectNodeProperties: NodePropertyMap, KeypathSearchable {
cornerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.cornerRadius.keyframes))

keypathProperties = [
"Position" : position,
PropertyName.position.rawValue : position,
"Size" : size,
"Roundness" : cornerRadius,
]
Expand Down
Expand Up @@ -33,12 +33,12 @@ final class StarNodeProperties: NodePropertyMap, KeypathSearchable {
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
PropertyName.position.rawValue : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Inner Radius" : innerRadius,
"Inner Roundedness" : innerRoundedness,
"Rotation" : rotation,
PropertyName.rotation.rawValue : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
Expand Down
Expand Up @@ -40,13 +40,13 @@ final class GroupNodeProperties: NodePropertyMap, KeypathSearchable {
}
keypathProperties = [
"Anchor Point" : anchor,
"Position" : position,
"Scale" : scale,
"Rotation" : rotationZ,
PropertyName.position.rawValue : position,
PropertyName.scale.rawValue : scale,
PropertyName.rotation.rawValue : rotationZ,
"Rotation X" : rotationX,
"Rotation Y" : rotationY,
"Rotation Z" : rotationZ,
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
"Skew" : skew,
"Skew Axis" : skewAxis,
]
Expand Down
Expand Up @@ -20,7 +20,7 @@ final class FillNodeProperties: NodePropertyMap, KeypathSearchable {
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.opacity.keyframes))
type = fill.fillRule
keypathProperties = [
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
PropertyName.color.rawValue : color,
]
properties = Array(keypathProperties.values)
Expand Down
Expand Up @@ -24,7 +24,7 @@ final class GradientFillProperties: NodePropertyMap, KeypathSearchable {
numberOfColors = gradientfill.numberOfColors
fillRule = gradientfill.fillRule
keypathProperties = [
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
Expand Down
Expand Up @@ -44,7 +44,7 @@ final class GradientStrokeProperties: NodePropertyMap, KeypathSearchable {
dashPhase = NodeProperty(provider: SingleValueProvider(LottieVector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
Expand Down
Expand Up @@ -36,7 +36,7 @@ final class StrokeNodeProperties: NodePropertyMap, KeypathSearchable {
dashPhase = NodeProperty(provider: SingleValueProvider(LottieVector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
PropertyName.opacity.rawValue : opacity,
PropertyName.color.rawValue : color,
"Stroke Width" : width,
"Dashes" : dashPattern,
Expand Down
Expand Up @@ -28,14 +28,14 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {

if let keyframeGroup = textAnimator.position {
position = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Position"] = position
properties[PropertyName.position.rawValue] = position
} else {
position = nil
}

if let keyframeGroup = textAnimator.scale {
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Scale"] = scale
properties[PropertyName.scale.rawValue] = scale
} else {
scale = nil
}
Expand Down Expand Up @@ -71,14 +71,14 @@ final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
if let keyframeGroup = textAnimator.rotationZ {
rotationZ = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Rotation Z"] = rotationZ
properties["Rotation"] = rotationZ
properties[PropertyName.rotation.rawValue] = rotationZ
} else {
rotationZ = nil
}

if let keyframeGroup = textAnimator.opacity {
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Opacity"] = opacity
properties[PropertyName.opacity.rawValue] = opacity
} else {
opacity = nil
}
Expand Down
1 change: 1 addition & 0 deletions Tests/Samples/Issues/issue_1837_opacity.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Tests/Samples/Issues/issue_1837_scale_rotation.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Tests/Samples/Issues/issue_2042.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Tests/SnapshotConfiguration.swift
Expand Up @@ -65,6 +65,19 @@ extension SnapshotConfiguration {
]),
]),

"Issues/issue_1837_opacity": .customValueProviders([
AnimationKeypath(keypath: "Dark Gray Solid 1.Transform.Opacity"): FloatValueProvider(10),
]),

"Issues/issue_1837_scale_rotation": .customValueProviders([
AnimationKeypath(keypath: "H2.Transform.Scale"): PointValueProvider(CGPoint(x: 200, y: 150)),
AnimationKeypath(keypath: "H2.Transform.Rotation"): FloatValueProvider(90),
]),

"Issues/issue_2042": .customValueProviders([
AnimationKeypath(keypath: "MASTER.Transform.Position"): PointValueProvider(CGPoint(x: 214, y: 120)),
]),

"Issues/issue_1664": .customValueProviders([
AnimationKeypath(keypath: "**.base_color.**.Color"): ColorValueProvider(.black),
]),
Expand Down