/** * 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', { /** * Property to hold lock bit on canvas element * @type {string} */ 'private const _LOCK': '__$$MapRenderLock$$', /** * Animation interval in milliseconds * @type {number} */ 'private const _ANIM_INTERVAL': 200, /** * 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': {}, /** * Animation timer * @type {number} */ 'private _animTimer': 0, /** * 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; // clear any running animations this._clearAnimation(); // 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, map_state ) { if ( !( Class.isA( ltjs.MapState, map_state ) ) ) { throw TypeError( 'Invalid MapState provided' ); } var objs = map.getObjects(), size = map.getDimensions(), omap = map.getObjectTileMap(), sizex = size[ 0 ], sizey = size[ 1 ], i = objs.length, // tiles to animate anim = [], // get the width and height from one of the tiles t = this._tiles.dirt.data, w = t.width, h = t.height; this._clearCanvases(); var _self = this; map_state.onChange( function( obj, pos ) { var oid = objs[ pos ], tid = ( obj ) ? obj.getTid() : 'dirt', tile = ( _self._tiles[ tid ] || {} ).first, v = _self._getTileVector( pos, sizex, sizey, w, h ), x = v[ 0 ], y = v[ 1 ]; if ( obj === null ) { _self._clearTile( tile, x, y ); return; } // tunnels are handled a bit differently than other objects if ( map.isObjectTunnel( oid ) ) { _self._renderTunnel( x, y, map.getTunnelColor( oid ) ); return; } // queue for animation if it contains more than a single frame if ( _self._canAnimate( tid ) ) { anim.push( [ tile, x, y ] ); } _self._drawTile( tile, x, y ); } ); map_state.flush(); // render each object (remember, we're dealing with columns, not rows; // see Map.getObjects()) this._beginAnimation( anim ); return this; }, /** * Retrieve a vector representing the x and y position coordinates of a tile * position * * @param {number} pos tile position * @param {number} sizex number of horizontal tiles in map * @param {number} sizey number of vertical tiles in map * @param {number} w tile width * @param {number} h tile height * * @return {Array.} x and y coordinates of tile position */ 'private _getTileVector': function( pos, sizex, sizey, w, h ) { return [ ( Math.floor( pos / sizex ) * w ), // x ( ( pos % sizey ) * h ) // y ]; }, /** * Clears overlay canvas * * This should be used before first rendering a map to ensure that any * artifacts from previous map renderings will be erased. * * We need only clear the overlay canvas, because the lower canvas will * always be overwritten with tiles in every location. Because none of the * tiles written to the lower canvas are masked, nothing from the previous * render would ever peek through (of course, putImageData() would overwrite * it even if that were the case). As such, clearing the lower canvas would * simply be a waste of time and only serve to degrade performance * (especially if this is being used with maps larger than the classic * 16x16). * * @return {undefined} */ 'private _clearCanvases': function() { var ctx = this._ctxObj, c = ctx.canvas; // we need only clear the overlay (to which masked tiles are rendered) ctx.clearRect( 0, 0, c.width, c.height ); }, /** * Determine if the provided tile can be animated * * This simply checks to see if the tile has more than one frame. * * @return {boolean} true if multiple frames, otherwise false */ 'private _canAnimate': function( tid ) { var tdata = this._tiles[ tid ]; return ( tdata.next !== tdata ); }, /** * 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( tile, x, y ) { var ctx = ( tile.masked ) ? this._ctxObj : this._ctx; ctx.putImageData( tile.data, x, y ); if ( tile.masked ) { this._ctx.putImageData( this._tiles.dirt.data, x, y ); } }, 'private _clearTile': function( ref, x, y ) { this._ctxObj.clearRect( x, y, ref.data.width, ref.data.height ); }, /** * 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} x left position * @param {number} y top position * @param {string} color tunnel color (CSS value) * * @return {undefined} */ 'private _renderTunnel': function( x, y, color ) { var 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 ); }, /** * Begin basic tile animation * * At each animation interval, each tile will be advanced a single frame and * rendered atop of the previous. * * @param {Array.>} anim array of tiles to * animate; tdata,x,y * * @return {number} animation timer id */ 'private _beginAnimation': function( anim ) { var _self = this; // clear any existing rendering animations this._clearAnimation(); return this._animTimer = setInterval( function() { var i = anim.length; while ( i-- ) { var cur = anim[ i ]; // draw next frame cur[ 0 ] = cur[ 0 ].next; _self._drawTile.apply( _self, anim[ i ] ); } }, this.__self.$( '_ANIM_INTERVAL' ) ); }, /** * Clear any running animation timers * * It is important that this be done when a MapRender instance is done being * used, or it will remain in memory indefinitely! * * @return {undefined} */ 'private _clearAnimation': function() { this._animTimer = +clearInterval( this._animTimer ); } } );