Skip to content

Commit

Permalink
Add rounded corner support to Core Animation rendering engine (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
calda authored Nov 3, 2022
1 parent d03f49a commit 86a80a4
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 20 deletions.
74 changes: 71 additions & 3 deletions Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@ extension CAShapeLayer {
for customPath: KeyframeGroup<BezierPath>,
context: LayerAnimationContext,
pathMultiplier: PathMultiplier = 1,
transformPath: (CGPath) -> CGPath = { $0 })
transformPath: (CGPath) -> CGPath = { $0 },
roundedCorners: RoundedCorners? = nil)
throws
{
let combinedKeyframes = try BezierPathKeyframe.combining(
path: customPath,
cornerRadius: roundedCorners?.radius)

try addAnimation(
for: .path,
keyframes: customPath.keyframes,
keyframes: combinedKeyframes.keyframes,
value: { pathKeyframe in
transformPath(pathKeyframe.cgPath().duplicated(times: pathMultiplier))
var path = pathKeyframe.path
if let cornerRadius = pathKeyframe.cornerRadius {
path = path.roundCorners(radius: cornerRadius.cgFloatValue)
}

return transformPath(path.cgPath().duplicated(times: pathMultiplier))
},
context: context)
}
Expand All @@ -39,3 +49,61 @@ extension CGPath {
return cgPath
}
}

// MARK: - BezierPathKeyframe

