lottie-ios 4.1.2 → 4.2.0

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 (96) hide show
  1. package/.github/workflows/main.yml +27 -9
  2. package/Lottie.xcodeproj/project.pbxproj +158 -70
  3. package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +2 -2
  4. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/IDEFindNavigatorScopes.plist +5 -0
  5. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  6. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +258 -0
  7. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +13 -2
  8. package/Package.swift +2 -1
  9. package/README.md +3 -3
  10. package/Rakefile +8 -4
  11. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +16 -2
  12. package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +1 -1
  13. package/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +1 -1
  14. package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +1 -1
  15. package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +6 -6
  16. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +76 -7
  17. package/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift +1 -1
  18. package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +1 -1
  19. package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +66 -102
  20. package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +2 -2
  21. package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +3 -3
  22. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +66 -17
  23. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +55 -32
  24. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +16 -12
  25. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +3 -3
  26. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +24 -11
  27. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +2 -2
  28. package/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift +1 -1
  29. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +13 -2
  30. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +9 -1
  31. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +22 -11
  32. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -1
  33. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +13 -2
  34. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +16 -7
  35. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift +1 -1
  36. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +2 -2
  37. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/RectNode.swift +1 -1
  38. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift +2 -2
  39. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +20 -8
  40. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift +1 -1
  41. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  42. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
  43. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  44. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +28 -9
  45. package/Sources/Private/Model/Assets/ImageAsset.swift +4 -3
  46. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -8
  47. package/Sources/Private/Model/DotLottie/DotLottieManifest.swift +3 -14
  48. package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +11 -1
  49. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+BackingConfiguration.swift +147 -0
  50. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Helpers.swift +351 -0
  51. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+MemoryFile.swift +183 -0
  52. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Progress.swift +66 -0
  53. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Reading.swift +144 -0
  54. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ReadingDeprecated.swift +49 -0
  55. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift +385 -0
  56. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+WritingDeprecated.swift +91 -0
  57. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ZIP64.swift +170 -0
  58. package/Sources/Private/Model/DotLottie/{Zip/ZipArchive.swift → ZipFoundation/Archive.swift} +150 -227
  59. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+Compression.swift +403 -0
  60. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+CompressionDeprecated.swift +44 -0
  61. package/Sources/Private/Model/DotLottie/{Zip → ZipFoundation}/Data+Serialization.swift +62 -0
  62. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+Serialization.swift → ZipFoundation/Entry+Serialization.swift} +7 -7
  63. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+ZIP64.swift → ZipFoundation/Entry+ZIP64.swift} +13 -19
  64. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry.swift → ZipFoundation/Entry.swift} +141 -10
  65. package/Sources/Private/Model/DotLottie/ZipFoundation/FileManager+ZIP.swift +368 -0
  66. package/Sources/Private/Model/DotLottie/ZipFoundation/README.md +24 -0
  67. package/Sources/Private/Model/DotLottie/ZipFoundation/URL+ZIP.swift +32 -0
  68. package/Sources/Private/Model/Extensions/Bundle.swift +5 -14
  69. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +31 -8
  70. package/Sources/Private/Model/Objects/Transform.swift +58 -17
  71. package/Sources/Private/Model/ShapeItems/Repeater.swift +41 -7
  72. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +61 -7
  73. package/Sources/Private/Model/Text/TextAnimator.swift +37 -5
  74. package/Sources/Private/RootAnimationLayer.swift +3 -1
  75. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +12 -4
  76. package/Sources/Private/Utility/Extensions/DataExtension.swift +14 -4
  77. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +11 -0
  78. package/Sources/Private/Utility/Primitives/ColorExtension.swift +10 -13
  79. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +28 -6
  80. package/Sources/Public/Animation/LottieAnimationHelpers.swift +12 -10
  81. package/Sources/Public/Animation/LottieAnimationView.swift +213 -186
  82. package/Sources/Public/DotLottie/DotLottieFile.swift +11 -34
  83. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +101 -74
  84. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +90 -0
  85. package/Sources/Public/iOS/LottieAnimationViewBase.swift +1 -1
  86. package/Sources/Public/macOS/LottieAnimationViewBase.macOS.swift +1 -1
  87. package/lottie-ios.podspec +1 -1
  88. package/package.json +1 -1
  89. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  90. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/valentinperignon.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  91. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  92. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  93. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  94. package/Sources/Private/Model/DotLottie/Zip/Data+Compression.swift +0 -134
  95. package/Sources/Private/Model/DotLottie/Zip/FileManager+ZIP.swift +0 -130
  96. package/Sources/Private/Utility/Interpolatable/KeyframeGroup+Extensions.swift +0 -59
