// 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 */ /** 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 = function Quintus(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 */ }; // 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") // 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; }; // Utility Methods // =============== // // 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 // 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. 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 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. Q._clone = function(obj) { return Q._extend({},obj); }; // Method that adds default properties onto // an object only if the key is undefined 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 Q._has = function(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); }; // Check if something is a string // NOTE: this fails for non-primitives Q._isString = function(obj) { return typeof obj === "string"; }; Q._isNumber = function(obj) { return Object.prototype.toString.call(obj) === '[object Number]'; }; // Check if something is a function Q._isFunction = function(obj) { return Object.prototype.toString.call(obj) === '[object Function]'; }; // Check if something is a function Q._isObject = function(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; }; // Check if something is a function Q._isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; // Check if something is undefined Q._isUndefined = function(obj) { return obj === void 0; }; // Removes a property from an object and returns it 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 // it is only created once. 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 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. 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. 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 remove 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; }; window.requestAnimationFrame(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... Q.pauseGame = function() { if(Q.loop) { window.cancelAnimationFrame(Q.loop); } Q.loop = null; }; // Unpause the game by restarting the requestAnimationFrame-based loop. Q.unpauseGame = function() { if(!Q.loop) { Q.lastGameLoopFrame = new Date().getTime(); Q.loop = window.requestAnimationFrame(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 */ Q.Class.prototype.isA = function(className) { return this.className === className; }; /** * Create a new Class that inherits from this class * * @method extend */ 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 @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 */ 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. 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 = document.createElement("div"); Q.wrapper.id = 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; window.addEventListener('orientationchange',function() { setTimeout(function() { window.scrollTo(0,1); }, 0); }); return Q; }; // Clear the canvas completely. 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); } }; // Return canvas image data given an Image object. 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. Q.assetTypes = { png: 'Image', jpg: 'Image', gif: 'Image', jpeg: 'Image', ogg: 'Audio', wav: 'Audio', m4a: 'Audio', mp3: 'Audio' }; // Determine the type of asset based on the lookup table above Q.assetType = function(asset) { /* Determine the lowercase extension of the file */ var fileParts = asset.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); // 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 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 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` 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); } }; 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 store the data // returned from an Ajax call. Q.loadAssetOther = function(key,src,callback,errorCallback) { var request = new XMLHttpRequest(); var fileParts = src.split("."), fileExt = fileParts[fileParts.length-1].toLowerCase(); 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 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. 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. // }); 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 loadded */ 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 // }); 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 // Good Docs where: // https://github.com/heygrady/transform/wiki/calculating-2d-matrices Q.Matrix2D = Q.Class.extend({ 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 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 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; }, // 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 ] ] 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 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 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 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 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; }, // Memory Hoggy version 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 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 property by this Matrix 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 component by this Matrix transformX: function(x,y) { return x * this.m[0] + y * this.m[1] + this.m[2]; }, // Return just the y component by this Matrix transformY: function(x,y) { return x * this.m[3] + y * this.m[4] + this.m[5]; }, // Release this Matrix to be reused release: function() { Q.matrices2d.push(this); return null; }, 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]); //ctx.setTransform(m[0],m[1],m[2],m[3],m[4],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() { 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 = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }());