API Docs for: v0.1.0
Show:

File: src\tilemap\Tilelayer.js

var Container = require('../display/Container'),
    Rectangle = require('../geom/Rectangle'),
    Vector = require('../math/Vector'),
    Texture = require('../display/Texture'),
    Tile = require('./Tile'),
    math = require('../math/math'),
    utils = require('../utils/utils'),
    inherit = require('../utils/inherit'),
    support = require('../utils/support');

/**
 * The Tilelayer is the visual tiled layer that actually displays on the screen
 *
 * This class will be created by the Tilemap, there shouldn't be a reason to
 * create an instance on your own.
 *
 * @class Tilelayer
 * @extends Container
 * @constructor
 * @param map {Tilemap} The tilemap instance that this belongs to
 * @param layer {Object} All the settings for the layer
 */
//see: https://github.com/GoodBoyDigital/pixi.js/issues/48
var Tilelayer = function(map, layer) {
    Container.call(this, layer);

    /**
     * The map instance this tilelayer belongs to
     *
     * @property map
     * @type Tilemap
     */
    this.map = map;

    /**
     * The state instance this tilelayer belongs to
     *
     * @property state
     * @type Game
     */
    this.state = map.state;

    /**
     * The state instance this tilelayer belongs to
     *
     * @property state
     * @type Game
     */
    this.state = map.state;

    /**
     * The current map of all tiles on the screen
     *
     * @property tiles
     * @type Object
     */
    this.tiles = [];

    /**
     * The name of the layer
     *
     * @property name
     * @type String
     * @default ''
     */
    this.name = layer.name || '';

    /**
     * The size of the layer
     *
     * @property size
     * @type Vector
     * @default new Vector(1, 1)
     */
    this.size = new Vector(layer.width || 0, layer.height || 0);

    /**
     * The tile IDs of the tilemap
     *
     * @property tileIds
     * @type Uint32Array
     */
    this.tileIds = support.typedArrays ? new Uint32Array(layer.data) : layer.data;

    /**
     * The user-defined properties of this group. Usually defined in the TiledEditor
     *
     * @property properties
     * @type Object
     */
    this.properties = utils.parseTiledProperties(layer.properties) || {};

    /**
     * The Tiled type of tile layer, should always be 'tilelayer'
     *
     * @property type
     * @type String
     * @default 'tilelayer'
     */
    this.type = layer.type || 'tilelayer';

    /**
     * Is this layer supposed to be preRendered?
     *
     * @property preRender
     * @type Boolean
     * @default false
     */
    this.preRender = layer.preRender || this.properties.preRender || false;

    /**
     * The size of a chunk when pre rendering
     *
     * @property chunkSize
     * @type Vector
     * @default new Vector(512, 512)
     */
    this.chunkSize = new Vector(
        layer.chunkSizeX || layer.chunkSize || this.properties.chunkSizeX || this.properties.chunkSize || 512,
        layer.chunkSizeY || layer.chunkSize || this.properties.chunkSizeY || this.properties.chunkSize || 512
    );

    //translate some tiled properties to our inherited properties
    this.position.x = layer.x || 0;
    this.position.y = layer.y || 0;
    this.alpha = layer.opacity !== undefined ? layer.opacity : 1;
    this.visible = layer.visible !== undefined ? layer.visible : true;

    //some private trackers
    this._preRendered = false;
    this._tilePool = [];
    this._buffered = { left: false, right: false, top: false, bottom: false };
    this._panDelta = new Vector();
    this._rendered = new Rectangle();
};

