387 lines
15 KiB
JavaScript
387 lines
15 KiB
JavaScript
|
/**
|
||
|
* Handles the masking of tile sets
|
||
|
*
|
||
|
* 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/>.
|
||
|
*
|
||
|
*
|
||
|
* This handles the masking and slicing of tiles found in LTG files (see
|
||
|
* LtgLoader for more information).
|
||
|
*
|
||
|
* We must think back to the good ol' days - before transparency was represented
|
||
|
* in the image format itself and before alphatransparency even existed. Bitmaps
|
||
|
* have no alpha channel like PNG, nor can they designate any color as
|
||
|
* transparent like GIFs. That is what the mask bitmap is for. In the case of
|
||
|
* the LT mask, black is used to denote opacity whereas white denotes
|
||
|
* transparency. Furthermore, not all tiles have masks associated with them
|
||
|
* (e.g. blocks and walls). Rather than those mask tiles being represented as
|
||
|
* solid black boxes, some tileset masks are solid *white*. This, as we will
|
||
|
* see, complicates our masking algorithm.
|
||
|
*
|
||
|
* When rendering the tiles, we obviously need to support transparency. In the
|
||
|
* browser, this must be done either with a GIF or an alpha channel. In other
|
||
|
* words --- we need to apply the mask to the tiles to result in a tile set with
|
||
|
* an alpha channel which can be directly drawn to the screen. Applying this
|
||
|
* mask when the LTG file is initially loaded will also improve performance by
|
||
|
* eliminating the need to re-apply the mask each and every time a particular
|
||
|
* tile is drawn.
|
||
|
*
|
||
|
* To apply this mask, since CSS masking is (at this time) in its infancy, we
|
||
|
* must use the canvas element. Canvas XOR masks, however, do not help us ---
|
||
|
* they apply the mask using the alpha channel, whereas we want to apply based
|
||
|
* on the brightness of a particular pixel. Therefore, our solution will be to
|
||
|
* grab the image data and manipulate each pixel individually, adjusting the
|
||
|
* alpha channel to either 255 for opaque or 0 for transparent. Since the mask
|
||
|
* is either black or white, we needn't calculate the brightness --- we can
|
||
|
* simply check the color value of a single channel (e.g. R) and make the pixel
|
||
|
* entirely transparent if the value is !== 0.
|
||
|
*
|
||
|
* Remember that certain tiles contain no mask. Since they are not filled with
|
||
|
* black, we must be able to recognize when we should *not* apply a mask;
|
||
|
* otherwise, the tile will be entire transparent! Coupling this unfortunate
|
||
|
* detail with the fact that putImageData() does not support slicing like
|
||
|
* drawImage() does, it makes more sense to store each tile individually in
|
||
|
* memory rather than as a single image. Otherwise, we would be forced to use
|
||
|
* drawImage() and re-apply the mask each time a tile is drawn, which is not
|
||
|
* worth the little extra memory that will be consumed by separate tile images.
|
||
|
* Given this implementation, we may then let the LtgLoader know specifically
|
||
|
* what tiles should have masks applied.
|
||
|
*
|
||
|
* With that, we should have an easy-to-use set of tile graphics ready for
|
||
|
* rendering.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Slices tiles and applies masks
|
||
|
*/
|
||
|
ltjs.TileMasker = Class( 'TileMasker',
|
||
|
{
|
||
|
'private const _TDATA': [
|
||
|
[ 'dirt', 0 ], /* dirt */
|
||
|
[ 'tup', 1 ], /* tank up */
|
||
|
[ 'tright', 1 ], /* tank right */
|
||
|
[ 'tdown', 1 ], /* tank down */
|
||
|
[ 'tleft', 1 ], /* tank left */
|
||
|
[ 'base', 0 ], /* base */
|
||
|
[ 'basealt', 0 ], /* base alt */
|
||
|
[ 'basealt2', 0 ], /* base alt 2 */
|
||
|
[ 'water', 0 ], /* water */
|
||
|
[ 'wateralt', 0 ], /* water alt */
|
||
|
|
||
|
[ 'wateralt2', 0 ], /* water alt 2 */
|
||
|
[ 'atdownb', 1 ], /* anti-tank, blown up, down */
|
||
|
[ 'block', 0 ], /* non-movable block */
|
||
|
[ 'mblock', 0 ], /* movable block */
|
||
|
[ 'brick', 0 ], /* brick */
|
||
|
[ 'atup', 1 ], /* anti-tank up */
|
||
|
[ 'atupalt', 1 ], /* anti-tank up alt */
|
||
|
[ 'atupalt2', 1 ], /* anti-tank up alt 2 */
|
||
|
[ 'mblockw', 0 ], /* movable block in water */
|
||
|
[ 'mirrorul', 1 ], /* mirror up-left */
|
||
|
|
||
|
[ 'mirrorur', 1 ], /* mirror up-right */
|
||
|
[ 'mirrordr', 1 ], /* mirror down-right */
|
||
|
[ 'mirrordl', 1 ], /* mirror down-left */
|
||
|
[ 'owup', 0 ], /* one-way up */
|
||
|
[ 'owupalt', 0 ], /* one-way up alt */
|
||
|
[ 'owupalt2', 0 ], /* one-way up alt 2 */
|
||
|
[ 'owright', 0 ], /* one-way right */
|
||
|
[ 'owrightalt', 0 ], /* one-way right alt */
|
||
|
[ 'owrightalt2', 0 ], /* one-way right alt 2 */
|
||
|
[ 'owdown', 0 ], /* one-way down */
|
||
|
|
||
|
[ 'owdownalt', 0 ], /* one-way down alt */
|
||
|
[ 'owdownalt2', 0 ], /* one-way down alt 2 */
|
||
|
[ 'owleft', 0 ], /* one-way left */
|
||
|
[ 'owleftalt', 0 ], /* one-way left alt */
|
||
|
[ 'owleftalt2', 0 ], /* one-way left alt 2 */
|
||
|
[ 'atright', 1 ], /* anti-tank right */
|
||
|
[ 'atrightalt', 1 ], /* anti-tank right alt */
|
||
|
[ 'atrightalt2', 1 ], /* anti-tank right alt 2 */
|
||
|
[ 'atdown', 1 ], /* anti-tank down */
|
||
|
[ 'atdownalt', 1 ], /* anti-tank down alt */
|
||
|
|
||
|
[ 'atdownalt2', 1 ], /* anti-tank down alt 2 */
|
||
|
[ 'atleft', 1 ], /* anti-tank left */
|
||
|
[ 'atleftalt', 1 ], /* anti-tank left alt */
|
||
|
[ 'atleftalt2', 1 ], /* anti-tank left alt 2 */
|
||
|
[ 'cblock', 0 ], /* crystal block */
|
||
|
[ 'cblockht', 0 ], /* crystal block hit by tank */
|
||
|
[ 'rmirrorul', 0 ], /* roto-mirror up-left */
|
||
|
[ 'rmirrorur', 0 ], /* roto-mirror up-right */
|
||
|
[ 'rmirrordr', 0 ], /* roto-mirror down-right */
|
||
|
[ 'rmirrordl', 0 ], /* roto-mirror down-left */
|
||
|
|
||
|
[ 'cblockhat', 0 ], /* crystal block hit by anti-tank */
|
||
|
[ 'atrightb', 1 ], /* anti-tank, blown up, right */
|
||
|
[ 'atleftb', 1 ], /* anti-tank, blown up, left */
|
||
|
[ 'atupb', 1 ], /* anti-tank, blown up, up */
|
||
|
[ 'tunnel', 1 ], /* wormhole/tunnel */
|
||
|
[ 'ice', 0 ], /* ice */
|
||
|
[ 'thinice', 0 ] /* thin ice */
|
||
|
],
|
||
|
|
||
|
'private const _TSIZE': [ 32, 32, 320, 192 ],
|
||
|
|
||
|
/**
|
||
|
* Canvas 2D context (used for masking and tile slicing)
|
||
|
* @type {CanvasRenderingContext2d}
|
||
|
*/
|
||
|
'private _context': null,
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Initialize loader with a 2D canvas context
|
||
|
*
|
||
|
* The context will be used for masking the game bitmap and slicing the
|
||
|
* tiles.
|
||
|
*/
|
||
|
__construct: function()
|
||
|
{
|
||
|
// rather than accepting a context, we will create our own canvas in
|
||
|
// memory to perform our operations (it will not be added to the DOM, so
|
||
|
// these operations will not be visible to the user)
|
||
|
var context = document.createElement( 'canvas' ).getContext( '2d' ),
|
||
|
sizes = this.__self.$( '_TSIZE' );
|
||
|
|
||
|
// size the canvas so that it can fit the entire tileset
|
||
|
context.canvas.width = sizes[ 2 ];
|
||
|
context.canvas.height = sizes[ 3 ];
|
||
|
|
||
|
this._context = context;
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Retrieve image data for each individual tile (pre-masked)
|
||
|
*
|
||
|
* Each tile will have the mask applied before being returned. This allows
|
||
|
* the tile to be rendered without any additional processing, but at the
|
||
|
* cost of additional overhead for the tile loading (which is well worth it,
|
||
|
* since we will be spending the majority of our time rendering tiles, not
|
||
|
* loading them).
|
||
|
*
|
||
|
* This operation is asynchronous, but the masking algorithm is not. If
|
||
|
* performance is a concern during the masking process (for example, if one
|
||
|
* were to create an extension to support very large tilesets), one can
|
||
|
* extend this class to make the operation asynchronous.
|
||
|
*
|
||
|
* @param {string} bmp_game game tileset bitmap (URL or data URL)
|
||
|
* @param {string} bmp_mask game tileset mask bitmap (URL or data URL)
|
||
|
*
|
||
|
* @param {function(Object)} callback function to call with tiles
|
||
|
*
|
||
|
* @return {ltjs.TileMasker} self
|
||
|
*/
|
||
|
'public getMaskedTiles': function( bmp_game, bmp_mask, callback )
|
||
|
{
|
||
|
var _self = this;
|
||
|
|
||
|
this._getImageData( bmp_mask, function( data_mask )
|
||
|
{
|
||
|
// we will render the game image after the mask so that it does not
|
||
|
// need to be re-rendered in order to pull out the image data
|
||
|
_self._renderImage( bmp_game, function()
|
||
|
{
|
||
|
_self.getMaskedTileSet( data_mask, callback );
|
||
|
} );
|
||
|
} );
|
||
|
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Apply mask to each tile and return individual tiles
|
||
|
*
|
||
|
* This method requires that the tileset has already been rendered to the
|
||
|
* canvas.
|
||
|
*
|
||
|
* Note that, although this method accepts a callback, it is not
|
||
|
* asynchronous. It does, however, allow subtypes to make this algorithm
|
||
|
* asynchronous should the need arise. See getMaskedTiles() for more
|
||
|
* information.
|
||
|
*
|
||
|
* @param {Object} data_mask image data for mask bitmap
|
||
|
* @param {function(Object)} callback function to call with tiles
|
||
|
*
|
||
|
* @return {undefined}
|
||
|
*/
|
||
|
'virtual protected getMaskedTileSet': function( data_mask, callback )
|
||
|
{
|
||
|
var tdata = this.__self.$( '_TDATA' ),
|
||
|
tiles = {},
|
||
|
i = -1,
|
||
|
len = tdata.length,
|
||
|
|
||
|
// get tile width and height in pixels, and the number of tiles in
|
||
|
// each row (xn)
|
||
|
sizes = this.__self.$( '_TSIZE' ),
|
||
|
tw = sizes[ 0 ],
|
||
|
th = sizes[ 1 ],
|
||
|
xn = ( sizes[ 2 ] / tw );
|
||
|
|
||
|
// create each tile (preserving order, thus no decrementing)
|
||
|
while ( ++i < len )
|
||
|
{
|
||
|
var name = tdata[ i ][ 0 ],
|
||
|
mask = tdata[ i ][ 1 ],
|
||
|
|
||
|
// calculate the X and Y position of this tile based on the tile
|
||
|
// and bitmap dimensions
|
||
|
x = ( ( i % xn ) * th ),
|
||
|
y = ( ( Math.floor( i / xn ) ) * tw );
|
||
|
|
||
|
// the third index indicates whether or not a mask should be applied
|
||
|
// to the tile
|
||
|
tiles[ name ] = ( mask === 1 )
|
||
|
? this.getMaskedTileData( data_mask, x, y )
|
||
|
: this.getTileData( x, y);
|
||
|
}
|
||
|
|
||
|
callback( tiles );
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Retrieve a tile with the mask applied
|
||
|
*
|
||
|
* This algorithm uses the image rendered to the canvas along with the given
|
||
|
* mask image data to alter the alpha channel of the tile, producing a tile
|
||
|
* with the appropriate transparency.
|
||
|
*
|
||
|
* The LaserTank mask considered black to be opaque and white to be
|
||
|
* transparent. Since those are the only two colors permitted, we can
|
||
|
* improve performance by checking only a single channel rather than
|
||
|
* calculating brightness. If not black, the respective pixel in the tile
|
||
|
* will be considered transparent.
|
||
|
*
|
||
|
* Only the image data for the requested tile will be returned. That is, the
|
||
|
* image data will represent a single tile and it can be rendered directly
|
||
|
* to the canvas.
|
||
|
*
|
||
|
* WARNING: Not all tiles have masks. This method should not be used unless
|
||
|
* the tile has a mask. The result is otherwise LTG-dependent, since some
|
||
|
* LTG files do not have fully opaque masks for those tiles.
|
||
|
*
|
||
|
* @param {Object} data_mask image data for the mask bitmap
|
||
|
* @param {number} x tile X position in game/mask bitmap
|
||
|
* @param {number} y tile Y position in game/mask bitmap
|
||
|
*
|
||
|
* @return {Object} image data for the requested tile
|
||
|
*/
|
||
|
'virtual protected getMaskedTileData': function( data_mask, x, y )
|
||
|
{
|
||
|
var raw = this.getTileData( x, y ),
|
||
|
w = raw.width,
|
||
|
h = raw.height,
|
||
|
mw = data_mask.width,
|
||
|
yi = h;
|
||
|
|
||
|
// apply the mask to the raw tile data (simple and easy-to-understand
|
||
|
// algorithm; we can refine it later if need be), looping through each
|
||
|
// pixel
|
||
|
while ( yi-- )
|
||
|
{
|
||
|
xi = w;
|
||
|
|
||
|
while ( xi-- )
|
||
|
{
|
||
|
// get the R value for the associated pixel in the mask bitmap
|
||
|
// (remember that, although we are dealing with applying the
|
||
|
// mask to a single tile, the mask image contains all tiles, so
|
||
|
// we must calculate its position accordingly)
|
||
|
var mi = ( ( ( yi + y ) * ( mw * 4 ) ) + ( ( xi + x ) * 4 ) ),
|
||
|
mr = data_mask.data[ mi ];
|
||
|
|
||
|
// manipulate the alpha channel of our tile; if the R value for
|
||
|
// the mask is not 0, then this pixel in our tile should be
|
||
|
// transparent (we need only check the R pixel since the mask
|
||
|
// consists of only black and white, so there is no need to
|
||
|
// calculate brightness)
|
||
|
raw.data[ ( ( yi * w * 4 ) + ( xi * 4 ) ) + 3 ] =
|
||
|
( mr === 0 ) ? 255 : 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return raw;
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Retrieve image data for the tile at the given position
|
||
|
*
|
||
|
* @param {number} x tile X position in bitmap
|
||
|
* @param {number} y tile Y position in bitmap
|
||
|
*
|
||
|
* @return {Object} image data for tile
|
||
|
**/
|
||
|
'protected getTileData': function( x, y )
|
||
|
{
|
||
|
var sizes = this.__self.$( '_TSIZE' );
|
||
|
return this._context.getImageData( x, y, sizes[ 0 ], sizes[ 1 ] );
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Render an image to the canvas
|
||
|
*
|
||
|
* This operation is asynchronous and supports loading of external
|
||
|
* resources. Note that an external resource that violates the browser's
|
||
|
* cross-site security policies will taint the canvas, preventing the
|
||
|
* masking operation. Using data URLs will avoid this issue entirely.
|
||
|
*
|
||
|
* @param {string} bmp image URL or data URL
|
||
|
* @param {function(Image)} callback function to call when complete
|
||
|
*
|
||
|
* @return {undefined}
|
||
|
*/
|
||
|
'private _renderImage': function( bmp, callback )
|
||
|
{
|
||
|
var _self = this,
|
||
|
img = new Image();
|
||
|
|
||
|
img.onload = function()
|
||
|
{
|
||
|
_self._context.drawImage( img, 0, 0 );
|
||
|
callback( img );
|
||
|
};
|
||
|
|
||
|
img.src = bmp;
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Retrieve the canvas image data of the given bitmap
|
||
|
*
|
||
|
* @param {string} bmp image URL or data URL
|
||
|
* @param {function(Image)} callback function to call when complete
|
||
|
*
|
||
|
* @return {undefined}
|
||
|
*/
|
||
|
'private _getImageData': function( bmp, callback )
|
||
|
{
|
||
|
var _self = this;
|
||
|
|
||
|
this._renderImage( bmp, function()
|
||
|
{
|
||
|
var size = _self.__self.$( '_TSIZE' ),
|
||
|
data = _self._context.getImageData( 0, 0, size[ 2 ], size[ 3 ] );
|
||
|
|
||
|
callback( data );
|
||
|
} );
|
||
|
}
|
||
|
} );
|