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.
Files changed (180) hide show
  1. package/.github/workflows/stale_issues.yml +17 -0
  2. package/.swiftpm/xcode/package.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  3. package/Lottie.xcodeproj/project.pbxproj +24 -16
  4. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (macOS).xcscheme +2 -2
  5. package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -81
  6. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  7. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -17
  8. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +114 -1
  9. package/Package.swift +1 -6
  10. package/Rakefile +41 -2
  11. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +1 -2
  12. package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +28 -0
  13. package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +31 -4
  14. package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +2 -2
  15. package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +34 -7
  16. package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +25 -14
  17. package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +61 -32
  18. package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +4 -1
  19. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +6 -1
  20. package/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift +2 -2
  21. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift +107 -26
  22. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +2 -1
  23. package/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift +48 -12
  24. package/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift +1 -1
  25. package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +4 -0
  26. package/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift +1 -1
  27. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +85 -0
  28. package/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift +17 -4
  29. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +124 -53
  30. package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +1 -1
  31. package/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift +1 -1
  32. package/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift +29 -0
  33. package/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift +1 -0
  34. package/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift +5 -0
  35. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +5 -0
  36. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +3 -0
  37. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
  38. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +17 -1
  39. package/Sources/Private/Model/Animation.swift +4 -4
  40. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +25 -0
  41. package/Sources/Private/Model/ShapeItems/Ellipse.swift +0 -1
  42. package/Sources/Private/Model/ShapeItems/Fill.swift +1 -1
  43. package/Sources/Private/Model/ShapeItems/GradientFill.swift +14 -1
  44. package/Sources/Private/Model/ShapeItems/GradientStroke.swift +0 -1
  45. package/Sources/Private/Model/ShapeItems/Merge.swift +0 -1
  46. package/Sources/Private/Model/ShapeItems/Rectangle.swift +0 -1
  47. package/Sources/Private/Model/ShapeItems/Repeater.swift +0 -1
  48. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -1
  49. package/Sources/Private/Model/ShapeItems/Star.swift +0 -1
  50. package/Sources/Private/Model/ShapeItems/Stroke.swift +0 -1
  51. package/Sources/Private/Model/ShapeItems/Trim.swift +0 -1
  52. package/Sources/Private/{MainThread/NodeRenderSystem/NodeProperties/ValueProviders → Utility/Interpolatable}/KeyframeInterpolator.swift +0 -0
  53. package/Sources/Public/Animation/AnimationView.swift +1 -1
  54. package/Sources/Public/iOS/BundleImageProvider.swift +2 -2
  55. package/Sources/Public/iOS/FilepathImageProvider.swift +1 -1
  56. package/Sources/Public/macOS/BundleImageProvider.macOS.swift +1 -1
  57. package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +1 -1
  58. package/Tests/AnimationKeypathTests.swift +10 -1
  59. package/Tests/PerformanceTests.swift +19 -20
  60. package/Tests/Samples/9squares_AlBoardman.json +1 -0
  61. package/Tests/Samples/Boat_Loader.json +1 -0
  62. package/Tests/Samples/HamburgerArrow.json +1 -0
  63. package/Tests/Samples/IconTransitions.json +1 -0
  64. package/Tests/Samples/Images/dog.png +0 -0
  65. package/Tests/Samples/Issues/issue_1125.json +1 -0
  66. package/Tests/Samples/Issues/issue_1260.json +1 -0
  67. package/Tests/Samples/Issues/issue_1403.json +1 -0
  68. package/Tests/Samples/Issues/issue_1407.json +1 -0
  69. package/Tests/Samples/Issues/issue_1460.json +1 -0
  70. package/Tests/Samples/Issues/issue_1488.json +1 -0
  71. package/Tests/Samples/Issues/issue_1505.json +1 -0
  72. package/Tests/Samples/Issues/issue_1541.json +1 -0
  73. package/Tests/Samples/Issues/issue_1557.json +1 -0
  74. package/Tests/Samples/Issues/issue_1603.json +1 -0
  75. package/Tests/Samples/Issues/issue_1628.json +1 -0
  76. package/Tests/Samples/Issues/issue_1636.json +1 -0
  77. package/Tests/Samples/Issues/issue_1643.json +1 -0
  78. package/Tests/Samples/Issues/issue_1655.json +1 -0
  79. package/Tests/Samples/Issues/issue_1664.json +1 -0
  80. package/Tests/Samples/Issues/issue_1683.json +1 -0
  81. package/Tests/Samples/Issues/issue_1687.json +1 -0
  82. package/Tests/Samples/Issues/issue_1711.json +1 -0
  83. package/Tests/Samples/Issues/issue_1717.json +1 -0
  84. package/Tests/Samples/Issues/issue_769.json +1 -0
  85. package/Tests/Samples/Issues/issue_885.json +1 -0
  86. package/Tests/Samples/Issues/issue_965.json +1 -0
  87. package/Tests/Samples/Issues/pr_1536.json +1 -0
  88. package/Tests/Samples/Issues/pr_1563.json +8439 -0
  89. package/Tests/Samples/Issues/pr_1592.json +5527 -0
  90. package/Tests/Samples/Issues/pr_1599.json +738 -0
  91. package/Tests/Samples/Issues/pr_1604_1.json +1 -0
  92. package/Tests/Samples/Issues/pr_1604_2.json +1 -0
  93. package/Tests/Samples/Issues/pr_1632_1.json +1 -0
  94. package/Tests/Samples/Issues/pr_1632_2.json +1 -0
  95. package/Tests/Samples/Issues/pr_1686.json +513 -0
  96. package/Tests/Samples/Issues/pr_1698.json +1 -0
  97. package/Tests/Samples/Issues/pr_1699.json +1 -0
  98. package/Tests/Samples/LottieFiles/LICENSE.md +14 -0
  99. package/Tests/Samples/LottieFiles/bounce_strokes.json +1 -0
  100. package/Tests/Samples/LottieFiles/cactus.json +1 -0
  101. package/Tests/Samples/LottieFiles/dog_car_ride.json +1 -0
  102. package/Tests/Samples/LottieFiles/draft_icon.json +1 -0
  103. package/Tests/Samples/LottieFiles/fireworks.json +1 -0
  104. package/Tests/Samples/LottieFiles/gradient_1.json +1 -0
  105. package/Tests/Samples/LottieFiles/gradient_2.json +1 -0
  106. package/Tests/Samples/LottieFiles/gradient_pill.json +1 -0
  107. package/Tests/Samples/LottieFiles/gradient_shapes.json +1 -0
  108. package/Tests/Samples/LottieFiles/gradient_square.json +1 -0
  109. package/Tests/Samples/LottieFiles/growth.json +1 -0
  110. package/Tests/Samples/LottieFiles/infinity_loader.json +1 -0
  111. package/Tests/Samples/LottieFiles/loading_dots_1.json +1 -0
  112. package/Tests/Samples/LottieFiles/loading_dots_2.json +1 -0
  113. package/Tests/Samples/LottieFiles/loading_dots_3.json +1 -0
  114. package/Tests/Samples/LottieFiles/loading_gradient_strokes.json +1 -0
  115. package/Tests/Samples/LottieFiles/settings_slider.json +1 -0
  116. package/Tests/Samples/LottieFiles/shop.json +1 -0
  117. package/Tests/Samples/LottieFiles/step_loader.json +1 -0
  118. package/Tests/Samples/LottieLogo1.json +1 -0
  119. package/Tests/Samples/LottieLogo1_masked.json +1 -0
  120. package/Tests/Samples/LottieLogo2.json +1 -0
  121. package/Tests/Samples/MotionCorpse_Jrcanest.json +1 -0
  122. package/Tests/Samples/Nonanimating/BasicLayers.json +1 -0
  123. package/Tests/Samples/Nonanimating/DisableNodesTest.json +1 -0
  124. package/Tests/Samples/Nonanimating/FirstText.json +1 -0
  125. package/Tests/Samples/Nonanimating/GeometryTransformTest.json +1 -0
  126. package/Tests/Samples/Nonanimating/Text_AnimatedProperties.json +1 -0
  127. package/Tests/Samples/Nonanimating/Text_Glyph.json +1 -0
  128. package/Tests/Samples/Nonanimating/Text_NoAnimation.json +1 -0
  129. package/Tests/Samples/Nonanimating/Text_NoGlyph.json +1 -0
  130. package/Tests/Samples/Nonanimating/Zoom.json +1 -0
  131. package/Tests/Samples/Nonanimating/_dog.json +1 -0
  132. package/Tests/Samples/Nonanimating/base64Test.json +1 -0
  133. package/Tests/Samples/Nonanimating/blend_mode_test.json +1 -0
  134. package/Tests/Samples/Nonanimating/keypathTest.json +1 -0
  135. package/Tests/Samples/Nonanimating/verifyLineHeight.json +1 -0
  136. package/Tests/Samples/PinJump.json +1 -0
  137. package/Tests/Samples/Switch.json +1 -0
  138. package/Tests/Samples/Switch_States.json +1 -0
  139. package/Tests/Samples/TwitterHeart.json +1 -0
  140. package/Tests/Samples/TwitterHeartButton.json +1 -0
  141. package/Tests/Samples/TypeFace/A.json +1 -0
  142. package/Tests/Samples/TypeFace/Apostrophe.json +1 -0
  143. package/Tests/Samples/TypeFace/B.json +1 -0
  144. package/Tests/Samples/TypeFace/BlinkingCursor.json +1 -0
  145. package/Tests/Samples/TypeFace/C.json +1 -0
  146. package/Tests/Samples/TypeFace/Colon.json +1 -0
  147. package/Tests/Samples/TypeFace/Comma.json +1 -0
  148. package/Tests/Samples/TypeFace/D.json +1 -0
  149. package/Tests/Samples/TypeFace/E.json +1 -0
  150. package/Tests/Samples/TypeFace/F.json +1 -0
  151. package/Tests/Samples/TypeFace/G.json +1 -0
  152. package/Tests/Samples/TypeFace/H.json +1 -0
  153. package/Tests/Samples/TypeFace/I.json +1 -0
  154. package/Tests/Samples/TypeFace/J.json +1 -0
  155. package/Tests/Samples/TypeFace/K.json +1 -0
  156. package/Tests/Samples/TypeFace/L.json +1 -0
  157. package/Tests/Samples/TypeFace/M.json +1 -0
  158. package/Tests/Samples/TypeFace/N.json +1 -0
  159. package/Tests/Samples/TypeFace/O.json +1 -0
  160. package/Tests/Samples/TypeFace/P.json +1 -0
  161. package/Tests/Samples/TypeFace/Q.json +1 -0
  162. package/Tests/Samples/TypeFace/R.json +1 -0
  163. package/Tests/Samples/TypeFace/S.json +1 -0
  164. package/Tests/Samples/TypeFace/T.json +1 -0
  165. package/Tests/Samples/TypeFace/U.json +1 -0
  166. package/Tests/Samples/TypeFace/V.json +1 -0
  167. package/Tests/Samples/TypeFace/W.json +1 -0
  168. package/Tests/Samples/TypeFace/X.json +1 -0
  169. package/Tests/Samples/TypeFace/Y.json +1 -0
  170. package/Tests/Samples/TypeFace/Z.json +1 -0
  171. package/Tests/Samples/Watermelon.json +1 -0
  172. package/Tests/Samples/setValueTest.json +1 -0
  173. package/Tests/Samples/timeremap.json +1 -0
  174. package/Tests/Samples/vcTransition1.json +1 -0
  175. package/Tests/Samples/vcTransition2.json +1 -0
  176. package/Tests/SnapshotConfiguration.swift +5 -0
  177. package/lottie-ios.podspec +2 -1
  178. package/package.json +1 -1
  179. package/.swift-version +0 -1
  180. 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 group that contains this `ShapeItem`, if applicable