inherit(Tilelayer, Container, {
    /**
     * Creates all the tile sprites needed to display the layer
     *
     * @method resize
     * @param width {Number} The number of tiles in the X direction to render
     * @param height {Number} The number of tiles in the Y direction to render
     * @return {Tilelayer} Returns itself.
     * @chainable
     */
    render: function(x, y, width, height) {
        if(this.preRender) {
            if(!this._preRendered) {
                this._preRender();
            } else {
                for(var c = this.children.length - 1; c > -1; --c) {
                    this.children[c].visible = true;
                }
            }

            return;
        }

        //copy down our tilesize
        if(!this.tileSize)
            this.tileSize = this.map.tileSize;

        //clear all the visual tiles
        this.clearTiles();

        //render the tiles on the screen
        this._renderTiles(x, y, width, height);

        return this;
    },
    /**
     * Renders the map onto different canvases, one per chunk. This only runs once
     * then the canvases are used as a textures for tiles the size of chunks.
     *
     * @method _preRender
     * @private
     */
    _preRender: function() {
        if(!this.visible)
            return;

        this._preRendered = true;
        this.tileSize = this.chunkSize.clone();

        var world = this.map,
            width = world.size.x * world.tileSize.x,
            height = world.size.y * world.tileSize.y,
            xChunks = math.ceil(width / this.chunkSize.x),
            yChunks = math.ceil(height / this.chunkSize.y);

        //for each chunk
        for(var x = 0; x < xChunks; ++x) {
            for(var y = 0; y < yChunks; ++y) {
                var cw = (x === xChunks - 1) ? width - (x * this.chunkSize.x) : this.chunkSize.x,
                    ch = (y === yChunks - 1) ? height - (y * this.chunkSize.y) : this.chunkSize.y;

                this._preRenderChunk(x, y, cw, ch);
            }
        }
    },
    /**
     * Renders a single chunk to a single canvas and creates/places the tile instance for it.
     *
     * @method _preRenderChunk
     * @param cx {Number} The x-coord of this chunk's top left
     * @param cy {Number} The y-coord of this chunk's top left
     * @param w {Number} The width of this chunk
     * @param h {Number} The height of this chunk
     * @private
     */
    _preRenderChunk: function(cx, cy, w, h) {
        var world = this.map,
            tsx = world.tileSize.x,
            tsy = world.tileSize.y,
            xTiles = w / tsx,
            yTiles = h / tsy,
            nx = (cx * this.chunkSize.x) % tsx,
            ny = (cy * this.chunkSize.y) % tsy,
            tx = math.floor(cx * this.chunkSize.x / tsx),
            ty = math.floor(cy * this.chunkSize.y / tsy),
            sx = world.size.x,
            sy = world.size.y,
            canvas = document.createElement('canvas'),
            ctx = canvas.getContext('2d');

        canvas.width = w;
        canvas.height = h;

        //draw all the tiles in this chunk to the canvas
        for(var x = 0; x < xTiles; ++x) {
            for(var y = 0; y < yTiles; ++y) {
                if(x + tx < sx && y + ty < sy) {
                    var id = ((x + tx) + ((y + ty) * sx)),
                        tid = this.tileIds[id],
                        set = world.getTileset(tid),
                        tex, frame;

                    if(set) {
                        tex = set.getTileTexture(tid);
                        frame = tex.frame;

                        ctx.drawImage(
                            tex.baseTexture.source,
                            frame.x,
                            frame.y,
                            frame.width,
                            frame.height,
                            (x * tsx) - nx + set.tileoffset.x,
                            (y * tsy) - ny + set.tileoffset.y,
                            frame.width,
                            frame.height
                        );
                    }
                }
            }
        }

        //use the canvas as a texture for a tile to display
        var tile = new Tile(Texture.fromCanvas(canvas));
        tile.setPosition(
            cx * this.chunkSize.x,
            cy * this.chunkSize.y
        );

        if(!this.tiles[cx])
            this.tiles[cx] = {};

        this.addChild(tile);
        this.tiles[cx][cy] = tile;
    },
    /**
     * Renders the tiles for the viewport
     *
     * @method _renderTiles
     * @param sx {Number} The x-coord in the map to start rendering
     * @param sy {Number} The y-coord in the map to start rendering
     * @param sw {Number} The width of the viewport
     * @param sh {Number} The height of the viewport
     * @private
     */
    _renderTiles: function(sx, sy, sw, sh) {
        //convert to tile coords
        sx = math.floor(sx / this.map.scaledTileSize.x);
        sy = math.floor(sy / this.map.scaledTileSize.y);

        //ensure we don't go below 0
        sx = sx < 0 ? 0 : sx;
        sy = sy < 0 ? 0 : sy;

        //convert to tile sizes
        sw = math.ceil(sw / this.map.scaledTileSize.x) + 1;
        sh = math.ceil(sh / this.map.scaledTileSize.y) + 1;

        //ensure we don't go outside the map size
        sw = (sx + sw > this.map.size.x) ? (this.map.size.x - sx) : sw;
        sh = (sy + sh > this.map.size.y) ? (this.map.size.y - sy) : sh;

        //render new sprites
        var endX = sx + sw,
            endY = sy + sh;

        for(var x = sx; x < endX; ++x) {
            for(var y = sy; y < endY; ++y) {
                this.moveTileSprite(-1, -1, x, y);
            }
        }

        //set rendered area
        this._rendered.x = sx;
        this._rendered.y = sy;
        this._rendered.width = sw;
        this._rendered.height = sh;

        //reset buffered status
        this._buffered.left = this._buffered.right = this._buffered.top = this._buffered.bottom = false;

        //reset panDelta
        this._panDelta.x = this.state.world.position.x % this.map.scaledTileSize.x;
        this._panDelta.y = this.state.world.position.y % this.map.scaledTileSize.y;
    },
    /**
     * Frees a tile in the list back into the pool
     *
     * @method _freeTile
     * @param tx {Number} The x-coord of the tile in tile coords (not world coords)
     * @param ty {Number} The y-coord of the tile in tile coords (not world coords)
     * @private
     */
    _freeTile: function(tx, ty) {
        if(this.tiles[tx] && this.tiles[tx][ty]) {
            this.clearTile(this.tiles[tx][ty]);
            this.tiles[tx][ty] = null;
        }
    },
    /**
     * Clears all the tiles currently used to render the layer
     *
     * @method clearTiles
     * @param remove {Boolean} Should this tile be completely removed (never to bee seen again)
     * @return {Tilelayer} Returns itself.
     * @chainable
     */
    clearTiles: function(remove) {
        var c;

        if(this.preRender && !remove) {
            for(c = this.children.length - 1; c > -1; --c) {
                this.children[c].visible = false;
            }

            return;
        }

        //force rerender later
        this._preRendered = false;

        for(c = this.children.length - 1; c > -1; --c) {
            this.clearTile(this.children[c], remove);
        }

        this.tiles.length = 0;

        return this;
    },
    /**
     * Clears a tile currently used to render the layer
     *
     * @method clearTile
     * @param tile {Tile} The tile object to clear
     * @param remove {Boolean} Should this tile be completely removed (never to bee seen again)
     * @return {Tilelayer} Returns itself.
     * @chainable
     */
    clearTile: function(tile, remove) {
        tile.visible = false;
        tile.disablePhysics();

        if(remove)
            this.removeChild(tile);
        else
            this._tilePool.push(tile);

        return this;
    },
    /**
     * Moves a tile sprite from one position to another, and creates a new tile
     * if the old position didn't have a sprite
     *
     * @method moveTileSprite
     * @param fromTileX {Number} The x coord of the tile in units of tiles (not pixels) to move from
     * @param fromTileY {Number} The y coord of the tile in units of tiles (not pixels) to move from
     * @param toTileX {Number} The x coord of the tile in units of tiles (not pixels) to move to
     * @param toTileY {Number} The y coord of the tile in units of tiles (not pixels) to move to
     * @return {Tile} The sprite to display
     */
    moveTileSprite: function(fromTileX, fromTileY, toTileX, toTileY) {
        //if off the map, just ignore it
        if(toTileX < 0 || toTileY < 0 || toTileX >= this.map.size.x || toTileY >= this.map.size.y) {
            //remove the from tile's physics
            if(this.tiles[fromTileX] && this.tiles[fromTileX][fromTileY]) {
                this.tiles[fromTileX][fromTileY].disablePhysics();
            }
            return;
        }

        var tile,
            id = (toTileX + (toTileY * this.size.x)),
            tileId = this.tileIds[id],
            set = this.map.getTileset(tileId),
            texture,
            props,
            position,
            hitArea,
            interactive;

        //if no tileset, just ensure the "from" tile is put back in the pool
        if(!set) {
            this._freeTile(fromTileX, fromTileY);
            return;
        }

        //grab some values for the tile
        texture = set.getTileTexture(tileId);
        props = set.getTileProperties(tileId);
        hitArea = props.hitArea || set.properties.hitArea;
        interactive = this._getInteractive(set, props);
        position = [
            (toTileX * this.map.tileSize.x) + set.tileoffset.x,
            (toTileY * this.map.tileSize.y) + set.tileoffset.y
        ];

        //due to the fact that we use top-left anchors for everything, but tiled uses bottom-left
        //we need to move the position of each tile down by a single map-tile height. That is why
        //there is an addition of "this.map.tileSize.y" to the coords
        position[1] +=  this.map.tileSize.y;

        //if there is one to move in the map, lets just move it
        if(this.tiles[fromTileX] && this.tiles[fromTileX][fromTileY]) {
            tile = this.tiles[fromTileX][fromTileY];
            this.tiles[fromTileX][fromTileY] = null;
            tile.disablePhysics();
        }
        //otherwise grab a new tile from the pool
        else {
            tile = this._tilePool.pop();
        }

        //if we couldn't find a tile from the pool, or one to move
        //then create a new tile
        if(!tile) {
            tile = new Tile(texture);
            tile.anchor.y = 1;
            this.addChild(tile);
        }

        tile.collisionType = props.type;
        tile.interactive = interactive;
        tile.hitArea = hitArea;
        tile.mass = props.mass || 0;

        tile.setTexture(texture);
        tile.setPosition(position[0], position[1]);
        tile.show();

        if(tile.mass) {
            tile.enablePhysics(this.state.physics);
        }

        //pass through all events
        if(interactive) {
            tile.click = this.onTileEvent.bind(this, 'click', tile);
            tile.mousedown = this.onTileEvent.bind(this, 'mousedown', tile);
            tile.mouseup = this.onTileEvent.bind(this, 'mouseup', tile);
            tile.mousemove = this.onTileEvent.bind(this, 'mousemove', tile);
            tile.mouseout = this.onTileEvent.bind(this, 'mouseout', tile);
            tile.mouseover = this.onTileEvent.bind(this, 'mouseover', tile);
            tile.mouseupoutside = this.onTileEvent.bind(this, 'mouseupoutside', tile);
        }

        //update sprite position in the map
        if(!this.tiles[toTileX])
            this.tiles[toTileX] = [];

        this.tiles[toTileX][toTileY] = tile;

        return tile;
    },
    /**
     * Called whenever a tile event occurs, this is used to echo to the parent.
     *
     * @method onTileEvent
     * @param eventName {String} The name of the event
     * @param tile {Tile} The tile the event happened to
     * @param data {mixed} The event data that was passed along
     * @private
     */
    onTileEvent: function(eventName, tile, data) {
        this.map.onTileEvent(eventName, tile, data);
    },
    /**
     * Checks if an object should be marked as interactive
     *
     * @method _getInteractive
     * @param set {Tileset} The tileset for the object
     * @param props {Object} The Tiled properties object
     * @return {Boolean} Whether or not the item is interactive
     * @private
     */
    _getInteractive: function(set, props) {
        //first check the lowest level value (on the tile iteself)
        return props.interactive || //obj interactive
                (set && set.properties.interactive) || //tileset interactive
                this.properties.interactive || //layer interactive
                this.map.properties.interactive; //map interactive
    },
    /**
     * Pans the layer around, rendering stuff if necessary
     *
     * @method pan
     * @param dx {Number|Point} The x amount to pan, if a Point is passed the dy param is ignored
     * @param dy {Number} The y ammount to pan
     * @return {Tilelayer} Returns itself.
     * @chainable
     */
    pan: function(dx, dy) {
        if(this.preRender)
            return;

        //track panning delta so we know when to render
        this._panDelta.x += dx;
        this._panDelta.y += dy;

        var tszX = this.map.scaledTileSize.x,
            tszY = this.map.scaledTileSize.y;

        //check if we need to build a buffer around the viewport
        //usually this happens on the first pan after a full render
        //caused by a viewport resize. We do this buffering here instead
        //of in the initial render because in the initial render, the buffer
        //may try to go negative which has no tiles. Plus doing it here
        //reduces the number of tiles that need to be created initially.

        //moving world right, so left will be exposed
        if(dx > 0 && !this._buffered.left)
            this._renderLeft(this._buffered.left = true);
        //moving world left, so right will be exposed
        else if(dx < 0 && !this._buffered.right)
            this._renderRight(this._buffered.right = true);

        //moving world down, so top will be exposed
        if(dy > 0 && !this._buffered.top)
            this._renderUp(this._buffered.top = true);
        //moving world up, so bottom will be exposed
        else if(dy < 0 && !this._buffered.bottom)
            this._renderDown(this._buffered.bottom = true);

        //Here is where the actual panning gets done, we check if the pan
        //delta is greater than a scaled tile and if so pan that direction.
        //The reason we do it in a while loop is because the delta can be
        //large than 1 scaled tile and may require multiple render pans
        //(this can happen if you can .pan(x, y) with large values)

        //moved position right, so render left
        while(this._panDelta.x >= tszX) {
            this._renderLeft();
            this._panDelta.x -= tszX;
        }

        //moved position left, so render right
        while(this._panDelta.x <= -tszX) {
            this._renderRight();
            this._panDelta.x += tszX;
        }

        //moved position down, so render up
        while(this._panDelta.y >= tszY) {
            this._renderUp();
            this._panDelta.y -= tszY;
        }

        //moved position up, so render down
        while(this._panDelta.y <= -tszY) {
            this._renderDown();
            this._panDelta.y += tszY;
        }
    },
    /**
     * Renders tiles to the left, pulling from the far right
     *
     * @method _renderLeft
     * @param [forceNew=false] {Boolean} If set to true, new tiles are created instead of trying to recycle
     * @private
     */
    _renderLeft: function(forceNew) {
        //move all the far right tiles to the left side
        for(var i = 0; i < this._rendered.height + 1; ++i) {
            this.moveTileSprite(
                forceNew ? -1 : this._rendered.right,
                forceNew ? -1 : this._rendered.top + i,
                this._rendered.left - 1,
                this._rendered.top + i
            );
        }
        this._rendered.x--;
        if(forceNew) this._rendered.width++;
    },
    /**
     * Renders tiles to the right, pulling from the far left
     *
     * @method _renderRight
     * @param [forceNew=false] {Boolean} If set to true, new tiles are created instead of trying to recycle
     * @private
     */
    _renderRight: function(forceNew) {
        //move all the far left tiles to the right side
        for(var i = 0; i < this._rendered.height + 1; ++i) {
            this.moveTileSprite(
                forceNew ? -1 : this._rendered.left,
                forceNew ? -1 : this._rendered.top + i,
                this._rendered.right + 1,
                this._rendered.top + i
            );
        }
        if(!forceNew) this._rendered.x++;
        if(forceNew) this._rendered.width++;
    },
    /**
     * Renders tiles to the top, pulling from the far bottom
     *
     * @method _renderUp
     * @param [forceNew=false] {Boolean} If set to true, new tiles are created instead of trying to recycle
     * @private
     */
    _renderUp: function(forceNew) {
        //move all the far bottom tiles to the top side
        for(var i = 0; i < this._rendered.width + 1; ++i) {
            this.moveTileSprite(
                forceNew ? -1 : this._rendered.left + i,
                forceNew ? -1 : this._rendered.bottom,
                this._rendered.left + i,
                this._rendered.top - 1
            );
        }
        this._rendered.y--;
        if(forceNew) this._rendered.height++;
    },
    /**
     * Renders tiles to the bottom, pulling from the far top
     *
     * @method _renderDown
     * @param [forceNew=false] {Boolean} If set to true, new tiles are created instead of trying to recycle
     * @private
     */
    _renderDown: function(forceNew) {
        //move all the far top tiles to the bottom side
        for(var i = 0; i < this._rendered.width + 1; ++i) {
            this.moveTileSprite(
                forceNew ? -1 : this._rendered.left + i,
                forceNew ? -1 : this._rendered.top,
                this._rendered.left + i,
                this._rendered.bottom + 1
            );
        }
        if(!forceNew) this._rendered.y++;
        if(forceNew) this._rendered.height++;
    },
    /**
     * Destroys the tile layer completely
     *
     * @method destroy
     */
    destroy: function() {
        Container.prototype.destroy.call(this);

        this.clearTiles(true);

        this.state = null;
        this.name = null;
        this.size = null;
        this.tileIds = null;
        this.properties = null;
        this.type = null;
        this.position.x = null;
        this.position.y = null;
        this.alpha = null;
        this.visible = null;
        this.preRender = null;
        this.chunkSize = null;

        this._preRendered = null;
        this._tilePool = null;
        this._buffered = null;
        this._panDelta = null;
        this._rendered = null;
    }
});

module.exports = Tilelayer;