API Docs for: v0.1.0
Show:

File: src\loader\Loader.js

// Thanks to PhotonStorm (http://photonstorm.com/) for this loader!
// heavily insprite by (stolen from): https://github.com/photonstorm/phaser/blob/master/src/loader/Loader.js

var utils = require('../utils/utils'),
    inherit = require('../utils/inherit'),
    support = require('../utils/support'),
    EventEmitter = require('../utils/EventEmitter'),
    C = require('../constants');

/**
 * The Loader loads and parses different game assets, such as sounds, textures,
 * TMX World files (exported from the [Tiled Editor](http://mapeditor.org)),
 * and Sprite Atlas files (published from [Texture Packer](http://www.codeandweb.com/texturepacker)).
 *
 * @class Loader
 * @extends Object
 * @uses EventEmitter
 * @constructor
 * @param game {Game} Game instance this belongs to
 */
var Loader = function(game) {
    EventEmitter.call(this);

    /**
     * The game instance this loader belongs to
     *
     * @property game
     * @type Game
     */
    this.game = game;

    /**
     * The array of asset keys
     *
     * @property assets
     * @type Array
     */
    this.keys = [];

    /**
     * The asset data
     *
     * @property assets
     * @type Array
     */
    this.assets = {};

    /**
     * Number of assets total to load
     *
     * @property total
     * @type Number
     */
    this.total = 0;

    /**
     * Number of assets done to load (for progress)
     *
     * @property done
     * @type Number
     */
    this.done = 0;

    /**
     * Whether the loader is actively loading the assets
     *
     * @property isLoading
     * @type Boolean
     */
    this.isLoading = false;

    /**
     * Whether the loader has finished loading
     *
     * @property isLoading
     * @type Boolean
     */
    this.hasLoaded = false;

    /**
     * The progress of the loader (0 - 100)
     *
     * @property progress
     * @type Number
     */
    this.progress = 0;

    /**
     * The cross origin value for loading images
     *
     * @property crossOrigin
     * @type String
     */
    this.crossOrigin = '';

    /**
     * The base URL to prepend to a url, requires the trailing slash
     *
     * @property baseUrl
     * @type String
     */
    this.baseUrl = '';

    /**
     * Fired when an item has started loading
     *
     * @event start
     * @param numAssets {Number} The number of assets that are going to be loaded
     */

    /**
     * Fired if a loader encounters an error
     *
     * @event error
     * @param error {mixed} The error that occured when loading
     * @param key {String} The key for the asset that was being loaded
     */

    /**
     * Fired when an item has loaded
     *
     * @event progress
     * @param progress {Number} The integer progress value, between 0 and 100.
     */

    /**
     * Fired when all the assets have loaded
     *
     * @event complete
     */
};

