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
@@ -104,10 +104,10 @@ extension GradientRenderLayer {
104
104
  // at any given time requires knowing the current `startPoint`,
105
105
  // we can't allow them to animate separately.
106
106
  let absoluteStartPoint = try gradient.startPoint
107
- .exactlyOneKeyframe(context: context, description: "gradient startPoint").value.pointValue
107
+ .exactlyOneKeyframe(context: context, description: "gradient startPoint").pointValue
108
108
 
109
109
  let absoluteEndPoint = try gradient.endPoint
110
- .exactlyOneKeyframe(context: context, description: "gradient endPoint").value.pointValue
110
+ .exactlyOneKeyframe(context: context, description: "gradient endPoint").pointValue
111
111
 
112
112
  startPoint = percentBasedPointInBounds(from: absoluteStartPoint)
113
113
 
@@ -14,14 +14,12 @@ extension CAShapeLayer {
14
14
  {
15
15
  try addAnimation(
16
16
  for: .path,
17
- keyframes: rectangle.size.keyframes,
18
- value: { sizeKeyframe in
17
+ keyframes: try rectangle.combinedKeyframes(context: context).keyframes,
18
+ value: { keyframe in
19
19
  BezierPath.rectangle(
20
- position: try rectangle.position
21
- .exactlyOneKeyframe(context: context, description: "rectangle position").value.pointValue,
22
- size: sizeKeyframe.sizeValue,
23
- cornerRadius: try rectangle.cornerRadius
24
- .exactlyOneKeyframe(context: context, description: "rectangle cornerRadius").value.cgFloatValue,
20
+ position: keyframe.position.pointValue,
21
+ size: keyframe.size.sizeValue,
22
+ cornerRadius: keyframe.cornerRadius.cgFloatValue,
25
23
  direction: rectangle.direction)
26
24
  .cgPath()
27
25
  .duplicated(times: pathMultiplier)
@@ -29,3 +27,32 @@ extension CAShapeLayer {
29
27
  context: context)
30
28
  }
31
29
  }
30
+
31
+ extension Rectangle {
32
+ /// Data that represents how to render a rectangle at a specific point in time
33
+ struct Keyframe {
34
+ let size: Vector3D
35
+ let position: Vector3D
36
+ let cornerRadius: Vector1D
37
+ }
38
+
39
+ /// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Rectangle
40
+ func combinedKeyframes(context: LayerAnimationContext) throws-> KeyframeGroup<Rectangle.Keyframe> {
41
+ let combinedKeyframes = Keyframes.combinedIfPossible(
42
+ size, position, cornerRadius,
43
+ makeCombinedResult: Rectangle.Keyframe.init)
44
+
45
+ if let combinedKeyframes = combinedKeyframes {
46
+ return combinedKeyframes
47
+ } else {
48
+ // If we weren't able to combine all of the keyframes, we have to take the timing values
49
+ // from one property and use a fixed value for the other properties.
50
+ return try size.map { sizeValue in
51
+ Keyframe(
52
+ size: sizeValue,
53
+ position: try position.exactlyOneKeyframe(context: context, description: "rectangle position"),
54
+ cornerRadius: try cornerRadius.exactlyOneKeyframe(context: context, description: "rectangle cornerRadius"))
55
+ }
56
+ }
57
+ }
58
+ }
@@ -120,25 +120,36 @@ extension Trim {
120
120
  let interpolatedStrokeEnd = strokeEnd.manuallyInterpolateKeyframes()
121
121
  let interpolatedStrokeOffset = offset.manuallyInterpolateKeyframes()
122
122
 
123
- let adjustedStrokeStart = try adjustKeyframesForTrimOffsets(
124
- strokeKeyframes: interpolatedStrokeStart,
125
- offsetKeyframes: interpolatedStrokeOffset,
126
- context: context)
127
-
128
- let adjustedStrokeEnd = try adjustKeyframesForTrimOffsets(
129
- strokeKeyframes: interpolatedStrokeEnd,
130
- offsetKeyframes: interpolatedStrokeOffset,
131
- context: context)
123
+ var adjustedStrokeStart = KeyframeGroup(
124
+ keyframes: try adjustKeyframesForTrimOffsets(
125
+ strokeKeyframes: interpolatedStrokeStart,
126
+ offsetKeyframes: interpolatedStrokeOffset,
127
+ context: context))
128
+
129
+ var adjustedStrokeEnd = KeyframeGroup(
130
+ keyframes: try adjustKeyframesForTrimOffsets(
131
+ strokeKeyframes: interpolatedStrokeEnd,
132
+ offsetKeyframes: interpolatedStrokeOffset,
133
+ context: context))
132
134
 
133
135
  // If maximum stroke value is larger than 100%, then we have to create copies of the path
134
136
  // so the total path length includes the maximum stroke
135
- let maximumStroke = adjustedStrokeEnd.map { $0.value.cgFloatValue }.max() ?? 100
136
- let pathMultiplier = Int(ceil(maximumStroke / 100.0))
137
+ let startStrokes = adjustedStrokeStart.keyframes.map { $0.value.cgFloatValue }
138
+ let endStrokes = adjustedStrokeEnd.keyframes.map { $0.value.cgFloatValue }
139
+ let minimumStrokeMultiplier = Double(floor((startStrokes.min() ?? 0) / 100.0))
140
+ let maximumStrokeMultiplier = Double(ceil((endStrokes.max() ?? 100) / 100.0))
141
+
142
+ if minimumStrokeMultiplier < 0 {
143
+ // Core Animation doesn't support negative stroke offsets, so we have to
144
+ // shift all of the offset values up by the minimum
145
+ adjustedStrokeStart = adjustedStrokeStart.map { Vector1D($0.value + (abs(minimumStrokeMultiplier) * 100.0)) }
146
+ adjustedStrokeEnd = adjustedStrokeEnd.map { Vector1D($0.value + (abs(minimumStrokeMultiplier) * 100.0)) }
147
+ }
137
148
 
138
149
  return (
139
- strokeStart: KeyframeGroup<Vector1D>(keyframes: adjustedStrokeStart),
140
- strokeEnd: KeyframeGroup<Vector1D>(keyframes: adjustedStrokeEnd),
141
- pathMultiplier: pathMultiplier)
150
+ strokeStart: adjustedStrokeStart,
151
+ strokeEnd: adjustedStrokeEnd,
152
+ pathMultiplier: Int(abs(maximumStrokeMultiplier) + abs(minimumStrokeMultiplier)))
142
153
  }
143
154
 
144
155
  // MARK: Private
@@ -36,25 +36,16 @@ extension CAShapeLayer {
36
36
  {
37
37
  try addAnimation(
38
38
  for: .path,
39
- keyframes: star.position.keyframes,
40
- value: { position in
41
- // We can only use one set of keyframes to animate a given CALayer keypath,
42
- // so we currently animate `position` and ignore any other keyframes.
43
- // TODO: Is there a way to support this properly?
39
+ keyframes: try star.combinedKeyframes(context: context).keyframes,
40
+ value: { keyframe in
44
41
  BezierPath.star(
45
- position: position.pointValue,
46
- outerRadius: try star.outerRadius
47
- .exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
48
- innerRadius: try star.innerRadius?
49
- .exactlyOneKeyframe(context: context, description: "innerRadius").value.cgFloatValue ?? 0,
50
- outerRoundedness: try star.outerRoundness
51
- .exactlyOneKeyframe(context: context, description: "outerRoundness").value.cgFloatValue,
52
- innerRoundedness: try star.innerRoundness?
53
- .exactlyOneKeyframe(context: context, description: "innerRoundness").value.cgFloatValue ?? 0,
54
- numberOfPoints: try star.points
55
- .exactlyOneKeyframe(context: context, description: "points").value.cgFloatValue,
56
- rotation: try star.rotation
57
- .exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
42
+ position: keyframe.position.pointValue,
43
+ outerRadius: keyframe.outerRadius.cgFloatValue,
44
+ innerRadius: keyframe.innerRadius.cgFloatValue,
45
+ outerRoundedness: keyframe.outerRoundness.cgFloatValue,
46
+ innerRoundedness: keyframe.innerRoundness.cgFloatValue,
47
+ numberOfPoints: keyframe.points.cgFloatValue,
48
+ rotation: keyframe.rotation.cgFloatValue,
58
49
  direction: star.direction)
59
50
  .cgPath()
60
51
  .duplicated(times: pathMultiplier)
@@ -71,21 +62,14 @@ extension CAShapeLayer {
71
62
  {
72
63
  try addAnimation(
73
64
  for: .path,
74
- keyframes: star.position.keyframes,
75
- value: { position in
76
- // We can only use one set of keyframes to animate a given CALayer keypath,
77
- // so we currently animate `position` and ignore any other keyframes.
78
- // TODO: Is there a way to support this properly?
65
+ keyframes: try star.combinedKeyframes(context: context).keyframes,
66
+ value: { keyframe in
79
67
  BezierPath.polygon(
80
- position: position.pointValue,
81
- numberOfPoints: try star.points
82
- .exactlyOneKeyframe(context: context, description: "numberOfPoints").value.cgFloatValue,
83
- outerRadius: try star.outerRadius
84
- .exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
85
- outerRoundedness: try star.outerRoundness
86
- .exactlyOneKeyframe(context: context, description: "outerRoundedness").value.cgFloatValue,
87
- rotation: try star.rotation
88
- .exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
68
+ position: keyframe.position.pointValue,
69
+ numberOfPoints: keyframe.points.cgFloatValue,
70
+ outerRadius: keyframe.outerRadius.cgFloatValue,
71
+ outerRoundedness: keyframe.outerRoundness.cgFloatValue,
72
+ rotation: keyframe.rotation.cgFloatValue,
89
73
  direction: star.direction)
90
74
  .cgPath()
91
75
  .duplicated(times: pathMultiplier)
@@ -93,3 +77,48 @@ extension CAShapeLayer {
93
77
  context: context)
94
78
  }
95
79
  }
80
+
81
+ extension Star {
82
+ /// Data that represents how to render a star at a specific point in time
83
+ struct Keyframe {
84
+ let position: Vector3D
85
+ let outerRadius: Vector1D
86
+ let innerRadius: Vector1D
87
+ let outerRoundness: Vector1D
88
+ let innerRoundness: Vector1D
89
+ let points: Vector1D
90
+ let rotation: Vector1D
91
+ }
92
+
93
+ /// Creates a single array of animatable keyframes from the separate arrays of keyframes in this star/polygon
94
+ func combinedKeyframes(context: LayerAnimationContext) throws -> KeyframeGroup<Keyframe> {
95
+ let combinedKeyframes = Keyframes.combinedIfPossible(
96
+ position,
97
+ outerRadius,
98
+ innerRadius ?? KeyframeGroup(Vector1D(0)),
99
+ outerRoundness,
100
+ innerRoundness ?? KeyframeGroup(Vector1D(0)),
101
+ points,
102
+ rotation,
103
+ makeCombinedResult: Star.Keyframe.init)
104
+
105
+ if let combinedKeyframes = combinedKeyframes {
106
+ return combinedKeyframes
107
+ } else {
108
+ // If we weren't able to combine all of the keyframes, we have to take the timing values
109
+ // from one property and use a fixed value for the other properties.
110
+ return try position.map { positionValue in
111
+ Keyframe(
112
+ position: positionValue,
113
+ outerRadius: try outerRadius.exactlyOneKeyframe(context: context, description: "star outerRadius"),
114
+ innerRadius: try innerRadius?.exactlyOneKeyframe(context: context, description: "star innerRadius")
115
+ ?? Vector1D(0),
116
+ outerRoundness: try outerRoundness.exactlyOneKeyframe(context: context, description: "star outerRoundness"),
117
+ innerRoundness: try innerRoundness?.exactlyOneKeyframe(context: context, description: "star innerRoundness")
118
+ ?? Vector1D(0),
119
+ points: try points.exactlyOneKeyframe(context: context, description: "star points"),
120
+ rotation: try rotation.exactlyOneKeyframe(context: context, description: "star rotation"))
121
+ }
122
+ }
123
+ }
124
+ }
@@ -57,7 +57,10 @@ extension CAShapeLayer {
57
57
  if let (dashPattern, dashPhase) = stroke.dashPattern?.shapeLayerConfiguration {
58
58
  lineDashPattern = try dashPattern.map {
59
59
  try KeyframeGroup(keyframes: $0)
60
- .exactlyOneKeyframe(context: context, description: "stroke dashPattern").value.cgFloatValue as NSNumber
60
+ .exactlyOneKeyframe(context: context, description: "stroke dashPattern").cgFloatValue as NSNumber
61
+ }
62
+ if lineDashPattern?.allSatisfy({ $0.floatValue.isZero }) == true {
63
+ lineDashPattern = nil
61
64
  }
62
65
 
63
66
  try addAnimation(
@@ -264,6 +264,11 @@ final class CoreAnimationLayer: BaseAnimationLayer {
264
264
 
265
265
  let timedProgressAnimation = animationProgressTracker.timed(with: context, for: self)
266
266
  timedProgressAnimation.delegate = currentAnimationConfiguration?.animationContext.closure
267
+
268
+ // Remove the progress animation once complete so we know when the animation
269
+ // has finished playing (if it doesn't loop infinitely)
270
+ timedProgressAnimation.isRemovedOnCompletion = true
271
+
267
272
  add(timedProgressAnimation, forKey: #keyPath(animationProgress))
268
273
  }
269
274
 
@@ -311,7 +316,7 @@ extension CoreAnimationLayer: RootAnimationLayer {
311
316
  var isAnimationPlaying: Bool? {
312
317
  switch playbackState {
313
318
  case .playing:
314
- return true
319
+ return animation(forKey: #keyPath(animationProgress)) != nil
315
320
  case nil, .paused:
316
321
  return false
317
322
  }
@@ -23,7 +23,7 @@ extension KeyframeGroup {
23
23
  fileID _: StaticString = #fileID,
24
24
  line _: UInt = #line)
25
25
  throws
26
- -> Keyframe<T>
26
+ -> T
27
27
  {
28
28
  try context.compatibilityAssert(
29
29
  keyframes.count == 1,
@@ -32,6 +32,6 @@ extension KeyframeGroup {
32
32
  for \(description) values (due to limitations of Core Animation `CAKeyframeAnimation`s).
33
33
  """)
34
34
 
35
- return keyframes[0]
35
+ return keyframes[0].value
36
36
  }
37
37
  }
@@ -4,46 +4,113 @@
4
4
  // MARK: - Keyframes
5
5
 
6
6
  enum Keyframes {
7
- /// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s
8
- /// into a single `KeyframeGroup` of `Keyframe<[T]>`s
9
- /// if all of the `KeyframeGroup`s have the exact same animation timing
7
+
8
+ // MARK: Internal
9
+
10
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of
11
+ /// of `Keyframe<[T]>`s if all of the `KeyframeGroup`s have the exact same animation timing
10
12
  static func combinedIfPossible<T>(_ allGroups: [KeyframeGroup<T>]) -> KeyframeGroup<[T]>? {
13
+ combinedIfPossible(allGroups, makeCombinedResult: { index in
14
+ allGroups.map { $0.valueForCombinedKeyframes(at: index) }
15
+ })
16
+ }
17
+
18
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of
19
+ /// of `Keyframe<[T]>`s if all of the `KeyframeGroup`s have the exact same animation timing
20
+ static func combinedIfPossible<T1, T2, CombinedResult>(
21
+ _ k1: KeyframeGroup<T1>,
22
+ _ k2: KeyframeGroup<T2>,
23
+ makeCombinedResult: (T1, T2) -> CombinedResult)
24
+ -> KeyframeGroup<CombinedResult>?
25
+ {
26
+ combinedIfPossible(
27
+ [k1, k2],
28
+ makeCombinedResult: { index in
29
+ makeCombinedResult(
30
+ k1.valueForCombinedKeyframes(at: index),
31
+ k2.valueForCombinedKeyframes(at: index))
32
+ })
33
+ }
34
+
35
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of
36
+ /// of `Keyframe<[T]>`s if all of the `KeyframeGroup`s have the exact same animation timing
37
+ static func combinedIfPossible<T1, T2, T3, CombinedResult>(
38
+ _ k1: KeyframeGroup<T1>,
39
+ _ k2: KeyframeGroup<T2>,
40
+ _ k3: KeyframeGroup<T3>,
41
+ makeCombinedResult: (T1, T2, T3) -> CombinedResult)
42
+ -> KeyframeGroup<CombinedResult>?
43
+ {
44
+ combinedIfPossible(
45
+ [k1, k2, k3],
46
+ makeCombinedResult: { index in
47
+ makeCombinedResult(
48
+ k1.valueForCombinedKeyframes(at: index),
49
+ k2.valueForCombinedKeyframes(at: index),
50
+ k3.valueForCombinedKeyframes(at: index))
51
+ })
52
+ }
53
+
54
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of
55
+ /// of `Keyframe<[T]>`s if all of the `KeyframeGroup`s have the exact same animation timing
56
+ static func combinedIfPossible<T1, T2, T3, T4, T5, T6, T7, CombinedResult>(
57
+ _ k1: KeyframeGroup<T1>,
58
+ _ k2: KeyframeGroup<T2>,
59
+ _ k3: KeyframeGroup<T3>,
60
+ _ k4: KeyframeGroup<T4>,
61
+ _ k5: KeyframeGroup<T5>,
62
+ _ k6: KeyframeGroup<T6>,
63
+ _ k7: KeyframeGroup<T7>,
64
+ makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7) -> CombinedResult)
65
+ -> KeyframeGroup<CombinedResult>?
66
+ {
67
+ combinedIfPossible(
68
+ [k1, k2, k3, k4, k5, k6, k7],
69
+ makeCombinedResult: { index in
70
+ makeCombinedResult(
71
+ k1.valueForCombinedKeyframes(at: index),
72
+ k2.valueForCombinedKeyframes(at: index),
73
+ k3.valueForCombinedKeyframes(at: index),
74
+ k4.valueForCombinedKeyframes(at: index),
75
+ k5.valueForCombinedKeyframes(at: index),
76
+ k6.valueForCombinedKeyframes(at: index),
77
+ k7.valueForCombinedKeyframes(at: index))
78
+ })
79
+ }
80
+
81
+ // MARK: Private
82
+
83
+ /// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s into a single `KeyframeGroup`
84
+ /// of `Keyframe<CombinedResult>`s if all of the `KeyframeGroup`s have the exact same animation timing
85
+ private static func combinedIfPossible<CombinedResult>(
86
+ _ allGroups: [AnyKeyframeGroup],
87
+ makeCombinedResult: (_ index: Int) -> CombinedResult)
88
+ -> KeyframeGroup<CombinedResult>?
89
+ {
90
+ let untypedGroups = allGroups.map { $0.untyped }
91
+
11
92
  // Animations with no timing information (e.g. with just a single keyframe)
12
93
  // can be trivially combined with any other set of keyframes, so we don't need
13
94
  // to check those.
14
- let animatingKeyframes = allGroups.filter { $0.keyframes.count > 1 }
95
+ let animatingKeyframes = untypedGroups.filter { $0.keyframes.count > 1 }
15
96
 
16
97
  guard
17
98
  !allGroups.isEmpty,
18
99
  animatingKeyframes.allSatisfy({ $0.hasSameTimingParameters(as: animatingKeyframes[0]) })
19
100
  else { return nil }
20
101
 
21
- var combinedKeyframes = ContiguousArray<Keyframe<[T]>>()
22
- let baseKeyframes = (animatingKeyframes.first ?? allGroups[0]).keyframes
102
+ var combinedKeyframes = ContiguousArray<Keyframe<CombinedResult>>()
103
+ let baseKeyframes = (animatingKeyframes.first ?? untypedGroups[0]).keyframes
23
104
 
24
105
  for index in baseKeyframes.indices {
25
106
  let baseKeyframe = baseKeyframes[index]
26
- let combinedValues = allGroups.map { otherKeyframes -> T in
27
- if otherKeyframes.keyframes.count == 1 {
28
- return otherKeyframes.keyframes[0].value
29
- } else {
30
- return otherKeyframes.keyframes[index].value
31
- }
32
- }
33
- combinedKeyframes.append(baseKeyframe.withValue(combinedValues))
107
+ let combinedValue = makeCombinedResult(index)
108
+ combinedKeyframes.append(baseKeyframe.withValue(combinedValue))
34
109
  }
35
110
 
36
111
  return KeyframeGroup(keyframes: combinedKeyframes)
37
112
  }
38
113
 
39
- /// Combines the given `[KeyframeGroup?]` of `Keyframe<T>`s
40
- /// into a single `KeyframeGroup` of `Keyframe<[T]>`s
41
- /// if all of the `KeyframeGroup`s have the exact same animation timing
42
- static func combinedIfPossible<T>(_ groups: [KeyframeGroup<T>?]) -> KeyframeGroup<[T]>? {
43
- let nonOptionalGroups = groups.compactMap { $0 }
44
- guard nonOptionalGroups.count == groups.count else { return nil }
45
- return combinedIfPossible(nonOptionalGroups)
46
- }
47
114
  }
48
115
 
49
116
  extension KeyframeGroup {
@@ -61,13 +128,27 @@ extension KeyframeGroup {
61
128
  }
62
129
 
63
130
  extension Keyframe {
64
- /// Whether or not this keyframe has the same timing parameters as the given keyframe
65
- func hasSameTimingParameters<T>(as other: Keyframe<T>) -> Bool {
131
+ /// Whether or not this keyframe has the same timing parameters as the given keyframe,
132
+ /// excluding `spatialInTangent` and `spatialOutTangent`.
133
+ fileprivate func hasSameTimingParameters<T>(as other: Keyframe<T>) -> Bool {
66
134
  time == other.time
67
135
  && isHold == other.isHold
68
136
  && inTangent == other.inTangent
69
137
  && outTangent == other.outTangent
70
- && spatialInTangent == other.spatialInTangent
71
- && spatialOutTangent == other.spatialOutTangent
138
+ // We intentionally don't compare spatial in/out tangents,
139
+ // since those values are only used in very specific cases
140
+ // (animating the x/y position of a layer), which aren't ever
141
+ // combined in this way.
142
+ }
143
+ }
144
+
145
+ extension KeyframeGroup {
146
+ /// The value to use for a combined set of keyframes, for the given index
147
+ fileprivate func valueForCombinedKeyframes(at index: Int) -> T {
148
+ if keyframes.count == 1 {
149
+ return keyframes[0].value
150
+ } else {
151
+ return keyframes[index].value
152
+ }
72
153
  }
73
154
  }
@@ -78,7 +78,8 @@ class BaseCompositionLayer: BaseAnimationLayer {
78
78
  private func setupSublayers() {
79
79
  if
80
80
  renderLayerContents,
81
- let masks = baseLayerModel.masks
81
+ let masks = baseLayerModel.masks?.filter({ $0.mode != .none }),
82
+ !masks.isEmpty
82
83
  {
83
84
  mask = MaskCompositionLayer(masks: masks)
84
85
  }
@@ -4,6 +4,9 @@
4
4
  import QuartzCore
5
5
 
6
6
  extension CALayer {
7
+
8
+ // MARK: Internal
9
+
7
10
  /// Sets up an `AnimationLayer` / `CALayer` hierarchy in this layer,
8
11
  /// using the given list of layers.
9
12
  @nonobjc
@@ -48,7 +51,7 @@ extension CALayer {
48
51
  }
49
52
 
50
53
  // Create an `AnimationLayer` for each `LayerModel`
51
- for (layerModel, maskLayerModel) in try layersInZAxisOrder.pairedLayersAndMasks(context: context) {
54
+ for (layerModel, mask) in try layersInZAxisOrder.pairedLayersAndMasks(context: context) {
52
55
  guard let layer = try layerModel.makeAnimationLayer(context: context) else {
53
56
  continue
54
57
  }
@@ -64,14 +67,14 @@ extension CALayer {
64
67
 
65
68
  // Create the `mask` layer for this layer, if it has a `MatteType`
66
69
  if
67
- let maskLayerModel = maskLayerModel,
68
- let maskLayer = try maskLayerModel.makeAnimationLayer(context: context)
70
+ let mask = mask,
71
+ let maskLayer = try maskLayer(for: mask.model, type: mask.matteType, context: context)
69
72
  {
70
73
  let maskParentTransformLayer = makeParentTransformLayer(
71
- childLayerModel: maskLayerModel,
74
+ childLayerModel: mask.model,
72
75
  childLayer: maskLayer,
73
76
  name: { parentLayerModel in
74
- "\(maskLayerModel.name) (mask of \(layerModel.name)) (parent, \(parentLayerModel.name))"
77
+ "\(mask.model.name) (mask of \(layerModel.name)) (parent, \(parentLayerModel.name))"
75
78
  })
76
79
 
77
80
  // Set up a parent container to host both the layer
@@ -96,6 +99,41 @@ extension CALayer {
96
99
  }
97
100
  }
98
101
 
102
+ // MARK: Fileprivate
103
+
104
+ /// Creates a mask `CALayer` from the given matte layer model, using the `MatteType`
105
+ /// from the layer that is being masked.
106
+ fileprivate func maskLayer(
107
+ for matteLayerModel: LayerModel,
108
+ type: MatteType,
109
+ context: LayerContext) throws
110
+ -> CALayer?
111
+ {
112
+ switch type {
113
+ case .add:
114
+ return try matteLayerModel.makeAnimationLayer(context: context)
115
+
116
+ case .invert:
117
+ guard let maskLayer = try matteLayerModel.makeAnimationLayer(context: context) else {
118
+ return nil
119
+ }
120
+
121
+ // We can invert the mask layer by having a large solid black layer with the
122
+ // given mask layer subtracted out using the `xor` blend mode. When applied to the
123
+ // layer being masked, this creates an inverted mask where only areas _outside_
124
+ // of the mask layer are visible.
125
+ // https://developer.apple.com/documentation/coregraphics/cgblendmode/xor
126
+ let base = BaseAnimationLayer()
127
+ base.backgroundColor = .rgb(0, 0, 0)
128
+ base.addSublayer(maskLayer)
129
+ maskLayer.compositingFilter = "xor"
130
+ return base
131
+
132
+ case .none, .unknown:
133
+ return nil
134
+ }
135
+ }
136
+
99
137
  }
100
138
 
101
139
  extension Collection where Element == LayerModel {
@@ -103,8 +141,10 @@ extension Collection where Element == LayerModel {
103
141
  /// a `LayerModel` to use as its mask, if applicable
104
142
  /// based on the layer's `MatteType` configuration.
105
143
  /// - Assumes the layers are sorted in z-axis order.
106
- fileprivate func pairedLayersAndMasks(context: LayerContext) throws -> [(layer: LayerModel, mask: LayerModel?)] {
107
- var layersAndMasks = [(layer: LayerModel, mask: LayerModel?)]()
144
+ fileprivate func pairedLayersAndMasks(context _: LayerContext) throws
145
+ -> [(layer: LayerModel, mask: (model: LayerModel, matteType: MatteType)?)]
146
+ {
147
+ var layersAndMasks = [(layer: LayerModel, mask: (model: LayerModel, matteType: MatteType)?)]()
108
148
  var unprocessedLayers = reversed()
109
149
 
110
150
  while let layer = unprocessedLayers.popLast() {
@@ -114,11 +154,7 @@ extension Collection where Element == LayerModel {
114
154
  matteType != .none,
115
155
  let maskLayer = unprocessedLayers.popLast()
116
156
  {
117
- try context.compatibilityAssert(
118
- matteType == .add,
119
- "The Core Animation rendering engine currently only supports `MatteMode.add`.")
120
-
121
- layersAndMasks.append((layer: layer, mask: maskLayer))
157
+ layersAndMasks.append((layer: layer, mask: (model: maskLayer, matteType: matteType)))
122
158
  }
123
159
 
124
160
  else {
@@ -59,7 +59,7 @@ final class GradientRenderLayer: CAGradientLayer {
59
59
  /// - This specific value is arbitrary and can be increased if necessary.
60
60
  /// Theoretically this should be "infinite", to match the behavior of
61
61
  /// `CGContext.drawLinearGradient` with `[.drawsAfterEndLocation, .drawsBeforeStartLocation]`.
62
- private let gradientPadding: CGFloat = 2_000
62
+ private let gradientPadding: CGFloat = 10_000
63
63
 
64
64
  private func updateLayout() {
65
65
  anchorPoint = .zero
@@ -27,6 +27,10 @@ extension LayerModel {
27
27
  func makeAnimationLayer(context: LayerContext) throws -> BaseCompositionLayer? {
28
28
  let context = context.forLayer(self)
29
29
 
30
+ if hidden {
31
+ return TransformLayer(layerModel: self)
32
+ }
33
+
30
34
  switch (type, self) {
31
35
  case (.precomp, let preCompLayerModel as PreCompLayerModel):
32
36
  let preCompLayer = PreCompLayer(preCompLayer: preCompLayerModel)
@@ -74,7 +74,7 @@ extension MaskCompositionLayer: AnimationLayer {
74
74
  }
75
75
  }
76
76
 
77
- // MARK: - MaskLayer
77
+ // MARK: MaskCompositionLayer.MaskLayer
78
78
 
79
79
  extension MaskCompositionLayer {
80
80
  final class MaskLayer: CAShapeLayer {