/// Data that represents how to render a bezier path at a specific point in time
struct BezierPathKeyframe {
let path: BezierPath
let cornerRadius: LottieVector1D?

/// Creates a single array of animatable keyframes from the given sets of keyframes
/// that can have different counts / timing parameters
static func combining(
path: KeyframeGroup<BezierPath>,
cornerRadius: KeyframeGroup<LottieVector1D>?) throws
-> KeyframeGroup<BezierPathKeyframe>
{
guard
let cornerRadius = cornerRadius,
cornerRadius.keyframes.contains(where: { $0.value.cgFloatValue > 0 })
else {
return path.map { path in
BezierPathKeyframe(path: path, cornerRadius: nil)
}
}

let combinedKeyframes = Keyframes.combinedIfPossible(
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))
}
}
16 changes: 13 additions & 3 deletions Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ extension CAShapeLayer {
func addAnimations(
for rectangle: Rectangle,
context: LayerAnimationContext,
pathMultiplier: PathMultiplier)
pathMultiplier: PathMultiplier,
roundedCorners: RoundedCorners?)
throws
{
let combinedKeyframes = try rectangle.combinedKeyframes(
context: context,
roundedCorners: roundedCorners)

try addAnimation(
for: .path,
keyframes: try rectangle.combinedKeyframes(context: context).keyframes,
keyframes: combinedKeyframes.keyframes,
value: { keyframe in
BezierPath.rectangle(
position: keyframe.position.pointValue,
Expand All @@ -37,7 +42,12 @@ extension Rectangle {
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Rectangle
func combinedKeyframes(context: LayerAnimationContext) throws-> KeyframeGroup<Rectangle.Keyframe> {
func combinedKeyframes(
context: LayerAnimationContext,
roundedCorners: RoundedCorners?) throws
-> KeyframeGroup<Rectangle.Keyframe>
{
let cornerRadius = roundedCorners?.radius ?? cornerRadius
let combinedKeyframes = Keyframes.combinedIfPossible(
size, position, cornerRadius,
makeCombinedResult: Rectangle.Keyframe.init)
Expand Down
21 changes: 18 additions & 3 deletions Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,38 @@ extension CAShapeLayer {
func addAnimations(
for shape: ShapeItem,
context: LayerAnimationContext,
pathMultiplier: PathMultiplier) throws
pathMultiplier: PathMultiplier,
roundedCorners: RoundedCorners?) throws
{
switch shape {
case let customShape as Shape:
try addAnimations(for: customShape.path, context: context, pathMultiplier: pathMultiplier)
try addAnimations(
for: customShape.path,
context: context,
pathMultiplier: pathMultiplier,
roundedCorners: roundedCorners)

case let combinedShape as CombinedShapeItem:
try addAnimations(for: combinedShape, context: context, pathMultiplier: pathMultiplier)
try context.compatibilityAssert(roundedCorners == nil, """
Rounded corners support is not currently implemented for combined shape items
""")

case let ellipse as Ellipse:
try addAnimations(for: ellipse, context: context, pathMultiplier: pathMultiplier)

case let rectangle as Rectangle:
try addAnimations(for: rectangle, context: context, pathMultiplier: pathMultiplier)
try addAnimations(
for: rectangle,
context: context,
pathMultiplier: pathMultiplier,
roundedCorners: roundedCorners)

case let star as Star:
try addAnimations(for: star, context: context, pathMultiplier: pathMultiplier)
try context.compatibilityAssert(roundedCorners == nil, """
Rounded corners support is currently not implemented for polygon items
""")

default:
// None of the other `ShapeItem` subclasses draw a `path`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ extension KeyframeGroup {
/// - 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`
///
func exactlyOneKeyframe(
context: CompatibilityTrackerProviding,
description: String,
Expand Down
12 changes: 9 additions & 3 deletions Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ final class ShapeItemLayer: BaseAnimationLayer {
""")
}

try shapeLayer.addAnimations(for: shape.item, context: context.for(shape), pathMultiplier: trimPathMultiplier ?? 1)
try shapeLayer.addAnimations(
for: shape.item,
context: context.for(shape),
pathMultiplier: trimPathMultiplier ?? 1,
roundedCorners: otherItems.first(RoundedCorners.self))

if let (fill, context) = otherItems.first(Fill.self, context: context) {
try shapeLayer.addAnimations(for: fill, context: context)
Expand All @@ -236,7 +240,8 @@ final class ShapeItemLayer: BaseAnimationLayer {
try layers.shapeMaskLayer.addAnimations(
for: shape.item,
context: context.for(shape),
pathMultiplier: 1)
pathMultiplier: 1,
roundedCorners: otherItems.first(RoundedCorners.self))

if let (gradientFill, context) = otherItems.first(GradientFill.self, context: context) {
layers.shapeMaskLayer.fillRule = gradientFill.fillRule.caFillRule
Expand All @@ -258,7 +263,8 @@ final class ShapeItemLayer: BaseAnimationLayer {
try layers.shapeMaskLayer.addAnimations(
for: shape.item,
context: context.for(shape),
pathMultiplier: trimPathMultiplier ?? 1)
pathMultiplier: trimPathMultiplier ?? 1,
roundedCorners: otherItems.first(RoundedCorners.self))

if let (gradientStroke, context) = otherItems.first(GradientStroke.self, context: context) {
try layers.gradientColorLayer.addGradientAnimations(for: gradientStroke, type: .rgb, context: context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,14 @@ final class RoundedCornersNode: AnimatorNode {
for pathContainer in upstreamPaths {
let pathObjects = pathContainer.removePaths(updateFrame: frame)
for path in pathObjects {
pathContainer.appendPath(
path.roundCorners(
radius: properties.radius.value.cgFloatValue),
updateFrame: frame)
let cornerRadius = properties.radius.value.cgFloatValue
if cornerRadius != 0 {
pathContainer.appendPath(
path.roundCorners(radius: cornerRadius),
updateFrame: frame)
} else {
pathContainer.appendPath(path, updateFrame: frame)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,8 @@ extension CompoundBezierPath {
}

extension BezierPath {
// Round corners of a single bezier
// Computes a new `BezierPath` with each corner rounded based on the given `radius`
func roundCorners(radius: CGFloat) -> BezierPath {
guard radius > 0 else {
return self
}
var newPath = BezierPath()
var uniquePath = BezierPath()

Expand Down

0 comments on commit 86a80a4

Please sign in to comment.