Skip to content

Instantly share code, notes, and snippets.

@IceCreamYou
Created September 5, 2016 08:01
Show Gist options
  • Save IceCreamYou/6b16d8350bc4125a08c76b499e0d4c59 to your computer and use it in GitHub Desktop.
Save IceCreamYou/6b16d8350bc4125a08c76b499e0d4c59 to your computer and use it in GitHub Desktop.
Extends MainLoop.js to support multiple loops.
/*
* Adds the ability to have multiple main loops to MainLoop.js.
*
* The functionality in this file modifies the MainLoop object.
*/
(function(root) {
// All the active MainLoop instances.
var instances = [],
// The default behavior for each hook is to do nothing. Using an empty
// function is slightly faster than a conditional existence check.
noop = function() {},
/**
* The MainLoop Instance class.
*
* Use this class when you need multiple main loops, for example when you
* are independently animating multiple canvases or if you are writing a
* library and your users want to use MainLoop to do things that your
* library does not cover. You can also use this if you have multiple
* modules that need to act during the main loop.
*
* Using this class will be slightly slower than just using one main loop.
* Additionally, using this class increases the chance of mistakes due to
* multiple loops touching shared state. Shared state is not always
* obvious. One example is that the `panic` parameter to the `update`
* callback is shared, so if multiple loops respond to it (for example, by
* prompting the user) the result may be unintended. For more discussion,
* see this thread: https://github.com/IceCreamYou/MainLoop.js/issues/5
*
* Note that MainLoop Instances do not have all the same capabilities as
* the root MainLoop object because the MainLoop object manages state that
* is global for everything in the active tab. Methods on the MainLoop
* object affect all MainLoop Instances.
*
* @class Instance
* @member MainLoop
*/
Instance = {
/**
* Starts this main loop.
*
* If the loop is already started, calling this method does nothing.
*
* See also `Instance#stop()`, `MainLoop.start()`, and
* `Instance#isRunning()`.
*/
start: function() {
if (!this.isRunning()) {
instances.push(this);
}
return this;
},
/**
* Stops this main loop.
*
* If the loop is already stopped, calling this method does nothing.
*
* See also `Instance#start()`, `MainLoop.stop()`, and
* `Instance#isRunning()`.
*/
stop: function() {
for (var i = instances.length - 1; i >= 0; i--) {
if (instances[i] === this) {
instances.splice(i, 1);
}
}
return this;
},
/**
* Returns whether this main loop is currently running.
*
* Note that it is possible for this method to return `true` while
* `MainLoop.isRunning()` returns `false` during the frame when the
* root main loop is started. See `MainLoop.isRunning()` for more
* detail on this and how to work around it if needed.
*
* See also `Instance#start()` and `Instance#stop()`.
*
* @return {Boolean}
* Whether this main loop is currently running.
*/
isRunning: function() {
return instances.indexOf(this) > -1;
},
// Instance callbacks. See the MainLoop documentation for explanations.
begin: noop,
update: noop,
draw: noop,
end: noop,
// Setters for the Instance callbacks. See the documentation for the
// corresponding functions on the MainLoop object for explanations.
setBegin: function(callback) { this.begin = callback; return this; },
setUpdate: function(callback) { this.update = callback; return this; },
setDraw: function(callback) { this.draw = callback; return this; },
setEnd: function(callback) { this.end = callback; return this; },
};
/**
* Creates a new main loop.
*
* MainLoop instance functionality can be used as a mixin by passing a
* prototype or instance of another class as the first parameter to this
* function.
*
* Example usage:
*
* ```javascript
* var ctx1 = document.getElementById('canvas1').getContext('2d'),
* ctx2 = document.getElementById('canvas2').getContext('2d');
*
* // Create a loop and do something in the "draw" hook
* var loop1 = MainLoop.createInstance().setDraw(function(timestep) {
* ctx1.fillRect((Math.cos(Date.now()/5) + 1) * ctx1.canvas.width - 10, 0, 10, 10);
* }).start();
*
* // Create a class that displays on a second canvas
* function Thing() {
* this.x = 0;
* this.y = 0;
* this.start();
* }
* MainLoop.createInstance(Thing.prototype);
* Thing.prototype.draw = function() {
* ctx2.fillRect(x - 5, y - 5, 10, 10);
* };
* var t = new Thing();
* ```
*
* @param {Object} [obj]
* If `null` or not specified, a new main loop will be created. Otherwise,
* the functionality of a main loop instance will be mixed into the passed
* object. This makes it easy to have classes that automatically register
* themselves with the main loop.
* @param {Object} [MainLoop]
* If this function is imported using a module system, it won't be able to
* find the MainLoop object on its own. Instead, the MainLoop object needs to
* be passed into this function at least once so that instances can register
* themselves to be called during the tab's render loop.
*
* @return {Object}
* A MainLoop Instance.
*
* @member Instance
*/
function createMainLoopInstance(obj, MainLoop) {
// Configure the MainLoop object if needed.
if (typeof MainLoop !== 'undefined') {
setUpMainLoop(MainLoop);
}
// Create a new object to be the MainLoop instance or mix in the
// functionality into an existing object if available.
if (!(obj instanceof Object) || obj === null) {
obj = {};
}
for (var key in Instance) {
if (Instance.hasOwnProperty(key)) {
obj[key] = Instance[key];
}
}
return obj;
}
/**
* Configure the MainLoop object to manage multiple loops.
*
* This modifies the MainLoop object.
*
* @param {Object} MainLoop
* The MainLoop object to modify.
*/
function setUpMainLoop(MainLoop) {
// Don't do anything if the MainLoop object has already been configured.
if (typeof MainLoop.setBegin === 'undefined') {
return;
}
// Run the instance callbacks during each hook.
MainLoop.setBegin(function(timestamp, frameDelta) {
for (var i = 0, l = instances.length; i < l; i++) instances[i].begin(timestamp, frameDelta);
});
MainLoop.setUpdate(function(timestep) {
for (var i = 0, l = instances.length; i < l; i++) instances[i].update(timestep);
});
MainLoop.setDraw(function(timestep) {
for (var i = 0, l = instances.length; i < l; i++) instances[i].draw(timestep);
});
MainLoop.setEnd(function(fps, panic) {
for (var i = 0, l = instances.length; i < l; i++) instances[i].end(fps, panic);
});
// Remove the hooks on the MainLoop object so that the ability to register
// instances is not overwritten.
delete MainLoop.setBegin;
delete MainLoop.setUpdate;
delete MainLoop.setDraw;
delete MainLoop.setEnd;
// Attach the instance creation function.
MainLoop.createInstance = createMainLoopInstance;
// Start the loop so that when instances call their `start` methods, their
// callbacks start getting called.
MainLoop.start();
}
// If we can detect the MainLoop object, go ahead and configure it.
if (typeof root.MainLoop !== 'undefined') {
setUpMainLoop(root.MainLoop);
}
// Support AMD.
if (typeof define === 'function' && define.amd) {
define(createMainLoopInstance);
}
// Support CommonJS.
else if (typeof module === 'object' && module !== null && typeof module.exports === 'object') {
module.exports = createMainLoopInstance;
}
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment