topical media & game development
mobile-game-ch13-quintus.js / js
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
(function(){
var initializing = false,
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// 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]) ?
(function(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;
};
})(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 = arguments.callee;
return Class;
};
})();
(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);
};
}());
var Quintus = function(opts) {
var Q = {};
// Some base options to be filled in later
Q.options = {
imagePath: "mobile-game-ch13-images-",
audioPath: "mobile-game-ch13-audio-",
dataPath: "mobile-game-ch13-data-",
audioSupported: [ 'mp3','ogg' ],
sound: true
};
if(opts) { _(Q.options).extend(opts); }
Q._normalizeArg = function(arg) {
if(_.isString(arg)) {
arg = arg.replace(/\s+/g,'').split(",");
}
if(!_.isArray(arg)) {
arg = [ arg ];
}
return arg;
};
// Shortcut to extend Quintus with new functionality
// binding the methods to Q
Q.extend = function(obj) {
_(Q).extend(obj);
return Q;
};
// Syntax for including other modules into quintus
Q.include = function(mod) {
_.each(Q._normalizeArg(mod),function(m) {
m = Quintus[m] || m;
m(Q);
});
return Q;
};
Q.gameLoop = function(callback) {
Q.lastGameLoopFrame = new Date().getTime();
Q.gameLoopCallbackWrapper = function(now) {
Q.loop = requestAnimationFrame(Q.gameLoopCallbackWrapper);
var dt = now - Q.lastGameLoopFrame;
if(dt > 100) { dt = 100; }
callback.apply(Q,[dt / 1000]);
Q.lastGameLoopFrame = now;
};
requestAnimationFrame(Q.gameLoopCallbackWrapper);
};
Q.pauseGame = function() {
if(Q.loop) {
cancelAnimationFrame(Q.loop);
}
Q.loop = null;
};
Q.unpauseGame = function() {
if(!Q.loop) {
Q.lastGameLoopFrame = new Date().getTime();
Q.loop = requestAnimationFrame(Q.gameLoopCallbackWrapper);
}
}
Q.Evented = Class.extend({
bind: function(event,target,callback) {
// Handle the case where there is no target provided
if(!callback) {
callback = target;
target = null;
}
// Handle case for callback that is a string
if(_.isString(callback)) {
callback = target[callback];
}
this.listeners = this.listeners || {};
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push([ target || this, callback]);
if(target) {
if(!target.binds) { target.binds = []; }
target.binds.push([this,event,callback]);
}
},
trigger: function(event,data) {
if(this.listeners && this.listeners[event]) {
for(var i=0,len = this.listeners[event].length;i<len;i++) {
var listener = this.listeners[event][i];
listener[1].call(listener[0],data);
}
}
},
unbind: function(event,target,callback) {
if(!target) {
if(this.listeners[event]) {
delete this.listeners[event];
}
} else {
var l = this.listeners && this.listeners[event];
if(l) {
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: 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.unbind(event,this);
}
}
}
});
Q.components = {};
Q.register = function(name,methods) {
methods.name = name;
Q.components[name] = Q.Component.extend(methods);
};
Q.Component = Q.Evented.extend({
init: function(entity) {
this.entity = entity;
if(this.extend) _.extend(entity,this.extend);
entity[this.name] = this;
entity.activeComponents.push(this.name);
if(this.added) this.added();
},
destroy: function() {
if(this.extend) {
var extensions = _.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.name);
if(idx != -1) {
this.entity.activeComponents.splice(idx,1);
}
this.debind();
if(this.destroyed) this.destroyed();
}
});
Q.GameObject = Q.Evented.extend({
has: function(component) {
return this[component] ? true : false;
},
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;
},
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;
},
destroy: function() {
if(this.destroyed) { return; }
this.debind();
if(this.parent && this.parent.remove) {
this.parent.remove(this);
}
this.trigger('removed');
this.destroyed = true;
}
});
Q.setup = function(id, options) {
var touchDevice = 'ontouchstart' in document;
options = options || {};
id = id || "quintus";
Q.el = $(_.isString(id) ? "#" + id : id);
if(Q.el.length === 0) {
Q.el = $("<canvas width='320' height='420'></canvas>")
.attr('id',id).appendTo("body");
}
var maxWidth = options.maxWidth || 5000,
maxHeight = options.maxHeight || 5000,
resampleWidth = options.resampleWidth,
resampleHeight = options.resampleHeight;
if(options.maximize) {
$("html, body").css({ padding:0, margin: 0 });
var w = Math.min(window.innerWidth,maxWidth);
var h = Math.min(window.innerHeight - 5,maxHeight)
if(touchDevice) {
Q.el.css({height: h * 2});
window.scrollTo(0,1);
w = Math.min(window.innerWidth,maxWidth);
h = Math.min(window.innerHeight - 5,maxHeight);
}
if(((resampleWidth && w > resampleWidth) ||
(resampleHeight && h > resampleHeight)) &&
touchDevice) {
Q.el.css({ width:w, height:h })
.attr({ width:w/2, height:h/2 });
} else {
Q.el.css({ width:w, height:h })
.attr({ width:w, height:h });
}
}
Q.wrapper = Q.el
.wrap("<div id='" + id + "_container'/>")
.parent()
.css({ width: Q.el.width(),
margin: '0 auto' });
Q.el.css('position','relative');
Q.ctx = Q.el[0].getContext &&
Q.el[0].getContext("2d");
if(touchDevice) { window.scrollTo(0,1); }
Q.width = parseInt(Q.el.attr('width'),10);
Q.height = parseInt(Q.el.attr('height'),10);
window.bind('orientationchange',function() {
setTimeout(function() { window.scrollTo(0,1); }, 0);
});
return Q;
};
Q.clear = function() {
Q.ctx.clearRect(0,0,Q.el[0].width,Q.el[0].height);
};
// Augmentable list of asset types
Q.assetTypes = {
// Image Assets
png: 'Image', jpg: 'Image', gif: 'Image', jpeg: 'Image',
// Audio Assets
ogg: 'Audio', wav: 'Audio', m4a: 'Audio', mp3: 'Audio'
};
// Determine the type of an asset with a lookup table
Q.assetType = function(asset) {
// Determine the lowercase extension of the file
var fileExt = _(asset.split(".")).last().toLowerCase();
// Lookup the asset in the assetTypes hash, or return other
return Q.assetTypes[fileExt] || 'Other';
};
// Loader for Images
Q.loadAssetImage = function(key,src,callback,errorCallback) {
var img = new Image();
img.on('load',function() { callback(key,img); });
img.on('error',errorCallback);
img.src = Q.options.imagePath + src;
};
Q.audioMimeTypes = { mp3: 'audio/mpeg',
ogg: 'audio/ogg; codecs="vorbis"',
m4a: 'audio/m4a',
wav: 'audio/wav' };
// Loader for Audio
Q.loadAssetAudio = function(key,src,callback,errorCallback) {
if(!document.createElement("audio").play || !Q.options.sound) {
callback(key,null);
return;
}
var snd = new Audio(),
baseName = Q._removeExtension(src),
extension = null,
filename = null;
// Find a supported type
extension =
_(Q.options.audioSupported)
.detect(function(extension) {
return snd.canPlayType(Q.audioMimeTypes[extension]) ?
extension : null;
});
// No supported audio = trigger ok callback anyway
if(!extension) {
callback(key,null);
return;
}
// If sound is turned off,
// call the callback immediately
snd.on('error',errorCallback);
snd.on('canplaythrough',function() {
callback(key,snd);
});
snd.src = Q.options.audioPath + baseName + "." + extension;
snd.load();
return snd;
};
// Loader for other file types, just store the data
// returned from an ajax call
Q.loadAssetOther = function(key,src,callback,errorCallback) {
/,"");
};
// Asset hash storing any loaded assets
Q.assets = {};
// Getter method to return an asset
Q.asset = function(name) {
return Q.assets[name];
};
// Load assets, and call our callback when done
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) { alert("Error Loading: " + itm ); })(itm);
};
// If the user passed in an array, convert it
// to a hash with lookups by filename
if(_.isArray(assets)) {
_.each(assets,function(itm) {
if(_.isObject(itm)) {
_.extend(assetObj,itm);
} else {
assetObj[itm] = itm;
}
});
} else if(_.isString(assets)) {
// Turn assets into an object if it's a string
assetObj[assets] = assets;
} else {
// Otherwise just use the assets as is
assetObj = assets;
}
// Find the # of assets we're loading
var assetsTotal = _(assetObj).keys().length,
assetsRemaining = assetsTotal;
// Closure'd per-asset callback gets called
// each time an asset is successfully loadded
var loadedCallback = function(key,obj) {
if(errors) return;
// 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
_.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]);
} 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
// and then preload them all at the same time
Q.preload = function(arg,options) {
if(_(arg).isFunction()) {
Q.load(_(Q.preloads).uniq(),arg,options);
Q.preloads = [];
} else {
Q.preloads = Q.preloads.concat(arg);
}
};
return Q;
}
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.