Skip to content

Commit

Permalink
Support animating multiple properties of shape bezier paths (#1690)
Browse files Browse the repository at this point in the history
  • Loading branch information
calda authored Aug 3, 2022
1 parent 938002d commit 5dd958a
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 80 deletions.
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?
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
}
if lineDashPattern?.allSatisfy({ $0.floatValue.isZero }) == true {
lineDashPattern = nil
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

0 comments on commit 5dd958a

Please sign in to comment.