lottie-ios 3.4.1 → 3.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/stale_issues.yml +17 -0
- package/.swiftpm/xcode/package.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcodeproj/project.pbxproj +24 -16
- package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (macOS).xcscheme +2 -2
- package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -81
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -17
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +114 -1
- package/Package.swift +1 -6
- package/Rakefile +41 -2
- package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +1 -2
- package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +28 -0
- package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +31 -4
- package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +2 -2
- package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +34 -7
- package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +25 -14
- package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +61 -32
- package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +4 -1
- package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +6 -1
- package/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift +2 -2
- package/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift +107 -26
- package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +2 -1
- package/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift +48 -12
- package/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift +1 -1
- package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +4 -0
- package/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift +1 -1
- package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +85 -0
- package/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift +17 -4
- package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +124 -53
- package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift +29 -0
- package/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift +1 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift +5 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +5 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +3 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +17 -1
- package/Sources/Private/Model/Animation.swift +4 -4
- package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +25 -0
- package/Sources/Private/Model/ShapeItems/Ellipse.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Fill.swift +1 -1
- package/Sources/Private/Model/ShapeItems/GradientFill.swift +14 -1
- package/Sources/Private/Model/ShapeItems/GradientStroke.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Merge.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Rectangle.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Repeater.swift +0 -1
- package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Star.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Stroke.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Trim.swift +0 -1
- package/Sources/Private/{MainThread/NodeRenderSystem/NodeProperties/ValueProviders → Utility/Interpolatable}/KeyframeInterpolator.swift +0 -0
- package/Sources/Public/Animation/AnimationView.swift +1 -1
- package/Sources/Public/iOS/BundleImageProvider.swift +2 -2
- package/Sources/Public/iOS/FilepathImageProvider.swift +1 -1
- package/Sources/Public/macOS/BundleImageProvider.macOS.swift +1 -1
- package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +1 -1
- package/Tests/AnimationKeypathTests.swift +10 -1
- package/Tests/PerformanceTests.swift +19 -20
- package/Tests/Samples/9squares_AlBoardman.json +1 -0
- package/Tests/Samples/Boat_Loader.json +1 -0
- package/Tests/Samples/HamburgerArrow.json +1 -0
- package/Tests/Samples/IconTransitions.json +1 -0
- package/Tests/Samples/Images/dog.png +0 -0
- package/Tests/Samples/Issues/issue_1125.json +1 -0
- package/Tests/Samples/Issues/issue_1260.json +1 -0
- package/Tests/Samples/Issues/issue_1403.json +1 -0
- package/Tests/Samples/Issues/issue_1407.json +1 -0
- package/Tests/Samples/Issues/issue_1460.json +1 -0
- package/Tests/Samples/Issues/issue_1488.json +1 -0
- package/Tests/Samples/Issues/issue_1505.json +1 -0
- package/Tests/Samples/Issues/issue_1541.json +1 -0
- package/Tests/Samples/Issues/issue_1557.json +1 -0
- package/Tests/Samples/Issues/issue_1603.json +1 -0
- package/Tests/Samples/Issues/issue_1628.json +1 -0
- package/Tests/Samples/Issues/issue_1636.json +1 -0
- package/Tests/Samples/Issues/issue_1643.json +1 -0
- package/Tests/Samples/Issues/issue_1655.json +1 -0
- package/Tests/Samples/Issues/issue_1664.json +1 -0
- package/Tests/Samples/Issues/issue_1683.json +1 -0
- package/Tests/Samples/Issues/issue_1687.json +1 -0
- package/Tests/Samples/Issues/issue_1711.json +1 -0
- package/Tests/Samples/Issues/issue_1717.json +1 -0
- package/Tests/Samples/Issues/issue_769.json +1 -0
- package/Tests/Samples/Issues/issue_885.json +1 -0
- package/Tests/Samples/Issues/issue_965.json +1 -0
- package/Tests/Samples/Issues/pr_1536.json +1 -0
- package/Tests/Samples/Issues/pr_1563.json +8439 -0
- package/Tests/Samples/Issues/pr_1592.json +5527 -0
- package/Tests/Samples/Issues/pr_1599.json +738 -0
- package/Tests/Samples/Issues/pr_1604_1.json +1 -0
- package/Tests/Samples/Issues/pr_1604_2.json +1 -0
- package/Tests/Samples/Issues/pr_1632_1.json +1 -0
- package/Tests/Samples/Issues/pr_1632_2.json +1 -0
- package/Tests/Samples/Issues/pr_1686.json +513 -0
- package/Tests/Samples/Issues/pr_1698.json +1 -0
- package/Tests/Samples/Issues/pr_1699.json +1 -0
- package/Tests/Samples/LottieFiles/LICENSE.md +14 -0
- package/Tests/Samples/LottieFiles/bounce_strokes.json +1 -0
- package/Tests/Samples/LottieFiles/cactus.json +1 -0
- package/Tests/Samples/LottieFiles/dog_car_ride.json +1 -0
- package/Tests/Samples/LottieFiles/draft_icon.json +1 -0
- package/Tests/Samples/LottieFiles/fireworks.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_1.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_2.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_pill.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_shapes.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_square.json +1 -0
- package/Tests/Samples/LottieFiles/growth.json +1 -0
- package/Tests/Samples/LottieFiles/infinity_loader.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_1.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_2.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_3.json +1 -0
- package/Tests/Samples/LottieFiles/loading_gradient_strokes.json +1 -0
- package/Tests/Samples/LottieFiles/settings_slider.json +1 -0
- package/Tests/Samples/LottieFiles/shop.json +1 -0
- package/Tests/Samples/LottieFiles/step_loader.json +1 -0
- package/Tests/Samples/LottieLogo1.json +1 -0
- package/Tests/Samples/LottieLogo1_masked.json +1 -0
- package/Tests/Samples/LottieLogo2.json +1 -0
- package/Tests/Samples/MotionCorpse_Jrcanest.json +1 -0
- package/Tests/Samples/Nonanimating/BasicLayers.json +1 -0
- package/Tests/Samples/Nonanimating/DisableNodesTest.json +1 -0
- package/Tests/Samples/Nonanimating/FirstText.json +1 -0
- package/Tests/Samples/Nonanimating/GeometryTransformTest.json +1 -0
- package/Tests/Samples/Nonanimating/Text_AnimatedProperties.json +1 -0
- package/Tests/Samples/Nonanimating/Text_Glyph.json +1 -0
- package/Tests/Samples/Nonanimating/Text_NoAnimation.json +1 -0
- package/Tests/Samples/Nonanimating/Text_NoGlyph.json +1 -0
- package/Tests/Samples/Nonanimating/Zoom.json +1 -0
- package/Tests/Samples/Nonanimating/_dog.json +1 -0
- package/Tests/Samples/Nonanimating/base64Test.json +1 -0
- package/Tests/Samples/Nonanimating/blend_mode_test.json +1 -0
- package/Tests/Samples/Nonanimating/keypathTest.json +1 -0
- package/Tests/Samples/Nonanimating/verifyLineHeight.json +1 -0
- package/Tests/Samples/PinJump.json +1 -0
- package/Tests/Samples/Switch.json +1 -0
- package/Tests/Samples/Switch_States.json +1 -0
- package/Tests/Samples/TwitterHeart.json +1 -0
- package/Tests/Samples/TwitterHeartButton.json +1 -0
- package/Tests/Samples/TypeFace/A.json +1 -0
- package/Tests/Samples/TypeFace/Apostrophe.json +1 -0
- package/Tests/Samples/TypeFace/B.json +1 -0
- package/Tests/Samples/TypeFace/BlinkingCursor.json +1 -0
- package/Tests/Samples/TypeFace/C.json +1 -0
- package/Tests/Samples/TypeFace/Colon.json +1 -0
- package/Tests/Samples/TypeFace/Comma.json +1 -0
- package/Tests/Samples/TypeFace/D.json +1 -0
- package/Tests/Samples/TypeFace/E.json +1 -0
- package/Tests/Samples/TypeFace/F.json +1 -0
- package/Tests/Samples/TypeFace/G.json +1 -0
- package/Tests/Samples/TypeFace/H.json +1 -0
- package/Tests/Samples/TypeFace/I.json +1 -0
- package/Tests/Samples/TypeFace/J.json +1 -0
- package/Tests/Samples/TypeFace/K.json +1 -0
- package/Tests/Samples/TypeFace/L.json +1 -0
- package/Tests/Samples/TypeFace/M.json +1 -0
- package/Tests/Samples/TypeFace/N.json +1 -0
- package/Tests/Samples/TypeFace/O.json +1 -0
- package/Tests/Samples/TypeFace/P.json +1 -0
- package/Tests/Samples/TypeFace/Q.json +1 -0
- package/Tests/Samples/TypeFace/R.json +1 -0
- package/Tests/Samples/TypeFace/S.json +1 -0
- package/Tests/Samples/TypeFace/T.json +1 -0
- package/Tests/Samples/TypeFace/U.json +1 -0
- package/Tests/Samples/TypeFace/V.json +1 -0
- package/Tests/Samples/TypeFace/W.json +1 -0
- package/Tests/Samples/TypeFace/X.json +1 -0
- package/Tests/Samples/TypeFace/Y.json +1 -0
- package/Tests/Samples/TypeFace/Z.json +1 -0
- package/Tests/Samples/Watermelon.json +1 -0
- package/Tests/Samples/setValueTest.json +1 -0
- package/Tests/Samples/timeremap.json +1 -0
- package/Tests/Samples/vcTransition1.json +1 -0
- package/Tests/Samples/vcTransition2.json +1 -0
- package/Tests/SnapshotConfiguration.swift +5 -0
- package/lottie-ios.podspec +2 -1
- package/package.json +1 -1
- package/.swift-version +0 -1
- package/Package.resolved +0 -88
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Created by Cal Stephens on 8/1/22.
|
|
2
|
+
// Copyright © 2022 Airbnb Inc. All rights reserved.
|
|
3
|
+
|
|
4
|
+
import QuartzCore
|
|
5
|
+
|
|
6
|
+
// MARK: - RepeaterLayer
|
|
7
|
+
|
|
8
|
+
/// A layer that renders a child layer at some offset using a `Repeater`
|
|
9
|
+
final class RepeaterLayer: BaseAnimationLayer {
|
|
10
|
+
|
|
11
|
+
// MARK: Lifecycle
|
|
12
|
+
|
|
13
|
+
init(repeater: Repeater, childLayer: CALayer, index: Int) {
|
|
14
|
+
repeaterTransform = RepeaterTransform(repeater: repeater, index: index)
|
|
15
|
+
super.init()
|
|
16
|
+
addSublayer(childLayer)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
required init?(coder _: NSCoder) {
|
|
20
|
+
fatalError("init(coder:) has not been implemented")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Called by CoreAnimation to create a shadow copy of this layer
|
|
24
|
+
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
|
|
25
|
+
override init(layer: Any) {
|
|
26
|
+
guard let typedLayer = layer as? Self else {
|
|
27
|
+
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
repeaterTransform = typedLayer.repeaterTransform
|
|
31
|
+
super.init(layer: typedLayer)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: Internal
|
|
35
|
+
|
|
36
|
+
override func setupAnimations(context: LayerAnimationContext) throws {
|
|
37
|
+
try super.setupAnimations(context: context)
|
|
38
|
+
try addTransformAnimations(for: repeaterTransform, context: context)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: Private
|
|
42
|
+
|
|
43
|
+
private let repeaterTransform: RepeaterTransform
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MARK: - RepeaterTransform
|
|
48
|
+
|
|
49
|
+
/// A transform model created from a `Repeater`
|
|
50
|
+
private struct RepeaterTransform {
|
|
51
|
+
|
|
52
|
+
// MARK: Lifecycle
|
|
53
|
+
|
|
54
|
+
init(repeater: Repeater, index: Int) {
|
|
55
|
+
anchorPoint = repeater.anchorPoint
|
|
56
|
+
scale = repeater.scale
|
|
57
|
+
|
|
58
|
+
rotation = repeater.rotation.map { rotation in
|
|
59
|
+
Vector1D(rotation.value * Double(index))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
position = repeater.position.map { position in
|
|
63
|
+
Vector3D(
|
|
64
|
+
x: position.x * Double(index),
|
|
65
|
+
y: position.y * Double(index),
|
|
66
|
+
z: position.z * Double(index))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: Internal
|
|
71
|
+
|
|
72
|
+
let anchorPoint: KeyframeGroup<Vector3D>
|
|
73
|
+
let position: KeyframeGroup<Vector3D>
|
|
74
|
+
let rotation: KeyframeGroup<Vector1D>
|
|
75
|
+
let scale: KeyframeGroup<Vector3D>
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: TransformModel
|
|
80
|
+
|
|
81
|
+
extension RepeaterTransform: TransformModel {
|
|
82
|
+
var _position: KeyframeGroup<Vector3D>? { position }
|
|
83
|
+
var _positionX: KeyframeGroup<Vector1D>? { nil }
|
|
84
|
+
var _positionY: KeyframeGroup<Vector1D>? { nil }
|
|
85
|
+
}
|
|
@@ -55,8 +55,13 @@ final class ShapeItemLayer: BaseAnimationLayer {
|
|
|
55
55
|
/// A `ShapeItem` that should be rendered by this layer
|
|
56
56
|
let item: ShapeItem
|
|
57
57
|
|
|
58
|
-
/// The
|
|
59
|
-
|
|
58
|
+
/// The set of groups that this item descends from
|
|
59
|
+
/// - Due to the way `GroupLayer`s are setup, the original `ShapeItem`
|
|
60
|
+
/// hierarchy from the `ShapeLayer` data model may no longer exactly
|
|
61
|
+
/// match the hierarchy of `GroupLayer` / `ShapeItemLayer`s constructed
|
|
62
|
+
/// at runtime. Since animation keypaths need to match the original
|
|
63
|
+
/// structure of the `ShapeLayer` data model, we track that info here.
|
|
64
|
+
let groupPath: [String]
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
override func setupAnimations(context: LayerAnimationContext) throws {
|
|
@@ -203,6 +208,13 @@ final class ShapeItemLayer: BaseAnimationLayer {
|
|
|
203
208
|
var trimPathMultiplier: PathMultiplier? = nil
|
|
204
209
|
if let (trim, context) = otherItems.first(Trim.self, context: context) {
|
|
205
210
|
trimPathMultiplier = try shapeLayer.addAnimations(for: trim, context: context)
|
|
211
|
+
|
|
212
|
+
try context.compatibilityAssert(
|
|
213
|
+
otherItems.first(Fill.self) == nil,
|
|
214
|
+
"""
|
|
215
|
+
The Core Animation rendering engine doesn't currently support applying
|
|
216
|
+
trims to filled shapes (only stroked shapes).
|
|
217
|
+
""")
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
try shapeLayer.addAnimations(for: shape.item, context: context.for(shape), pathMultiplier: trimPathMultiplier ?? 1)
|
|
@@ -227,6 +239,7 @@ final class ShapeItemLayer: BaseAnimationLayer {
|
|
|
227
239
|
pathMultiplier: 1)
|
|
228
240
|
|
|
229
241
|
if let (gradientFill, context) = otherItems.first(GradientFill.self, context: context) {
|
|
242
|
+
layers.shapeMaskLayer.fillRule = gradientFill.fillRule.caFillRule
|
|
230
243
|
try layers.gradientColorLayer.addGradientAnimations(for: gradientFill, type: .rgb, context: context)
|
|
231
244
|
try layers.gradientAlphaLayer?.addGradientAnimations(for: gradientFill, type: .alpha, context: context)
|
|
232
245
|
}
|
|
@@ -292,8 +305,8 @@ extension LayerAnimationContext {
|
|
|
292
305
|
func `for`(_ item: ShapeItemLayer.Item) -> LayerAnimationContext {
|
|
293
306
|
var context = self
|
|
294
307
|
|
|
295
|
-
|
|
296
|
-
context.currentKeypath.keys.append(
|
|
308
|
+
for parentGroupName in item.groupPath {
|
|
309
|
+
context.currentKeypath.keys.append(parentGroupName)
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
context.currentKeypath.keys.append(item.item.name)
|
|
@@ -13,7 +13,7 @@ final class ShapeLayer: BaseCompositionLayer {
|
|
|
13
13
|
init(shapeLayer: ShapeLayerModel, context: LayerContext) throws {
|
|
14
14
|
self.shapeLayer = shapeLayer
|
|
15
15
|
super.init(layerModel: shapeLayer)
|
|
16
|
-
try
|
|
16
|
+
try setUpGroups(context: context)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
required init?(coder _: NSCoder) {
|
|
@@ -35,6 +35,28 @@ final class ShapeLayer: BaseCompositionLayer {
|
|
|
35
35
|
|
|
36
36
|
private let shapeLayer: ShapeLayerModel
|
|
37
37
|
|
|
38
|
+
private func setUpGroups(context: LayerContext) throws {
|
|
39
|
+
// If the layer has a `Repeater`, the `Group`s are duplicated and offset
|
|
40
|
+
// based on the copy count of the repeater.
|
|
41
|
+
if let repeater = shapeLayer.items.first(where: { $0 is Repeater }) as? Repeater {
|
|
42
|
+
try setUpRepeater(repeater, context: context)
|
|
43
|
+
} else {
|
|
44
|
+
try setupGroups(from: shapeLayer.items, parentGroup: nil, parentGroupPath: [], context: context)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private func setUpRepeater(_ repeater: Repeater, context: LayerContext) throws {
|
|
49
|
+
let items = shapeLayer.items.filter { !($0 is Repeater) }
|
|
50
|
+
let copyCount = Int(try repeater.copies.exactlyOneKeyframe(context: context, description: "repeater copies").value)
|
|
51
|
+
|
|
52
|
+
for index in 0..<copyCount {
|
|
53
|
+
for groupLayer in try makeGroupLayers(from: items, parentGroup: nil, parentGroupPath: [], context: context) {
|
|
54
|
+
let repeatedLayer = RepeaterLayer(repeater: repeater, childLayer: groupLayer, index: index)
|
|
55
|
+
addSublayer(repeatedLayer)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
// MARK: - GroupLayer
|
|
@@ -44,9 +66,10 @@ final class GroupLayer: BaseAnimationLayer {
|
|
|
44
66
|
|
|
45
67
|
// MARK: Lifecycle
|
|
46
68
|
|
|
47
|
-
init(group: Group,
|
|
69
|
+
init(group: Group, items: [ShapeItemLayer.Item], groupPath: [String], context: LayerContext) throws {
|
|
48
70
|
self.group = group
|
|
49
|
-
self.
|
|
71
|
+
self.items = items
|
|
72
|
+
self.groupPath = groupPath
|
|
50
73
|
super.init()
|
|
51
74
|
try setupLayerHierarchy(context: context)
|
|
52
75
|
}
|
|
@@ -63,7 +86,8 @@ final class GroupLayer: BaseAnimationLayer {
|
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
group = typedLayer.group
|
|
66
|
-
|
|
89
|
+
items = typedLayer.items
|
|
90
|
+
groupPath = typedLayer.groupPath
|
|
67
91
|
super.init(layer: typedLayer)
|
|
68
92
|
}
|
|
69
93
|
|
|
@@ -82,52 +106,62 @@ final class GroupLayer: BaseAnimationLayer {
|
|
|
82
106
|
|
|
83
107
|
private let group: Group
|
|
84
108
|
|
|
85
|
-
/// `
|
|
86
|
-
///
|
|
87
|
-
private let
|
|
109
|
+
/// `ShapeItemLayer.Item`s rendered by this `Group`
|
|
110
|
+
/// - In the original `ShapeLayer` data model, these items could have originated from a different group
|
|
111
|
+
private let items: [ShapeItemLayer.Item]
|
|
112
|
+
|
|
113
|
+
/// The keypath that represents this group, with respect to the parent `ShapeLayer`
|
|
114
|
+
/// - Due to the way `GroupLayer`s are setup, the original `ShapeItem`
|
|
115
|
+
/// hierarchy from the `ShapeLayer` data model may no longer exactly
|
|
116
|
+
/// match the hierarchy of `GroupLayer` / `ShapeItemLayer`s constructed
|
|
117
|
+
/// at runtime. Since animation keypaths need to match the original
|
|
118
|
+
/// structure of the `ShapeLayer` data model, we track that info here.
|
|
119
|
+
private let groupPath: [String]
|
|
88
120
|
|
|
89
|
-
/// `ShapeItem`s (other than nested `Group`s) that are
|
|
90
|
-
private lazy var nonGroupItems =
|
|
91
|
-
.filter { !($0 is Group) }
|
|
92
|
-
.map { ShapeItemLayer.Item(item: $0, parentGroup: group) }
|
|
93
|
-
+ inheritedItems
|
|
121
|
+
/// `ShapeItem`s (other than nested `Group`s) that are rendered by this layer
|
|
122
|
+
private lazy var nonGroupItems = items.filter { !($0.item is Group) }
|
|
94
123
|
|
|
95
124
|
private func setupLayerHierarchy(context: LayerContext) throws {
|
|
96
125
|
// Groups can contain other groups, so we may have to continue
|
|
97
126
|
// recursively creating more `GroupLayer`s
|
|
98
|
-
try setupGroups(from: group.items, parentGroup: group, context: context)
|
|
127
|
+
try setupGroups(from: group.items, parentGroup: group, parentGroupPath: groupPath, context: context)
|
|
99
128
|
|
|
100
129
|
// Create `ShapeItemLayer`s for each subgroup of shapes that should be rendered as a single unit
|
|
101
130
|
// - These groups are listed from front-to-back, so we have to add the sublayers in reverse order
|
|
102
131
|
for shapeRenderGroup in nonGroupItems.shapeRenderGroups.reversed() {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// because combining multiple paths into a single `CGPath` (instead of rendering them in separate layers)
|
|
109
|
-
// allows `CAShapeLayerFillRule.evenOdd` to be applied if the paths overlap. We just can't do this
|
|
110
|
-
// in all cases, due to limitations of Core Animation.
|
|
111
|
-
//
|
|
112
|
-
// As a fall back when this is not possible, we render each shape in its own `CAShapeLayer`,
|
|
113
|
-
// which causes the `fillRule` to be applied incorrectly in cases where the paths overlap.
|
|
114
|
-
// We can't really detect when this happens, so this is a case where `RenderingEngineMode.automatic`
|
|
115
|
-
// can behave incorrectly. In the future we could fix this by precomputing the full combined CGPath for each
|
|
116
|
-
// individual frame in the animation (like we do for some trim animations as of #1612).
|
|
132
|
+
// When there are multiple path-drawing items, they're supposed to be rendered
|
|
133
|
+
// in a single `CAShapeLayer` (instead of rendering them in separate layers) so
|
|
134
|
+
// `CAShapeLayerFillRule.evenOdd` can be applied correctly if the paths overlap.
|
|
135
|
+
// Since a `CAShapeLayer` only supports animating a single `CGPath` from a single `KeyframeGroup<BezierPath>`,
|
|
136
|
+
// this requires combining all of the path-drawing items into a single set of keyframes.
|
|
117
137
|
if
|
|
118
138
|
shapeRenderGroup.pathItems.count > 1,
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
// We currently only support this codepath for `Shape` items that directly contain bezier path keyframes.
|
|
140
|
+
// We could also support this for other path types like rectangles, ellipses, and polygons with more work.
|
|
141
|
+
shapeRenderGroup.pathItems.allSatisfy({ $0.item is Shape }),
|
|
121
142
|
// `Trim`s are currently only applied correctly using individual `ShapeItemLayer`s,
|
|
122
143
|
// because each path has to be trimmed separately.
|
|
123
144
|
!shapeRenderGroup.otherItems.contains(where: { $0.item is Trim })
|
|
124
145
|
{
|
|
125
|
-
let
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
let allPathKeyframes = shapeRenderGroup.pathItems.compactMap { ($0.item as? Shape)?.path }
|
|
147
|
+
let combinedShape: CombinedShapeItem
|
|
148
|
+
|
|
149
|
+
// If all of the path-drawing `ShapeItem`s have keyframes with the same timing information,
|
|
150
|
+
// we can combine the `[KeyframeGroup<BezierPath>]` (which have to animate in separate layers)
|
|
151
|
+
// into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation.
|
|
152
|
+
if let combinedShapeKeyframes = Keyframes.combinedIfPossible(allPathKeyframes) {
|
|
153
|
+
combinedShape = CombinedShapeItem(shapes: combinedShapeKeyframes, name: group.name)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Otherwise, in order for the path fills to be rendered correctly, we have to manually
|
|
157
|
+
// interpolate the path for each shape at each frame ahead of time so we can combine them
|
|
158
|
+
// into a single set of bezier path keyframes.
|
|
159
|
+
else {
|
|
160
|
+
combinedShape = .manuallyInterpolating(shapes: allPathKeyframes, name: group.name, context: context)
|
|
161
|
+
}
|
|
128
162
|
|
|
129
163
|
let sublayer = try ShapeItemLayer(
|
|
130
|
-
shape: ShapeItemLayer.Item(item: combinedShape,
|
|
164
|
+
shape: ShapeItemLayer.Item(item: combinedShape, groupPath: shapeRenderGroup.pathItems[0].groupPath),
|
|
131
165
|
otherItems: shapeRenderGroup.otherItems,
|
|
132
166
|
context: context)
|
|
133
167
|
|
|
@@ -135,7 +169,8 @@ final class GroupLayer: BaseAnimationLayer {
|
|
|
135
169
|
}
|
|
136
170
|
|
|
137
171
|
// Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently,
|
|
138
|
-
// we have to create a separate `ShapeItemLayer` for each one.
|
|
172
|
+
// we have to create a separate `ShapeItemLayer` for each one. This may render
|
|
173
|
+
// incorrectly if there are multiple paths that overlap with each other.
|
|
139
174
|
else {
|
|
140
175
|
for pathDrawingItem in shapeRenderGroup.pathItems {
|
|
141
176
|
let sublayer = try ShapeItemLayer(
|
|
@@ -155,42 +190,78 @@ extension CALayer {
|
|
|
155
190
|
/// Sets up `GroupLayer`s for each `Group` in the given list of `ShapeItem`s
|
|
156
191
|
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
|
|
157
192
|
/// - Other `ShapeItem` are applied to all sublayers
|
|
158
|
-
fileprivate func setupGroups(
|
|
159
|
-
|
|
193
|
+
fileprivate func setupGroups(
|
|
194
|
+
from items: [ShapeItem],
|
|
195
|
+
parentGroup: Group?,
|
|
196
|
+
parentGroupPath: [String],
|
|
197
|
+
context: LayerContext) throws
|
|
198
|
+
{
|
|
199
|
+
let groupLayers = try makeGroupLayers(
|
|
200
|
+
from: items,
|
|
201
|
+
parentGroup: parentGroup,
|
|
202
|
+
parentGroupPath: parentGroupPath,
|
|
203
|
+
context: context)
|
|
204
|
+
|
|
205
|
+
for groupLayer in groupLayers {
|
|
206
|
+
addSublayer(groupLayer)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Creates a `GroupLayer` for each `Group` in the given list of `ShapeItem`s
|
|
211
|
+
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
|
|
212
|
+
/// - Other `ShapeItem` are applied to all sublayers
|
|
213
|
+
fileprivate func makeGroupLayers(
|
|
214
|
+
from items: [ShapeItem],
|
|
215
|
+
parentGroup: Group?,
|
|
216
|
+
parentGroupPath: [String],
|
|
217
|
+
context: LayerContext) throws
|
|
218
|
+
-> [GroupLayer]
|
|
219
|
+
{
|
|
220
|
+
var (groupItems, otherItems) = items
|
|
221
|
+
.filter { !$0.hidden }
|
|
222
|
+
.grouped(by: { $0 is Group })
|
|
160
223
|
|
|
161
224
|
// If this shape doesn't have any groups but just has top-level shape items,
|
|
162
225
|
// we can create a placeholder group with those items. (Otherwise the shape items
|
|
163
226
|
// would be silently ignored, since we expect all shape layers to have a top-level group).
|
|
164
227
|
if groupItems.isEmpty, parentGroup == nil {
|
|
165
|
-
groupItems = [Group(items: otherItems, name: "
|
|
228
|
+
groupItems = [Group(items: otherItems, name: "")]
|
|
166
229
|
otherItems = []
|
|
167
230
|
}
|
|
168
231
|
|
|
232
|
+
// `ShapeItem`s either draw a path, or modify how a path is rendered.
|
|
233
|
+
// - If this group doesn't have any items that draw a path, then its
|
|
234
|
+
// items are applied to all of this group's children.
|
|
235
|
+
let inheritedItemsForChildGroups: [ShapeItemLayer.Item]
|
|
236
|
+
if !otherItems.contains(where: { $0.drawsCGPath }) {
|
|
237
|
+
inheritedItemsForChildGroups = otherItems.map {
|
|
238
|
+
ShapeItemLayer.Item(item: $0, groupPath: parentGroupPath)
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
inheritedItemsForChildGroups = []
|
|
242
|
+
}
|
|
243
|
+
|
|
169
244
|
// Groups are listed from front to back,
|
|
170
245
|
// but `CALayer.sublayers` are listed from back to front.
|
|
171
246
|
let groupsInZAxisOrder = groupItems.reversed()
|
|
172
247
|
|
|
173
|
-
|
|
174
|
-
guard let group = group as? Group else {
|
|
248
|
+
return try groupsInZAxisOrder.compactMap { group in
|
|
249
|
+
guard let group = group as? Group else { return nil }
|
|
175
250
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
inheritedItems = []
|
|
251
|
+
var pathForChildren = parentGroupPath
|
|
252
|
+
if !group.name.isEmpty {
|
|
253
|
+
pathForChildren.append(group.name)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let childItems = group.items.map {
|
|
257
|
+
ShapeItemLayer.Item(item: $0, groupPath: pathForChildren)
|
|
186
258
|
}
|
|
187
259
|
|
|
188
|
-
|
|
260
|
+
return try GroupLayer(
|
|
189
261
|
group: group,
|
|
190
|
-
|
|
262
|
+
items: childItems + inheritedItemsForChildGroups,
|
|
263
|
+
groupPath: pathForChildren,
|
|
191
264
|
context: context)
|
|
192
|
-
|
|
193
|
-
addSublayer(groupLayer)
|
|
194
265
|
}
|
|
195
266
|
}
|
|
196
267
|
}
|
|
@@ -41,7 +41,7 @@ final class TextLayer: BaseCompositionLayer {
|
|
|
41
41
|
// Instead, we use the same `CoreTextRenderLayer` (with a custom `draw` implementation)
|
|
42
42
|
// used by the Main Thread rendering engine. This means the Core Animation engine can't
|
|
43
43
|
// _animate_ text properties, but it can display static text without any issues.
|
|
44
|
-
let text = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text")
|
|
44
|
+
let text = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text")
|
|
45
45
|
|
|
46
46
|
// The Core Animation engine doesn't currently support `TextAnimator`s.
|
|
47
47
|
// - We could add support for animating the transform-related properties without much trouble.
|
|
@@ -17,7 +17,7 @@ class CompositionLayer: CALayer, KeypathSearchable {
|
|
|
17
17
|
|
|
18
18
|
init(layer: LayerModel, size: CGSize) {
|
|
19
19
|
transformNode = LayerTransformNode(transform: layer.transform)
|
|
20
|
-
if let masks = layer.masks {
|
|
20
|
+
if let masks = layer.masks?.filter({ $0.mode != .none }), !masks.isEmpty {
|
|
21
21
|
maskLayer = MaskContainerLayer(masks: masks)
|
|
22
22
|
} else {
|
|
23
23
|
maskLayer = nil
|
|
@@ -151,6 +151,16 @@ final class CoreTextRenderLayer: CALayer {
|
|
|
151
151
|
strokeFrame = nil
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// This fixes a vertical padding issue that arises when drawing some fonts.
|
|
155
|
+
// For some reason some fonts, such as Helvetica draw with and ascender that is greater than the one reported by CTFontGetAscender.
|
|
156
|
+
// I suspect this is actually an issue with the Attributed string, but cannot reproduce.
|
|
157
|
+
|
|
158
|
+
if let fillFrame = fillFrame {
|
|
159
|
+
ctx.adjustWithLineOrigins(in: fillFrame, with: font)
|
|
160
|
+
} else if let strokeFrame = strokeFrame {
|
|
161
|
+
ctx.adjustWithLineOrigins(in: strokeFrame, with: font)
|
|
162
|
+
}
|
|
163
|
+
|
|
154
164
|
if !strokeOnTop, let strokeFrame = strokeFrame {
|
|
155
165
|
CTFrameDraw(strokeFrame, ctx)
|
|
156
166
|
}
|
|
@@ -318,3 +328,22 @@ final class CoreTextRenderLayer: CALayer {
|
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
}
|
|
331
|
+
|
|
332
|
+
extension CGContext {
|
|
333
|
+
|
|
334
|
+
fileprivate func adjustWithLineOrigins(in frame: CTFrame, with font: CTFont?) {
|
|
335
|
+
guard let font = font else { return }
|
|
336
|
+
|
|
337
|
+
let count = CFArrayGetCount(CTFrameGetLines(frame))
|
|
338
|
+
|
|
339
|
+
guard count > 0 else { return }
|
|
340
|
+
|
|
341
|
+
var o = [CGPoint](repeating: .zero, count: 1)
|
|
342
|
+
CTFrameGetLineOrigins(frame, CFRange(location: count - 1, length: 1), &o)
|
|
343
|
+
|
|
344
|
+
let diff = CTFontGetDescent(font) - o[0].y
|
|
345
|
+
if diff > 0 {
|
|
346
|
+
translateBy(x: 0, y: diff)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -79,6 +79,11 @@ extension Array where Element == ShapeItem {
|
|
|
79
79
|
/// Now add all child paths to current tree
|
|
80
80
|
nodeTree.paths.append(contentsOf: tree.paths)
|
|
81
81
|
nodeTree.renderContainers.append(node.container)
|
|
82
|
+
} else if item is Repeater {
|
|
83
|
+
LottieLogger.shared.assertionFailure("""
|
|
84
|
+
The Main Thread rendering engine doesn't currently support repeaters.
|
|
85
|
+
To play an animation with repeaters, you can use the Core Animation rendering engine instead.
|
|
86
|
+
""")
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
if let pathNode = nodeTree.rootNode as? PathNode {
|
|
@@ -198,6 +198,11 @@ final class GradientFillRenderer: PassThroughOutputNode, Renderable {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
var fillRule: CAShapeLayerFillRule {
|
|
202
|
+
get { maskLayer.fillRule }
|
|
203
|
+
set { maskLayer.fillRule = newValue }
|
|
204
|
+
}
|
|
205
|
+
|
|
201
206
|
func render(_: CGContext) {
|
|
202
207
|
// do nothing
|
|
203
208
|
}
|
package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift
CHANGED
|
@@ -22,6 +22,7 @@ final class GradientFillProperties: NodePropertyMap, KeypathSearchable {
|
|
|
22
22
|
colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.colors.keyframes))
|
|
23
23
|
gradientType = gradientfill.gradientType
|
|
24
24
|
numberOfColors = gradientfill.numberOfColors
|
|
25
|
+
fillRule = gradientfill.fillRule
|
|
25
26
|
keypathProperties = [
|
|
26
27
|
"Opacity" : opacity,
|
|
27
28
|
"Start Point" : startPoint,
|
|
@@ -42,6 +43,7 @@ final class GradientFillProperties: NodePropertyMap, KeypathSearchable {
|
|
|
42
43
|
|
|
43
44
|
let gradientType: GradientType
|
|
44
45
|
let numberOfColors: Int
|
|
46
|
+
let fillRule: FillRule
|
|
45
47
|
|
|
46
48
|
let keypathProperties: [String: AnyNodeProperty]
|
|
47
49
|
let properties: [AnyNodeProperty]
|
|
@@ -98,5 +100,6 @@ final class GradientFillNode: AnimatorNode, RenderNode {
|
|
|
98
100
|
fillRender.colors = fillProperties.colors.value.map { CGFloat($0) }
|
|
99
101
|
fillRender.type = fillProperties.gradientType
|
|
100
102
|
fillRender.numberOfColors = fillProperties.numberOfColors
|
|
103
|
+
fillRender.fillRule = fillProperties.fillRule.caFillRule
|
|
101
104
|
}
|
|
102
105
|
}
|
package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift
CHANGED
|
@@ -140,7 +140,7 @@ final class GradientStrokeNode: AnimatorNode, RenderNode {
|
|
|
140
140
|
|
|
141
141
|
/// Get dash lengths
|
|
142
142
|
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
|
|
143
|
-
if dashLengths.count > 0 {
|
|
143
|
+
if dashLengths.count > 0, !dashLengths.allSatisfy({ $0.isZero }) {
|
|
144
144
|
strokeRender.strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
|
|
145
145
|
strokeRender.strokeRender.dashLengths = dashLengths
|
|
146
146
|
} else {
|
|
@@ -118,7 +118,7 @@ final class StrokeNode: AnimatorNode, RenderNode {
|
|
|
118
118
|
|
|
119
119
|
/// Get dash lengths
|
|
120
120
|
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
|
|
121
|
-
if dashLengths.count > 0 {
|
|
121
|
+
if dashLengths.count > 0, !dashLengths.allSatisfy({ $0.isZero }) {
|
|
122
122
|
strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
|
|
123
123
|
strokeRender.dashLengths = dashLengths
|
|
124
124
|
} else {
|
|
@@ -148,6 +148,22 @@ extension Array where Element == DashElement {
|
|
|
148
148
|
dashPatterns.append(dash.value.keyframes)
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
dashPatterns = ContiguousArray(dashPatterns.map { pattern in
|
|
153
|
+
ContiguousArray(pattern.map { keyframe -> Keyframe<Vector1D> in
|
|
154
|
+
// The recommended way to create a stroke of round dots, in theory,
|
|
155
|
+
// is to use a value of 0 followed by the stroke width, but for
|
|
156
|
+
// some reason Core Animation incorrectly (?) renders these as pills
|
|
157
|
+
// instead of circles. As a workaround, for parity with Lottie on other
|
|
158
|
+
// platforms, we can change `0`s to `0.01`: https://stackoverflow.com/a/38036486
|
|
159
|
+
if keyframe.value.cgFloatValue == 0 {
|
|
160
|
+
return keyframe.withValue(Vector1D(0.01))
|
|
161
|
+
} else {
|
|
162
|
+
return keyframe
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
151
167
|
return (dashPatterns, dashPhase)
|
|
152
168
|
}
|
|
153
169
|
}
|
|
@@ -31,8 +31,8 @@ public final class Animation: Codable, DictionaryInitializable {
|
|
|
31
31
|
startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
|
|
32
32
|
endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
|
|
33
33
|
framerate = try container.decode(Double.self, forKey: .framerate)
|
|
34
|
-
width = try container.decode(
|
|
35
|
-
height = try container.decode(
|
|
34
|
+
width = try container.decode(Double.self, forKey: .width)
|
|
35
|
+
height = try container.decode(Double.self, forKey: .height)
|
|
36
36
|
layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
|
|
37
37
|
glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
|
|
38
38
|
fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
|
|
@@ -137,10 +137,10 @@ public final class Animation: Codable, DictionaryInitializable {
|
|
|
137
137
|
let type: CoordinateSpace
|
|
138
138
|
|
|
139
139
|
/// The height of the composition in points.
|
|
140
|
-
let width:
|
|
140
|
+
let width: Double
|
|
141
141
|
|
|
142
142
|
/// The width of the composition in points.
|
|
143
|
-
let height:
|
|
143
|
+
let height: Double
|
|
144
144
|
|
|
145
145
|
/// The list of animation layers
|
|
146
146
|
let layers: [LayerModel]
|
|
@@ -195,3 +195,28 @@ extension Keyframe {
|
|
|
195
195
|
spatialOutTangent: spatialOutTangent)
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
|
|
199
|
+
extension KeyframeGroup {
|
|
200
|
+
/// Maps the values of each individual keyframe in this group
|
|
201
|
+
func map<NewValue>(_ transformation: (T) throws -> NewValue) rethrows -> KeyframeGroup<NewValue> {
|
|
202
|
+
KeyframeGroup<NewValue>(keyframes: ContiguousArray(try keyframes.map { keyframe in
|
|
203
|
+
keyframe.withValue(try transformation(keyframe.value))
|
|
204
|
+
}))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// MARK: - AnyKeyframeGroup
|
|
209
|
+
|
|
210
|
+
/// A type-erased wrapper for `KeyframeGroup`s
|
|
211
|
+
protocol AnyKeyframeGroup {
|
|
212
|
+
var untyped: KeyframeGroup<Any> { get }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MARK: - KeyframeGroup + AnyKeyframeGroup
|
|
216
|
+
|
|
217
|
+
extension KeyframeGroup: AnyKeyframeGroup {
|
|
218
|
+
/// An untyped copy of these keyframes
|
|
219
|
+
var untyped: KeyframeGroup<Any> {
|
|
220
|
+
map { $0 as Any }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -17,7 +17,6 @@ enum FillRule: Int, Codable {
|
|
|
17
17
|
|
|
18
18
|
// MARK: - Fill
|
|
19
19
|
|
|
20
|
-
/// An item that defines a fill render
|
|
21
20
|
final class Fill: ShapeItem {
|
|
22
21
|
|
|
23
22
|
// MARK: Lifecycle
|
|
@@ -54,6 +53,7 @@ final class Fill: ShapeItem {
|
|
|
54
53
|
/// The color keyframes for the fill
|
|
55
54
|
let color: KeyframeGroup<Color>
|
|
56
55
|
|
|
56
|
+
/// The fill rule to use when filling a path
|
|
57
57
|
let fillRule: FillRule
|
|
58
58
|
|
|
59
59
|
override func encode(to encoder: Encoder) throws {
|