spice.js

import { Seed } from './seed.js';

const defaults = {
    duration: 0,
    delay: 0,
    from: {},
    to: {},
    easing: (t) => t, // Linear.None
    render: () => { },
    onEnd: () => { }
};

/**
 * A Spice is the most basic animatable object which properties can be interpolated from its starting
 * value(s) to its end value(s), using an easing function.
 * @example
import { Spice } from 'paprika-tween';
import { Cubic } from 'paprika-tween/easing';
const spice = new Spice({
    duration: 45,
    delay: 2,
    easing: Cubic.InOut,
    from: { size: 10 },
    to: { size: 520 },
    render: ({ size }) => {
       console.log(size);
    },
    onEnd: ({ size }) => console.log(props)
});
spice.start(0);
spice.frame(15);
 * @since 1.0.0
 */
export class Spice extends Seed {
    /**
     * Creates a new Spice instance with the given options.
     * @param {Object} options
     * @param {number} options.duration - The duration of the interpolation. The time scale should be the same as the
     * starting time and the [frame()]{@linkcode Spice#frame} time.
     * @param {number} [options.delay] - The delay time to start the interpolation.
     * @param {Object} options.from - An object with key/value pairs of numeric properties to interpolate from.
     * @param {Object} options.to - An object with the numeric properties to interpolate to.
     * @param {function} [options.easing] - The easing function with which calculate the value of the property at a given time.
     * You can use your custom function or a function available at [paprika-tween/easing]{@link module:paprika-tween/easing}.
     * Default is <code>Linear.None</code> (no easing).
     * @param {function} options.render - A callback function that will be called after each [render]{@linkcode Spice#frame}.
     * It receives three arguments: the first being an object with the properties interpolated for the given time,
     * the second the amount of interpolation applied from <code>0</code> to <code>1</code>,
     * and the third the instance of the current Spice.
     * @param {function} [options.onEnd] - Function called when the interpolation reaches the end. It receive an argument as
     * an object with the properties interpolated to its end values.
     * @since 1.0.0
     */
    constructor(options) {
        super();
        Object.assign(this, defaults, options);
    }
    /**
     * Sets the starting time of the interpolation at the given <code>time</code> argument.<br>
     * If <code>time</code> is not provided, the timestamp from
     * [performance.now()]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance/now}
     * will be used instead.
     * @param {(DOMHighResTimeStamp|number)} [time] - The initial number from where to start the animation.
     * @since 1.0.0
    * @example
import { Spice } from 'paprika-tween';
const spice = new Spice({ ... });
spice.start(5);
     */
    start(time) {
        this._startTime = time ?? performance.now();
        this._startTime += this.delay;
        this._interpolated = Object.assign(Object.create(null), this.to);
    }
    /**
     * Moves the interpolation of the properties of the spice by the given time, which is
     * relative to the starting time.<br>
     * If <code>time</code> is not provided, the timestamp from
     * [performance.now()]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance/now}
     * will be used instead.
     * @param {(DOMHighResTimeStamp|number)} [time] - The amount of time to interpolate since the animations started.
     * @since 1.0.0
    * @example
import { Spice } from 'paprika-tween';
const spice = new Spice({
    duration: 10,
    from: { width: 100 },
    to: { width: 550 },
    render: (props) => { ... }
});
spice.start(0);
spice.frame(2);
     */
    frame(time) {
        time ??= performance.now();
        let elapsed = this.elapse(time);
        // Don't render if the elapsed time has not changed
        if (this.elapsed === elapsed) {
            return;
        }
        this.elapsed = elapsed;
        const value = this.easing(elapsed);
        let start,
            end,
            key;
        for (key in this._interpolated) {
            start = this.from[key] ?? 0;
            end = this.to[key];
            this._interpolated[key] = start + (end - start) * value;
        }
        this.render(this._interpolated, value, this);
        if (elapsed === 1) {
            this.onEnd(this._interpolated, this);
        }
    }
    /**
     * Removes the interpolatable properties of the instance and its callback functions, making the instance eligible
     * for the garbage collector.
     * @since 1.0.0
     */
    dispose() {
        this.from = null;
        this.to = null;
        this.render = null;
        this.onEnd = null;
    }
    /**
     * Modifies the properties of the spice with the given object.
     * @param {Object} options - See [Spice constructor]{@linkcode Spice} for the available properties of the <code>options</code> object.
     * @returns {Spice}
     * @since 1.0.0
     */
    update(options) {
        Object.assign(this, options);
        return this;
    }
}