@@ -60,8 +60,12 @@ struct CustomizableProperty<ValueRepresentation> {
60
60
  /// The name of a customizable property that can be used in an `AnimationKeypath`
61
61
  /// - These values should be shared between the two rendering engines,
62
62
  /// since they form the public API of the `AnimationKeypath` system.
63
- enum PropertyName: String {
63
+ enum PropertyName: String, CaseIterable {
64
64
  case color = "Color"
65
+ case opacity = "Opacity"
66
+ case scale = "Scale"
67
+ case position = "Position"
68
+ case rotation = "Rotation"
65
69
  }
66
70
 
67
71
  // MARK: CALayer properties
@@ -71,7 +75,7 @@ extension LayerProperty {
71
75
  .init(
72
76
  caLayerKeypath: "transform.translation",
73
77
  defaultValue: CGPoint(x: 0, y: 0),
74
- customizableProperty: nil /* currently unsupported */ )
78
+ customizableProperty: .position)
75
79
  }
76
80
 
77
81
  static var positionX: LayerProperty<CGFloat> {
@@ -99,19 +103,19 @@ extension LayerProperty {
99
103
  .init(
100
104
  caLayerKeypath: "transform.scale.x",
101
105
  defaultValue: 1,
102
- customizableProperty: nil /* currently unsupported */ )
106
+ customizableProperty: .scaleX)
103
107
  }
104
108
 
105
109
  static var scaleY: LayerProperty<CGFloat> {
106
110
  .init(
107
111
  caLayerKeypath: "transform.scale.y",
108
112
  defaultValue: 1,
109
- customizableProperty: nil /* currently unsupported */ )
113
+ customizableProperty: .scaleY)
110
114
  }
111
115
 
112
- static var rotation: LayerProperty<CGFloat> {
116
+ static var rotationX: LayerProperty<CGFloat> {
113
117
  .init(
114
- caLayerKeypath: "transform.rotation",
118
+ caLayerKeypath: "transform.rotation.x",
115
119
  defaultValue: 0,
116
120
  customizableProperty: nil /* currently unsupported */ )
117
121
  }
@@ -123,6 +127,13 @@ extension LayerProperty {
123
127
  customizableProperty: nil /* currently unsupported */ )
124
128
  }
125
129
 
130
+ static var rotationZ: LayerProperty<CGFloat> {
131
+ .init(
132
+ caLayerKeypath: "transform.rotation.z",
133
+ defaultValue: 0,
134
+ customizableProperty: .rotation)
135
+ }
136
+
126
137
  static var anchorPoint: LayerProperty<CGPoint> {
127
138
  .init(
128
139
  caLayerKeypath: #keyPath(CALayer.anchorPoint),
@@ -136,7 +147,7 @@ extension LayerProperty {
136
147
  .init(
137
148
  caLayerKeypath: #keyPath(CALayer.opacity),
138
149
  defaultValue: 1,
139
- customizableProperty: nil /* currently unsupported */ )
150
+ customizableProperty: .opacity)
140
151
  }
141
152
 
142
153
  static var transform: LayerProperty<CATransform3D> {
@@ -249,4 +260,62 @@ extension CustomizableProperty {
249
260
  return .rgba(CGFloat(color.r), CGFloat(color.g), CGFloat(color.b), CGFloat(color.a))
250
261
  })
251
262
  }
