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);
};
};