Skip to content

Commit

Permalink
Support combining shapes with different keyframe timings into the sam…
Browse files Browse the repository at this point in the history
…e CAShapeLayer (airbnb#1699)
  • Loading branch information
calda authored and Igor Moroz committed May 22, 2024
1 parent d899888 commit 2f048d6
Show file tree
Hide file tree
Showing 18 changed files with 64 additions and 37 deletions.
16 changes: 8 additions & 8 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
08EF21DC289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */; };
08EF21DD289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */; };
08EF21DE289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */; };
08F8B20D2898A7B100CB5323 /* RepeaterLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8B20C2898A7B100CB5323 /* RepeaterLayer.swift */; };
08F8B20E2898A7B100CB5323 /* RepeaterLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8B20C2898A7B100CB5323 /* RepeaterLayer.swift */; };
08F8B20F2898A7B100CB5323 /* RepeaterLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F8B20C2898A7B100CB5323 /* RepeaterLayer.swift */; };
Expand Down Expand Up @@ -265,9 +268,6 @@
2E9C96BD2822F43100677516 /* AnyValueContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95882822F43000677516 /* AnyValueContainer.swift */; };
2E9C96BE2822F43100677516 /* AnyValueContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95882822F43000677516 /* AnyValueContainer.swift */; };
2E9C96BF2822F43100677516 /* AnyValueContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95882822F43000677516 /* AnyValueContainer.swift */; };
2E9C96C02822F43100677516 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958A2822F43000677516 /* KeyframeInterpolator.swift */; };
2E9C96C12822F43100677516 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958A2822F43000677516 /* KeyframeInterpolator.swift */; };
2E9C96C22822F43100677516 /* KeyframeInterpolator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958A2822F43000677516 /* KeyframeInterpolator.swift */; };
2E9C96C32822F43100677516 /* SingleValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958B2822F43000677516 /* SingleValueProvider.swift */; };
2E9C96C42822F43100677516 /* SingleValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958B2822F43000677516 /* SingleValueProvider.swift */; };
2E9C96C52822F43100677516 /* SingleValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C958B2822F43000677516 /* SingleValueProvider.swift */; };
Expand Down Expand Up @@ -590,6 +590,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyframeInterpolator.swift; sourceTree = "<group>"; };
08F8B20C2898A7B100CB5323 /* RepeaterLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeaterLayer.swift; sourceTree = "<group>"; };
08F8B210289990B700CB5323 /* Samples */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Samples; sourceTree = "<group>"; };
08F8B212289990CB00CB5323 /* SnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -684,7 +685,6 @@
2E9C95862822F43000677516 /* NodePropertyMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodePropertyMap.swift; sourceTree = "<group>"; };
2E9C95872822F43000677516 /* KeypathSearchable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeypathSearchable.swift; sourceTree = "<group>"; };
2E9C95882822F43000677516 /* AnyValueContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyValueContainer.swift; sourceTree = "<group>"; };
2E9C958A2822F43000677516 /* KeyframeInterpolator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyframeInterpolator.swift; sourceTree = "<group>"; };
2E9C958B2822F43000677516 /* SingleValueProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleValueProvider.swift; sourceTree = "<group>"; };
2E9C958C2822F43000677516 /* GroupInterpolator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInterpolator.swift; sourceTree = "<group>"; };
2E9C958E2822F43000677516 /* ItemsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemsExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1150,7 +1150,6 @@
2E9C95892822F43000677516 /* ValueProviders */ = {
isa = PBXGroup;
children = (
2E9C958A2822F43000677516 /* KeyframeInterpolator.swift */,
2E9C958B2822F43000677516 /* SingleValueProvider.swift */,
2E9C958C2822F43000677516 /* GroupInterpolator.swift */,
);
Expand Down Expand Up @@ -1307,6 +1306,7 @@
2E9C95CF2822F43100677516 /* InterpolatableExtensions.swift */,
2E9C95D02822F43100677516 /* KeyframeExtensions.swift */,
7E48BF5F2860D4FA00A39198 /* KeyframeGroup+Extensions.swift */,
08EF21DB289C643B0097EA47 /* KeyframeInterpolator.swift */,
);
path = Interpolatable;
sourceTree = "<group>";
Expand Down Expand Up @@ -1697,6 +1697,7 @@
2EAF5AE927A0798700E00531 /* AnimationTextProvider.swift in Sources */,
2E9C96662822F43100677516 /* LayerTransformNode.swift in Sources */,
2E9C97412822F43100677516 /* TestHelpers.swift in Sources */,
08EF21DC289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */,
2E9C96152822F43100677516 /* Transform.swift in Sources */,
2E9C97472822F43100677516 /* CGFloatExtensions.swift in Sources */,
2EAF5AC527A0798700E00531 /* UIColorExtension.swift in Sources */,
Expand All @@ -1711,7 +1712,6 @@
2E9C965D2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
2E9C964E2822F43100677516 /* SolidCompositionLayer.swift in Sources */,
2E9C963F2822F43100677516 /* Asset.swift in Sources */,
2E9C96C02822F43100677516 /* KeyframeInterpolator.swift in Sources */,
2E9C96F92822F43100677516 /* BaseCompositionLayer.swift in Sources */,
2EAF5A9B27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */,
2E9C969F2822F43100677516 /* TextAnimatorNode.swift in Sources */,
Expand Down Expand Up @@ -1908,6 +1908,7 @@
2EAF5AEA27A0798700E00531 /* AnimationTextProvider.swift in Sources */,
2E9C96672822F43100677516 /* LayerTransformNode.swift in Sources */,
2E9C97422822F43100677516 /* TestHelpers.swift in Sources */,
08EF21DD289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */,
2E9C96162822F43100677516 /* Transform.swift in Sources */,
2E9C97482822F43100677516 /* CGFloatExtensions.swift in Sources */,
2EAF5AC627A0798700E00531 /* UIColorExtension.swift in Sources */,
Expand All @@ -1922,7 +1923,6 @@
2E9C965E2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
2E9C964F2822F43100677516 /* SolidCompositionLayer.swift in Sources */,
2E9C96402822F43100677516 /* Asset.swift in Sources */,
2E9C96C12822F43100677516 /* KeyframeInterpolator.swift in Sources */,
2E9C96FA2822F43100677516 /* BaseCompositionLayer.swift in Sources */,
2EAF5A9C27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */,
2E9C96A02822F43100677516 /* TextAnimatorNode.swift in Sources */,
Expand Down Expand Up @@ -2099,6 +2099,7 @@
2EAF5AEB27A0798700E00531 /* AnimationTextProvider.swift in Sources */,
2E9C96682822F43100677516 /* LayerTransformNode.swift in Sources */,
2E9C97432822F43100677516 /* TestHelpers.swift in Sources */,
08EF21DE289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */,
2E9C96172822F43100677516 /* Transform.swift in Sources */,
2E9C97492822F43100677516 /* CGFloatExtensions.swift in Sources */,
2EAF5AC727A0798700E00531 /* UIColorExtension.swift in Sources */,
Expand All @@ -2113,7 +2114,6 @@
2E9C965F2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
2E9C96502822F43100677516 /* SolidCompositionLayer.swift in Sources */,
2E9C96412822F43100677516 /* Asset.swift in Sources */,
2E9C96C22822F43100677516 /* KeyframeInterpolator.swift in Sources */,
2E9C96FB2822F43100677516 /* BaseCompositionLayer.swift in Sources */,
2EAF5A9D27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */,
2E9C96A12822F43100677516 /* TextAnimatorNode.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,31 @@ final class CombinedShapeItem: ShapeItem {
let shapes: KeyframeGroup<[BezierPath]>

}

extension CombinedShapeItem {
/// Manually combines the given shape keyframes by manually interpolating at each frame
static func manuallyInterpolating(
shapes: [KeyframeGroup<BezierPath>],
name: String,
context: LayerContext)
-> CombinedShapeItem
{
let animationTimeRange = Int(context.animation.startFrame)...Int(context.animation.endFrame)

let interpolators = shapes.map { shape in
KeyframeInterpolator(keyframes: shape.keyframes)
}

let interpolatedKeyframes = animationTimeRange.map { frame in
Keyframe(
value: interpolators.compactMap { interpolator in
interpolator.value(frame: AnimationFrameTime(frame)) as? BezierPath
},
time: AnimationFrameTime(frame))
}

return CombinedShapeItem(
shapes: KeyframeGroup(keyframes: ContiguousArray(interpolatedKeyframes)),
name: name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,6 @@ 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]>? {
let nonOptionalGroups = groups.compactMap { $0 }
guard nonOptionalGroups.count == groups.count else { return nil }
return combinedIfPossible(nonOptionalGroups)
}

// MARK: Private

/// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s into a single `KeyframeGroup`
Expand Down
46 changes: 26 additions & 20 deletions Sources/Private/CoreAnimation/Layers/ShapeLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,31 +124,36 @@ final class GroupLayer: BaseAnimationLayer {
// 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.
//
// As a fall back when this is not possible, we render each shape in its own `CAShapeLayer`,
// which causes the `fillRule` to be applied incorrectly in cases where the paths overlap.
// We can't really detect when this happens, so this is a case where `RenderingEngineMode.automatic`
// can behave incorrectly. In the future we could fix this by precomputing the full combined CGPath for each
// individual frame in the animation (like we do for some trim animations as of #1612).
// When there are multiple path-drawing items, they're supposed to be rendered
// in a single `CAShapeLayer` (instead of rendering them in separate layers) so
// `CAShapeLayerFillRule.evenOdd` can be applied correctly if the paths overlap.
// Since a `CAShapeLayer` only supports animating a single `CGPath` from a single `KeyframeGroup<BezierPath>`,
// this requires combining all of the path-drawing items into a single set of keyframes.
if
shapeRenderGroup.pathItems.count > 1,
let combinedShapeKeyframes = Keyframes.combinedIfPossible(
shapeRenderGroup.pathItems.map { ($0.item as? Shape)?.path }),
// We currently only support this codepath for `Shape` items that directly contain bezier path keyframes.
// We could also support this for other path types like rectangles, ellipses, and polygons with more work.
shapeRenderGroup.pathItems.allSatisfy({ $0.item is Shape }),
// `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 allPathKeyframes = shapeRenderGroup.pathItems.compactMap { ($0.item as? Shape)?.path }
let combinedShape: CombinedShapeItem

// 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.
if let combinedShapeKeyframes = Keyframes.combinedIfPossible(allPathKeyframes) {
combinedShape = CombinedShapeItem(shapes: combinedShapeKeyframes, name: group.name)
}

// Otherwise, in order for the path fills to be rendered correctly, we have to manually
// interpolate the path for each shape at each frame ahead of time so we can combine them
// into a single set of bezier path keyframes.
else {
combinedShape = .manuallyInterpolating(shapes: allPathKeyframes, name: group.name, context: context)
}

let sublayer = try ShapeItemLayer(
shape: ShapeItemLayer.Item(item: combinedShape, parentGroup: group),
Expand All @@ -159,7 +164,8 @@ final class GroupLayer: BaseAnimationLayer {
}

// Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently,
// we have to create a separate `ShapeItemLayer` for each one.
// we have to create a separate `ShapeItemLayer` for each one. This may render
// incorrectly if there are multiple paths that overlap with each other.
else {
for pathDrawingItem in shapeRenderGroup.pathItems {
let sublayer = try ShapeItemLayer(
Expand Down
1 change: 1 addition & 0 deletions Tests/Samples/Issues/pr_1699.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Supports Core Animation engine
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 2f048d6

Please sign in to comment.