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

Support animating multiple properties of shape bezier paths #1690

Merged
merged 5 commits into from
Aug 3, 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
35 changes: 31 additions & 4 deletions Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,42 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: ellipse.size.keyframes,
value: { sizeKeyframe in
keyframes: ellipse.combinedKeyframes(context: context).keyframes,
value: { keyframe in
BezierPath.ellipse(
size: sizeKeyframe.sizeValue,
center: try ellipse.position.exactlyOneKeyframe(context: context, description: "ellipse position").value.pointValue,
size: keyframe.size.sizeValue,
center: keyframe.position.pointValue,
direction: ellipse.direction)
.cgPath()
.duplicated(times: pathMultiplier)
},
context: context)
}
}

extension Ellipse {
/// Data that represents how to render an ellipse at a specific point in time
struct Keyframe {
let size: Vector3D
let position: Vector3D
}

/// 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(
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"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ extension GradientRenderLayer {
// 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").value.pointValue
.exactlyOneKeyframe(context: context, description: "gradient startPoint").pointValue

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

startPoint = percentBasedPointInBounds(from: absoluteStartPoint)

Expand Down
41 changes: 34 additions & 7 deletions Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,45 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: rectangle.size.keyframes,
value: { sizeKeyframe in
keyframes: try rectangle.combinedKeyframes(context: context).keyframes,
value: { keyframe in
BezierPath.rectangle(
position: try rectangle.position
.exactlyOneKeyframe(context: context, description: "rectangle position").value.pointValue,
size: sizeKeyframe.sizeValue,
cornerRadius: try rectangle.cornerRadius
.exactlyOneKeyframe(context: context, description: "rectangle cornerRadius").value.cgFloatValue,
position: keyframe.position.pointValue,
size: keyframe.size.sizeValue,
cornerRadius: keyframe.cornerRadius.cgFloatValue,
direction: rectangle.direction)
.cgPath()
.duplicated(times: pathMultiplier)
},
context: context)
}
}

extension Rectangle {
/// Data that represents how to render a rectangle at a specific point in time
struct Keyframe {
let size: Vector3D
let position: Vector3D
let cornerRadius: Vector1D
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Rectangle
func combinedKeyframes(context: LayerAnimationContext) throws-> KeyframeGroup<Rectangle.Keyframe> {
let combinedKeyframes = Keyframes.combinedIfPossible(
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"))
}
}
}
}
93 changes: 61 additions & 32 deletions Sources/Private/CoreAnimation/Animations/StarAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,16 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: star.position.keyframes,
value: { position in
// We can only use one set of keyframes to animate a given CALayer keypath,
// so we currently animate `position` and ignore any other keyframes.
// TODO: Is there a way to support this properly?
keyframes: try star.combinedKeyframes(context: context).keyframes,
value: { keyframe in
BezierPath.star(
position: position.pointValue,
outerRadius: try star.outerRadius
.exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
innerRadius: try star.innerRadius?
.exactlyOneKeyframe(context: context, description: "innerRadius").value.cgFloatValue ?? 0,
outerRoundedness: try star.outerRoundness
.exactlyOneKeyframe(context: context, description: "outerRoundness").value.cgFloatValue,
innerRoundedness: try star.innerRoundness?
.exactlyOneKeyframe(context: context, description: "innerRoundness").value.cgFloatValue ?? 0,
numberOfPoints: try star.points
.exactlyOneKeyframe(context: context, description: "points").value.cgFloatValue,
rotation: try star.rotation
.exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
position: keyframe.position.pointValue,
outerRadius: keyframe.outerRadius.cgFloatValue,
innerRadius: keyframe.innerRadius.cgFloatValue,
outerRoundedness: keyframe.outerRoundness.cgFloatValue,
innerRoundedness: keyframe.innerRoundness.cgFloatValue,
numberOfPoints: keyframe.points.cgFloatValue,
rotation: keyframe.rotation.cgFloatValue,
direction: star.direction)
.cgPath()
.duplicated(times: pathMultiplier)
Expand All @@ -71,25 +62,63 @@ extension CAShapeLayer {
{
try addAnimation(
for: .path,
keyframes: star.position.keyframes,
value: { position in
// We can only use one set of keyframes to animate a given CALayer keypath,
// so we currently animate `position` and ignore any other keyframes.
// TODO: Is there a way to support this properly?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO: Is there a way to support this properly?

Good news, yes! 😝

keyframes: try star.combinedKeyframes(context: context).keyframes,
value: { keyframe in
BezierPath.polygon(
position: position.pointValue,
numberOfPoints: try star.points
.exactlyOneKeyframe(context: context, description: "numberOfPoints").value.cgFloatValue,
outerRadius: try star.outerRadius
.exactlyOneKeyframe(context: context, description: "outerRadius").value.cgFloatValue,
outerRoundedness: try star.outerRoundness
.exactlyOneKeyframe(context: context, description: "outerRoundedness").value.cgFloatValue,
rotation: try star.rotation
.exactlyOneKeyframe(context: context, description: "rotation").value.cgFloatValue,
position: keyframe.position.pointValue,
numberOfPoints: keyframe.points.cgFloatValue,
outerRadius: keyframe.outerRadius.cgFloatValue,
outerRoundedness: keyframe.outerRoundness.cgFloatValue,
rotation: keyframe.rotation.cgFloatValue,
direction: star.direction)
.cgPath()
.duplicated(times: pathMultiplier)
},
context: context)
}
}

extension Star {
/// Data that represents how to render a star at a specific point in time
struct Keyframe {
let position: Vector3D
let outerRadius: Vector1D
let innerRadius: Vector1D
let outerRoundness: Vector1D
let innerRoundness: Vector1D
let points: Vector1D
let rotation: Vector1D
}

/// 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(
position,
outerRadius,
innerRadius ?? KeyframeGroup(Vector1D(0)),
outerRoundness,
innerRoundness ?? KeyframeGroup(Vector1D(0)),
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")
?? Vector1D(0),
outerRoundness: try outerRoundness.exactlyOneKeyframe(context: context, description: "star outerRoundness"),
innerRoundness: try innerRoundness?.exactlyOneKeyframe(context: context, description: "star innerRoundness")
?? Vector1D(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 @@ -57,7 +57,7 @@ extension CAShapeLayer {
if let (dashPattern, dashPhase) = stroke.dashPattern?.shapeLayerConfiguration {
lineDashPattern = try dashPattern.map {
try KeyframeGroup(keyframes: $0)
.exactlyOneKeyframe(context: context, description: "stroke dashPattern").value.cgFloatValue as NSNumber
.exactlyOneKeyframe(context: context, description: "stroke dashPattern").cgFloatValue as NSNumber
}

try addAnimation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension KeyframeGroup {
fileID _: StaticString = #fileID,
line _: UInt = #line)
throws
-> Keyframe<T>
-> T
{
try context.compatibilityAssert(
keyframes.count == 1,
Expand All @@ -32,6 +32,6 @@ extension KeyframeGroup {
for \(description) values (due to limitations of Core Animation `CAKeyframeAnimation`s).
""")

return keyframes[0]
return keyframes[0].value
}
}
Loading