263
+
264
+ static var opacity: CustomizableProperty<CGFloat> {
265
+ .init(
266
+ name: [.opacity],
267
+ conversion: { typeErasedValue in
268
+ guard let vector = typeErasedValue as? LottieVector1D else { return nil }
269
+
270
+ // Lottie animation files express opacity as a numerical percentage value
271
+ // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
272
+ // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
273
+ return vector.cgFloatValue / 100
274
+ })
275
+ }
276
+
277
+ static var scaleX: CustomizableProperty<CGFloat> {
278
+ .init(
279
+ name: [.scale],
280
+ conversion: { typeErasedValue in
281
+ guard let vector = typeErasedValue as? LottieVector3D else { return nil }
282
+
283
+ // Lottie animation files express scale as a numerical percentage value
284
+ // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
285
+ // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
286
+ return vector.pointValue.x / 100
287
+ })
288
+ }
289
+
290
+ static var scaleY: CustomizableProperty<CGFloat> {
291
+ .init(
292
+ name: [.scale],
293
+ conversion: { typeErasedValue in
294
+ guard let vector = typeErasedValue as? LottieVector3D else { return nil }
295
+
296
+ // Lottie animation files express scale as a numerical percentage value
297
+ // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
298
+ // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
299
+ return vector.pointValue.y / 100
300
+ })
301
+ }
302
+
303
+ static var rotation: CustomizableProperty<CGFloat> {
304
+ .init(
305
+ name: [.rotation],
306
+ conversion: { typeErasedValue in
307
+ guard let vector = typeErasedValue as? LottieVector1D else { return nil }
308
+
309
+ // Lottie animation files express rotation in degrees
310
+ // (e.g. 90º, 180º, 360º) so we covert to radians to get the
311
+ // values expected by Core Animation (e.g. π/2, π, 2π)
312
+ return vector.cgFloatValue * .pi / 180
313
+ })
314
+ }
315
+
316
+ static var position: CustomizableProperty<CGPoint> {
317
+ .init(
318
+ name: [.position],
319
+ conversion: { ($0 as? LottieVector3D)?.pointValue })
320
+ }
252
321
  }
