/** * Renders a given map * * Copyright (C) 2012 Mike Gerwitz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ /** * Renders a map to a canvas */ ltjs.MapRender = Class( 'MapRender', { /** * Tunnel bitmask * @type {number} */ 'private const _TMASK': 0x40, /** * Property to hold lock bit on canvas element * @type {string} */ 'private const _LOCK': '__$$MapRenderLock$$', /** * 2d context to which map should be drawn * @type {CanvasRenderingContext2d} */ 'private _ctx': null, /** * 2d context to which masked game objects should be drawn * @type {CanvasRenderingContext2d} */ 'private _ctxObj': null, /** * Tile set to be rendered * @type {Object} */ 'private _tiles': {}, /** * Initialize renderer with a canvas context and a tile set * * An additional canvas of equal dimensions will be created and laid atop of * the provided canvas to render masked game objects. * * @param {CanvasRenderingContext2d} ctx canvas 2d context * @param {Object} tiles tile set to render */ __construct: function( ctx, tiles ) { this._ctx = ctx; this._tiles = tiles; // ensure that we are exclusively rendering to this canvas (no other // MapRenders) this._lockCanvas(); this._ctxObj = this._getObjCanvas(); }, /** * Lock the canvas to prevent other MapRender instances from rendering to it * * The purpose of this is to provide feedback to the user/developer in the * event that multiple MapRender instances are attempting to render to the * same canvas, which would certainly cause display issues and confusion. * * @return {undefined} */ 'private _lockCanvas': function() { var o = this._ctx, l = this.__self.$( '_LOCK' ); // simple one-line check to both set the lock and fail if the lock is // already set (implying that something else is already rendering to the // canvas) if ( ( o[ l ] ^= 1 ) !== 1 ) { // reset the lock o[ l ] = 1; throw Error( 'Could not set exclusive lock on canvas (in use by another ' + 'MapRender instance)' ); } }, /** * Remove exclusive lock on canvas to permit other MapRender instances to * render to it * * This will also destroy the overlay canvas that the masked objects were * rendered to. The remaining canvas will not be cleared. * * @return {MapRender} self */ 'public freeCanvas': function() { var c = this._ctxObj.canvas; // destroy the overlay canvas c.parentNode.removeChild( c ); // free the lock this._ctx[ this.__self.$( '_LOCK' ) ] = 0; return this; }, /** * Create the overlaying canvas for masked elements and return a 2d context * * @return {CanvasRenderingContext2d} 2d context for new canvas element */ 'private _getObjCanvas': function() { var canvas = this._ctx.canvas, canvas_obj = document.createElement( 'canvas' ); // mimic the dimensions and positions of the original canvas canvas_obj.style.position = 'absolute'; canvas_obj.width = canvas.width; canvas_obj.height = canvas.height; canvas_obj.style.left = ( canvas.offsetLeft + 'px' ); canvas_obj.style.top = ( canvas.offsetTop + 'px' ); // append the new canvas to the DOM under the same parent canvas.parentNode.appendChild( canvas_obj ); return canvas_obj.getContext( '2d' ); }, /** * Render the provided map * * @param {Map} map map to render * * @return {MapRender} self */ 'public render': function( map ) { if ( !( Class.isA( ltjs.Map, map ) ) ) { throw TypeError( 'Invalid Map provided' ); } var objs = map.getObjects(), size = map.getDimensions(), i = objs.length, omap = map.getObjectTileMap(), tunnel_colors = map.getTunnelColors(), // get the width and height from one of the tiles t = this._tiles.dirt.data, w = t.width, h = t.height; // render each object (remember, we're dealing with columns, not rows; // see Map.getObjects()) while ( i-- ) { var oid = objs[ i ], tid = omap[ oid ], x = ( Math.floor( i / size[ 0 ] ) * w ), y = ( ( i % size[ 1 ] ) * h ); // tunnels are handled a bit differently than other objects if ( this._isTunnel( oid ) ) { this._renderTunnel( oid, x, y, tunnel_colors ); continue; } this._drawTile( tid, x, y ); } return this; }, /** * Draw the tile identified by the given id * * The tile will be drawn to the appropriate canvas depending on whether or * not it has been masked. If it does have a mask, it will be drawn to the * overlaying canvas and the dirt tile will be drawn underneath it. * * @param {string} tid tile id * @param {number} x left position * @param {number} y top position * * @return {undefined} */ 'private _drawTile': function( tid, x, y ) { var tile = this._tiles[ tid ], ctx = ( tile.masked ) ? this._ctxObj : this._ctx; ctx.putImageData( this._tiles[ tid ].first.data, x, y ); if ( tile.masked ) { this._ctx.putImageData( this._tiles.dirt.data, x, y ); } }, /** * Returns whether the given object id is a tunnel * * @param {number} oid object id * * @return {boolean} TRUE if object is a tunnel, otherwise false */ 'private _isTunnel': function( oid ) { return ( oid & this.__self.$( '_TMASK' ) ); }, /** * Render the given tunnel * * The tunnel background color (which will peek through the mask) is * rendered to the base canvas, whereas the tunnel tile itself is rendered * on the overlaying canvas. * * @param {number} oid tunnel object id * @param {number} x left position * @param {number} y top position * * @param {Array.} colors tunnel colors * * @return {undefined} */ 'private _renderTunnel': function( oid, x, y, colors ) { // get the tunnel id by stripping off the tunnel bitmask and grab the // associated color var tunnel_id = ( ( +oid ^ this.__self.$( '_TMASK' ) ) / 2 ), color = colors[ tunnel_id ] || 'black', tdata = this._tiles.tunnel.data; // fill tile with the appropriate background color for this tile this._ctx.fillStyle = color; this._ctx.fillRect( x, y, tdata.width, tdata.height ); // render the tile to the overlay canvas this._ctxObj.putImageData( tdata, x, y ); } } );