interpolate.js

import { Tweenable, composeEasingObject, tweenProps } from './tweenable'
/** @typedef {import("./index").shifty.easingFunction} shifty.easingFunction */

// Fake a Tweenable and patch some internals.  This approach allows us to
// skip uneccessary processing and object recreation, cutting down on garbage
// collection pauses.
const mockTweenable = new Tweenable()
const { filters } = Tweenable

/**
 * Compute the midpoint of two Objects.  This method effectively calculates a
 * specific frame of animation that {@link Tweenable#tween} does many times
 * over the course of a full tween.
 *
 * ```
 * import { interpolate } from 'shifty';
 *
 * const interpolatedValues = interpolate({
 *     width: '100px',
 *     opacity: 0,
 *     color: '#fff'
 *   }, {
 *     width: '200px',
 *     opacity: 1,
 *     color: '#000'
 *   },
 *   0.5
 * );
 *
 * console.log(interpolatedValues); // Logs: {opacity: 0.5, width: "150px", color: "rgb(127,127,127)"}
 * ```
 *
 * @method shifty.interpolate
 * @template T
 * @param {T} from The starting values to tween from.
 * @param {T} to The ending values to tween to.
 * @param {number} position The normalized position value (between `0.0` and
 * `1.0`) to interpolate the values between `from` and `to` for.  `from`
 * represents `0` and `to` represents `1`.
 * @param {Record<string, string | shifty.easingFunction> | string | shifty.easingFunction} easing
 * The easing curve(s) to calculate the midpoint against.  You can
 * reference any easing function attached to {@link Tweenable.formulas},
 * or provide the {@link shifty.easingFunction}(s) directly.  If omitted, this
 * defaults to "linear".
 * @param {number} [delay=0] Optional delay to pad the beginning of the
 * interpolated tween with.  This increases the range of `position` from (`0`
 * through `1`) to (`0` through `1 + delay`).  So, a delay of `0.5` would
 * increase all valid values of `position` to numbers between `0` and `1.5`.
 * @return {T}
 */
export const interpolate = (from, to, position, easing, delay = 0) => {
  const current = { ...from }
  const easingObject = composeEasingObject(from, easing)

  mockTweenable._filters.length = 0

  mockTweenable.set({})
  mockTweenable._currentState = current
  mockTweenable._originalState = from
  mockTweenable._targetState = to
  mockTweenable._easing = easingObject

  for (const name in filters) {
    if (filters[name].doesApply(mockTweenable)) {
      mockTweenable._filters.push(filters[name])
    }
  }

  // Any defined value transformation must be applied
  mockTweenable._applyFilter('tweenCreated')
  mockTweenable._applyFilter('beforeTween')

  const interpolatedValues = tweenProps(
    position,
    current,
    from,
    to,
    1,
    delay,
    easingObject
  )

  // Transform data in interpolatedValues back into its original format
  mockTweenable._applyFilter('afterTween')

  return interpolatedValues
}