Interpolatable.swift 7.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
// 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: - LottieColor + Interpolatable

extension LottieColor: Interpolatable {
  public func interpolate(to: LottieColor, amount: CGFloat) -> LottieColor {
    LottieColor(
      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: - LottieVector1D + Interpolatable

extension LottieVector1D: Interpolatable {
  public func interpolate(to: LottieVector1D, amount: CGFloat) -> LottieVector1D {
    value.interpolate(to: to.value, amount: amount).vectorValue
  }
}

// MARK: - LottieVector2D + SpatialInterpolatable

extension LottieVector2D: SpatialInterpolatable {
  public func interpolate(
    to: LottieVector2D,
    amount: CGFloat,
    spatialOutTangent: CGPoint?,
    spatialInTangent: CGPoint?)
    -> LottieVector2D
  {
    pointValue.interpolate(
      to: to.pointValue,
      amount: amount,
      spatialOutTangent: spatialOutTangent,
      spatialInTangent: spatialInTangent)
      .vector2dValue
  }
}

// MARK: - LottieVector3D + SpatialInterpolatable

extension LottieVector3D: SpatialInterpolatable {
  public func interpolate(
    to: LottieVector3D,
    amount: CGFloat,
    spatialOutTangent: CGPoint?,
    spatialInTangent: CGPoint?)
    -> LottieVector3D
  {
    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 LottieVector3D(
        x: point.x,
        y: point.y,
        z: CGFloat(z.interpolate(to: to.z, amount: amount)))
    }

    return LottieVector3D(
      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)
    }
  }
}

// MARK: - Optional + Interpolatable, AnyInterpolatable

extension Optional: Interpolatable, AnyInterpolatable where Wrapped: Interpolatable {
  public func interpolate(to: Wrapped?, amount: CGFloat) -> Wrapped? {
    guard let self, let to else { return nil }
    return self.interpolate(to: to, amount: amount)
  }
}

// MARK: - Hold

/// An `Interpolatable` container that animates using "hold" keyframes.
/// The keyframes do not animate, and instead always display the value from the most recent keyframe.
/// This is necessary when passing non-interpolatable values to a method that requires an `Interpolatable` conformance.
struct Hold<T>: Interpolatable {
  let value: T

  func interpolate(to: Hold<T>, amount: CGFloat) -> Hold<T> {
    if amount < 1 {
      self
    } else {
      to
    }
  }
}