API Docs for: 0.1
Show:

File: js/timing.js

/*
 * This code is licensed under the GPL-2 licence.
 */

/**
 * Consumes timing information and runs handlers bound to animation
 * labels.
 *
 * @author Simon Potter
 * @class TimingManager
 * @constructor
 * @param {Array|Object} timingInfo Timing information, as
 * exported by the [animaker package](https://github.com/pmur002/animaker) in [R](http://www.r-project.org/). For more information, refer to `?export` in R once animaker has loaded.
 * @param {String} [timeUnit="ms"] Determine what time units the
 * timing information exported from animaker refers to. Must be one of
 * `"ms"` (milliseconds), `"s"` (seconds), or `"m"` (minutes).
 */
var TimingManager = function(timingInfo, timeUnit) {
    // If we've just exported a single animation, force it
    // to be an array to generalise the rest of the code to arrays.
    if (! _.isArray(timingInfo))
        timingInfo = [timingInfo];

    // Assume milliseconds by default, as that's natural in JS
    timeUnit = timeUnit || "ms";
    if (! (_.isString(timeUnit) &&
           _.contains(["ms", "s", "m"], timeUnit)))
        throw new Error("Invalid 'timeUnit': Must be one of 'ms', 's', 'm'");

    // Where all our animation actions will be stored
    var callbacks = {};
    // When playing back, store promises to display here
    var promises = [];

    // Converts a unit of time to milliseconds
    var toMs = function(t) {
        if (timeUnit === "ms")
            return t;
        if (timeUnit === "s")
            return t * 1000;
        if (timeUnit === "m")
            return t * 60 * 1000;
        throw new Error("Invalid 'timeUnit': " + timeUnit);
    };

    /**
     * Registers an action to an animation
     *
     * @method register
     * @param {Object} fns An object where the keys are the labels for an animation, and the values are a function to register as an action to that animation
     * @param {Boolean} [overwrite=false] Allows us to overwrite existing actions for animations.
     */
    this.register = function(fns, overwrite) {
        for (var f in fns) {
            if (! callbacks[f] || overwrite)
                callbacks[f] = fns[f];
        }
    };

    // Checks whether we have any actions associated with animations
    var ensureNonEmpty = function() {
        if (_.isEmpty(callbacks))
            throw new Error("No actions assigned to animations, see 'register()'");
    };

    /**
     * Plays all animations associated with actions
     *
     * @method play
     * @param {Number} [t=0] An optional delay (in `timeUnit`s) to add to the entire animation.
     */
    this.play = function(t) {
        ensureNonEmpty();
        t = t || 0; // Default to 0 ms
        _.each(timingInfo, function(anim) {
            if (callbacks[anim.label]) {
                promises.push(Promise.delay(t + toMs(anim.start))
                    .cancellable()
                    .then(function() {
                        callbacks[anim.label](anim);
                    })
                    .catch(Promise.CancellationError, function(e) {
                        e = null;
                    }));
            } else {
                console.warn("Ignoring playback of animation: " + anim.label);
            }
        });
        Promise.all(promises);
    };

    /**
     * Cancels all pending animations that are queued by the timing manager.
     *
     * @method cancel
     */
    this.cancel = function() {
        _.each(promises, function(p) {
            p.cancel();
        });
        promises = [];
    };

    /**
     * Returns all of the timing information about the frames that are to be played at a given time.
     *
     * @method frameTiming
     * @param {Number} [t=0] The time in *milliseconds* to select an animation from
     * @return {Array} A list of matching animations to play at the current time
     */
    this.frameTiming = function(t) {
        t = t || 0; // Default to 0
        return _.filter(timingInfo, function(info) {
            return (t >= toMs(info.start)) &&
                    (t < toMs(info.start + info.durn));
        });
    };

    /**
     * Plays all animations associated with actions at a given rate per second.
     *
     * @method frameApply
     * @param {Number} [fps=10] How many frames per second are going to be drawn. By default this is 10.
     * @param {Number} [t=0] An optional delay (in `timeUnit`s) to add to the entire animation
     */
    this.frameApply = function(fps, t) {
        ensureNonEmpty();
        t = t || 0;
        t = toMs(t);
        fps = fps || 10;
        if (fps <= 0)
            throw new Error("Frames per second must be > 0");
        var increment = 1000 / fps;
        var durn = 0;
        _.each(timingInfo, function(info) {
            durn = Math.max(durn, toMs(info.start + info.durn));
        });
        var times = [];
        var i;
        for (i = 0; (i * increment) < durn; i++) {
            times.push(t + (i * increment));
        }

        // Do the playback after a delay in ms
        var playFrame = function(anim, t) {
            if (callbacks[anim.label]) {
                promises.push(Promise.delay(t)
                    .cancellable()
                    .then(function() {
                        callbacks[anim.label](anim);
                    })
                    .catch(Promise.CancellationError, function(e) {
                        e = null;
                    }));
            } else {
                console.warn("Ignoring playback of animation: " + anim.label);
            }
        };

        // A convenience generator function for playing back timing information
        var singleTiming = function(t) {
            return function(info) {
                playFrame(info, t);
            };
        };

        // Play each frame
        for (i = t; i < _.last(times); i += increment) {
            var currentTiming = this.frameTiming(i);
            if (currentTiming) {
                _.each(currentTiming, singleTiming(i));
            }
        }

        Promise.all(promises);
    };
};