// 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 */ };
/**
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<arr.length;i++) {
if(arr[i] !== void 0 && last !== arr[i]) {
output.push(arr[i]);
}
last = arr[i];
}
return output;
};
/**
Returns a new array with the same entries as the source but in a random order.
@method Q._shuffle
@param {Array} arr
@returns {Array} copy or arr in shuffled order
@for Quintus
*/
Q._shuffle = function(obj) {
var shuffled = [], rand;
Q._each(obj, function(value, index, list) {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
});
return shuffled;
};
/**
Return an object's keys as a new Array
@method Q._keys
@param {Object} obj
@returns {Array}
@for Quintus
*/
Q._keys = Object.keys || function(obj) {
if(Q._isObject(obj)) { throw new TypeError('Invalid object'); }
var keys = [];
for (var key in obj) { if (Q._has(obj, key)) { keys[keys.length] = key; } }
return keys;
};
/**
Return an array in the range from start to stop
@method Q._range
@param {Integer} start
@param {Integer} stop
@param {Integer} [step]
@returns {Array}
@for Quintus
*/
Q._range = function(start,stop,step) {
step = step || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
var idIndex = 0;
/**
Return a new unique identifier
@method Q._uniqueId
@returns {Integer}
@for Quintus
*/
Q._uniqueId = function() {
return idIndex++;
};
/**
Options
Default engine options defining the paths
where images, audio and other data files should be found
relative to the base HTML file. As well as a couple of other
options.
These can be overriden by passing in options to the `Quintus()`
factory method, for example:
// Override the imagePath to default to /assets/images/
var Q = Quintus({ imagePath: "/assets/images/" });
If you follow the default convention from the examples, however,
you should be able to call `Quintus()` without any options.
Default Options
{
imagePath: "images/",
audioPath: "audio/",
dataPath: "data/",
audioSupported: [ 'mp3','ogg' ],
sound: true,
frameTimeLimit: 100
}
@property Q.options
@type Object
@for Quintus
*/
Q.options = {
imagePath: "images/",
audioPath: "audio/",
dataPath: "data/",
audioSupported: [ 'mp3','ogg' ],
sound: true,
frameTimeLimit: 100
};
if(opts) { Q._extend(Q.options,opts); }
/**
Game Loop support
By default the engine doesn't start a game loop until you actually tell it to.
Usually the loop is started the first time you call `Q.stageScene`, but if you
aren't using the `Scenes` module you can explicitly start the game loop yourself
and control **exactly** what the engine does each cycle. For example:
var Q = Quintus().setup();
var ball = new Q.Sprite({ .. });
Q.gameLoop(function(dt) {
Q.clear();
ball.step(dt);
ball.draw(Q.ctx);
});
The callback will be called with fraction of a second that has elapsed since
the last call to the loop method.
@method Q.gameLoop
@param {Function} callback
@for Quintus
*/
Q.gameLoop = function(callback) {
Q.lastGameLoopFrame = new Date().getTime();
// Short circuit the loop check in case multiple scenes
// are staged immediately
Q.loop = true;
// Keep track of the frame we are on (so that animations can be synced
// to the next frame)
Q._loopFrame = 0;
// Wrap the callback to save it and standardize the passed
// in time.
Q.gameLoopCallbackWrapper = function() {
var now = new Date().getTime();
Q._loopFrame++;
Q.loop = window.requestAnimationFrame(Q.gameLoopCallbackWrapper);
var dt = now - Q.lastGameLoopFrame;
/* Prevent fast-forwarding by limiting the length of a single frame. */
if(dt > 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...
@method Q.pauseGame
@for Quintus
*/
Q.pauseGame = function() {
if(Q.loop) {
window.cancelAnimationFrame(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 = 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
* @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<event.length;i++) {
this.on(event[i],target,callback);
}
return;
}
// Handle the case where there is no target provided,
// swapping the target and callback parameters.
if(!callback) {
callback = target;
target = null;
}
// If there's still no callback, default to the event name
if(!callback) {
callback = event;
}
// Handle case for callback that is a string, this will
// pull the callback from the target object or from this
// object.
if(Q._isString(callback)) {
callback = (target || this)[callback];
}
// To keep `Q.Evented` objects from needing a constructor,
// the `listeners` object is created on the fly as needed.
// `listeners` keeps a list of callbacks indexed by event name
// for quick lookup.
this.listeners = this.listeners || {};
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push([ target || this, callback]);
// With a provided target, the target object keeps track of
// the events it is bound to, which allows for automatic
// unbinding on destroy.
if(target) {
if(!target.binds) { target.binds = []; }
target.binds.push([this,event,callback]);
}
},
/**
Triggers an event, passing in some optional additional data about
the event.
@method trigger
@for Q.Evented
@param {String} event - name of event
@param {Object} [data] - optional data to pass to the callback
*/
trigger: function(event,data) {
// First make sure there are any listeners, then check for any listeners
// on this specific event, if not, early out.
if(this.listeners && this.listeners[event]) {
// Call each listener in the context of either the target passed into
// `on` or the object itself.
for(var i=0,len = this.listeners[event].length;i<len;i++) {
var listener = this.listeners[event][i];
listener[1].call(listener[0],data);
}
}
},
/**
Unbinds an event. Can be called with 1, 2, or 3 parameters, each
of which unbinds a more specific listener.
@method off
@for Q.Evented
@param {String} event - name of event
@param {Object} [target] - optionally limit to a specific target
@param {Function} [callback] - optionally limit to one specific callback
*/
off: function(event,target,callback) {
// Without a target, remove all teh listeners.
if(!target) {
if(this.listeners[event]) {
delete this.listeners[event];
}
} else {
// If the callback is a string, find a method of the
// same name on the target.
if(Q._isString(callback) && target[callback]) {
callback = target[callback];
}
var l = this.listeners && this.listeners[event];
if(l) {
// Loop from the end to the beginning, which allows us
// to remove elements without having to affect the loop.
for(var i = l.length-1;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<len;i++) {
var boundEvent = this.binds[i],
source = boundEvent[0],
event = boundEvent[1];
source.off(event,this);
}
}
}
});
/**
The master list of registered components, indexed in an object by name.
@property Q.components
@type Object
@for Quintus
*/
Q.components = {};
/**
Components
==============
Components are self-contained pieces of functionality that can be added onto and removed
from objects. The allow for a more dynamic functionality tree than using inheritance (i.e.
by favoring composition over inheritance) and are added and removed on the fly at runtime.
(yes, I know everything in JS is at runtime, but you know what I mean, geez)
Combining components with events makes it easy to create reusable pieces of
functionality that can be decoupled from each other.
The base class for components. These are usually not derived directly but are instead
created by calling `Q.register` to register a new component given a set of methods the
component supports. Components are created automatically when they are added to a
`Q.GameObject` with the `add` method.
Many components also define an `added` method, which is called automatically by the
`init` constructor after a component has been added to an object. This is a good time
to add event listeners on the object.
@class Q.Component
@events Q.Evented
@for Quintus
*/
Q.Evented.extend("Component",{
// Components are created when they are added onto a `Q.GameObject` entity. The entity
// is directly extended with any methods inside of an `extend` property and then the
// component itself is added onto the entity as well.
init: function(entity) {
this.entity = entity;
if(this.extend) { Q._extend(entity,this.extend); }
entity[this.name] = this;
entity.activeComponents.push(this.componentName);
if(entity.stage && entity.stage.addToList) {
entity.stage.addToList(this.componentName,entity);
}
if(this.added) { this.added(); }
},
/**
`destroy` is called automatically when a component is removed from an entity. It is
not called, however, when an entity is destroyed (for performance reasons).
It's job is to remove any methods that were added with `extend` and then remove and
debind itself from the entity. It will also call `destroyed` if the component has
a method by that name.
@method destroy
@for Q.Component
*/
destroy: function() {
if(this.extend) {
var extensions = Q._keys(this.extend);
for(var i=0,len=extensions.length;i<len;i++) {
delete this.entity[extensions[i]];
}
}
delete this.entity[this.name];
var idx = this.entity.activeComponents.indexOf(this.componentName);
if(idx !== -1) {
this.entity.activeComponents.splice(idx,1);
if(this.entity.stage && this.entity.stage.addToList) {
this.entity.stage.addToLists(this.componentName,this.entity);
}
}
this.debind();
if(this.destroyed) { this.destroyed(); }
}
});
/**
Game Objects
============
This is the base class most Quintus objects are derived from, it extends
`Q.Evented` and adds component support to an object, allowing components to
be added and removed from an object. It also defines a destroyed method
which will debind the object, remove it from it's parent (usually a scene)
if it has one, and trigger a destroyed event.
@class Q.GameObject
@extends Q.Evented
@for Quintus
*/
Q.Evented.extend("GameObject",{
/**
Simple check to see if a component already exists
on an object by searching for a property of the same name.
@method has
@for Q.GameObject
@param {String} component - name of component to test against
@returns {Boolean}
*/
has: function(component) {
return this[component] ? true : false;
},
/**
Adds one or more components to an object. Accepts either
a comma separated string or an array of strings that map
to component names.
Instantiates a new component object of the correct type
(if the component exists) and then triggers an addComponent
event.
For example:
this.add("2d, aiBounce")
Returns the object to allow chaining.
@for Q.GameObject
@method add
@param {String} components - comma separated list of components to add
@return {Object} returns this for chaining purposes
*/
add: function(components) {
components = Q._normalizeArg(components);
if(!this.activeComponents) { this.activeComponents = []; }
for(var i=0,len=components.length;i<len;i++) {
var name = components[i],
Comp = Q.components[name];
if(!this.has(name) && Comp) {
var c = new Comp(this);
this.trigger('addComponent',c);
}
}
return this;
},
/**
Removes one or more components from an object. Accepts the
same style of parameters as `add`. Triggers a delComponent event
and and calls destroy on the component.
Returns the element to allow chaining.
@for Q.GameObject
@method del
@param {String} components - comma separated list of components to remove
@return {Object} returns this for chaining purposes
*/
del: function(components) {
components = Q._normalizeArg(components);
for(var i=0,len=components.length;i<len;i++) {
var name = components[i];
if(name && this.has(name)) {
this.trigger('delComponent',this[name]);
this[name].destroy();
}
}
return this;
},
/**
Destroys the object by calling debind and removing the
object from it's parent. Will trigger a destroyed event
callback.
@for Q.GameObject
@method del
@param {String} components - comma separated list of components to remove
@return {Object} returns this for chaining purposes
*/
destroy: function() {
if(this.isDestroyed) { return; }
this.trigger('destroyed');
this.debind();
if(this.stage && this.stage.remove) {
this.stage.remove(this);
}
this.isDestroyed = true;
if(this.destroyed) { this.destroyed(); }
}
});
/**
Registers a component with the engine, making it available to `Q.GameObject`'s
This creates a new descendent class of `Q.Component` with new methods added in.
@for Quintus
@method Q.component
@param {String} name - component name
@param {Object} metehods - hash of methods for the component
*/
Q.component = function(name,methods) {
if(!methods) { return Q.components[name]; }
methods.name = name;
methods.componentName = "." + name;
return (Q.components[name] = Q.Component.extend(name + "Component",methods));
};
/**
Generic Game State object that can be used to
track of the current state of the Game, for example when the player starts
a new game you might want to keep track of their score and remaining lives:
Q.reset({ score: 0, lives: 2 });
Then in your game might want to add to the score:
Q.state.inc("score",50);
In your hud, you can listen for change events on the state to update your
display:
Q.state.on("change.score",function() { .. update the score display .. });
@class Q.GameState
@extends Q.GameObject
*/
Q.GameObject.extend("GameState",{
init: function(p) {
this.p = Q._extend({},p);
this.listeners = {};
},
/**
Resets the state to value p, triggers a reset event.
@method reset
@param {Object} p - properties to reinitialize to
*/
reset: function(p) { this.init(p); this.trigger("reset"); },
// Internal helper method to set an individual property
_triggerProperty: function(value,key) {
if(this.p[key] !== value) {
this.p[key] = value;
this.trigger("change." + key,value);
}
},
/**
Set one or more properties, trigger events on those
properties changing.
@example
Q.state.set({ lives: 5, hitPoints: 4 });
// Triggers 3 events: change.lives, change.hitPoints, change
Q.state.set("lives",5);
// Triggers 2 events: change.lives, change
@method set
@param {Object or String} properties - hash of properties to set, or property name
@param {Var} [value] - if setting 1 property, the value of that property
*/
set: function(properties,value) {
if(Q._isObject(properties)) {
Q._each(properties,this._triggerProperty,this);
} else {
this._triggerProperty(value,properties);
}
this.trigger("change");
},
/**
Increment an individual property by amount, uses set internally
@method inc
@param {String} property
@param {Integer} amount - amount to increment by
*/
inc: function(property,amount) {
this.set(property,this.get(property) + amount);
},
/**
Increment an individual property by amount, uses set internally
@method dec
@param {String} property
@param {Integer} amount - amount to decrement by
*/
dec: function(property,amount) {
this.set(property,this.get(property) - amount);
},
/**
Return an individual property
@method get
@param {String} property
@return {Var} value of the property
*/
get: function(property) {
return this.p[property];
}
});
/**
Top-level `Q.GameState` instance, generally used for global state in the game
@for Quintus
@property Q.state
@type Q.GameState
*/
Q.state = new Q.GameState();
/**
Reset the global game state
@for Quintus
@method Q.reset
*/
Q.reset = function() { Q.state.reset(); };
Q.touchDevice = ('ontouchstart' in document);
/**
Canvas Methods
The `setup` and `clear` method are the only two canvas-specific methods in
the core of Quintus. `imageData` also uses canvas but it can be used in
any type of game.
Setup will either create a new canvas element and append it
to the body of the document or use an existing one. It will then
pull out the width and height of the canvas for engine use.
It also adds a wrapper container around the element.
If the `maximize` is set to true, the canvas element is maximized
on the page and the scroll trick is used to try to get the address bar away.
The engine will also resample the game to CSS dimensions at twice pixel
dimensions if the `resampleWidth` or `resampleHeight` options are set.
TODO: add support for auto-resize w/ engine event notifications
Available options:
{
width: 320, // width of created canvas
height: 420, // height of created canvas
maximize: false // set to true to maximize to screen, "touch" to maximize on touch devices
}
@for Quintus
@method Q.setup
@param {String} [id="quintus"] - id of the canvas element to trigger quintus on
@param {Object} [options] - options hash
*/
Q.setup = function(id, options) {
if(Q._isObject(id)) {
options = id;
id = null;
}
options = options || {};
id = id || "quintus";
if(Q._isString(id)) {
Q.el = document.getElementById(id);
} else {
Q.el = id;
}
if(!Q.el) {
Q.el = document.createElement("canvas");
Q.el.width = options.width || 320;
Q.el.height = options.height || 420;
Q.el.id = id;
document.body.appendChild(Q.el);
}
var w = parseInt(Q.el.width,10),
h = parseInt(Q.el.height,10);
var maxWidth = options.maxWidth || 5000,
maxHeight = options.maxHeight || 5000,
resampleWidth = options.resampleWidth,
resampleHeight = options.resampleHeight,
upsampleWidth = options.upsampleWidth,
upsampleHeight = options.upsampleHeight;
if(options.maximize === true || (Q.touchDevice && options.maximize === 'touch')) {
document.body.style.padding = 0;
document.body.style.margin = 0;
w = Math.min(window.innerWidth,maxWidth);
h = Math.min(window.innerHeight - 5,maxHeight);
if(Q.touchDevice) {
Q.el.style.height = (h*2) + "px";
window.scrollTo(0,1);
w = Math.min(window.innerWidth,maxWidth);
h = Math.min(window.innerHeight,maxHeight);
}
} else if(Q.touchDevice) {
window.scrollTo(0,1);
}
if((upsampleWidth && w <= upsampleWidth) ||
(upsampleHeight && h <= upsampleHeight)) {
Q.el.style.height = h + "px";
Q.el.style.width = w + "px";
Q.el.width = w * 2;
Q.el.height = h * 2;
}
else if(((resampleWidth && w > 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.
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 datda 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();
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 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
});
@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() {
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);
};
}
}());