Skip to content

Commit

Permalink
Add rounded corner support to Core Animation rendering engine (airbnb…
Browse files Browse the repository at this point in the history
  • Loading branch information
calda authored and Igor Moroz committed May 22, 2024
1 parent e579768 commit 8a1dc55
Show file tree
Hide file tree
Showing 16 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 8a1dc55

Please sign in to comment.