Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for animating startPoint and endPoint of radial gradients #1798

Merged
merged 6 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@
2E9C970E2822F43100677516 /* CALayer+fillBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */; };
2E9C970F2822F43100677516 /* CALayer+fillBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */; };
2E9C97102822F43100677516 /* CALayer+fillBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */; };
2E9C97112822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combinedIfPossible.swift */; };
2E9C97122822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combinedIfPossible.swift */; };
2E9C97132822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combinedIfPossible.swift */; };
2E9C97112822F43100677516 /* Keyframes+combined.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combined.swift */; };
2E9C97122822F43100677516 /* Keyframes+combined.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combined.swift */; };
2E9C97132822F43100677516 /* Keyframes+combined.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AB2822F43100677516 /* Keyframes+combined.swift */; };
2E9C97142822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */; };
2E9C97152822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */; };
2E9C97162822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */; };
Expand Down Expand Up @@ -784,7 +784,7 @@
2E9C95A72822F43100677516 /* CompatibilityTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompatibilityTracker.swift; sourceTree = "<group>"; };
2E9C95A82822F43100677516 /* ValueProviderStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueProviderStore.swift; sourceTree = "<group>"; };
2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CALayer+fillBounds.swift"; sourceTree = "<group>"; };
2E9C95AB2822F43100677516 /* Keyframes+combinedIfPossible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Keyframes+combinedIfPossible.swift"; sourceTree = "<group>"; };
2E9C95AB2822F43100677516 /* Keyframes+combined.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Keyframes+combined.swift"; sourceTree = "<group>"; };
2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KeyframeGroup+exactlyOneKeyframe.swift"; sourceTree = "<group>"; };
2E9C95AE2822F43100677516 /* CAAnimation+TimingConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CAAnimation+TimingConfiguration.swift"; sourceTree = "<group>"; };
2E9C95AF2822F43100677516 /* ShapeAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ShapeAnimation.swift; sourceTree = "<group>"; tabWidth = 4; };
Expand Down Expand Up @@ -1319,7 +1319,7 @@
isa = PBXGroup;
children = (
2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */,
2E9C95AB2822F43100677516 /* Keyframes+combinedIfPossible.swift */,
2E9C95AB2822F43100677516 /* Keyframes+combined.swift */,
2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */,
);
path = Extensions;
Expand Down Expand Up @@ -1950,7 +1950,7 @@
2E9C97382822F43100677516 /* VisibilityAnimation.swift in Sources */,
2EAF5AD727A0798700E00531 /* Vectors.swift in Sources */,
2E9C95E22822F43100677516 /* Group.swift in Sources */,
2E9C97112822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */,
2E9C97112822F43100677516 /* Keyframes+combined.swift in Sources */,
0887347B28F0CCDD00458627 /* LottieAnimationView.swift in Sources */,
2EAF5AD127A0798700E00531 /* AnimatedControl.swift in Sources */,
2E9C966F2822F43100677516 /* LayerTextProvider.swift in Sources */,
Expand Down Expand Up @@ -2185,7 +2185,7 @@
2E9C97392822F43100677516 /* VisibilityAnimation.swift in Sources */,
2EAF5AD827A0798700E00531 /* Vectors.swift in Sources */,
2E9C95E32822F43100677516 /* Group.swift in Sources */,
2E9C97122822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */,
2E9C97122822F43100677516 /* Keyframes+combined.swift in Sources */,
0887347C28F0CCDD00458627 /* LottieAnimationView.swift in Sources */,
2EAF5AD227A0798700E00531 /* AnimatedControl.swift in Sources */,
2E9C96702822F43100677516 /* LayerTextProvider.swift in Sources */,
Expand Down Expand Up @@ -2398,7 +2398,7 @@
2E9C973A2822F43100677516 /* VisibilityAnimation.swift in Sources */,
2EAF5AD927A0798700E00531 /* Vectors.swift in Sources */,
2E9C95E42822F43100677516 /* Group.swift in Sources */,
2E9C97132822F43100677516 /* Keyframes+combinedIfPossible.swift in Sources */,
2E9C97132822F43100677516 /* Keyframes+combined.swift in Sources */,
0887347D28F0CCDD00458627 /* LottieAnimationView.swift in Sources */,
2EAF5AD327A0798700E00531 /* AnimatedControl.swift in Sources */,
2E9C96712822F43100677516 /* LayerTextProvider.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,37 +73,8 @@ struct BezierPathKeyframe {
}
}

let combinedKeyframes = Keyframes.combinedIfPossible(
return Keyframes.combined(
path, cornerRadius,
makeCombinedResult: BezierPathKeyframe.init)

if let combinedKeyframes = combinedKeyframes {
return combinedKeyframes
}

// If we weren't able to combine all of the keyframes, then we can manually interpolate
// the path and corner radius at each time value
let pathInterpolator = KeyframeInterpolator(keyframes: path.keyframes)
let cornerRadiusInterpolator = KeyframeInterpolator(keyframes: cornerRadius.keyframes)

let times = path.keyframes.map { $0.time } + cornerRadius.keyframes.map { $0.time }
let minimumTime = times.min() ?? 0
let maximumTime = times.max() ?? 0
let animationLocalTimeRange = Int(minimumTime)...Int(maximumTime)

let interpolatedKeyframes = animationLocalTimeRange.compactMap { localTime -> Keyframe<BezierPathKeyframe>? in
let frame = AnimationFrameTime(localTime)
guard let interpolatedPath = pathInterpolator.value(frame: frame) as? BezierPath else {
return nil
}

return Keyframe(
value: BezierPathKeyframe(
path: interpolatedPath,
cornerRadius: cornerRadiusInterpolator.value(frame: frame) as? LottieVector1D),
time: frame)
}

return KeyframeGroup(keyframes: ContiguousArray(interpolatedKeyframes))
}
}
18 changes: 3 additions & 15 deletions Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: ellipse.combinedKeyframes(context: context).keyframes,
keyframes: ellipse.combinedKeyframes().keyframes,
value: { keyframe in
BezierPath.ellipse(
size: keyframe.size.sizeValue,
Expand All @@ -35,21 +35,9 @@ extension Ellipse {
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Ellipse
func combinedKeyframes(context: LayerAnimationContext) throws-> KeyframeGroup<Ellipse.Keyframe> {
let combinedKeyframes = Keyframes.combinedIfPossible(
func combinedKeyframes() throws -> KeyframeGroup<Ellipse.Keyframe> {
Keyframes.combined(
size, position,
makeCombinedResult: Ellipse.Keyframe.init)

if let combinedKeyframes = combinedKeyframes {
return combinedKeyframes
} else {
// If we weren't able to combine all of the keyframes, we have to take the timing values
// from one property and use a fixed value for the other properties.
return try size.map { sizeValue in
Keyframe(
size: sizeValue,
position: try position.exactlyOneKeyframe(context: context, description: "ellipse position"))
}
}
}
}
42 changes: 25 additions & 17 deletions Sources/Private/CoreAnimation/Animations/GradientAnimations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,31 @@ extension GradientRenderLayer {
private func addRadialGradientAnimations(for gradient: GradientShapeItem, context: LayerAnimationContext) throws {
type = .radial

// To draw the correct gradients, we have to derive a custom `endPoint`
// relative to the `startPoint` value. Since calculating the `endPoint`
// at any given time requires knowing the current `startPoint`,
// we can't allow them to animate separately.
let absoluteStartPoint = try gradient.startPoint
.exactlyOneKeyframe(context: context, description: "gradient startPoint").pointValue

let absoluteEndPoint = try gradient.endPoint
.exactlyOneKeyframe(context: context, description: "gradient endPoint").pointValue

startPoint = percentBasedPointInBounds(from: absoluteStartPoint)

let radius = absoluteStartPoint.distanceTo(absoluteEndPoint)
endPoint = percentBasedPointInBounds(
from: CGPoint(
x: absoluteStartPoint.x + radius,
y: absoluteStartPoint.y + radius))
let combinedKeyframes = Keyframes.combined(
gradient.startPoint, gradient.endPoint,
makeCombinedResult: { absoluteStartPoint, absoluteEndPoint -> (startPoint: CGPoint, endPoint: CGPoint) in
// Convert the absolute start / end points to the relative structure used by Core Animation
let relativeStartPoint = percentBasedPointInBounds(from: absoluteStartPoint.pointValue)
let radius = absoluteStartPoint.pointValue.distanceTo(absoluteEndPoint.pointValue)
let relativeEndPoint = percentBasedPointInBounds(
from: CGPoint(
x: absoluteStartPoint.x + radius,
y: absoluteStartPoint.y + radius))

return (startPoint: relativeStartPoint, endPoint: relativeEndPoint)
})

try addAnimation(
for: .startPoint,
keyframes: combinedKeyframes.keyframes,
value: \.startPoint,
context: context)

try addAnimation(
for: .endPoint,
keyframes: combinedKeyframes.keyframes,
value: \.endPoint,
context: context)
}
}

Expand Down
27 changes: 3 additions & 24 deletions Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ extension CAShapeLayer {
roundedCorners: RoundedCorners?)
throws
{
let combinedKeyframes = try rectangle.combinedKeyframes(
context: context,
roundedCorners: roundedCorners)

try addAnimation(
for: .path,
keyframes: combinedKeyframes.keyframes,
keyframes: try rectangle.combinedKeyframes(roundedCorners: roundedCorners).keyframes,
value: { keyframe in
BezierPath.rectangle(
position: keyframe.position.pointValue,
Expand All @@ -42,27 +38,10 @@ extension Rectangle {
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Rectangle
func combinedKeyframes(
context: LayerAnimationContext,
roundedCorners: RoundedCorners?) throws
-> KeyframeGroup<Rectangle.Keyframe>
{
func combinedKeyframes(roundedCorners: RoundedCorners?) throws -> KeyframeGroup<Rectangle.Keyframe> {
let cornerRadius = roundedCorners?.radius ?? cornerRadius
let combinedKeyframes = Keyframes.combinedIfPossible(
return Keyframes.combined(
size, position, cornerRadius,
makeCombinedResult: Rectangle.Keyframe.init)

if let combinedKeyframes = combinedKeyframes {
return combinedKeyframes
} else {
// If we weren't able to combine all of the keyframes, we have to take the timing values
// from one property and use a fixed value for the other properties.
return try size.map { sizeValue in
Keyframe(
size: sizeValue,
position: try position.exactlyOneKeyframe(context: context, description: "rectangle position"),
cornerRadius: try cornerRadius.exactlyOneKeyframe(context: context, description: "rectangle cornerRadius"))
}
}
}
}
13 changes: 5 additions & 8 deletions Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extension CAShapeLayer {
/// Adds animations for `strokeStart` and `strokeEnd` from the given `Trim` object
@nonobjc
func addAnimations(for trim: Trim, context: LayerAnimationContext) throws -> PathMultiplier {
let (strokeStartKeyframes, strokeEndKeyframes, pathMultiplier) = try trim.caShapeLayerKeyframes(context: context)
let (strokeStartKeyframes, strokeEndKeyframes, pathMultiplier) = try trim.caShapeLayerKeyframes()

try addAnimation(
for: .strokeStart,
Expand Down Expand Up @@ -102,7 +102,7 @@ extension Trim {
/// The `strokeStart` and `strokeEnd` keyframes to apply to a `CAShapeLayer`,
/// plus a `pathMultiplier` that should be applied to the layer's `path` so that
/// trim values larger than 100% can be displayed properly.
fileprivate func caShapeLayerKeyframes(context: LayerAnimationContext)
fileprivate func caShapeLayerKeyframes()
throws
-> (strokeStart: KeyframeGroup<LottieVector1D>, strokeEnd: KeyframeGroup<LottieVector1D>, pathMultiplier: PathMultiplier)
{
Expand Down Expand Up @@ -139,14 +139,12 @@ extension Trim {
var adjustedStrokeStart = KeyframeGroup(
keyframes: try adjustKeyframesForTrimOffsets(
strokeKeyframes: interpolatedStrokeStart,
offsetKeyframes: interpolatedStrokeOffset,
context: context))
offsetKeyframes: interpolatedStrokeOffset))

var adjustedStrokeEnd = KeyframeGroup(
keyframes: try adjustKeyframesForTrimOffsets(
strokeKeyframes: interpolatedStrokeEnd,
offsetKeyframes: interpolatedStrokeOffset,
context: context))
offsetKeyframes: interpolatedStrokeOffset))

// If maximum stroke value is larger than 100%, then we have to create copies of the path
// so the total path length includes the maximum stroke
Expand Down Expand Up @@ -201,8 +199,7 @@ extension Trim {
/// - Precondition: The keyframes must be interpolated using `KeyframeGroup.manuallyInterpolateKeyframes()`
private func adjustKeyframesForTrimOffsets(
strokeKeyframes: ContiguousArray<Keyframe<LottieVector1D>>,
offsetKeyframes: ContiguousArray<Keyframe<LottieVector1D>>,
context _: LayerAnimationContext)
offsetKeyframes: ContiguousArray<Keyframe<LottieVector1D>>)
throws -> ContiguousArray<Keyframe<LottieVector1D>>
{
guard
Expand Down
27 changes: 4 additions & 23 deletions Sources/Private/CoreAnimation/Animations/StarAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: try star.combinedKeyframes(context: context).keyframes,
keyframes: try star.combinedKeyframes().keyframes,
value: { keyframe in
BezierPath.star(
position: keyframe.position.pointValue,
Expand All @@ -62,7 +62,7 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: try star.combinedKeyframes(context: context).keyframes,
keyframes: try star.combinedKeyframes().keyframes,
value: { keyframe in
BezierPath.polygon(
position: keyframe.position.pointValue,
Expand Down Expand Up @@ -91,8 +91,8 @@ extension Star {
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this star/polygon
func combinedKeyframes(context: LayerAnimationContext) throws -> KeyframeGroup<Keyframe> {
let combinedKeyframes = Keyframes.combinedIfPossible(
func combinedKeyframes() throws -> KeyframeGroup<Keyframe> {
Keyframes.combined(
position,
outerRadius,
innerRadius ?? KeyframeGroup(LottieVector1D(0)),
Expand All @@ -101,24 +101,5 @@ extension Star {
points,
rotation,
makeCombinedResult: Star.Keyframe.init)

if let combinedKeyframes = combinedKeyframes {
return combinedKeyframes
} else {
// If we weren't able to combine all of the keyframes, we have to take the timing values
// from one property and use a fixed value for the other properties.
return try position.map { positionValue in
Keyframe(
position: positionValue,
outerRadius: try outerRadius.exactlyOneKeyframe(context: context, description: "star outerRadius"),
innerRadius: try innerRadius?.exactlyOneKeyframe(context: context, description: "star innerRadius")
?? LottieVector1D(0),
outerRoundness: try outerRoundness.exactlyOneKeyframe(context: context, description: "star outerRoundness"),
innerRoundness: try innerRoundness?.exactlyOneKeyframe(context: context, description: "star innerRoundness")
?? LottieVector1D(0),
points: try points.exactlyOneKeyframe(context: context, description: "star points"),
rotation: try rotation.exactlyOneKeyframe(context: context, description: "star rotation"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,9 @@
extension KeyframeGroup {
/// Retrieves the first `Keyframe` from this group,
/// and asserts that there are not any extra keyframes that would be ignored
///
/// - There are several places in Lottie animation definitions where multiple
/// sets of keyframe timings can be provided for properties that have to
/// be applied to a single `CALayer` property (for example, the definition for a
/// `Rectangle` technically lets you animate `size`, `position`, and `cornerRadius`
/// separately, but these all have to be combined into a single `CAKeyframeAnimation`
/// on the `CAShapeLayer.path` property.
///
/// - In those sorts of cases, we currently choose one one `KeyframeGroup` to provide the
/// timing information, and disallow simultaneous animations on the other properties.
///
/// - We could support animating all of the values simultaneously if we manually
/// interpolated the property for each individual frame, like we do in
/// `CombinedShapeItem.manuallyInterpolating` and `BezierPathKeyframe.combining`
///
/// - This should only be used in cases where it's fundamentally not possible to
/// support animating a given property (e.g. if Core Animation itself doesn't
/// support the property).
func exactlyOneKeyframe(
context: CompatibilityTrackerProviding,
description: String,
Expand All @@ -33,7 +21,7 @@ extension KeyframeGroup {
keyframes.count == 1,
"""
The Core Animation rendering engine does not support animating multiple keyframes
for \(description) values (due to limitations of Core Animation `CAKeyframeAnimation`s).
for \(description) values, due to limitations of Core Animation.
""")

return keyframes[0].value
Expand Down
Loading