59
- let parentGroup: Group?
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
- if let group = item.parentGroup {
296
- context.currentKeypath.keys.append(group.name)
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 setupGroups(from: shapeLayer.items, parentGroup: nil, context: context)
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, inheritedItems: [ShapeItemLayer.Item], context: LayerContext) throws {
69
+ init(group: Group, items: [ShapeItemLayer.Item], groupPath: [String], context: LayerContext) throws {
48
70
  self.group = group
49
- self.inheritedItems = inheritedItems
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
- inheritedItems = typedLayer.inheritedItems
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
- /// `ShapeItem`s that were listed in the parent's `items: [ShapeItem]` array
86
- /// - This layer's parent is either the root `ShapeLayerModel` or some other `Group`
87
- private let inheritedItems: [ShapeItemLayer.Item]
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 included in this group
90
- private lazy var nonGroupItems = group.items
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
- // If all of the path-drawing `ShapeItem`s have keyframes with the same timing information,
104
- // we can combine the `[KeyframeGroup<BezierPath>]` (which have to animate in separate layers)
105
- // into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation.
106
- //
107
- // This is how Groups with multiple path-drawing items are supposed to be rendered,
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
- let combinedShapeKeyframes = Keyframes.combinedIfPossible(
120
- shapeRenderGroup.pathItems.map { ($0.item as? Shape)?.path }),
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 combinedShape = CombinedShapeItem(
126
- shapes: combinedShapeKeyframes,
127
- name: group.name)
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, parentGroup: group),
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(from items: [ShapeItem], parentGroup: Group?, context: LayerContext) throws {
159
- var (groupItems, otherItems) = items.grouped(by: { $0 is Group })
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: "Group")]
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
- for group in groupsInZAxisOrder {
174
- guard let group = group as? Group else { continue }
248
+ return try groupsInZAxisOrder.compactMap { group in
249
+ guard let group = group as? Group else { return nil }
175
250
 
176
- // `ShapeItem`s either draw a path, or modify how a path is rendered.
177
- // - If this group doesn't have any items that draw a path, then its
178
- // items are applied to all of this groups children.
179
- let inheritedItems: [ShapeItemLayer.Item]
180
- if !otherItems.contains(where: { $0.drawsCGPath }) {
181
- inheritedItems = otherItems.map {
182
- ShapeItemLayer.Item(item: $0, parentGroup: parentGroup)
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
- let groupLayer = try GroupLayer(
260
+ return try GroupLayer(
189
261
  group: group,
190
- inheritedItems: inheritedItems,
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").value
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
+ }
@@ -42,6 +42,7 @@ final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
42
42
  let wrapperLayer = CALayer()
43
43
 
44
44
  func frameUpdated(frame _: CGFloat) {
45
+ setNeedsDisplay()
45
46
  displayIfNeeded()
46
47
  }
47
48
 
@@ -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
  }
@@ -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
  }
@@ -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(Int.self, forKey: .width)
35
- height = try container.decode(Int.self, forKey: .height)
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: Int
140
+ let width: Double
141
141
 
142
142
  /// The width of the composition in points.
143
- let height: Int
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 PathDirection: Int, Codable {
17
17
 
18
18
  // MARK: - Ellipse
19
19
 
20
- /// An item that define an ellipse shape
21
20
  final class Ellipse: ShapeItem {
22
21
 
23
22
  // MARK: Lifecycle
@@ -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 {