/** * 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 . * * * 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 ); } ); } } );