Repository: airbnb/lottie-ios
File: Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift (Swift)

Code before:

    let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes)
    let endInterpolator = KeyframeInterpolator(keyframes: end.keyframes)

    for keyframeTime in keyframeTimes {
      guard
        let startAtTime = startInterpolator.value(frame: keyframeTime) as? Vector1D,
        let endAtTime = endInterpolator.value(frame: keyframeTime) as? Vector1D
      else { continue }

      if startAtTime.cgFloatValue < endAtTime.cgFloatValue {
        return false
      }
    }

    return true
  }

  /// Adjusts a `KeyframeGroup` associated with a `strokeStart` or `strokeEnd` to account
  /// for trim offsets. The CoreAnimation rendering engine does not fully support trim offsets. If applying
  /// the offsets results in a `KeyframeGroup` with all values in the range `[0, 1]` then the offsets
  /// can be supported.
  private func adjustKeyframesForTrimOffsets(
    strokeKeyframes: ContiguousArray<Keyframe<Vector1D>>,
    offsetKeyframes: ContiguousArray<Keyframe<Vector1D>>,
    context _: LayerAnimationContext) throws -> ContiguousArray<Keyframe<Vector1D>> {
    guard
      !offsetKeyframes.isEmpty,
      offsetKeyframes.contains(where: { $0.value.cgFloatValue != 0 })
    else {
      return strokeKeyframes
    }

    // Map each keyframe time to its associated stroke/offset
    var timeMap = [AnimationFrameTime: [Keyframe<Vector1D>?]]()
    for stroke in strokeKeyframes {
      timeMap[stroke.time] = [stroke, nil]
    }
    for offset in offsetKeyframes {
      if var existing = timeMap[offset.time] {
        existing[1] = offset
        timeMap[offset.time] = existing
      } else {
        timeMap[offset.time] = [nil, offset]
      }
    }

    // Each time will be mapped to a new, adjusted keyframe
    var output = ContiguousArray<Keyframe<Vector1D>>()
    var lastKeyframe: Keyframe<Vector1D>?
    var lastOffset: Keyframe<Vector1D>?

Review comment (suggestion):
Interesting, so this is basically combining the `trim.strokeStart` / `trim.strokeEnd` keyframes with the `offset` keyframes? Good to know. Could you add some discussion about this as a doc comment (discussion about why we have to combine these two values into a single set of keyframes would be super valuable!)

Code after:
      strokeKeyframes: interpolatedStrokeEnd,
      offsetKeyframes: interpolatedStrokeOffset,
      context: context)

    // If maximum stroke value is larger than 100%, then we have to create copies of the path
    // so the total path length includes the maximum stroke
    let maximumStroke = adjustedStrokeEnd.map { $0.value.cgFloatValue }.max() ?? 100
    let pathMultiplier = Int(ceil(maximumStroke / 100.0))

    return (
      strokeStart: KeyframeGroup<Vector1D>(keyframes: adjustedStrokeStart),
      strokeEnd: KeyframeGroup<Vector1D>(keyframes: adjustedStrokeEnd),
      pathMultiplier: pathMultiplier)
  }

  // MARK: Private

  /// Checks whether or not the value for `trim.start` is greater
  /// than the value for every `trim.end` at every keyframe.
  private func startValueIsAlwaysGreaterThanEndValue() -> Bool {
    let keyframeTimes = Set(start.keyframes.map { $0.time } + end.keyframes.map { $0.time })

    let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes)
    let endInterpolator = KeyframeInterpolator(keyframes: end.keyframes)

    for keyframeTime in keyframeTimes {
      guard
        let startAtTime = startInterpolator.value(frame: keyframeTime) as? Vector1D,
        let endAtTime = endInterpolator.value(frame: keyframeTime) as? Vector1D
      else { continue }

      if startAtTime.cgFloatValue < endAtTime.cgFloatValue {
        return false
      }
    }

    return true
  }

  /// Adjusted stroke keyframes to account for offset keyframes by merging them into a single keyframe collection
  ///
  /// Since stroke keyframes and offset keyframes can be defined on different animation curves, they must be
  /// manually interpolated prior to invoking this method. Manually interpolating the keyframes will redefine both
  /// keyframe groups such that they can be interpolated linearly.
  ///
  /// - Precondition: The keyframes must be interpolated using `KeyframeGroup.manuallyInterpolateKeyframes()`
  private func adjustKeyframesForTrimOffsets(
    strokeKeyframes: ContiguousArray<Keyframe<Vector1D>>,
    offsetKeyframes: ContiguousArray<Keyframe<Vector1D>>,
    context _: LayerAnimationContext) throws -> ContiguousArray<Keyframe<Vector1D>>
  {