lottie-ios 3.4.1 → 3.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/stale_issues.yml +17 -0
- package/.swiftpm/xcode/package.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcodeproj/project.pbxproj +24 -16
- package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (macOS).xcscheme +2 -2
- package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -81
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -17
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +114 -1
- package/Package.swift +1 -6
- package/Rakefile +41 -2
- package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +1 -2
- package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +28 -0
- package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +31 -4
- package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +2 -2
- package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +34 -7
- package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +25 -14
- package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +61 -32
- package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +4 -1
- package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +6 -1
- package/Sources/Private/CoreAnimation/Extensions/KeyframeGroup+exactlyOneKeyframe.swift +2 -2
- package/Sources/Private/CoreAnimation/Extensions/Keyframes+combinedIfPossible.swift +107 -26
- package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +2 -1
- package/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift +48 -12
- package/Sources/Private/CoreAnimation/Layers/GradientRenderLayer.swift +1 -1
- package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +4 -0
- package/Sources/Private/CoreAnimation/Layers/MaskCompositionLayer.swift +1 -1
- package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +85 -0
- package/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift +17 -4
- package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +124 -53
- package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift +29 -0
- package/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift +1 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift +5 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +5 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +3 -0
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +17 -1
- package/Sources/Private/Model/Animation.swift +4 -4
- package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +25 -0
- package/Sources/Private/Model/ShapeItems/Ellipse.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Fill.swift +1 -1
- package/Sources/Private/Model/ShapeItems/GradientFill.swift +14 -1
- package/Sources/Private/Model/ShapeItems/GradientStroke.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Merge.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Rectangle.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Repeater.swift +0 -1
- package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Star.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Stroke.swift +0 -1
- package/Sources/Private/Model/ShapeItems/Trim.swift +0 -1
- package/Sources/Private/{MainThread/NodeRenderSystem/NodeProperties/ValueProviders → Utility/Interpolatable}/KeyframeInterpolator.swift +0 -0
- package/Sources/Public/Animation/AnimationView.swift +1 -1
- package/Sources/Public/iOS/BundleImageProvider.swift +2 -2
- package/Sources/Public/iOS/FilepathImageProvider.swift +1 -1
- package/Sources/Public/macOS/BundleImageProvider.macOS.swift +1 -1
- package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +1 -1
- package/Tests/AnimationKeypathTests.swift +10 -1
- package/Tests/PerformanceTests.swift +19 -20
- package/Tests/Samples/9squares_AlBoardman.json +1 -0
- package/Tests/Samples/Boat_Loader.json +1 -0
- package/Tests/Samples/HamburgerArrow.json +1 -0
- package/Tests/Samples/IconTransitions.json +1 -0
- package/Tests/Samples/Images/dog.png +0 -0
- package/Tests/Samples/Issues/issue_1125.json +1 -0
- package/Tests/Samples/Issues/issue_1260.json +1 -0
- package/Tests/Samples/Issues/issue_1403.json +1 -0
- package/Tests/Samples/Issues/issue_1407.json +1 -0
- package/Tests/Samples/Issues/issue_1460.json +1 -0
- package/Tests/Samples/Issues/issue_1488.json +1 -0
- package/Tests/Samples/Issues/issue_1505.json +1 -0
- package/Tests/Samples/Issues/issue_1541.json +1 -0
- package/Tests/Samples/Issues/issue_1557.json +1 -0
- package/Tests/Samples/Issues/issue_1603.json +1 -0
- package/Tests/Samples/Issues/issue_1628.json +1 -0
- package/Tests/Samples/Issues/issue_1636.json +1 -0
- package/Tests/Samples/Issues/issue_1643.json +1 -0
- package/Tests/Samples/Issues/issue_1655.json +1 -0
- package/Tests/Samples/Issues/issue_1664.json +1 -0
- package/Tests/Samples/Issues/issue_1683.json +1 -0
- package/Tests/Samples/Issues/issue_1687.json +1 -0
- package/Tests/Samples/Issues/issue_1711.json +1 -0
- package/Tests/Samples/Issues/issue_1717.json +1 -0
- package/Tests/Samples/Issues/issue_769.json +1 -0
- package/Tests/Samples/Issues/issue_885.json +1 -0
- package/Tests/Samples/Issues/issue_965.json +1 -0
- package/Tests/Samples/Issues/pr_1536.json +1 -0
- package/Tests/Samples/Issues/pr_1563.json +8439 -0
- package/Tests/Samples/Issues/pr_1592.json +5527 -0
- package/Tests/Samples/Issues/pr_1599.json +738 -0
- package/Tests/Samples/Issues/pr_1604_1.json +1 -0
- package/Tests/Samples/Issues/pr_1604_2.json +1 -0
- package/Tests/Samples/Issues/pr_1632_1.json +1 -0
- package/Tests/Samples/Issues/pr_1632_2.json +1 -0
- package/Tests/Samples/Issues/pr_1686.json +513 -0
- package/Tests/Samples/Issues/pr_1698.json +1 -0
- package/Tests/Samples/Issues/pr_1699.json +1 -0
- package/Tests/Samples/LottieFiles/LICENSE.md +14 -0
- package/Tests/Samples/LottieFiles/bounce_strokes.json +1 -0
- package/Tests/Samples/LottieFiles/cactus.json +1 -0
- package/Tests/Samples/LottieFiles/dog_car_ride.json +1 -0
- package/Tests/Samples/LottieFiles/draft_icon.json +1 -0
- package/Tests/Samples/LottieFiles/fireworks.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_1.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_2.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_pill.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_shapes.json +1 -0
- package/Tests/Samples/LottieFiles/gradient_square.json +1 -0
- package/Tests/Samples/LottieFiles/growth.json +1 -0
- package/Tests/Samples/LottieFiles/infinity_loader.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_1.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_2.json +1 -0
- package/Tests/Samples/LottieFiles/loading_dots_3.json +1 -0
- package/Tests/Samples/LottieFiles/loading_gradient_strokes.json +1 -0
- package/Tests/Samples/LottieFiles/settings_slider.json +1 -0
- package/Tests/Samples/LottieFiles/shop.json +1 -0
- package/Tests/Samples/LottieFiles/step_loader.json +1 -0
- package/Tests/Samples/LottieLogo1.json +1 -0
- package/Tests/Samples/LottieLogo1_masked.json +1 -0
- package/Tests/Samples/LottieLogo2.json +1 -0
- package/Tests/Samples/MotionCorpse_Jrcanest.json +1 -0
- package/Tests/Samples/Nonanimating/BasicLayers.json +1 -0
- package/Tests/Samples/Nonanimating/DisableNodesTest.json +1 -0
- package/Tests/Samples/Nonanimating/FirstText.json +1 -0
- package/Tests/Samples/Nonanimating/GeometryTransformTest.json +1 -0
- package/Tests/Samples/Nonanimating/Text_AnimatedProperties.json +1 -0
- package/Tests/Samples/Nonanimating/Text_Glyph.json +1 -0
- package/Tests/Samples/Nonanimating/Text_NoAnimation.json +1 -0
- package/Tests/Samples/Nonanimating/Text_NoGlyph.json +1 -0
- package/Tests/Samples/Nonanimating/Zoom.json +1 -0
- package/Tests/Samples/Nonanimating/_dog.json +1 -0
- package/Tests/Samples/Nonanimating/base64Test.json +1 -0
- package/Tests/Samples/Nonanimating/blend_mode_test.json +1 -0
- package/Tests/Samples/Nonanimating/keypathTest.json +1 -0
- package/Tests/Samples/Nonanimating/verifyLineHeight.json +1 -0
- package/Tests/Samples/PinJump.json +1 -0
- package/Tests/Samples/Switch.json +1 -0
- package/Tests/Samples/Switch_States.json +1 -0
- package/Tests/Samples/TwitterHeart.json +1 -0
- package/Tests/Samples/TwitterHeartButton.json +1 -0
- package/Tests/Samples/TypeFace/A.json +1 -0
- package/Tests/Samples/TypeFace/Apostrophe.json +1 -0
- package/Tests/Samples/TypeFace/B.json +1 -0
- package/Tests/Samples/TypeFace/BlinkingCursor.json +1 -0
- package/Tests/Samples/TypeFace/C.json +1 -0
- package/Tests/Samples/TypeFace/Colon.json +1 -0
- package/Tests/Samples/TypeFace/Comma.json +1 -0
- package/Tests/Samples/TypeFace/D.json +1 -0
- package/Tests/Samples/TypeFace/E.json +1 -0
- package/Tests/Samples/TypeFace/F.json +1 -0
- package/Tests/Samples/TypeFace/G.json +1 -0
- package/Tests/Samples/TypeFace/H.json +1 -0
- package/Tests/Samples/TypeFace/I.json +1 -0
- package/Tests/Samples/TypeFace/J.json +1 -0
- package/Tests/Samples/TypeFace/K.json +1 -0
- package/Tests/Samples/TypeFace/L.json +1 -0
- package/Tests/Samples/TypeFace/M.json +1 -0
- package/Tests/Samples/TypeFace/N.json +1 -0
- package/Tests/Samples/TypeFace/O.json +1 -0
- package/Tests/Samples/TypeFace/P.json +1 -0
- package/Tests/Samples/TypeFace/Q.json +1 -0
- package/Tests/Samples/TypeFace/R.json +1 -0
- package/Tests/Samples/TypeFace/S.json +1 -0
- package/Tests/Samples/TypeFace/T.json +1 -0
- package/Tests/Samples/TypeFace/U.json +1 -0
- package/Tests/Samples/TypeFace/V.json +1 -0
- package/Tests/Samples/TypeFace/W.json +1 -0
- package/Tests/Samples/TypeFace/X.json +1 -0
- package/Tests/Samples/TypeFace/Y.json +1 -0
- package/Tests/Samples/TypeFace/Z.json +1 -0
- package/Tests/Samples/Watermelon.json +1 -0
- package/Tests/Samples/setValueTest.json +1 -0
- package/Tests/Samples/timeremap.json +1 -0
- package/Tests/Samples/vcTransition1.json +1 -0
- package/Tests/Samples/vcTransition2.json +1 -0
- package/Tests/SnapshotConfiguration.swift +5 -0
- package/lottie-ios.podspec +2 -1
- package/package.json +1 -1
- package/.swift-version +0 -1
- package/Package.resolved +0 -88
|
@@ -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").
|
|
107
|
+
.exactlyOneKeyframe(context: context, description: "gradient startPoint").pointValue
|
|
108
108
|
|
|
109
109
|
let absoluteEndPoint = try gradient.endPoint
|
|
110
|
-
.exactlyOneKeyframe(context: context, description: "gradient endPoint").
|
|
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.
|
|
18
|
-
value: {
|
|
17
|
+
keyframes: try rectangle.combinedKeyframes(context: context).keyframes,
|
|
18
|
+
value: { keyframe in
|
|
19
19
|
BezierPath.rectangle(
|
|
20
|
-
position:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
136
|
-
let
|
|
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:
|
|
140
|
-
strokeEnd:
|
|
141
|
-
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.
|
|
40
|
-
value: {
|
|
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:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
75
|
-
value: {
|
|
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:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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").
|
|
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
|
|
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
|
-
->
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 =
|
|
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<
|
|
22
|
-
let baseKeyframes = (animatingKeyframes.first ??
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
|
68
|
-
let maskLayer = try
|
|
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:
|
|
74
|
+
childLayerModel: mask.model,
|
|
72
75
|
childLayer: maskLayer,
|
|
73
76
|
name: { parentLayerModel in
|
|
74
|
-
"\(
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|