254 lines
7.0 KiB
Swift
254 lines
7.0 KiB
Swift
// Created by Cal Stephens on 1/24/22.
|
|
// Copyright © 2022 Airbnb Inc. All rights reserved.
|
|
|
|
import CoreGraphics
|
|
|
|
// MARK: - Interpolatable
|
|
|
|
/// A type that can be interpolated between two values
|
|
public protocol Interpolatable: AnyInterpolatable {
|
|
/// Interpolates the `self` to the given number by `amount`.
|
|
/// - Parameter to: The number to interpolate to.
|
|
/// - Parameter amount: The amount to interpolate,
|
|
/// relative to 0.0 (self) and 1.0 (to).
|
|
/// `amount` can be greater than one and less than zero,
|
|
/// and interpolation should not be clamped.
|
|
///
|
|
/// ```
|
|
/// let number = 5
|
|
/// let interpolated = number.interpolateTo(10, amount: 0.5)
|
|
/// print(interpolated) // 7.5
|
|
/// ```
|
|
///
|
|
/// ```
|
|
/// let number = 5
|
|
/// let interpolated = number.interpolateTo(10, amount: 1.5)
|
|
/// print(interpolated) // 12.5
|
|
/// ```
|
|
func interpolate(to: Self, amount: CGFloat) -> Self
|
|
}
|
|
|
|
// MARK: - SpatialInterpolatable
|
|
|
|
/// A type that can be interpolated between two values,
|
|
/// additionally using optional `spatialOutTangent` and `spatialInTangent` values.
|
|
/// - If your implementation doesn't use the `spatialOutTangent` and `spatialInTangent`
|
|
/// parameters, prefer implementing the simpler `Interpolatable` protocol.
|
|
public protocol SpatialInterpolatable: AnyInterpolatable {
|
|
/// Interpolates the `self` to the given number by `amount`.
|
|
/// - Parameter to: The number to interpolate to.
|
|
/// - Parameter amount: The amount to interpolate,
|
|
/// relative to 0.0 (self) and 1.0 (to).
|
|
/// `amount` can be greater than one and less than zero,
|
|
/// and interpolation should not be clamped.
|
|
func interpolate(
|
|
to: Self,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> Self
|
|
}
|
|
|
|
// MARK: - AnyInterpolatable
|
|
|
|
/// The base protocol that is implemented by both `Interpolatable` and `SpatialInterpolatable`
|
|
/// Types should not directly implement this protocol.
|
|
public protocol AnyInterpolatable {
|
|
/// Interpolates by calling either `Interpolatable.interpolate`
|
|
/// or `SpatialInterpolatable.interpolate`.
|
|
/// Should not be implemented or called by consumers.
|
|
func _interpolate(
|
|
to: Self,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> Self
|
|
}
|
|
|
|
extension Interpolatable {
|
|
public func _interpolate(
|
|
to: Self,
|
|
amount: CGFloat,
|
|
spatialOutTangent _: CGPoint?,
|
|
spatialInTangent _: CGPoint?)
|
|
-> Self
|
|
{
|
|
interpolate(to: to, amount: amount)
|
|
}
|
|
}
|
|
|
|
extension SpatialInterpolatable {
|
|
/// Helper that interpolates this `SpatialInterpolatable`
|
|
/// with `nil` spatial in/out tangents
|
|
public func interpolate(to: Self, amount: CGFloat) -> Self {
|
|
interpolate(
|
|
to: to,
|
|
amount: amount,
|
|
spatialOutTangent: nil,
|
|
spatialInTangent: nil)
|
|
}
|
|
|
|
public func _interpolate(
|
|
to: Self,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> Self
|
|
{
|
|
interpolate(
|
|
to: to,
|
|
amount: amount,
|
|
spatialOutTangent: spatialOutTangent,
|
|
spatialInTangent: spatialInTangent)
|
|
}
|
|
}
|
|
|
|
// MARK: - Double + Interpolatable
|
|
|
|
extension Double: Interpolatable { }
|
|
|
|
// MARK: - CGFloat + Interpolatable
|
|
|
|
extension CGFloat: Interpolatable { }
|
|
|
|
// MARK: - Float + Interpolatable
|
|
|
|
extension Float: Interpolatable { }
|
|
|
|
extension Interpolatable where Self: BinaryFloatingPoint {
|
|
public func interpolate(to: Self, amount: CGFloat) -> Self {
|
|
self + ((to - self) * Self(amount))
|
|
}
|
|
}
|
|
|
|
// MARK: - CGRect + Interpolatable
|
|
|
|
extension CGRect: Interpolatable {
|
|
public func interpolate(to: CGRect, amount: CGFloat) -> CGRect {
|
|
CGRect(
|
|
x: origin.x.interpolate(to: to.origin.x, amount: amount),
|
|
y: origin.y.interpolate(to: to.origin.y, amount: amount),
|
|
width: width.interpolate(to: to.width, amount: amount),
|
|
height: height.interpolate(to: to.height, amount: amount))
|
|
}
|
|
}
|
|
|
|
// MARK: - CGSize + Interpolatable
|
|
|
|
extension CGSize: Interpolatable {
|
|
public func interpolate(to: CGSize, amount: CGFloat) -> CGSize {
|
|
CGSize(
|
|
width: width.interpolate(to: to.width, amount: amount),
|
|
height: height.interpolate(to: to.height, amount: amount))
|
|
}
|
|
}
|
|
|
|
// MARK: - CGPoint + SpatialInterpolatable
|
|
|
|
extension CGPoint: SpatialInterpolatable {
|
|
public func interpolate(
|
|
to: CGPoint,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> CGPoint
|
|
{
|
|
guard
|
|
let outTan = spatialOutTangent,
|
|
let inTan = spatialInTangent
|
|
else {
|
|
return CGPoint(
|
|
x: x.interpolate(to: to.x, amount: amount),
|
|
y: y.interpolate(to: to.y, amount: amount))
|
|
}
|
|
|
|
let cp1 = self + outTan
|
|
let cp2 = to + inTan
|
|
return interpolate(to, outTangent: cp1, inTangent: cp2, amount: amount)
|
|
}
|
|
}
|
|
|
|
// MARK: - Color + Interpolatable
|
|
|
|
extension Color: Interpolatable {
|
|
public func interpolate(to: Color, amount: CGFloat) -> Color {
|
|
Color(
|
|
r: r.interpolate(to: to.r, amount: amount),
|
|
g: g.interpolate(to: to.g, amount: amount),
|
|
b: b.interpolate(to: to.b, amount: amount),
|
|
a: a.interpolate(to: to.a, amount: amount))
|
|
}
|
|
}
|
|
|
|
// MARK: - Vector1D + Interpolatable
|
|
|
|
extension Vector1D: Interpolatable {
|
|
public func interpolate(to: Vector1D, amount: CGFloat) -> Vector1D {
|
|
value.interpolate(to: to.value, amount: amount).vectorValue
|
|
}
|
|
}
|
|
|
|
// MARK: - Vector2D + SpatialInterpolatable
|
|
|
|
extension Vector2D: SpatialInterpolatable {
|
|
public func interpolate(
|
|
to: Vector2D,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> Vector2D
|
|
{
|
|
pointValue.interpolate(
|
|
to: to.pointValue,
|
|
amount: amount,
|
|
spatialOutTangent: spatialOutTangent,
|
|
spatialInTangent: spatialInTangent)
|
|
.vector2dValue
|
|
}
|
|
}
|
|
|
|
// MARK: - Vector3D + SpatialInterpolatable
|
|
|
|
extension Vector3D: SpatialInterpolatable {
|
|
public func interpolate(
|
|
to: Vector3D,
|
|
amount: CGFloat,
|
|
spatialOutTangent: CGPoint?,
|
|
spatialInTangent: CGPoint?)
|
|
-> Vector3D
|
|
{
|
|
if spatialInTangent != nil || spatialOutTangent != nil {
|
|
// TODO Support third dimension spatial interpolation
|
|
let point = pointValue.interpolate(
|
|
to: to.pointValue,
|
|
amount: amount,
|
|
spatialOutTangent: spatialOutTangent,
|
|
spatialInTangent: spatialInTangent)
|
|
|
|
return Vector3D(
|
|
x: point.x,
|
|
y: point.y,
|
|
z: CGFloat(z.interpolate(to: to.z, amount: amount)))
|
|
}
|
|
|
|
return Vector3D(
|
|
x: x.interpolate(to: to.x, amount: amount),
|
|
y: y.interpolate(to: to.y, amount: amount),
|
|
z: z.interpolate(to: to.z, amount: amount))
|
|
}
|
|
}
|
|
|
|
// MARK: - Array + Interpolatable, AnyInterpolatable
|
|
|
|
extension Array: Interpolatable, AnyInterpolatable where Element: Interpolatable {
|
|
public func interpolate(to: [Element], amount: CGFloat) -> [Element] {
|
|
LottieLogger.shared.assert(
|
|
count == to.count,
|
|
"When interpolating Arrays, both array sound have the same element count.")
|
|
|
|
return zip(self, to).map { lhs, rhs in
|
|
lhs.interpolate(to: rhs, amount: amount)
|
|
}
|
|
}
|
|
}
|