inherit(Loader, Object, {
    /**
     * Check whether asset exists with a specific key.
     *
     * @method hasKey
     * @param key {String} Key of the asset you want to check.
     * @return {Boolean} Return true if exists, otherwise return false.
     */
    hasKey: function(key) {
        return !!this.assets[key];
    },

    /**
     * Reset loader, this will remove all loaded assets from the loader's stored list (but not from the cache).
     *
     * @method reset
     * @return {Loader} Returns itself.
     * @chainable
     */
    reset: function() {
        this.progress = 0;
        this.total = 0;
        this.done = 0;
        this.hasLoaded = false;
        this.isLoading = false;
        this.assets = {};
        this.keys.length = 0;

        return this;
    },

    /**
     * Adds an asset to be loaded
     *
     * @method add
     * @param type {String} The type of asset ot load (image, spritesheet, textureatlas, bitmapfont, tilemap, tileset, audio, etc)
     * @param key {String} The unique key of the asset to identify it
     * @param url {String} The URL to load the resource from
     * @param [options] {Object} Extra options to apply to the asset, different asset types may require extra options
     * @param [options.crossOrigin=false] {Boolean} True if an image load should be treated as crossOrigin
     * @return {Loader} Returns itself.
     * @chainable
     */
    add: function(type, key, url, opts) {
        var entry = {
            type: type,
            key: key,
            url: url,
            image: null,
            data: null,
            error: false,
            loaded: false
        };

        if(opts !== undefined) {
            for(var p in opts) {
                entry[p] = opts[p];
            }
        }

        this.assets[key] = entry;
        this.keys.push(key);
        this.total++;

        return this;
    },

    /**
     * Add an image to the Loader.
     *
     * @method image
     * @param key {String} Unique asset key of this image file.
     * @param url {String} URL of image file.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    image: function(key, url, overwrite) {
        if(overwrite || !this.hasKey(key))
            this.add('image', key, url);

        return this;
    },

    /**
     * Add a text file to the Loader.
     *
     * @method text
     * @param key {String} Unique asset key of this image file.
     * @param url {String} URL of image file.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    text: function(key, url, overwrite) {
        if(overwrite || !this.hasKey(key))
            this.add('text', key, url);

        return this;
    },

    /**
     * Add a sprite sheet image to the Loader.
     *
     * @method spritesheet
     * @param key {String} Unique asset key of this image file.
     * @param url {String} URL of image file.
     * @param frameWidth {Number} Width of each single frame.
     * @param frameHeight {Number} Height of each single frame.
     * @param numFrames {Number} How many frames in this sprite sheet.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    spritesheet: function(key, url, frameWidth, frameHeight, numFrames, overwrite) {
        if(overwrite || !this.hasKey(key))
            this.add('spritesheet', key, url, {
                frameWidth: frameWidth,
                frameHeight: frameHeight,
                numFrames: numFrames
            });

        return this;
    },

    /**
     * Add an audio file to the Loader.
     *
     * @method audio
     * @param key {String} Unique asset key of this image file.
     * @param urls {Array<String>} URLs of audio files.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    audio: function(key, urls, overwrite) {
        if(overwrite || !this.hasKey(key))
            this.add('audio', key, urls);

        return this;
    },

    /**
     * Add a tilemap to the Loader.
     *
     * @method tilemap
     * @param key {String} Unique asset key of the tilemap data.
     * @param url {String} The url of the map data file (csv/json/xml)
     * @param [data] {String|Object} The data for the map, (to use instead of loading from a URL)
     * @param [format=FILE_FORMAT.JSON] {Number} The format of the map data.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    tilemap: function(key, url, data, format, overwrite) {
        if(overwrite || !this.hasKey(key)) {
            if(!format) format = C.FILE_FORMAT.JSON;

            if(typeof data === 'string') {
                switch(format) {
                    case C.FILE_FORMAT.JSON:
                        data = JSON.parse(data);
                        break;

                    case C.FILE_FORMAT.XML:
                        data = C.utils.parseXML(data);
                        break;

                    case C.FILE_FORMAT.CSV:
                        break;
                }
            }

            this.add('tilemap', key, url, {
                data: data,
                format: format
            });
        }

        return this;
    },

    /**
     * Add a bitmap font to the Loader.
     *
     * @method bitmapFont
     * @param key {String} Unique asset key of the bitmap font.
     * @param textureURL {String} The url of the font image file.
     * @param [dataUrl] {String} The url of the font data file (xml/fnt)
     * @param [data] {Object} An optional XML data object (to use instead of loading from a URL)
     * @param [format=FILE_FORMAT.XML] {FILE_FORMAT} The format of the bitmap font data.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    bitmapFont: function(key, textureUrl, dataUrl, data, format, overwrite) {
        if(overwrite || !this.hasKey(key)) {
            if(!format) format = C.FILE_FORMAT.XML;

            if(typeof data === 'string') {
                switch(format) {
                    case C.FILE_FORMAT.XML:
                        data = utils.parseXML(data);
                        break;

                    case C.FILE_FORMAT.JSON:
                        data = JSON.parse(data);
                        break;
                }
            }

            this.add('bitmapfont', key, textureUrl, {
                dataUrl: dataUrl,
                data: data,
                format: format
            });
        }

        return this;
    },

    /**
     * Add a JSON-Array formatted texture atlas. Equivalent to running
     * `atlas(key, textureURL, dataUrl, data, gf.ATLAS_FORMAT.JSON_ARRAY);`
     *
     * @param key {string} Unique asset key of the texture atlas file.
     * @param textureUrl {string} The url of the texture atlas image file.
     * @param [dataUrl] {string} The url of the texture atlas data file (json/xml)
     * @param [data] {object} A JSON or XML data object (to use instead of loading from a URL)
     * @return {Loader} Returns itself.
     * @chainable
     */
    atlasJSONArray: function(key, textureURL, dataUrl, data) {
        return this.atlas(key, textureURL, dataUrl, data, C.ATLAS_FORMAT.JSON_ARRAY);
    },

    /**
     * Add a JSON-Hash formatted texture atlas. Equivalent to running
     * `atlas(key, textureURL, dataUrl, data, gf.ATLAS_FORMAT.JSON_HASH);`
     *
     * @param key {string} Unique asset key of the texture atlas file.
     * @param textureUrl {string} The url of the texture atlas image file.
     * @param [dataUrl] {string} The url of the texture atlas data file (json/xml)
     * @param [data] {object} A JSON or XML data object (to use instead of loading from a URL)
     * @return {Loader} Returns itself.
     * @chainable
     */
    atlasJSONHash: function(key, textureURL, dataUrl, data) {
        return this.atlas(key, textureURL, dataUrl, data, C.ATLAS_FORMAT.JSON_HASH);
    },

    /**
     * Add an XML formatted texture atlas. Equivalent to running
     * `atlas(key, textureURL, dataUrl, data, gf.ATLAS_FORMAT.XML_STARLING);`
     *
     * @param key {string} Unique asset key of the texture atlas file.
     * @param textureUrl {string} The url of the texture atlas image file.
     * @param [dataUrl] {string} The url of the texture atlas data file (json/xml)
     * @param [data] {object} A JSON or XML data object (to use instead of loading from a URL)
     * @return {Loader} Returns itself.
     * @chainable
     */
    atlasXML: function(key, textureURL, dataUrl, data) {
        return this.atlas(key, textureURL, dataUrl, data, C.ATLAS_FORMAT.XML_STARLING);
    },

    /**
     * Add a new texture atlas loading request.
     * @param key {string} Unique asset key of the texture atlas file.
     * @param textureUrl {string} The url of the texture atlas image file.
     * @param [dataUrl] {string} The url of the texture atlas data file (json/xml)
     * @param [data] {object} A JSON or XML data object (to use instead of loading from a URL)
     * @param [format] {number} A value describing the format of the data.
     * @param [overwrite=false] {Boolean} If an entry with a matching key already exists this will over-write it.
     * @return {Loader} Returns itself.
     * @chainable
     */
    atlas: function(key, textureUrl, dataUrl, data, format, overwrite) {
        if(overwrite || !this.hasKey(key)) {
            if(!format) format = C.ATLAS_FORMAT.JSON_ARRAY;

            if(typeof data === 'string') {
                switch(format) {
                    case C.ATLAS_FORMAT.XML_STARLING:
                        data = utils.parseXML(data);
                        break;

                    case C.ATLAS_FORMAT.JSON_ARRAY:
                    case C.ATLAS_FORMAT.JSON_HASH:
                        data = JSON.parse(data);
                        break;
                }
            }

            this.add('textureatlas', key, textureUrl, {
                dataUrl: dataUrl,
                data: data,
                format: format
            });
        }

        return this;
    },

    /**
     * Starts the loading of all the assets that are queued to load
     *
     * @method start
     * @return {Loader} Returns itself.
     * @chainable
     */
    start: function() {
        if(this.isLoading) return;

        this.progress = 0;
        this.hasLoaded = false;
        this.isLoading = true;

        this.emit('start', this.keys.length);

        if(this.keys.length > 0) {
            while(this.keys.length > 0)
                this.loadFile();
        } else {
            this.progress = 100;
            this.hasLoaded = true;
            this.emit('complete');
        }

        return this;
    },

    /**
     * Loads a single asset from the queued assets in this Loader. To load a single file first queue it by using
     * one of the methods named for an asset (like `audio`, `image`, `tilemap`, etc.), then call this to load the
     * first in the queue.
     *
     * Note: To load the entire queue at once use `start`.
     *
     * @method loadFile
     * @return {Loader} Returns itself.
     * @chainable
     */
    loadFile: function() {
        var file = this.assets[this.keys.shift()],
            self = this;

        switch(file.type) {
            //load images
            case 'image':
            case 'spritesheet':
            case 'textureatlas':
            case 'bitmapfont':
                file.image = new Image();
                file.image.name = file.key;
                file.image.addEventListener('load', this.fileComplete.bind(this, file.key), false);
                file.image.addEventListener('error', this.fileError.bind(this, file.key), false);
                file.image.crossOrigin = file.crossOrigin !== undefined ? file.crossOrigin : this.crossOrigin;
                file.image.src = this.baseUrl + file.url;
                break;

            //load tilemap
            case 'tilemap':
                utils.ajax({
                    url: this.baseUrl + file.url,
                    dataType: this._getFormatAjaxType(file.format),
                    load: function(data) {
                        file.data = data;
                        self.fileComplete(file.key);
                    },
                    error: function(err) {
                        self.fileError(file.key, err);
                    }
                });
                break;

            //load audio
            case 'audio':
                file.url = this.getAudioUrl(file.url);

                if(file.url) {
                    if(support.webAudio) {
                        utils.ajax({
                            url: this.baseUrl + file.url,
                            dataType: 'arraybuffer',
                            load: function(data) {
                                file.data = data;
                                self.fileComplete(file.key);
                            },
                            error: function(err) {
                                self.fileError(file.key, err);
                            }
                        });
                    } else if(support.htmlAudio) {
                        file.data = new Audio();
                        file.data.name = file.key;
                        file.data.preload = 'auto';
                        file.data.src = this.baseUrl + file.url;

                        file.data.addEventListener('error', file._bndError = this.fileError.bind(this, file.key), false);
                        file.data.addEventListener('canplaythrough', file._bndComplete = this.fileComplete.bind(this, file.key), false);
                        file.data.load();
                    }
                } else {
                    this.fileError(file.key, 'No supported audio URL could be determined!');
                }
                break;

            case 'text':
                utils.ajax({
                    url: this.baseUrl + file.url,
                    dataType: 'text',
                    load: function(data) {
                        file.data = data;
                        self.fileComplete(file.key);
                    },
                    error: function(err) {
                        self.fileError(file.key, err);
                    }
                });
                break;
        }

        return this;
    },

    /**
     * Chooses the audio url to use based on browser support.
     *
     * @method getAudioUrl
     * @param urls {Array<String>} An array of URLs to choose from, chooses the first in the array to be
     *      supported by the browser.
     * @return {String} Returns the URL that was chosen, or `undefined` if none are supported.
     */
    getAudioUrl: function(urls) {
        for(var i = 0, il = urls.length; i < il; ++i) {
            var url = urls[i],
                ext = url.match(/.+\.([^?]+)(\?|$)/);

            ext = (ext && ext.length >= 2) ? ext[1] : url.match(/data\:audio\/([^?]+);/)[1];

            //if we can play this url, then set the source of the player
            if(support.codec[ext]) {
                return url;
            }
        }
    },

    /**
     * Error occured when load a file.
     *
     * @method fileError
     * @param key {String} Key of the error loading file.
     * @param error {mixed} The error that was thrown.
     * @private
     */
    fileError: function(key, error) {
        this.assets[key].loaded = true;
        this.assets[key].error = error;

        this.fileDone(key, error);
    },

    /**
     * Called when a file is successfully loaded.
     *
     * @method fileComplete
     * @param key {string} Key of the successfully loaded file.
     * @private
     */
    fileComplete: function(key) {
        if(!this.assets[key])
            return utils.warn('fileComplete key is invalid!', key);

        this.assets[key].loaded = true;

        var file = this.assets[key],
            done = true,
            self = this;

        switch(file.type) {
            case 'image':
                this.game.cache.addImage(file);
                break;

            case 'spritesheet':
                this.game.cache.addSpriteSheet(file);
                break;

            case 'tilemap':
                file.baseUrl = file.url.replace(/[^\/]*$/, '');
                file.numImages = file.numLoaded = 0;
                file.images = [];

                if(file.format === C.FILE_FORMAT.JSON) {
                    done = false;
                    this._loadJsonTilesets(file);
                } else if(file.format === C.FILE_FORMAT.XML) {
                    done = false;
                    this._loadXmlTilesets(file);
                }
                break;

            case 'textureatlas':
                done = false;
                this._dataget(file, function() {
                    self.game.cache.addTextureAtlas(file);
                });
                break;

            case 'bitmapfont':
                done = false;
                this._dataget(file, function() {
                    self.game.cache.addBitmapFont(file);
                });
                break;

            case 'audio':
                if(support.webAudio) {
                    file.webAudio = true;
                    file.decoded = false;
                } else {
                    file.data.removeEventListener('error', file._bndError);
                    file.data.removeEventListener('canplaythrough', file._bndComplete);
                }

                this.game.cache.addAudio(file);
                break;

            case 'text':
                this.game.cache.addText(file);
                break;
        }

        if(done) {
            this.fileDone(file.key);
        }
    },

    /**
     * Called when a file is done (error or loaded)
     *
     * @method fileDone
     * @param key {String} Key of the file done
     * @param error {mixed} The error that occurred (if there was one)
     * @private
     */
    fileDone: function(key, error) {
        this.done++;
        this.progress = Math.round((this.done / this.total) * 100);

        this.emit('progress', this.progress);

        if(error) {
            utils.warn('Error loading file "' + key + '", error received:', error);
            this.emit('error', error, key);
        }

        if(this.progress >= 100) {
            this.progress = 100;
            this.hasLoaded = true;
            this.isLoading = false;

            this.emit('complete');
        }
    },

    /**
     * Returns the ajax type that represents each format type
     *
     * @method _getFormatAjaxType
     * @param type {ATLAS_FORMAT|FILE_FORMAT} The format to get an ajax type for
     * @private
     */
    _getFormatAjaxType: function(type) {
        switch(type) {
            case C.ATLAS_FORMAT.JSON_ARRAY:
            case C.ATLAS_FORMAT.JSON_HASH:
            case C.FILE_FORMAT.JSON:
                return 'json';

            case C.ATLAS_FORMAT.XML_STARLING:
            case C.FILE_FORMAT.XML:
                return 'xml';

            case C.FILE_FORMAT.CSV:
                return 'text';
        }
    },

    /**
     * Gets a file's data via ajax.
     *
     * @method _dataget
     * @param file {Object} The file descriptor object
     * @param [callback] {Function} The callback to call once the file has loaded. `fileDone` or `fileError` will be
     *      called for you.
     * @private
     */
    _dataget: function(file, cb) {
        var self = this;

        if(!file.dataUrl) {
            setTimeout(cb, 1);
        } else {
            utils.ajax({
                url: this.baseUrl + file.dataUrl,
                dataType: this._getFormatAjaxType(file.format),
                load: function(data) {
                    file.data = data;
                    if(cb) cb();
                    self.fileDone(file.key);
                },
                error: function(err) {
                    self.fileError(file.key, err);
                }
            });
        }
    },

    /**
     * Loads the tilesets found in a JSON formatted tilemap object.
     *
     * @method _loadJsonTilesets
     * @param file {Object} The file descriptor object
     * @private
     */
    _loadJsonTilesets: function(file) {
        var data = file.data,
            baseUrl = file.baseUrl;

        //loop through each tileset and load the texture
        for(var i = 0, il = data.tilesets.length; i < il; ++i) {
            var set = data.tilesets[i],
                img;

            if(!set.image) continue;

            file.numImages++;

            img = new Image();
            img.addEventListener('load', this._onTilesetLoaded.bind(this, file), false);
            img.addEventListener('error', this._onTilesetError.bind(this, file), false);
            img.crossOrigin = file.crossOrigin !== undefined ? file.crossOrigin : this.crossOrigin;
            img.src = this.baseUrl + baseUrl + set.image;

            file.images.push(img);
        }
    },

    /**
     * Loads the tilesets found in a XML formatted tilemap object.
     *
     * @method _loadXmlTilesets
     * @param file {Object} The file descriptor object
     * @private
     */
    _loadXmlTilesets: function(file) {
        var data = file.data,
            baseUrl = file.baseUrl,
            tilesets = data.getElementsByTagName('tileset');

        for(var i = 0, il = tilesets.length; i < il; ++i) {
            var set = tilesets[i],
                imgElm = set.getElementsByTagName('image')[0],
                img;

            if(!imgElm) continue;

            file.numImages++;

            img = new Image();
            img.addEventListener('load', this._onTilesetLoaded.bind(this, file), false);
            img.addEventListener('error', this._onTilesetError.bind(this, file), false);
            img.crossOrigin = file.crossOrigin !== undefined ? file.crossOrigin : this.crossOrigin;
            img.src = this.baseUrl + baseUrl + imgElm.attributes.getNamedItem('source').nodeValue;

            file.images.push(img);
        }
    },

    /**
     * Called each time a tileset is loaded successfully.
     *
     * @method _onTilesetLoaded
     * @param file {Object} The file descriptor object.
     * @private
     */
    _onTilesetLoaded: function(file) {
        file.numLoaded++;

        if(file.numImages === file.numLoaded) {
            this.game.cache.addTilemap(file);
            this.fileDone(file.key);
        }
    },

    /**
     * Called each time a tileset has an error when loading.
     *
     * @method _onTilesetError
     * @param file {Object} The file descriptor object.
     * @param error {mixed} The error thrown when loading.
     * @private
     */
    _onTilesetError: function(file, error) {
        file.error = error;
        file.numLoaded++;

        if(file.numImages === file.numLoaded) {
            this.fileDone(file.key, error);
        }
    }
});

module.exports = Loader;