/** * @module Core * @namespace springroll */ (function(undefined) { // classes to import var TimeUtils = include('springroll.TimeUtils'), EventDispatcher = include('springroll.EventDispatcher'), ApplicationOptions = include('springroll.ApplicationOptions'), DelayedCall = include('springroll.DelayedCall'); /** * Application is the main entry point for using SpringRoll, creating * an application allows the creation of displays and adding of module * functionality (e.g. sound, captions, etc). All timing and asynchronous * events should be handled by the Application to control the play * and pause. Any update, Ticker-type functions, should use the Applications * update event. * * var app = new Application(); * * @class Application * @extends springroll.EventDispatcher * @constructor * @param {Object} [options] The options for creating the application, * see `springroll.ApplicationOptions` for the specific options * that can be overridden and set. * @param {Function} [init=null] The callback when initialized */ var Application = function(options, init) { if (_instance) { throw "Only one Application can be opened at a time"; } _instance = this; EventDispatcher.call(this); /** * Initialization options/query string parameters, these properties are read-only * Application properties like raf, fps, don't have any affect on the options object. * @property {springroll.ApplicationOptions} options * @readOnly */ this.options = new ApplicationOptions(this, options); /** * Primary renderer for the application, for simply accessing * Application.instance.display.stage; * The first display added becomes the primary display automatically. * @property {Display} display * @public */ this.display = null; /** * Override this to do post constructor initialization * @property {Function} init */ this.init = init || null; /** * The preload progress * @property {springroll.AssetLoad} pluginLoad * @protected */ this.pluginLoad = null; // Reset the displays _displaysMap = {}; _displays = []; // Add the _tick bind _tickCallback = this._tick.bind(this); // Call any global libraries to initialize Application._plugins.forEach(function(plugin) { plugin.setup.call(_instance); }); // Options are initialized after plugins // so plugins can define their own options this.options.init(); /** * The name of the game, useful for debugging purposes * @property {String} name * @default "" */ this.name = this.options.name; //other initialization stuff too //if there are some specific properties on the options, use them to make a display //call init after handling loading up a versions file or any other needed asynchronous //stuff? setTimeout(this._preInit.bind(this), 0); }; /** * The current version of the library * @property {String} version * @static * @readOnly */ Application.version = VERSION; // Reference to the prototype var s = EventDispatcher.prototype; var p = EventDispatcher.extend(Application); /** * The collection of function references to call when initializing the application * these are registered by external modules. * @property {Array} _plugins * @private * @static */ Application._plugins = []; /** * The number of ms since the last frame update * @private * @property {int} _lastFrameTime */ var _lastFrameTime = 0, /** * The bound callback for listening to tick events * @private * @property {Function} _tickCallback */ _tickCallback = null, /** * If the current application is paused * @private * @property {Boolean} _paused */ _paused = false, /** * If the current application is enabled * @private * @property {Boolean} _enabled */ _enabled = true, /** * The id of the active requestAnimationFrame or setTimeout call. * @property {Number} _tickId * @private */ _tickId = -1, /** * If requestionAnimationFrame should be used * @private * @property {Bool} _useRAF * @default false */ _useRAF = false, /** * The number of milliseconds per frame * @property {int} _msPerFrame * @private */ _msPerFrame = 0, /** * The collection of displays * @property {Array} _displays * @private */ _displays = null, /** * The displays by canvas id * @property {Object} _displaysMap * @private */ _displaysMap = null; /** * Fired when initialization of the application is ready * @event init */ /** * The handler for the plugin progress * @event pluginProgress */ /** * Fired when initialization of the application is done * @event afterInit */ /** * Fired when before initialization of the application * @event beforeInit */ /** * Fired when an update is called, every frame update * @event update * @param {int} elasped The number of milliseconds since the last frame update */ /** * Fired when the pause state is toggled * @event pause * @param {boolean} paused If the application is now paused */ /** * When a display is added. * @event displayAdded * @param {springroll.AbstractDisplay} [display] The current display being added */ /** * When a display is removed. * @event displayRemoved * @param {string} [displayId] The display alias */ /** * Fired when the application becomes paused * @event paused */ /** * Fired when the application resumes from a paused state * @event resumed */ /** * Fired when the application is destroyed * @event destroy */ /** * Get the singleton instance of the application * @property {Application} instance * @static * @public */ var _instance = null; Object.defineProperty(Application, "instance", { get: function() { return _instance; } }); /** * The internal initialization * @method _preInit * @private */ p._preInit = function() { if (this.destroyed) return; var options = this.options; _useRAF = options.raf; options.on('raf', function(value) { _useRAF = value; }); options.on('fps', function(value) { if (typeof value != "number") return; _msPerFrame = (1000 / value) | 0; }); //add the initial display if specified if (options.canvasId && options.display) { this.addDisplay( options.canvasId, options.display, options.displayOptions ); } var tasks = []; // Add the plugin ready functions to the list // of async tasks to start-up Application._plugins.forEach(function(plugin) { if (plugin.preload) { tasks.push(plugin.preload.bind(_instance)); } }); // Run the asyncronous tasks in series this.pluginLoad = this.load(tasks, { complete: this._doInit.bind(this), progress: onPluginProgress.bind(this), autoStart: false, startAll: false }); // Manually start load this.pluginLoad.start(); }; /** * Progress handler for the plugin load * @method onPluginProgress * @private * @param {Number} progress Plugins preloaded amount from 0 - 1 */ var onPluginProgress = function(progress) { this.trigger('pluginProgress', progress); }; /** * Initialize the application * @method _doInit * @protected */ p._doInit = function() { if (this.destroyed) return; this.pluginLoad = null; this.trigger('beforeInit'); //start update loop this.paused = false; // Dispatch the init event this.trigger('init'); // Call the init function, bind to app if (this.init) this.init.call(this); this.trigger('afterInit'); }; /** * Enables at the application level which enables * and disables all the displays. * @property {Boolean} enabled * @default true */ Object.defineProperty(p, "enabled", { set: function(enabled) { _enabled = enabled; _displays.forEach(function(display) { display.enabled = enabled; }); }, get: function() { return _enabled; } }); /** * Manual pause for the entire application, this suspends * anything driving the the application update events. Include * Animator, Captions, Sound and other media playback. * @property {Boolean} paused */ Object.defineProperty(p, "paused", { get: function() { return _paused; }, set: function(value) { _paused = !!value; this.internalPaused(_paused); } }); /** * Handle the internal pause of the application * @protected * @method internalPaused * @param {Boolean} paused If the application should be paused or not */ p.internalPaused = function(paused) { this.trigger('pause', paused); this.trigger(paused ? 'paused' : 'resumed', paused); if (paused) { if (_tickId != -1) { if (_useRAF) { cancelAnimationFrame(_tickId); } else clearTimeout(_tickId); _tickId = -1; } } else { if (_tickId == -1 && _tickCallback) { _lastFrameTime = TimeUtils.now(); _tickId = _useRAF ? requestAnimFrame(_tickCallback) : setTargetedTimeout(_tickCallback); } } }; /** * Makes a setTimeout with a time based on _msPerFrame and the amount of time spent in the * current tick. * @method setTargetedTimeout * @param {Function} callback The tick function to call. * @param {int} timeInFrame=0 The amount of time spent in the current tick in milliseconds. * @private */ var setTargetedTimeout = function(callback, timeInFrame) { var timeToCall = _msPerFrame; //subtract the time spent in the frame to actually hit the target fps if (timeInFrame) timeToCall = Math.max(0, _msPerFrame - timeInFrame); return setTimeout(callback, timeToCall); }; /** * Add a display. If this is the first display added, then it will be stored as this.display. * @method addDisplay * @param {String} id The id of the canvas element, this will be used to grab the Display later * also the Display should be the one to called document.getElementById(id) * and not the application sinc we don't care about the DOMElement as this * point * @param {function} displayConstructor The function to call to create the display instance * @param {Object} [options] Optional Display specific options * @return {Display} The created display. */ p.addDisplay = function(id, displayConstructor, options) { if (_displaysMap[id]) { throw "Display exists with id '" + id + "'"; } // Creat the display var display = new displayConstructor(id, options); // Add it to the collections _displaysMap[id] = display; _displays.push(display); // Inherit the enabled state from the application display.enabled = _enabled; if (!this.display) { this.display = display; } this.trigger('displayAdded', display); return display; }; /** * Get all the displays * @property {Array} displays * @readOnly */ Object.defineProperty(p, 'displays', { get: function() { return _displays; } }); /** * Gets a specific renderer by the canvas id. * @method getDisplay * @param {String} id The id of the canvas * @return {Display} The requested display. */ p.getDisplay = function(id) { return _displaysMap[id]; }; /** * Removes and destroys a display * @method removeDisplay * @param {String} id The Display's id (also the canvas ID) */ p.removeDisplay = function(id) { var display = _displaysMap[id]; if (display) { _displays.splice(_displays.indexOf(display), 1); display.destroy(); delete _displaysMap[id]; this.trigger('displayRemoved', id); } }; /** * _tick would be bound in _tickCallback * @method _tick * @private */ p._tick = function() { if (_paused) { _tickId = -1; return; } var now = TimeUtils.now(); var elapsed = now - _lastFrameTime; _lastFrameTime = now; //trigger the update event this.trigger('update', elapsed); //then update all displays //displays may be null if a tick happens while we are in the process of destroying if (_displays) { for (var i = 0; i < _displays.length; i++) { _displays[i].render(elapsed); } } //request the next tick //request the next animation frame if (_tickCallback) { _tickId = _useRAF ? requestAnimFrame(_tickCallback) : setTargetedTimeout(_tickCallback, TimeUtils.now() - _lastFrameTime); } }; /** * Works just like `window.setTimeout` but respects the pause * state of the Application. * @method setTimeout * @param {Function} callback The callback function, passes one argument which is the DelayedCall instance * @param {int} delay The time in milliseconds or the number of frames (useFrames must be true) * @param {Boolean} [useFrames=false] If the delay is frames (true) or millseconds (false) * @param {[type]} [autoDestroy=true] If the DelayedCall object should be destroyed after completing * @return {springroll.DelayedCall} The object for pausing, restarting, destroying etc. */ p.setTimeout = function(callback, delay, useFrames, autoDestroy) { return new DelayedCall(callback, delay, false, autoDestroy, useFrames); }; /** * Works just like `window.setInterval` but respects the pause * state of the Application. * @method setInterval * @param {Function} callback The callback function, passes one argument which is the DelayedCall instance * @param {int} delay The time in milliseconds or the number of frames (useFrames must be true) * @param {Boolean} [useFrames=false] If the delay is frames (true) or millseconds (false) * @return {springroll.DelayedCall} The object for pausing, restarting, destroying etc. */ p.setInterval = function(callback, delay, useFrames) { return new DelayedCall(callback, delay, true, false, useFrames); }; /** * Destroys the application and all active displays and plugins * @method destroy */ p.destroy = function() { // Only destroy the application once if (this.destroyed) return; this.paused = true; this.trigger('destroy'); // Destroy in the reverse priority order var plugins = Application._plugins.slice().reverse(); plugins.forEach(function(plugin) { plugin.teardown.call(_instance); }); _displays.forEach(function(display) { display.destroy(); }); _displays = null; _displaysMap = null; _instance = _tickCallback = null; this.display = null; this.options.destroy(); this.options = null; s.destroy.call(this); }; /** * The toString debugging method * @method toString * @return {String} The reprsentation of this class */ p.toString = function() { return "[Application name='" + this.name + "']"; }; // Add to the name space namespace('springroll').Application = Application; }());