407 lines
11 KiB
JavaScript
407 lines
11 KiB
JavaScript
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
/**
|
|
* 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.<number>} 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.<Array.<Object,number,number>>} 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 );
|
|
}
|
|
} );
|