topical media & game development

talk show tell print

mobile-game-present-lib-quintus.js / js



  //     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.
  
  // Engine Bootstrapping
  // ====================
  
  // 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();
  //
  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 });
    //     
    //
    var Q = function(selector,scope,options) {   
      return Q.select(selector,scope,options);
    };
  
    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);
        }
      }
    };
  
    // 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<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.
    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
    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;
    };
  
    Q._range = function(start,stop,step) {
      step = arguments[2] || 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 unique identifier
    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.
    Q.options = {
      imagePath: "mobile-game-present-images-",
      audioPath: "mobile-game-present-audio-",
      dataPath:  "mobile-game-present-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.
    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(now) {
        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...
    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
     */
    (function(){
      var initializing = false, 
          fnTest = /xyz/.test(function(){ var xyz;}) ? /\b_super\b/ : /.*/;
      /* The base Class implementation (does nothing) */
      Q.Class = function(){};
      
      /* Create a new Class that inherits from this 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. Evented objects can trigger events and other objects can
    // bind to those events.
    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.
      on: function(event,target,callback) {
        // 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. 
      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.
      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.
      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);
           }
         }
       }
  
     });
  
     
    // 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 master list of registered components, indexed in an object by name.
    Q.components = {};
  
    // 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.
    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.parent && entity.parent.addToList) {
          entity.parent.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.
      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.parent && this.entity.parent.addToList) {
            this.entity.parent.addToLists(this.componentName,this.entity);
          }
        }
        this.debind();
        if(this.destroyed) { this.destroyed(); }
      }
    });
  
    // 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.
    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.
      has: function(component) {
        return this[component] ? true : false; 
      },
  
      // See if a object is a specific class
      isA: function(className) {
        return this.className === className;
      },
  
      // 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.
      //
      // Returns the object to allow chaining.
      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.
      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.
      destroy: function() {
        if(this.isDestroyed) { return; }
        this.trigger('destroyed');
        this.debind();
        if(this.parent && this.parent.remove) {
          this.parent.remove(this);
        }
        this.isDestroyed = true;
      }
    });
  
    // This 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.
    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));
    };
  
    // 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, remove
    // jQuery.
  
    Q.touchDevice = ('ontouchstart' in document);
  
    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),
          cssW = w,
          cssH = h;
  
      var maxWidth = options.maxWidth || 5000,
          maxHeight = options.maxHeight || 5000,
          resampleWidth = options.resampleWidth,
          resampleHeight = options.resampleHeight,
          upsampleWidth = options.upsampleWidth,
          upsampleHeight = options.upsampleHeight;
  
      if(options.maximize === 'resize') {
        document.body.style.padding = 0;
        document.body.style.margin = 0;
  
        var cssCalculatedW = Math.min(window.innerWidth,maxWidth);
        var cssCalculatedH = Math.min(window.innerHeight - 5,maxHeight);
  
        if(Q.touchDevice) {
          Q.el.style.height = (h*2) + "px";
          window.scrollTo(0,1);
  
          cssCalculatedW = Math.min(window.innerWidth,maxWidth);
          cssCalculatedH = Math.min(window.innerHeight,maxHeight);
        }
  
        if(cssCalculatedW + 50 < w || cssCalculatedH + 50 < h) {
          var wRatio = cssCalculatedW / w,
              hRatio = cssCalculatedH / h;
  
          var ratio = wRatio < hRatio ? wRatio : hRatio;
  
          cssW = w * ratio;
          cssH = h * ratio;
        }
      }
      else 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);
        }
      }
  
      if((upsampleWidth && w <= upsampleWidth) ||
      (upsampleHeight && h <= upsampleHeight)) {
        w *= 2;
        h *= 2;
      }
      else if(((resampleWidth && w > resampleWidth) ||
      (resampleHeight && h > resampleHeight)) && 
      Q.touchDevice) { 
        w /= 2;
        h /= 2;
      }
  
      Q.el.style.height = cssH + "px";
      Q.el.style.width = cssW + "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 = cssW + "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 = cssW;
      Q.cssHeight = cssH;
    
      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();
  
      /* Lookup the asset in the assetTypes hash, or return other */
      return Q.assetTypes[fileExt] || 'Other';
    };
  
    // 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;
  
      if(src.match(/https?:\/\//)) {
        img.src = src;
      } else {
        img.src = 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' };
  
    // 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 snd = new Audio(),
          baseName = Q._removeExtension(src),
          extension = null,
          filename = null;
  
      /* Find a supported type */
      extension = 
        Q._detect(Q.options.audioSupported,
           function(extension) {
           return snd.canPlayType(Q.audioMimeTypes[extension]) ? 
                                  extension : null;
      });
  
      /* No supported audio = trigger ok callback anyway */
      if(!extension) {
        callback(key,null);
        return;
      }
  
      snd.addEventListener("error",errorCallback);
      snd.addEventListener('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) {
      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.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: 
    //    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) {
        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) {
        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;
      }
  
    });
  
    // 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);
          };
      }
  }());
  
  


(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.