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.
- package/.github/workflows/main.yml +27 -9
- package/Lottie.xcodeproj/project.pbxproj +158 -70
- package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +2 -2
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/IDEFindNavigatorScopes.plist +5 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +258 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +13 -2
- package/Package.swift +2 -1
- package/README.md +3 -3
- package/Rakefile +8 -4
- package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +16 -2
- package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +6 -6
- package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +76 -7
- package/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +66 -102
- package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +2 -2
- package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +3 -3
- package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +66 -17
- package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +55 -32
- package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +16 -12
- package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +3 -3
- package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +24 -11
- package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +2 -2
- package/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift +1 -1
- package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +13 -2
- package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +9 -1
- package/Sources/Private/CoreAnimation/ValueProviderStore.swift +22 -11
- package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +13 -2
- package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +16 -7
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +2 -2
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/RectNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift +2 -2
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +20 -8
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +28 -9
- package/Sources/Private/Model/Assets/ImageAsset.swift +4 -3
- package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -8
- package/Sources/Private/Model/DotLottie/DotLottieManifest.swift +3 -14
- package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +11 -1
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+BackingConfiguration.swift +147 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Helpers.swift +351 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+MemoryFile.swift +183 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Progress.swift +66 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Reading.swift +144 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ReadingDeprecated.swift +49 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift +385 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+WritingDeprecated.swift +91 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ZIP64.swift +170 -0
- package/Sources/Private/Model/DotLottie/{Zip/ZipArchive.swift → ZipFoundation/Archive.swift} +150 -227
- package/Sources/Private/Model/DotLottie/ZipFoundation/Data+Compression.swift +403 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Data+CompressionDeprecated.swift +44 -0
- package/Sources/Private/Model/DotLottie/{Zip → ZipFoundation}/Data+Serialization.swift +62 -0
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+Serialization.swift → ZipFoundation/Entry+Serialization.swift} +7 -7
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+ZIP64.swift → ZipFoundation/Entry+ZIP64.swift} +13 -19
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry.swift → ZipFoundation/Entry.swift} +141 -10
- package/Sources/Private/Model/DotLottie/ZipFoundation/FileManager+ZIP.swift +368 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/README.md +24 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/URL+ZIP.swift +32 -0
- package/Sources/Private/Model/Extensions/Bundle.swift +5 -14
- package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +31 -8
- package/Sources/Private/Model/Objects/Transform.swift +58 -17
- package/Sources/Private/Model/ShapeItems/Repeater.swift +41 -7
- package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +61 -7
- package/Sources/Private/Model/Text/TextAnimator.swift +37 -5
- package/Sources/Private/RootAnimationLayer.swift +3 -1
- package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +12 -4
- package/Sources/Private/Utility/Extensions/DataExtension.swift +14 -4
- package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +11 -0
- package/Sources/Private/Utility/Primitives/ColorExtension.swift +10 -13
- package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +28 -6
- package/Sources/Public/Animation/LottieAnimationHelpers.swift +12 -10
- package/Sources/Public/Animation/LottieAnimationView.swift +213 -186
- package/Sources/Public/DotLottie/DotLottieFile.swift +11 -34
- package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +101 -74
- package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +90 -0
- package/Sources/Public/iOS/LottieAnimationViewBase.swift +1 -1
- package/Sources/Public/macOS/LottieAnimationViewBase.macOS.swift +1 -1
- package/lottie-ios.podspec +1 -1
- package/package.json +1 -1
- package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/valentinperignon.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/Sources/Private/Model/DotLottie/Zip/Data+Compression.swift +0 -134
- package/Sources/Private/Model/DotLottie/Zip/FileManager+ZIP.swift +0 -130
- 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:
|
|
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:
|
|
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:
|
|
113
|
+
customizableProperty: .scaleY)
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
static var
|
|
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:
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
var
|
|
146
|
-
|
|
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
|
|
173
|
-
///
|
|
174
|
-
private func
|
|
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
|
|
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
|
-
///
|
|
195
|
-
///
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
250
|
-
}
|
|
230
|
+
})
|
|
251
231
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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()
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
28
|
-
var
|
|
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
|
|
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
|
|
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
|
|
98
|
-
let yKeyframes = transformModel._positionY
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: .
|
|
237
|
-
keyframes: transformModel.
|
|
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.
|
|
305
|
+
transformModel.rotationX,
|
|
306
|
+
transformModel.rotationY,
|
|
307
|
+
transformModel.rotationZ,
|
|
261
308
|
transformModel.skew,
|
|
262
309
|
transformModel.skewAxis,
|
|
263
|
-
makeCombinedResult: { anchor, position, scale,
|
|
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
|
-
|
|
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
|
|
324
|
+
keyframes: combinedTransformKeyframes,
|
|
276
325
|
value: { $0 },
|
|
277
326
|
context: context)
|
|
278
327
|
}
|