/*global module:false */ // Quintus Game Engine // (c) 2012 Pascal Rettig, Cykod LLC // Quintus may be freely distributed under the MIT license or GPLv2 License. // For all details and documentation: // http://html5quintus.com // /** Quintus HTML5 Game Engine The code in `quintus.js` defines the base `Quintus()` method which create an instance of the engine. The basic engine doesn't do a whole lot - it provides an architecture for extension, a game loop, and a method for creating or binding to an exsiting canvas context. The engine has dependencies on Underscore.js and jQuery, although the jQuery dependency will be removed in the future. Most of the game-specific functionality is in the various other modules: * `quintus_input.js` - `Input` module, which allows for user input via keyboard and touchscreen * `quintus_sprites.js` - `Sprites` module, which defines a basic `Q.Sprite` class along with spritesheet support in `Q.SpriteSheet`. * `quintus_scenes.js` - `Scenes` module. It defines the `Q.Scene` class, which allows creation of reusable scenes, and the `Q.Stage` class, which handles managing a number of sprites at once. * `quintus_anim.js` - `Anim` module, which adds in support for animations on sprites along with a `viewport` component to follow the player around and a `Q.Repeater` class that can create a repeating, scrolling background. @module Quintus */ var quintusCore = function(exportTarget,key) { "use strict"; /** Top-level Quintus engine factory wrapper, creates new instances of the engine by calling: var Q = Quintus({ ... }); Any initial setup methods also all return the `Q` object, allowing any initial setup calls to be chained together. var Q = Quintus() .include("Input, Sprites, Scenes") .setup('quintus', { maximize: true }) .controls(); `Q` is used internally as the object name, and is used in most of the examples, but multiple instances of the engine on the same page can have different names. var Game1 = Quintus(), Game2 = Quintus(); @class Quintus **/ var Quintus = exportTarget[key] = function(opts) { /** A la jQuery - the returned `Q` object is actually a method that calls `Q.select`. `Q.select` doesn't do anything initially, but can be overridden by a module to allow selection of game objects. The `Scenes` module adds in the select method which selects from the default stage. var Q = Quintus().include("Sprites, Scenes"); ... Game Code ... // Set the angry property on all Enemy1 class objects to true Q("Enemy1").p({ angry: true }); @method Q @for Quintus */ var Q = function(selector,scope,options) { return Q.select(selector,scope,options); }; /** Default no-op select method. Replaced with the Quintus.Scene class @method Q.select @for Quintus */ Q.select = function() { /* No-op */ }; /** Default no-op select method. Replaced with the Quintus.Scene class Syntax for including other modules into quintus, can accept a comma-separated list of strings, an array of strings, or an array of actual objects. Example: Q.include("Input, Sprites, Scenes") @method Q.include @param {String} mod - A comma separated list of module names @return {Quintus} returns Quintus instance for chaining. @for Quintus */ Q.include = function(mod) { Q._each(Q._normalizeArg(mod),function(name) { var m = Quintus[name] || name; if(!Q._isFunction(m)) { throw "Invalid Module:" + name; } m(Q); }); return Q; }; /** An internal utility method (utility methods are prefixed with underscores) It's used to take a string of comma separated names and turn it into an `Array` of names. If an array of names is passed in, it's left as is. Example usage: Q._normalizeArg("Sprites, Scenes, Physics "); // returns [ "Sprites", "Scenes", "Physics" ] Used by `Q.include` and `Q.Sprite.add` to add modules and components, respectively. Most of these utility methods are a subset of Underscore.js, Most are pulled directly from underscore and some are occasionally optimized for speed and memory usage in lieu of flexibility. Underscore.js is (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. Underscore is freely distributable under the MIT license. http://underscorejs.org @method Q._normalizeArg @param {String or Array} arg - Either a comma separated string or an array @return {Array} array of normalized names @for Quintus */ Q._normalizeArg = function(arg) { if(Q._isString(arg)) { arg = arg.replace(/\s+/g,'').split(","); } if(!Q._isArray(arg)) { arg = [ arg ]; } return arg; }; /** Extends a destination object with a source object (modifies destination object) @method Q._extend @param {Object} dest - destination object @param {Object} source - source object @return {Object} returns the dest object @for Quintus */ Q._extend = function(dest,source) { if(!source) { return dest; } for (var prop in source) { dest[prop] = source[prop]; } return dest; }; /** Return a shallow copy of an object. Sub-objects (and sub-arrays) are not cloned. (uses extend internally) @method Q._clone @param {Object} obj - object to clone @return {Object} cloned object @for Quintus */ Q._clone = function(obj) { return Q._extend({},obj); }; /** Method that adds default properties onto an object only if the key on dest is undefined @method Q._defaults @param {Object} dest - destination object @param {Object} source - source object @return {Object} returns the dest object @for Quintus */ Q._defaults = function(dest,source) { if(!source) { return dest; } for (var prop in source) { if(dest[prop] === void 0) { dest[prop] = source[prop]; } } return dest; }; /** Shortcut for hasOwnProperty @method Q._defaults @param {Object} object - destination object @param {String} key - key to check for @return {Boolean} @for Quintus */ Q._has = function(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); }; /** Check if something is a string NOTE: this fails for non-primitives @method Q._isString @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isString = function(obj) { return typeof obj === "string"; }; /** Check if something is a number @method Q._isNumber @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isNumber = function(obj) { return Object.prototype.toString.call(obj) === '[object Number]'; }; /** Check if something is a function @method Q._isFunction @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isFunction = function(obj) { return Object.prototype.toString.call(obj) === '[object Function]'; }; /** Check if something is an Object @method Q._isObject @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isObject = function(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; }; /** Check if something is an Array @method Q._isArray @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; /** Check if something is undefined @method Q._isUndefined @param {Var} obj - object to check @return {Boolean} @for Quintus */ Q._isUndefined = function(obj) { return obj === void 0; }; /** Removes a property from an object and returns it if it exists @method Q._popProperty @param {Object} obj @param {String} property - property to pop off the object @return {Var} popped property @for Quintus */ Q._popProperty = function(obj,property) { var val = obj[property]; delete obj[property]; return val; }; /** Basic iteration method. This can often be a performance handicap when the callback iterator is created inline, as this leads to lots of functions that need to be GC'd. Better is to define the iterator as a private method so. Uses the built in `forEach` method @method Q._each @param {Array or Object} obj @param {Function iterator function, `this` is used for each object @for Quintus */ Q._each = function(obj,iterator,context) { if (obj == null) { return; } if (obj.forEach) { obj.forEach(iterator,context); } else if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { iterator.call(context, obj[i], i, obj); } } else { for (var key in obj) { iterator.call(context, obj[key], key, obj); } } }; /** Invoke the named property on each element of the array @method Q._invoke @param {Array} arr @param {String} property - property to invoke @param {Var} [arg1] @param {Var} [arg2] @for Quintus */ Q._invoke = function(arr,property,arg1,arg2) { if (arr === null) { return; } for (var i = 0, l = arr.length; i < l; i++) { arr[i][property](arg1,arg2); } }; /** Basic detection method, returns the first instance where the iterator returns truthy. @method Q._detect @param {Array or Object} obj @param {Function} iterator @param {Object} context @param {Var} [arg1] @param {Var} [arg2] @returns {Var} first truthy value @for Quintus */ Q._detect = function(obj,iterator,context,arg1,arg2) { var result; if (obj === null) { return; } if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { result = iterator.call(context, obj[i], i, arg1,arg2); if(result) { return result; } } return false; } else { for (var key in obj) { result = iterator.call(context, obj[key], key, arg1,arg2); if(result) { return result; } } return false; } }; /** Returns a new Array with entries set to the return value of the iterator. @method Q._detect @param {Array or Object} obj @param {Function} iterator @param {Object} context @returns {Array} @for Quintus */ Q._map = function(obj, iterator, context) { var results = []; if (obj === null) { return results; } if (obj.map) { return obj.map(iterator, context); } Q._each(obj, function(value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); if (obj.length === +obj.length) { results.length = obj.length; } return results; }; /** Returns a sorted copy of unique array elements with null removed @method Q._uniq @param {Array} arr @returns {Array} uniq'd sorted copy of array @for Quintus */ Q._uniq = function(arr) { arr = arr.slice().sort(); var output = []; var last = null; for(var i=0;i Q.options.frameTimeLimit) { dt = Q.options.frameTimeLimit; } callback.apply(Q,[dt / 1000]); Q.lastGameLoopFrame = now; }; Q.scheduleFrame(Q.gameLoopCallbackWrapper); return Q; }; /** Pause the entire game by canceling the requestAnimationFrame call. If you use setTimeout or setInterval in your game, those will, of course, keep on rolling... @method Q.pauseGame @for Quintus */ Q.pauseGame = function() { if(Q.loop) { Q.cancelFrame(Q.loop); } Q.loop = null; }; /** Unpause the game by restarting the requestAnimationFrame-based loop. Pause the entire game by canceling the requestAnimationFrame call. If you use setTimeout or setInterval in your game, those will, of course, keep on rolling... @method Q.pauseGame @for Quintus */ Q.unpauseGame = function() { if(!Q.loop) { Q.lastGameLoopFrame = new Date().getTime(); Q.loop = Q.scheduleFrame(Q.gameLoopCallbackWrapper); } }; /** The base Class object Quintus uses the Simple JavaScript inheritance Class object, created by John Resig and described on his blog: [http://ejohn.org/blog/simple-javascript-inheritance/](http://ejohn.org/blog/simple-javascript-inheritance/) The class is used wholesale, with the only differences being that instead of appearing in a top-level namespace, the `Class` object is available as `Q.Class` and a second argument on the `extend` method allows for adding class level methods and the class name is passed in a parameter for introspection purposes. Classes can be created by calling `Q.Class.extend(name,{ .. })`, although most of the time you'll want to use one of the derivitive classes, `Q.Evented` or `Q.GameObject` which have a little bit of functionality built-in. `Q.Evented` adds event binding and triggering support and `Q.GameObject` adds support for components and a destroy method. The main things Q.Class get you are easy inheritance, a constructor method called `init()`, dynamic addition of a this._super method when a method is overloaded (be careful with this as it adds some overhead to method calls.) Calls to `instanceof` also all work as you'd hope. By convention, classes should be added onto to the `Q` object and capitalized, so if you wanted to create a new class for your game, you'd write: Q.Class.extend("MyClass",{ ... }); Examples: Q.Class.extend("Bird",{ init: function(name) { this.name = name; }, speak: function() { console.log(this.name); }, fly: function() { console.log("Flying"); } }); Q.Bird.extend("Penguin",{ speak: function() { console.log(this.name + " the penguin"); }, fly: function() { console.log("Can't fly, sorry..."); } }); var randomBird = new Q.Bird("Frank"), pengy = new Q.Penguin("Pengy"); randomBird.fly(); // Logs "Flying" pengy.fly(); // Logs "Can't fly,sorry..." randomBird.speak(); // Logs "Frank" pengy.speak(); // Logs "Pengy the penguin" console.log(randomBird instanceof Q.Bird); // true console.log(randomBird instanceof Q.Penguin); // false console.log(pengy instanceof Q.Bird); // true console.log(pengy instanceof Q.Penguin); // true Simple JavaScript Inheritance By John Resig http://ejohn.org/ MIT Licensed. Inspired by base2 and Prototype @class Q.Class @for Quintus */ (function(){ var initializing = false, fnTest = /xyz/.test(function(){ var xyz;}) ? /\b_super\b/ : /.*/; /** The base Class implementation (does nothing) * * @constructor * @for Q.Class */ Q.Class = function(){}; /** * See if a object is a specific class * * @method isA * @param {String} className - class to check against */ Q.Class.prototype.isA = function(className) { return this.className === className; }; /** * Create a new Class that inherits from this class * * @method extend * @param {String} className * @param {Object} properties - hash of properties (init will be the constructor) * @param {Object} [classMethods] - optional class methods to add to the class */ Q.Class.extend = function(className, prop, classMethods) { /* No name, don't add onto Q */ if(!Q._isString(className)) { classMethods = prop; prop = className; className = null; } var _super = this.prototype, ThisClass = this; /* Instantiate a base class (but only create the instance, */ /* don't run the init constructor) */ initializing = true; var prototype = new ThisClass(); initializing = false; function _superFactory(name,fn) { return function() { var tmp = this._super; /* Add a new ._super() method that is the same method */ /* but on the super-class */ this._super = _super[name]; /* The method only need to be bound temporarily, so we */ /* remove it when we're done executing */ var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; } /* Copy the properties over onto the new prototype */ for (var name in prop) { /* Check if we're overwriting an existing function */ prototype[name] = typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name]) ? _superFactory(name,prop[name]) : prop[name]; } /* The dummy class constructor */ function Class() { /* All construction is actually done in the init method */ if ( !initializing && this.init ) { this.init.apply(this, arguments); } } /* Populate our constructed prototype object */ Class.prototype = prototype; /* Enforce the constructor to be what we expect */ Class.prototype.constructor = Class; /* And make this class extendable */ Class.extend = Q.Class.extend; /* If there are class-level Methods, add them to the class */ if(classMethods) { Q._extend(Class,classMethods); } if(className) { /* Save the class onto Q */ Q[className] = Class; /* Let the class know its name */ Class.prototype.className = className; Class.className = className; } return Class; }; }()); // Event Handling // ============== /** The `Q.Evented` class adds event handling onto the base `Q.Class` class. Q.Evented objects can trigger events and other objects can bind to those events. @class Q.Evented @extends Q.Class @for Quintus */ Q.Class.extend("Evented",{ /** Binds a callback to an event on this object. If you provide a `target` object, that object will add this event to it's list of binds, allowing it to automatically remove it when it is destroyed. @method on @for Q.Evented @param {String} event - name or comma separated list of events @param {Object} [target] - optional context for callback, defaults to the Evented @param {Function} [callback] - callback (optional - defaults to name of event on context */ on: function(event,target,callback) { if(Q._isArray(event) || event.indexOf(",") !== -1) { event = Q._normalizeArg(event); for(var i=0;i=0;i--) { if(l[i][0] === target) { if(!callback || callback === l[i][1]) { this.listeners[event].splice(i,1); } } } } } }, /** `debind` is called to remove any listeners an object had on other objects. The most common case is when an object is destroyed you'll want all the event listeners to be removed for you. @method debind @for Q.Evented */ debind: function() { if(this.binds) { for(var i=0,len=this.binds.length;i resampleWidth) || (resampleHeight && h > resampleHeight)) && Q.touchDevice) { Q.el.style.height = h + "px"; Q.el.style.width = w + "px"; Q.el.width = w / 2; Q.el.height = h / 2; } else { Q.el.style.height = h + "px"; Q.el.style.width = w + "px"; Q.el.width = w; Q.el.height = h; } var elParent = Q.el.parentNode; if(elParent && !Q.wrapper) { Q.wrapper = document.createElement("div"); Q.wrapper.id = Q.el.id + '_container'; Q.wrapper.style.width = w + "px"; Q.wrapper.style.margin = "0 auto"; Q.wrapper.style.position = "relative"; elParent.insertBefore(Q.wrapper,Q.el); Q.wrapper.appendChild(Q.el); } Q.el.style.position = 'relative'; Q.ctx = Q.el.getContext && Q.el.getContext("2d"); Q.width = parseInt(Q.el.width,10); Q.height = parseInt(Q.el.height,10); Q.cssWidth = w; Q.cssHeight = h; //scale to fit if(options.scaleToFit) { var factor = 1; var winW = window.innerWidth*factor; var winH = window.innerHeight*factor; var winRatio = winW/winH; var gameRatio = Q.el.width/Q.el.height; var scaleRatio = gameRatio < winRatio ? winH/Q.el.height : winW/Q.el.width; var scaledW = Q.el.width * scaleRatio; var scaledH = Q.el.height * scaleRatio; Q.el.style.width = scaledW + "px"; Q.el.style.height = scaledH + "px"; if(Q.el.parentNode) { Q.el.parentNode.style.width = scaledW + "px"; Q.el.parentNode.style.height = scaledH + "px"; } Q.cssWidth = parseInt(scaledW,10); Q.cssHeight = parseInt(scaledH,10); //center vertically when adjusting to width if(gameRatio > winRatio) { var topPos = (winH - scaledH)/2; Q.el.style.top = topPos+'px'; } } window.addEventListener('orientationchange',function() { setTimeout(function() { window.scrollTo(0,1); }, 0); }); return Q; }; /** Clear the canvas completely. If you want it cleared to a specific color - set `Q.clearColor` to that color @method Q.clear @for Quintus */ Q.clear = function() { if(Q.clearColor) { Q.ctx.globalAlpha = 1; Q.ctx.fillStyle = Q.clearColor; Q.ctx.fillRect(0,0,Q.width,Q.height); } else { Q.ctx.clearRect(0,0,Q.width,Q.height); } }; Q.setImageSmoothing = function(enabled) { Q.ctx.mozImageSmoothingEnabled = enabled; Q.ctx.webkitImageSmoothingEnabled = enabled; Q.ctx.msImageSmoothingEnabled = enabled; Q.ctx.imageSmoothingEnabled = enabled; }; /** Return canvas image data given an Image object. @method Q.imageData @for Quintus @param {Image} img - image to get image data for */ Q.imageData = function(img) { var canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; var ctx = canvas.getContext("2d"); ctx.drawImage(img,0,0); return ctx.getImageData(0,0,img.width,img.height); }; /** Asset Loading Support The engine supports loading assets of different types using `load` or `preload`. Assets are stored by their name so the same asset won't be loaded twice if it already exists. Augmentable list of asset types, loads a specific asset type if the file type matches, otherwise defaults to a Ajax load of the data. You can new types of assets based on file extension by adding to `assetTypes` and adding a method called loadAssetTYPENAME where TYPENAME is the name of the type you added in. Default bindings are: * png, jpg, gif, jpeg -> Image * ogg, wav, m4a, mp3 -> Audio * Everything else -> Data To add a new file extension in to an existing type you can just add it to asset types: Q.assetTypes['bmp'] = "Image"; To add in a new loader, you'll need to define a method for that type and add to the `Q.assetTypes` object, e.g.: Q.loadAssetVideo = function(key,src,callback,errorCallback) { var vid = new Video(); vid.addEventListener("canplaythrough",function() { callback(key,vid); }); vid.onerror = errorCallback; vid.src = Q.assetUrl(Q.options.imagePath,src); }; Q.assetTypes['mp4'] = 'Video' @for Quintus @property Q.assetTypes @type Object */ Q.assetTypes = { png: 'Image', jpg: 'Image', gif: 'Image', jpeg: 'Image', ogg: 'Audio', wav: 'Audio', m4a: 'Audio', mp3: 'Audio' }; /** Return the file extension of a filename @for Quintus @method Q._fileExtension @param {String} filename @return {String} lowercased extension */ Q._fileExtension = function(filename) { var fileParts = filename.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); return fileExt; }; /** Determine the type of asset based on the `Q.assetTypes` lookup table @for Quintus @method Q.assetType @param {String} asset */ Q.assetType = function(asset) { /* Determine the lowercase extension of the file */ var fileExt = Q._fileExtension(asset); // Use the web audio loader instead of the regular loader // if it's supported. var fileType = Q.assetTypes[fileExt]; if(fileType === 'Audio' && Q.audio && Q.audio.type === "WebAudio") { fileType = 'WebAudio'; } /* Lookup the asset in the assetTypes hash, or return other */ return fileType || 'Other'; }; /** Either return an absolute URL, or add a base to a relative URL @for Quintus @method Q.assetUrl @param {String} base - base for relative paths @param {String} url - url to resolve to asset url @return {String} resolved url */ Q.assetUrl = function(base,url) { var timestamp = ""; if(Q.options.development) { timestamp = (/\?/.test(url) ? "&" : "?") + "_t=" +new Date().getTime(); } if(/^https?:\/\//.test(url) || url[0] === "/") { return url + timestamp; } else { return base + url + timestamp; } }; /** Loader for Images, creates a new `Image` object and uses the load callback to determine the image has been loaded @for Quintus @method Q.loadAssetImage @param {String} key @param {String} src @param {Function} callback @param {Function} errorCallback */ Q.loadAssetImage = function(key,src,callback,errorCallback) { var img = new Image(); img.onload = function() { callback(key,img); }; img.onerror = errorCallback; img.src = Q.assetUrl(Q.options.imagePath,src); }; // List of mime types given an audio file extension, used to // determine what sound types the browser can play using the // built-in `Sound.canPlayType` Q.audioMimeTypes = { mp3: 'audio/mpeg', ogg: 'audio/ogg; codecs="vorbis"', m4a: 'audio/m4a', wav: 'audio/wav' }; Q._audioAssetExtension = function() { if(Q._audioAssetPreferredExtension) { return Q._audioAssetPreferredExtension; } var snd = new Audio(); /* Find a supported type */ return Q._audioAssetPreferredExtension = Q._detect(Q.options.audioSupported, function(extension) { return snd.canPlayType(Q.audioMimeTypes[extension]) ? extension : null; }); }; /** Loader for Audio assets. By default chops off the extension and will automatically determine which of the supported types is playable by the browser and load that type. Which types are available are determined by the file extensions listed in the Quintus `options.audioSupported` @for Quintus @method Q.loadAssetAudio @param {String} key @param {String} src @param {Function} callback @param {Function} errorCallback */ Q.loadAssetAudio = function(key,src,callback,errorCallback) { if(!document.createElement("audio").play || !Q.options.sound) { callback(key,null); return; } var baseName = Q._removeExtension(src), extension = Q._audioAssetExtension(), filename = null, snd = new Audio(); /* No supported audio = trigger ok callback anyway */ if(!extension) { callback(key,null); return; } snd.addEventListener("error",errorCallback); // Don't wait for canplaythrough on mobile if(!Q.touchDevice) { snd.addEventListener('canplaythrough',function() { callback(key,snd); }); } snd.src = Q.assetUrl(Q.options.audioPath,baseName + "." + extension); snd.load(); if(Q.touchDevice) { callback(key,snd); } }; /** Asset loader for Audio files if using the WebAudio API engine @for Quintus @method Q.loadAssetWebAudio @param {String} key @param {String} src @param {Function} callback @param {Function} errorCallback */ Q.loadAssetWebAudio = function(key,src,callback,errorCallback) { var request = new XMLHttpRequest(), baseName = Q._removeExtension(src), extension = Q._audioAssetExtension(); request.open("GET", Q.assetUrl(Q.options.audioPath,baseName + "." + extension), true); request.responseType = "arraybuffer"; // Our asynchronous callback request.onload = function() { var audioData = request.response; Q.audioContext.decodeAudioData(request.response, function(buffer) { callback(key,buffer); }, errorCallback); }; request.send(); }; /** Loader for other file types, just stores the data returned from an Ajax call. Just makes a Ajax request for all other file types @for Quintus @method Q.loadAssetOther @param {String} key @param {String} src @param {Function} callback @param {Function} errorCallback */ Q.loadAssetOther = function(key,src,callback,errorCallback) { var request = new XMLHttpRequest(); var fileParts = src.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); if(document.location.origin === "file://" || document.location.origin === "null") { if(!Q.fileURLAlert) { Q.fileURLAlert = true; alert("Quintus Error: Loading assets is not supported from file:// urls - please run from a local web-server and try again"); } return errorCallback(); } request.onreadystatechange = function() { if(request.readyState === 4) { if(request.status === 200) { if(fileExt === 'json') { callback(key,JSON.parse(request.responseText)); } else { callback(key,request.responseText); } } else { errorCallback(); } } }; request.open("GET", Q.assetUrl(Q.options.dataPath,src), true); request.send(null); }; /** Helper method to return a name without an extension @for Quintus @method _removeExtension @param {String} filename @return {String} filename without an extension */ Q._removeExtension = function(filename) { return filename.replace(/\.(\w{3,4})$/,""); }; // Asset hash storing any loaded assets Q.assets = {}; /** Getter method to return an asset by its name. Asset names default to their filenames, but can be overridden by passing a hash to `load` to set different names. @for Quintus @method asset @param {String} name - name of asset to lookup */ Q.asset = function(name) { return Q.assets[name]; }; /** Load assets, and call our callback when done. Also optionally takes a `progressCallback` which will be called with the number of assets loaded and the total number of assets to allow showing of a progress. Assets can be passed in as an array of file names, and Quintus will use the file names as the name for reference, or as a hash of `{ name: filename }`. Example usage: Q.load(['sprites.png','sprites.,json'],function() { Q.stageScene("level1"); // or something to start the game. }); @for Quintus @method Q.load @param {String, Array or Array} assets - comma separated string, array or Object hash of assets to load @param {Function} callback - called when done loading @param {Object} options */ Q.load = function(assets,callback,options) { var assetObj = {}; /* Make sure we have an options hash to work with */ if(!options) { options = {}; } /* Get our progressCallback if we have one */ var progressCallback = options.progressCallback; var errors = false, errorCallback = function(itm) { errors = true; (options.errorCallback || function(itm) { throw("Error Loading: " + itm ); })(itm); }; /* Convert to an array if it's a string */ if(Q._isString(assets)) { assets = Q._normalizeArg(assets); } /* If the user passed in an array, convert it */ /* to a hash with lookups by filename */ if(Q._isArray(assets)) { Q._each(assets,function(itm) { if(Q._isObject(itm)) { Q._extend(assetObj,itm); } else { assetObj[itm] = itm; } }); } else { /* Otherwise just use the assets as is */ assetObj = assets; } /* Find the # of assets we're loading */ var assetsTotal = Q._keys(assetObj).length, assetsRemaining = assetsTotal; /* Closure'd per-asset callback gets called */ /* each time an asset is successfully loaded */ var loadedCallback = function(key,obj,force) { if(errors) { return; } // Prevent double callbacks (I'm looking at you Firefox, canplaythrough if(!Q.assets[key]||force) { /* Add the object to our asset list */ Q.assets[key] = obj; /* We've got one less asset to load */ assetsRemaining--; /* Update our progress if we have it */ if(progressCallback) { progressCallback(assetsTotal - assetsRemaining,assetsTotal); } } /* If we're out of assets, call our full callback */ /* if there is one */ if(assetsRemaining === 0 && callback) { /* if we haven't set up our canvas element yet, */ /* assume we're using a canvas with id 'quintus' */ callback.apply(Q); } }; /* Now actually load each asset */ Q._each(assetObj,function(itm,key) { /* Determine the type of the asset */ var assetType = Q.assetType(itm); /* If we already have the asset loaded, */ /* don't load it again */ if(Q.assets[key]) { loadedCallback(key,Q.assets[key],true); } else { /* Call the appropriate loader function */ /* passing in our per-asset callback */ /* Dropping our asset by name into Q.assets */ Q["loadAsset" + assetType](key,itm, loadedCallback, function() { errorCallback(itm); }); } }); }; // Array to store any assets that need to be // preloaded Q.preloads = []; /** Let us gather assets to load at a later time, and then preload them all at the same time with a single callback. Options are passed through to the Q.load method if used. Example usage: Q.preload("sprites.png"); ... Q.preload("sprites.json"); ... Q.preload(function() { Q.stageScene("level1"); // or something to start the game }); @for Quintus @method Q.preload @param {String or Function} arg - comma separated string of assets to load, or callback @param {Object} [options] - options to pass to load */ Q.preload = function(arg,options) { if(Q._isFunction(arg)) { Q.load(Q._uniq(Q.preloads),arg,options); Q.preloads = []; } else { Q.preloads = Q.preloads.concat(arg); } }; // Math Methods // ============== // // Math methods, for rotating and scaling points // A list of matrices available Q.matrices2d = []; Q.matrix2d = function() { return Q.matrices2d.length > 0 ? Q.matrices2d.pop().identity() : new Q.Matrix2D(); }; /** A 2D matrix class, optimized for 2D points, where the last row of the matrix will always be 0,0,1 Do not call `new Q.Matrix2D` - use the provided Q.matrix2D factory function for GC happiness var matrix = Q.matrix2d(); Good Docs here: https://github.com/heygrady/transform/wiki/calculating-2d-matrices Used internally by Quintus for all transforms / collision detection. Most of the methods modify the matrix they are called upon and are chainable. @class Q.Matrix2D @for Quintus @extends Q.Class */ Q.Matrix2D = Q.Class.extend({ /** Initialize a matrix from a source or with the identify matrix @constructor @for Q.Matrix2D */ init: function(source) { if(source) { this.m = []; this.clone(source); } else { this.m = [1,0,0,0,1,0]; } }, /** Turn this matrix into the identity @for Q.Matrix2D @method identity @chainable */ identity: function() { var m = this.m; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 1; m[5] = 0; return this; }, /** Clone another matrix into this one @for Q.Matrix2D @method clone @param {Q.Matrix2D} matrix - matrix to clone @chainable */ clone: function(matrix) { var d = this.m, s = matrix.m; d[0]=s[0]; d[1]=s[1]; d[2] = s[2]; d[3]=s[3]; d[4]=s[4]; d[5] = s[5]; return this; }, /** multiply two matrices (leaving the result in this) a * b = [ [ a11*b11 + a12*b21 ], [ a11*b12 + a12*b22 ], [ a11*b31 + a12*b32 + a13 ] , [ a21*b11 + a22*b21 ], [ a21*b12 + a22*b22 ], [ a21*b31 + a22*b32 + a23 ] ] @for Q.Matrix2D @method clone @param {Q.Matrix2D} matrix - matrix to multiply by @chainable */ multiply: function(matrix) { var a = this.m, b = matrix.m; var m11 = a[0]*b[0] + a[1]*b[3]; var m12 = a[0]*b[1] + a[1]*b[4]; var m13 = a[0]*b[2] + a[1]*b[5] + a[2]; var m21 = a[3]*b[0] + a[4]*b[3]; var m22 = a[3]*b[1] + a[4]*b[4]; var m23 = a[3]*b[2] + a[4]*b[5] + a[5]; a[0]=m11; a[1]=m12; a[2] = m13; a[3]=m21; a[4]=m22; a[5] = m23; return this; }, /** Multiply this matrix by a rotation matrix rotated radians radians @for Q.Matrix2D @method rotate @param {Float} radians - angle to rotate by @chainable */ rotate: function(radians) { if(radians === 0) { return this; } var cos = Math.cos(radians), sin = Math.sin(radians), m = this.m; var m11 = m[0]*cos + m[1]*sin; var m12 = m[0]*-sin + m[1]*cos; var m21 = m[3]*cos + m[4]*sin; var m22 = m[3]*-sin + m[4]*cos; m[0] = m11; m[1] = m12; // m[2] == m[2] m[3] = m21; m[4] = m22; // m[5] == m[5] return this; }, /** Helper method to rotate by a set number of degrees (calls rotate internally) @for Q.Matrix2D @method rotateDeg @param {Float} degrees @chainable */ rotateDeg: function(degrees) { if(degrees === 0) { return this; } return this.rotate(Math.PI * degrees / 180); }, /** Multiply this matrix by a scaling matrix scaling sx and sy @for Q.Matrix2D @method scale @param {Float} sx - scale in x dimension (scaling is uniform unless `sy` is provided) @param {Float} [sy] - scale in the y dimension @chainable */ scale: function(sx,sy) { var m = this.m; if(sy === void 0) { sy = sx; } m[0] *= sx; m[1] *= sy; m[3] *= sx; m[4] *= sy; return this; }, /** Multiply this matrix by a translation matrix translate by tx and ty @for Q.Matrix2D @method translate @param {Float} tx @param {Float} ty @chainable */ translate: function(tx,ty) { var m = this.m; m[2] += m[0]*tx + m[1]*ty; m[5] += m[3]*tx + m[4]*ty; return this; }, /** Transform x and y coordinates by this matrix Memory Hoggy version, returns a new Array @for Q.Matrix2D @method transform @param {Float} x @param {Float} y */ transform: function(x,y) { return [ x * this.m[0] + y * this.m[1] + this.m[2], x * this.m[3] + y * this.m[4] + this.m[5] ]; }, /** Transform an object with an x and y property by this Matrix @for Q.Matrix2D @method transformPt @param {Object} obj @return {Object} obj */ transformPt: function(obj) { var x = obj.x, y = obj.y; obj.x = x * this.m[0] + y * this.m[1] + this.m[2]; obj.y = x * this.m[3] + y * this.m[4] + this.m[5]; return obj; }, /** Transform an array with an x and y elements by this Matrix and put the result in the outArr @for Q.Matrix2D @method transformArr @param {Array} inArr - input array @param {Array} outArr - output array @return {Object} obj */ transformArr: function(inArr,outArr) { var x = inArr[0], y = inArr[1]; outArr[0] = x * this.m[0] + y * this.m[1] + this.m[2]; outArr[1] = x * this.m[3] + y * this.m[4] + this.m[5]; return outArr; }, /** Return just the x coordinate transformed by this Matrix @for Q.Matrix2D @method transformX @param {Float} x @param {Float} y @return {Float} x transformed */ transformX: function(x,y) { return x * this.m[0] + y * this.m[1] + this.m[2]; }, /** Return just the y coordinate transformed by this Matrix @for Q.Matrix2D @method transformY @param {Float} x @param {Float} y @return {Float} y transformed */ transformY: function(x,y) { return x * this.m[3] + y * this.m[4] + this.m[5]; }, /** Release this Matrix to be reused @for Q.Matrix2D @method release */ release: function() { Q.matrices2d.push(this); return null; }, /** Set the complete transform on a Canvas 2D context @for Q.Matrix2D @method setContextTransform @param {Context2D} ctx - 2D canvs context */ setContextTransform: function(ctx) { var m = this.m; // source: // m[0] m[1] m[2] // m[3] m[4] m[5] // 0 0 1 // // destination: // m11 m21 dx // m12 m22 dy // 0 0 1 // setTransform(m11, m12, m21, m22, dx, dy) ctx.transform(m[0],m[3],m[1],m[4],m[2],m[5]); } }); // And that's it.. // =============== // // Return the `Q` object from the `Quintus()` factory method. Create awesome games. Repeat. return Q; }; // Lastly, add in the `requestAnimationFrame` shim, if necessary. Does nothing // if `requestAnimationFrame` is already on the `window` object. (function() { if (typeof window === 'undefined') { return; } var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }()); return Quintus; }; if(typeof exports === 'undefined') { quintusCore(this,"Quintus"); } else { var Quintus = quintusCore(module,"exports"); }