@@ -40,7 +40,7 @@ extension CALayer {
40
40
  func addOpacityAnimation(for opacity: OpacityAnimationModel, context: LayerAnimationContext) throws {
41
41
  try addAnimation(
42
42
  for: .opacity,
43
- keyframes: opacity.opacity.keyframes,
43
+ keyframes: opacity.opacity,
44
44
  value: {
45
45
  // Lottie animation files express opacity as a numerical percentage value
46
46
  // (e.g. 0%, 50%, 100%) so we divide by 100 to get the decimal values
@@ -15,7 +15,7 @@ extension CAShapeLayer {
15
15
  {
16
16
  try addAnimation(
17
17
  for: .path,
18
- keyframes: try rectangle.combinedKeyframes(roundedCorners: roundedCorners).keyframes,
18
+ keyframes: try rectangle.combinedKeyframes(roundedCorners: roundedCorners),
19
19
  value: { keyframe in
20
20
  BezierPath.rectangle(
21
21
  position: keyframe.position.pointValue,
@@ -57,7 +57,7 @@ extension CAShapeLayer {
57
57
 
58
58
  try addAnimation(
59
59
  for: .fillColor,
60
- keyframes: fill.color.keyframes,
60
+ keyframes: fill.color,
61
61
  value: \.cgColorValue,
62
62
  context: context)
63
63
 
@@ -71,7 +71,7 @@ extension CAShapeLayer {
71
71
 
72
72
  try addAnimation(
73
73
  for: .strokeStart,
74
- keyframes: strokeStartKeyframes.keyframes,
74
+ keyframes: strokeStartKeyframes,
75
75
  value: { strokeStart in
76
76
  // Lottie animation files express stoke trims as a numerical percentage value
77
77
  // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
@@ -81,7 +81,7 @@ extension CAShapeLayer {
81
81
 
82
82
  try addAnimation(
83
83
  for: .strokeEnd,
84
- keyframes: strokeEndKeyframes.keyframes,
84
+ keyframes: strokeEndKeyframes,
85
85
  value: { strokeEnd in
86
86
  // Lottie animation files express stoke trims as a numerical percentage value
87
87
  // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
@@ -112,14 +112,24 @@ extension Trim {
112
112
 
113
113
  // CAShapeLayer requires strokeStart to be less than strokeEnd. This
114
114
  // isn't required by the Lottie schema, so some animations may have
115
- // strokeStart and strokeEnd flipped. If we detect this is the case,
116
- // then swap them.
117
- if startValueIsAlwaysGreaterThanEndValue() {
115
+ // strokeStart and strokeEnd flipped.
116
+ if startValueIsAlwaysLessOrEqualToThanEndValue() {
117
+ // If the start value is always _less than_ or equal to the end value
118
+ // then we can use the given values without any modifications
119
+ strokeStart = start
120
+ strokeEnd = end
121
+ } else if startValueIsAlwaysGreaterThanOrEqualToEndValue() {
122
+ // If the start value is always _greater than_ or equal to the end value,
123
+ // then we can just swap the start / end keyframes. This lets us avoid
124
+ // manually interpolating the keyframes values at each frame, which
125
+ // would be more expensive.
118
126
  strokeStart = end
119
127
  strokeEnd = start
120
128
  } else {
121
- strokeStart = start
122
- strokeEnd = end
129
+ // Otherwise if the start / end values ever swap places we have to
130
+ // fix the order on a per-keyframe basis, which may require manually
131
+ // interpolating the keyframe values at each frame.
132
+ (strokeStart, strokeEnd) = interpolatedAtEachFrame()
123
133
  }
124
134
 
125
135
  // If there are no offsets, then the stroke values can be used as-is
@@ -130,22 +140,20 @@ extension Trim {
130
140
  return (strokeStart, strokeEnd, 1)
131
141
  }
132
142
 
133
- // Otherwise, adjust the stroke values to account for the offsets
134
- // 1. Interpolate the keyframes so they are all on a linear timing function
135
- // 2. Merge by summing the stroke values with the offset values
136
- let interpolatedStrokeStart = strokeStart.manuallyInterpolateKeyframes()
137
- let interpolatedStrokeEnd = strokeEnd.manuallyInterpolateKeyframes()
138
- let interpolatedStrokeOffset = offset.manuallyInterpolateKeyframes()
139
-
140
- var adjustedStrokeStart = KeyframeGroup(
141
- keyframes: try adjustKeyframesForTrimOffsets(
142
- strokeKeyframes: interpolatedStrokeStart,
143
- offsetKeyframes: interpolatedStrokeOffset))
144
-
145
- var adjustedStrokeEnd = KeyframeGroup(
146
- keyframes: try adjustKeyframesForTrimOffsets(
147
- strokeKeyframes: interpolatedStrokeEnd,
148
- offsetKeyframes: interpolatedStrokeOffset))
143
+ // Apply the offset to the start / end values at each frame
144
+ let offsetStrokeKeyframes = Keyframes.combined(
145
+ strokeStart,
146
+ strokeEnd,
147
+ offset,
148
+ makeCombinedResult: { start, end, offset -> (start: LottieVector1D, end: LottieVector1D) in
149
+ // Compute the adjusted value by converting the offset value to a stroke value
150
+ let offsetStart = start.cgFloatValue + (offset.cgFloatValue / 360 * 100)
151
+ let offsetEnd = end.cgFloatValue + (offset.cgFloatValue / 360 * 100)
152
+ return (start: LottieVector1D(offsetStart), end: LottieVector1D(offsetEnd))
153
+ })
154
+
155
+ var adjustedStrokeStart = offsetStrokeKeyframes.map { $0.start }
156
+ var adjustedStrokeEnd = offsetStrokeKeyframes.map { $0.end }
149
157
 
150
158
  // If maximum stroke value is larger than 100%, then we have to create copies of the path
151
159
  // so the total path length includes the maximum stroke
@@ -169,9 +177,23 @@ extension Trim {
169
177
 
170
178
  // MARK: Private
171
179
 
172
- /// Checks whether or not the value for `trim.start` is greater
173
- /// than the value for every `trim.end` at every keyframe.
174
- private func startValueIsAlwaysGreaterThanEndValue() -> Bool {
180
+ /// Checks whether or not the value for `trim.start` is less than
181
+ /// or equal to the value for every `trim.end` at every frame.
182
+ private func startValueIsAlwaysLessOrEqualToThanEndValue() -> Bool {
183
+ startAndEndValuesAllSatisfy { startValue, endValue in
184
+ startValue <= endValue
185
+ }
186
+ }
187
+
188
+ /// Checks whether or not the value for `trim.start` is greater than
189
+ /// or equal to the value for every `trim.end` at every frame.
190
+ private func startValueIsAlwaysGreaterThanOrEqualToEndValue() -> Bool {
191
+ startAndEndValuesAllSatisfy { startValue, endValue in
192
+ startValue >= endValue
193
+ }
194
+ }
195
+
196
+ private func startAndEndValuesAllSatisfy(_ condition: (_ start: CGFloat, _ end: CGFloat) -> Bool) -> Bool {
175
197
  let keyframeTimes = Set(start.keyframes.map { $0.time } + end.keyframes.map { $0.time })
176
198
 
177
199
  let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes)
@@ -183,7 +205,7 @@ extension Trim {
183
205
  let endAtTime = endInterpolator.value(frame: keyframeTime) as? LottieVector1D
184
206
  else { continue }
185
207
 
186
- if startAtTime.cgFloatValue < endAtTime.cgFloatValue {
208
+ if !condition(startAtTime.cgFloatValue, endAtTime.cgFloatValue) {
187
209
  return false
188
210
  }
189
211
  }
@@ -191,82 +213,24 @@ extension Trim {
191
213
  return true
192
214
  }
193
215
 
194
- /// Adjusted stroke keyframes to account for offset keyframes by merging them into a single keyframe collection
195
- ///
196
- /// Since stroke keyframes and offset keyframes can be defined on different animation curves, they must be
197
- /// manually interpolated prior to invoking this method. Manually interpolating the keyframes will redefine both
198
- /// keyframe groups such that they can be interpolated linearly.
199
- ///
200
- /// - Precondition: The keyframes must be interpolated using `KeyframeGroup.manuallyInterpolateKeyframes()`
201
- private func adjustKeyframesForTrimOffsets(
202
- strokeKeyframes: ContiguousArray<Keyframe<LottieVector1D>>,
203
- offsetKeyframes: ContiguousArray<Keyframe<LottieVector1D>>)
204
- throws -> ContiguousArray<Keyframe<LottieVector1D>>
216
+ /// Interpolates the start and end keyframes, at each frame if necessary,
217
+ /// so that the value of `strokeStart` is always less than `strokeEnd`.
218
+ private func interpolatedAtEachFrame()
219
+ -> (strokeStart: KeyframeGroup<LottieVector1D>, strokeEnd: KeyframeGroup<LottieVector1D>)
205
220
  {
206
- guard
207
- !strokeKeyframes.isEmpty,
208
- !offsetKeyframes.isEmpty
209
- else {
210
- return strokeKeyframes
211
- }
212
-
213
- // Map each time to its corresponding stroke/offset keyframe
214
- var timeMap = [AnimationFrameTime: [Keyframe<LottieVector1D>?]]()
215
- for stroke in strokeKeyframes {
216
- timeMap[stroke.time] = [stroke, nil]
217
- }
218
- for offset in offsetKeyframes {
219
- if var existing = timeMap[offset.time] {
220
- existing[1] = offset
221
- timeMap[offset.time] = existing
222
- } else {
223
- timeMap[offset.time] = [nil, offset]
224
- }
225
- }
226
-
227
- // Each time will be mapped to a new, adjusted keyframe
228
- var output = ContiguousArray<Keyframe<LottieVector1D>>()
229
- var lastKeyframe: Keyframe<LottieVector1D>?
230
- var lastOffset: Keyframe<LottieVector1D>?
231
-
232
- for (time, values) in timeMap.sorted(by: { $0.0 < $1.0 }) {
233
- // Extract keyframe/offset associated with this timestamp
234
- let keyframe = values[0]
235
- let offset = values[1]
236
- lastKeyframe = keyframe ?? lastKeyframe
237
- lastOffset = offset ?? lastOffset
238
-
239
- guard let currentKeyframe = lastKeyframe else {
240
- // No keyframes are output until the first keyframe occurs
241
- continue
242
- }
243
-
244
- guard let currentOffset = lastOffset else {
245
- // Scalar isHold keyframes are not output as they offset the offset keyframes
246
- if !(strokeKeyframes.count == 1 && currentKeyframe.isHold) {
247
- output.append(currentKeyframe)
221
+ let combinedKeyframes = Keyframes.combined(
222
+ start,
223
+ end,
224
+ makeCombinedResult: { startValue, endValue -> (start: LottieVector1D, end: LottieVector1D) in
225
+ if startValue.cgFloatValue < endValue.cgFloatValue {
226
+ return (start: startValue, end: endValue)
227
+ } else {
228
+ return (start: endValue, end: startValue)
248
229
  }
249
- continue
250
- }
230
+ })
251
231
 
252
- // Compute the adjusted value by converting the offset value to a stroke value
253
- let strokeValue = currentKeyframe.value.value
254
- let offsetValue = currentOffset.value.value
255
- let adjustedValue = strokeValue + (offsetValue / 360 * 100)
256
-
257
- // The tangent values are all `nil` as the keyframes should have been manually interpolated
258
- let adjustedKeyframe = Keyframe<LottieVector1D>(
259
- value: LottieVector1D(adjustedValue),
260
- time: time,
261
- isHold: currentKeyframe.isHold,
262
- inTangent: nil,
263
- outTangent: nil,
264
- spatialInTangent: nil,
265
- spatialOutTangent: nil)
266
-
267
- output.append(adjustedKeyframe)
268
- }
269
-
270
- return output
232
+ return (
233
+ strokeStart: combinedKeyframes.map { $0.start },
234
+ strokeEnd: combinedKeyframes.map { $0.end })
271
235
  }
272
236
  }
@@ -36,7 +36,7 @@ extension CAShapeLayer {
36
36
  {
37
37
  try addAnimation(
38
38
  for: .path,
39
- keyframes: try star.combinedKeyframes().keyframes,
39
+ keyframes: try star.combinedKeyframes(),
40
40
  value: { keyframe in
41
41
  BezierPath.star(
42
42
  position: keyframe.position.pointValue,
@@ -62,7 +62,7 @@ extension CAShapeLayer {
62
62
  {
63
63
  try addAnimation(
64
64
  for: .path,
65
- keyframes: try star.combinedKeyframes().keyframes,
65
+ keyframes: try star.combinedKeyframes(),
66
66
  value: { keyframe in
67
67
  BezierPath.polygon(
68
68
  position: keyframe.position.pointValue,
@@ -54,14 +54,14 @@ extension CAShapeLayer {
54
54
  if let strokeColor = stroke.strokeColor {
55
55
  try addAnimation(
56
56
  for: .strokeColor,
57
- keyframes: strokeColor.keyframes,
57
+ keyframes: strokeColor,
58
58
  value: \.cgColorValue,
59
59
  context: context)
60
60
  }
61
61
 
62
62
  try addAnimation(
63
63
  for: .lineWidth,
64
- keyframes: stroke.width.keyframes,
64
+ keyframes: stroke.width,
65
65
  value: \.cgFloatValue,
66
66
  context: context)
67
67
 
@@ -79,7 +79,7 @@ extension CAShapeLayer {
79
79
 
80
80
  try addAnimation(
81
81
  for: .lineDashPhase,
82
- keyframes: dashPhase,
82
+ keyframes: KeyframeGroup(keyframes: dashPhase),
83
83
  value: \.cgFloatValue,
84
84
  context: context)
85
85
  }
@@ -24,8 +24,14 @@ protocol TransformModel {
24
24
  /// The scale of the transform
25
25
  var scale: KeyframeGroup<LottieVector3D> { get }
26
26
 
27
- /// The rotation of the transform. Note: This is single dimensional rotation.
28
- var rotation: KeyframeGroup<LottieVector1D> { get }
27
+ /// The rotation of the transform on X axis.
28
+ var rotationX: KeyframeGroup<LottieVector1D> { get }
29
+
30
+ /// The rotation of the transform on Y axis.
31
+ var rotationY: KeyframeGroup<LottieVector1D> { get }
32
+
33
+ /// The rotation of the transform on Z axis.
34
+ var rotationZ: KeyframeGroup<LottieVector1D> { get }
29
35
  }
30
36
 
31
37
  // MARK: - Transform + TransformModel
@@ -75,7 +81,7 @@ extension CALayer {
75
81
  try addPositionAnimations(from: transformModel, context: context)
76
82
  try addAnchorPointAnimation(from: transformModel, context: context)
77
83
  try addScaleAnimations(from: transformModel, context: context)
78
- try addRotationAnimation(from: transformModel, context: context)
84
+ try addRotationAnimations(from: transformModel, context: context)
79
85
  }
80
86
  }
81
87
 
@@ -87,15 +93,15 @@ extension CALayer {
87
93
  context: LayerAnimationContext)
88
94
  throws
89
95
  {
90
- if let positionKeyframes = transformModel._position?.keyframes {
96
+ if let positionKeyframes = transformModel._position {
91
97
  try addAnimation(
92
98
  for: .position,
93
99
  keyframes: positionKeyframes,
94
100
  value: \.pointValue,
95
101
  context: context)
96
102
  } else if
97
- let xKeyframes = transformModel._positionX?.keyframes,
98
- let yKeyframes = transformModel._positionY?.keyframes
103
+ let xKeyframes = transformModel._positionX,
104
+ let yKeyframes = transformModel._positionY
99
105
  {
100
106
  try addAnimation(
101
107
  for: .positionX,
@@ -123,7 +129,7 @@ extension CALayer {
123
129
  {
124
130
  try addAnimation(
125
131
  for: .anchorPoint,
126
- keyframes: transformModel.anchorPoint.keyframes,
132
+ keyframes: transformModel.anchorPoint,
127
133
  value: { absoluteAnchorPoint in
128
134
  guard bounds.width > 0, bounds.height > 0 else {
129
135
  context.logger.assertionFailure("Size must be non-zero before an animation can be played")
@@ -148,7 +154,7 @@ extension CALayer {
148
154
  {
149
155
  try addAnimation(
150
156
  for: .scaleX,
151
- keyframes: transformModel.scale.keyframes,
157
+ keyframes: transformModel.scale,
152
158
  value: { scale in
153
159
  // Lottie animation files express scale as a numerical percentage value
154
160
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
@@ -201,7 +207,7 @@ extension CALayer {
201
207
 
202
208
  try addAnimation(
203
209
  for: .rotationY,
204
- keyframes: transformModel.scale.keyframes,
210
+ keyframes: transformModel.scale,
205
211
  value: { scale in
206
212
  if scale.x < 0 {
207
213
  return .pi
@@ -214,7 +220,7 @@ extension CALayer {
214
220
 
215
221
  try addAnimation(
216
222
  for: .scaleY,
217
- keyframes: transformModel.scale.keyframes,
223
+ keyframes: transformModel.scale,
218
224
  value: { scale in
219
225
  // Lottie animation files express scale as a numerical percentage value
220
226
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
@@ -227,14 +233,53 @@ extension CALayer {
227
233
  context: context)
228
234
  }
229
235
 
230
- private func addRotationAnimation(
236
+ private func addRotationAnimations(
231
237
  from transformModel: TransformModel,
232
238
  context: LayerAnimationContext)
233
239
  throws
234
240
  {
241
+ let containsXRotationValues = transformModel.rotationX.keyframes.contains(where: { $0.value.cgFloatValue != 0 })
242
+ let containsYRotationValues = transformModel.rotationY.keyframes.contains(where: { $0.value.cgFloatValue != 0 })
243
+
244
+ // When `rotation.x` or `rotation.y` is used, it doesn't render property in test snapshots
245
+ // but do renders correctly on the simulator / device
246
+ if TestHelpers.snapshotTestsAreRunning {
247
+ if containsXRotationValues {
248
+ context.logger.warn("""
249
+ `rotation.x` values are not displayed correctly in snapshot tests
250
+ """)
251
+ }
252
+
253
+ if containsYRotationValues {
254
+ context.logger.warn("""
255
+ `rotation.y` values are not displayed correctly in snapshot tests
256
+ """)
257
+ }
258
+ }
259
+
260
+ // Lottie animation files express rotation in degrees
261
+ // (e.g. 90º, 180º, 360º) so we covert to radians to get the
262
+ // values expected by Core Animation (e.g. π/2, π, 2π)
263
+
264
+ try addAnimation(
265
+ for: .rotationX,
266
+ keyframes: transformModel.rotationX,
267
+ value: { rotationDegrees in
268
+ rotationDegrees.cgFloatValue * .pi / 180
269
+ },
270
+ context: context)
271
+
272
+ try addAnimation(
273
+ for: .rotationY,
274
+ keyframes: transformModel.rotationY,
275
+ value: { rotationDegrees in
276
+ rotationDegrees.cgFloatValue * .pi / 180
277
+ },
278
+ context: context)
279
+
235
280
  try addAnimation(
236
- for: .rotation,
237
- keyframes: transformModel.rotation.keyframes,
281
+ for: .rotationZ,
282
+ keyframes: transformModel.rotationZ,
238
283
  value: { rotationDegrees in
239
284
  // Lottie animation files express rotation in degrees
240
285
  // (e.g. 90º, 180º, 360º) so we covert to radians to get the
@@ -257,22 +302,26 @@ extension CALayer {
257
302
  transformModel.anchor,
258
303
  transformModel.position,
259
304
  transformModel.scale,
260
- transformModel.rotation,
305
+ transformModel.rotationX,
306
+ transformModel.rotationY,
307
+ transformModel.rotationZ,
261
308
  transformModel.skew,
262
309
  transformModel.skewAxis,
263
- makeCombinedResult: { anchor, position, scale, rotation, skew, skewAxis in
310
+ makeCombinedResult: { anchor, position, scale, rotationX, rotationY, rotationZ, skew, skewAxis in
264
311
  CATransform3D.makeTransform(
265
312
  anchor: anchor.pointValue,
266
313
  position: position.pointValue,
267
314
  scale: scale.sizeValue,
268
- rotation: rotation.cgFloatValue,
315
+ rotationX: rotationX.cgFloatValue,
316
+ rotationY: rotationY.cgFloatValue,
317
+ rotationZ: rotationZ.cgFloatValue,
269
318
  skew: skew.cgFloatValue,
270
319
  skewAxis: skewAxis.cgFloatValue)
271
320
  })
272
321
 
273
322
  try addAnimation(
274
323
  for: .transform,
275
- keyframes: combinedTransformKeyframes.keyframes,
324
+ keyframes: combinedTransformKeyframes,
276
325
  value: { $0 },
277
326
  context: context)
278
327
  }