gl-tiled
Version:
A Tiled editor renderer for WebGL.
998 lines (981 loc) • 55.1 kB
JavaScript
/*!
* gl-tiled - v1.0.0
* Compiled Fri, 29 May 2020 20:11:41 UTC
*
* gl-tiled is licensed under the MIT License.
* http://www.opensource.org/licenses/mit-license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.glTiled = {}));
}(this, (function (exports) { 'use strict';
(function (ELayerType) {
ELayerType[ELayerType["UNKNOWN"] = 0] = "UNKNOWN";
ELayerType[ELayerType["Tilelayer"] = 1] = "Tilelayer";
ELayerType[ELayerType["Imagelayer"] = 2] = "Imagelayer";
// Objectgroup,
})(exports.ELayerType || (exports.ELayerType = {}));
function loadImage(url, cache, cb) {
var asset = cache && cache[url];
if (asset) {
var img_1 = asset.data || asset;
if (cb)
cb(null, img_1);
return img_1;
}
var onLoadHandler = function () {
img.removeEventListener('load', onLoadHandler, false);
img.removeEventListener('error', onErrorHandler, false);
if (cb)
cb(null, img);
};
var onErrorHandler = function (e) {
img.removeEventListener('load', onLoadHandler, false);
img.removeEventListener('error', onErrorHandler, false);
if (cb)
cb(e, img);
};
var img = new Image();
img.src = url;
img.addEventListener('load', onLoadHandler, false);
img.addEventListener('error', onErrorHandler, false);
return img;
}
function parseColorStr(colorStr, outColor) {
if (colorStr) {
if (colorStr.length === 9) {
outColor[3] = parseInt(colorStr.substr(1, 2), 16) / 255;
outColor[0] = parseInt(colorStr.substr(3, 2), 16) / 255;
outColor[1] = parseInt(colorStr.substr(5, 2), 16) / 255;
outColor[2] = parseInt(colorStr.substr(7, 2), 16) / 255;
}
else if (colorStr.length === 7) {
outColor[3] = 1.0;
outColor[0] = parseInt(colorStr.substr(1, 2), 16) / 255;
outColor[1] = parseInt(colorStr.substr(3, 2), 16) / 255;
outColor[2] = parseInt(colorStr.substr(5, 2), 16) / 255;
}
}
}
var GLImagelayer = /** @class */ (function () {
function GLImagelayer(desc, assetCache) {
var _this = this;
this.desc = desc;
this.type = exports.ELayerType.Imagelayer;
this.scrollScaleX = 1;
this.scrollScaleY = 1;
this.gl = null;
this.texture = null;
this.image = null;
this.alpha = typeof desc.opacity === 'number' ? desc.opacity : 1.0;
// parse the transparent color
this._transparentColor = new Float32Array(4);
if (desc.transparentcolor)
parseColorStr(desc.transparentcolor, this._transparentColor);
loadImage(desc.image, assetCache, function (errEvent, img) {
_this.image = img;
_this.upload();
});
}
GLImagelayer.prototype.glInitialize = function (gl) {
this.glTerminate();
this.gl = gl;
this.texture = gl.createTexture();
this.upload();
};
GLImagelayer.prototype.glTerminate = function () {
if (!this.gl)
return;
if (this.texture) {
this.gl.deleteTexture(this.texture);
this.texture = null;
}
this.gl = null;
};
GLImagelayer.prototype.upload = function () {
if (!this.gl || !this.image)
return;
this.setupTexture();
this.uploadData(false);
};
GLImagelayer.prototype.uploadUniforms = function (shader) {
if (!this.gl || !this.image)
return;
var gl = this.gl;
gl.uniform1f(shader.uniforms.uAlpha, this.alpha);
gl.uniform4fv(shader.uniforms.uTransparentColor, this._transparentColor);
gl.uniform2f(shader.uniforms.uSize, this.image.width, this.image.height);
};
GLImagelayer.prototype.uploadData = function (doBind) {
if (doBind === void 0) { doBind = true; }
if (!this.gl || !this.image)
return;
var gl = this.gl;
if (doBind)
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image);
};
GLImagelayer.prototype.setupTexture = function (doBind) {
if (doBind === void 0) { doBind = true; }
if (!this.gl)
return;
var gl = this.gl;
if (doBind)
gl.bindTexture(gl.TEXTURE_2D, this.texture);
// TODO: Allow user to set filtering
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
};
return GLImagelayer;
}());
(function (TilesetFlags) {
TilesetFlags[TilesetFlags["FlippedAntiDiagonal"] = 536870912] = "FlippedAntiDiagonal";
TilesetFlags[TilesetFlags["FlippedVertical"] = 1073741824] = "FlippedVertical";
TilesetFlags[TilesetFlags["FlippedHorizontal"] = 2147483648] = "FlippedHorizontal";
TilesetFlags[TilesetFlags["All"] = -536870912] = "All";
TilesetFlags[TilesetFlags["FlippedAntiDiagonalFlag"] = 2] = "FlippedAntiDiagonalFlag";
TilesetFlags[TilesetFlags["FlippedVerticalFlag"] = 4] = "FlippedVerticalFlag";
TilesetFlags[TilesetFlags["FlippedHorizontalFlag"] = -8] = "FlippedHorizontalFlag";
})(exports.TilesetFlags || (exports.TilesetFlags = {}));
var GLTileset = /** @class */ (function () {
function GLTileset(desc, assetCache) {
this.desc = desc;
this.gl = null;
/** The images in this tileset. */
this.images = [];
/** The gl textures in this tileset */
this.textures = [];
this._lidToTileMap = {};
// load the images
if (this.desc.image) {
this._addImage(this.desc.image, assetCache);
}
if (this.desc.tiles) {
for (var i = 0; i < this.desc.tiles.length; ++i) {
var tile = this.desc.tiles[i];
this._lidToTileMap[tile.id] = tile;
if (tile.image) {
this._addImage(tile.image, assetCache);
}
}
}
}
Object.defineProperty(GLTileset.prototype, "lastgid", {
/** The last gid in this tileset */
get: function () {
return this.desc.firstgid + this.desc.tilecount;
},
enumerable: true,
configurable: true
});
/**
* Returns true if the given gid is contained in this tileset
*
* @param gid The global ID of the tile in a map.
*/
GLTileset.prototype.containsGid = function (gid) {
return this.containsLocalId(this.getTileLocalId(gid));
};
/**
* Returns true if the given index is contained in this tileset
*
* @param index The local index of a tile in this tileset.
*/
GLTileset.prototype.containsLocalId = function (index) {
return index >= 0 && index < this.desc.tilecount;
};
/**
* Returns the tile ID for a given gid. Assumes it is within range
*
* @param gid The global ID of the tile in a map.
*/
GLTileset.prototype.getTileLocalId = function (gid) {
return (gid & ~exports.TilesetFlags.All) - this.desc.firstgid;
};
/**
* Gathers the properties of a tile
*
* @param gid The global ID of the tile in a map.
*/
GLTileset.prototype.getTileProperties = function (gid) {
if (!gid)
return null;
var localId = this.getTileLocalId(gid);
if (!this.containsLocalId(localId))
return null;
return {
coords: {
x: localId % this.desc.columns,
y: Math.floor(localId / this.desc.columns),
},
imgIndex: this.images.length > 1 ? localId : 0,
flippedX: (gid & exports.TilesetFlags.FlippedHorizontal) != 0,
flippedY: (gid & exports.TilesetFlags.FlippedVertical) != 0,
flippedAD: (gid & exports.TilesetFlags.FlippedAntiDiagonal) != 0,
tile: this._lidToTileMap[localId],
};
};
GLTileset.prototype.bind = function (startSlot) {
var gl = this.gl;
for (var i = 0; i < this.textures.length; ++i) {
gl.activeTexture(startSlot + i);
gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
}
};
GLTileset.prototype.glInitialize = function (gl) {
this.glTerminate();
this.gl = gl;
for (var i = 0; i < this.images.length; ++i) {
// If there is already an image then that means the image finished
// loading at some point, so we need to recreate the texture. If there
// isn't an image here, then the loading callback will hit at some
// point and create the texture for us there.
if (this.images[i]) {
this._createTexture(i);
}
}
};
GLTileset.prototype.glTerminate = function () {
if (!this.gl)
return;
var gl = this.gl;
for (var i = 0; i < this.textures.length; ++i) {
var tex = this.textures[i];
if (tex) {
gl.deleteTexture(tex);
}
}
this.textures.length = 0;
this.gl = null;
};
GLTileset.prototype._addImage = function (src, assets) {
var _this = this;
var imgIndex = this.images.length;
this.images.push(null);
this.textures.push(null);
loadImage(src, assets, function (errEvent, img) {
if (!errEvent) {
_this.images[imgIndex] = img;
_this._createTexture(imgIndex);
}
});
};
GLTileset.prototype._createTexture = function (imgIndex) {
if (!this.gl)
return;
var gl = this.gl;
var img = this.images[imgIndex];
var tex = this.textures[imgIndex] = gl.createTexture();
if (!tex || !img) {
throw new Error('Failed to create WebGL texture for tileset.');
}
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
// TODO: Allow user to set filtering, but also need a way to do linear
// filtering without tile tearing when zooming in.
// Possibility: Render at scale 1 to a framebuffer, scale the frambuffer linearly
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
};
return GLTileset;
}());
/**
* Due to the storage format used tileset images are limited to
* 256 x 256 tiles, and there can only be up to 256 tilesets. Similarly
* a multi-image tileset can only have up-to 256 images.
*
* Since a tileset sheet with 256x256 tiles at 16x16 tile size is 4096x4096
* pixels I think this restriciton is probably OK. Additonally if you actually
* approach the 256 image/tileset limit it will likely be a GPU bandwidth issue
* long before it is an issue with our storage format here.
*
*/
var GLTilelayer = /** @class */ (function () {
function GLTilelayer(desc, tilesets) {
this.desc = desc;
this.type = exports.ELayerType.Tilelayer;
this.gl = null;
this.scrollScaleX = 1;
this.scrollScaleY = 1;
this.texture = null;
this._animations = [];
this._inverseTileCount = new Float32Array(2);
this._repeatTiles = true;
this._inverseTileCount[0] = 1 / desc.width;
this._inverseTileCount[1] = 1 / desc.height;
this.textureData = new Uint8Array(desc.width * desc.height * 4);
this.alpha = desc.opacity;
// If this isn't true then we probably did something wrong or got bad data...
// This has caught me putting in base64 data instead of array data more than once!
if ((desc.width * desc.height) !== this.desc.data.length)
throw new Error('Sizes are off!');
this._buildMapTexture(tilesets);
}
Object.defineProperty(GLTilelayer.prototype, "repeatTiles", {
get: function () {
return this._repeatTiles;
},
set: function (v) {
if (v !== this._repeatTiles) {
this._repeatTiles = v;
this._setupTexture(); // delay until next draw?
}
},
enumerable: true,
configurable: true
});
GLTilelayer.prototype.glInitialize = function (gl) {
this.glTerminate();
this.gl = gl;
this.texture = gl.createTexture();
this._upload();
};
GLTilelayer.prototype.glTerminate = function () {
if (!this.gl)
return;
if (this.texture) {
this.gl.deleteTexture(this.texture);
this.texture = null;
}
this.gl = null;
};
/**
* Updates the layer's animations by the given delta time.
*
* @param dt Delta time in milliseconds to perform an update for.
*/
GLTilelayer.prototype.update = function (dt) {
var needsUpload = false;
for (var i = 0; i < this._animations.length; ++i) {
var anim = this._animations[i];
anim.elapsedTime = (anim.elapsedTime + dt) % anim.maxTime;
for (var f = 0; f < anim.frames.length; ++f) {
var frame = anim.frames[f];
if (anim.elapsedTime >= frame.startTime && anim.elapsedTime < frame.endTime) {
if (anim.activeFrame !== f) {
needsUpload = true;
anim.activeFrame = f;
this.textureData[anim.index] = frame.props.coords.x;
this.textureData[anim.index + 1] = frame.props.coords.y;
}
break;
}
}
}
if (needsUpload)
this._uploadData(true);
};
GLTilelayer.prototype.uploadUniforms = function (shader) {
if (!this.gl)
return;
var gl = this.gl;
gl.uniform1f(shader.uniforms.uAlpha, this.alpha);
gl.uniform1i(shader.uniforms.uRepeatTiles, this._repeatTiles ? 1 : 0);
gl.uniform2fv(shader.uniforms.uInverseLayerTileCount, this._inverseTileCount);
};
GLTilelayer.prototype._upload = function () {
this._setupTexture();
this._uploadData(false);
};
GLTilelayer.prototype._uploadData = function (doBind) {
if (!this.gl)
return;
var gl = this.gl;
if (doBind)
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, // level
gl.RGBA, // internal format
this.desc.width, this.desc.height, 0, // border
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type
this.textureData);
};
GLTilelayer.prototype._setupTexture = function (doBind) {
if (doBind === void 0) { doBind = true; }
if (!this.gl)
return;
var gl = this.gl;
if (doBind)
gl.bindTexture(gl.TEXTURE_2D, this.texture);
// MUST be filtered with NEAREST or tile lookup fails
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
if (this._repeatTiles) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
}
else {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
};
/**
* Builds the texture used as the map for this layer. Each texture has the data
* necessary for the shader to lookup the correct texel to display.
*
* @param tilesets The list of tilesets, who's images will be uploaded to the GPU elsewhere.
*/
GLTilelayer.prototype._buildMapTexture = function (tilesets) {
// TODO:
// - Might be faster to build this texture on the GPU in a framebuffer?
// - Should it then be read back into RAM so it can be modified on CPU?
// - Should it just be calculated at runtime in the main shader (upload tileset metadata)?
// * Isn't this last one the same as what I do here? I'd still
// have to format the tileset data for upload...
// - Can I upload animation data and just lookup the right frame in the shader? That would
// mean I don't have to upload a new layer texture each frame like I do now.
var index = 0;
var data = this.desc.data;
dataloop: for (var i = 0; i < data.length; ++i) {
var gid = data[i];
var imgIndex = 0;
if (gid) {
for (var t = 0; t < tilesets.length; ++t) {
var tileset = tilesets[t];
var tileprops = tileset.getTileProperties(gid);
if (tileprops) {
if (tileprops.tile && tileprops.tile.animation) {
this._addAnimation(index, tileset, tileprops.tile.animation);
}
this.textureData[index++] = tileprops.coords.x;
this.textureData[index++] = tileprops.coords.y;
this.textureData[index++] = tileprops.imgIndex + imgIndex;
this.textureData[index++] =
(tileprops.flippedX ? exports.TilesetFlags.FlippedHorizontalFlag : 0)
| (tileprops.flippedY ? exports.TilesetFlags.FlippedVerticalFlag : 0)
| (tileprops.flippedAD ? exports.TilesetFlags.FlippedAntiDiagonalFlag : 0);
continue dataloop;
}
imgIndex += tilesets[t].images.length;
}
}
// if we reach here, it was because either this tile is 0, meaning
// there is no tile here. Or, we failed to find the tileset for it.
// if we failed to find a tileset, or the gid was 0, just write an empty entry.
this.textureData[index++] = 255;
this.textureData[index++] = 255;
this.textureData[index++] = 255;
this.textureData[index++] = 255;
}
};
GLTilelayer.prototype._addAnimation = function (index, tileset, animationFrames) {
var maxTime = 0;
this._animations.push({
index: index,
activeFrame: -1,
elapsedTime: 0,
frames: animationFrames.map(function (v) {
var animTileGid = v.tileid + tileset.desc.firstgid;
var animTileProps = tileset.getTileProperties(animTileGid);
return {
duration: v.duration,
tileid: v.tileid,
props: animTileProps,
startTime: maxTime,
endTime: (maxTime += v.duration),
};
}),
maxTime: 0,
});
this._animations[this._animations.length - 1].maxTime = maxTime;
};
return GLTilelayer;
}());
function assertNever(x) {
throw new Error("Unexpected object: " + x);
}
function hasOwnKey(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
/**
* Helper class to manage GL shader programs.
*
*/
var GLProgram = /** @class */ (function () {
/**
* @param gl The rendering context.
* @param vertexSrc The vertex shader source as an array of strings.
* @param fragmentSrc The fragment shader source as an array of strings.
* @param attributeLocations A key value pair showing which location
* each attribute should sit eg `{ position: 0, uvs: 1 }`.
*/
function GLProgram(gl, vertexSrc, fragmentSrc, attributeLocations) {
/** The attribute locations of this program */
this.attributes = {};
/** The uniform locations of this program */
this.uniforms = {};
this.program = GLProgram.compileProgram(gl, vertexSrc, fragmentSrc, attributeLocations);
// build a list of attribute locations
var aCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
for (var i = 0; i < aCount; ++i) {
var attrib = gl.getActiveAttrib(this.program, i);
if (attrib) {
this.attributes[attrib.name] = gl.getAttribLocation(this.program, attrib.name);
}
}
// build a list of uniform locations
var uCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
for (var i = 0; i < uCount; ++i) {
var uniform = gl.getActiveUniform(this.program, i);
if (uniform) {
var name_1 = uniform.name.replace('[0]', '');
var loc = gl.getUniformLocation(this.program, name_1);
if (loc) {
this.uniforms[name_1] = loc;
}
}
}
}
/**
* @param gl The rendering context.
* @param vertexSrc The vertex shader source as an array of strings.
* @param fragmentSrc The fragment shader source as an array of strings.
* @param attributeLocations A key value pair showing which location
* each attribute should sit eg `{ position: 0, uvs: 1 }`.
*/
GLProgram.compileProgram = function (gl, vertexSrc, fragmentSrc, attributeLocations) {
var glVertShader = GLProgram.compileShader(gl, gl.VERTEX_SHADER, vertexSrc);
var glFragShader = GLProgram.compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
var program = gl.createProgram();
if (!program) {
throw new Error('Failed to create WebGL program object.');
}
gl.attachShader(program, glVertShader);
gl.attachShader(program, glFragShader);
// optionally, set the attributes manually for the program rather than letting WebGL decide..
if (attributeLocations) {
for (var k in attributeLocations) {
if (!hasOwnKey(attributeLocations, k))
continue;
var location_1 = attributeLocations[k];
if (location_1) {
gl.bindAttribLocation(program, location_1, k);
}
}
}
gl.linkProgram(program);
// if linking fails, then log and cleanup
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
var errLog = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
gl.deleteShader(glVertShader);
gl.deleteShader(glFragShader);
throw new Error("Could not link shader program. Log:\n" + errLog);
}
// clean up some shaders
gl.deleteShader(glVertShader);
gl.deleteShader(glFragShader);
return program;
};
/**
* Compiles source into a program.
*
* @param gl The rendering context.
* @param type The type, can be either gl.VERTEX_SHADER or gl.FRAGMENT_SHADER.
* @param source The fragment shader source as an array of strings.
*/
GLProgram.compileShader = function (gl, type, source) {
var shader = gl.createShader(type);
if (!shader) {
throw new Error('Failed to create WebGL shader object.');
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
var errLog = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error("Failed to compile shader. Log:\n" + errLog);
}
return shader;
};
return GLProgram;
}());
var backgroundVS = "precision lowp float;\n\nattribute vec2 aPosition;\n\nvoid main()\n{\n gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n";
var backgroundFS = "precision lowp float;\n\nuniform vec4 uColor;\n\nvoid main()\n{\n gl_FragColor = uColor;\n}";
var tilelayerVS = "precision highp float;\n\nattribute vec2 aPosition;\nattribute vec2 aTexture;\n\nuniform float uInverseTileScale;\n\nuniform vec2 uOffset;\nuniform vec2 uViewportSize;\nuniform vec2 uInverseLayerTileCount;\nuniform vec2 uInverseLayerTileSize;\n\nvarying vec2 vPixelCoord;\nvarying vec2 vTextureCoord;\n\nvoid main()\n{\n // round offset to the nearest multiple of the inverse scale\n // this essentially clamps offset to whole \"pixels\"\n vec2 offset = uOffset + (uInverseTileScale / 2.0);\n offset -= mod(offset, uInverseTileScale);\n\n vPixelCoord = (aTexture * uViewportSize) + offset;\n vTextureCoord = vPixelCoord * uInverseLayerTileCount * uInverseLayerTileSize;\n\n gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n";
var tilelayerFS = "precision mediump float;\n\n// TODO: There is a bit too much branching here, need to try and simplify a bit\n\n#pragma define(NUM_TILESETS)\n#pragma define(NUM_TILESET_IMAGES)\n\nvarying vec2 vPixelCoord;\nvarying vec2 vTextureCoord;\n\nuniform sampler2D uLayer;\nuniform sampler2D uTilesets[NUM_TILESET_IMAGES];\n\nuniform vec2 uTilesetTileSize[NUM_TILESET_IMAGES];\nuniform vec2 uTilesetTileOffset[NUM_TILESET_IMAGES];\nuniform vec2 uInverseTilesetTextureSize[NUM_TILESET_IMAGES];\nuniform float uAlpha;\nuniform int uRepeatTiles;\n\nconst float Flag_FlippedAntiDiagonal = 2.0;\nconst float Flag_FlippedVertical = 4.0;\nconst float Flag_FlippedHorizontal = 8.0;\nconst vec4 c_one4 = vec4(1.0, 1.0, 1.0, 1.0);\n\n// returns 1.0 if flag is set, 0.0 is not\nfloat hasFlag(float value, float flag)\n{\n float byteVal = 1.0;\n\n // early out in trivial cases\n if (value == 0.0)\n return 0.0;\n\n // Only 4 since our highest flag is `8`, so we only need to check 4 bits\n for (int i = 0; i < 4; ++i)\n {\n if (mod(value, 2.0) > 0.0 && mod(flag, 2.0) > 0.0)\n return 1.0;\n\n value = floor(value / 2.0);\n flag = floor(flag / 2.0);\n\n if (!(value > 0.0 && flag > 0.0))\n return 0.0;\n }\n\n return 0.0;\n}\n\nvec2 getTilesetTileSize(int index)\n{\n for (int i = 0; i < NUM_TILESET_IMAGES; ++i)\n if (i == index)\n return uTilesetTileSize[i];\n\n return vec2(0.0, 0.0);\n}\n\nvec2 getTilesetTileOffset(int index)\n{\n for (int i = 0; i < NUM_TILESET_IMAGES; ++i)\n if (i == index)\n return uTilesetTileOffset[i];\n\n return vec2(0.0, 0.0);\n}\n\nvec4 getColor(int index, vec2 coord)\n{\n for (int i = 0; i < NUM_TILESET_IMAGES; ++i)\n if (i == index)\n return texture2D(uTilesets[i], coord * uInverseTilesetTextureSize[i]);\n\n return vec4(0.0, 0.0, 0.0, 0.0);\n}\n\nvoid main()\n{\n if (uRepeatTiles == 0 && (vTextureCoord.x < 0.0 || vTextureCoord.x > 1.0 || vTextureCoord.y < 0.0 || vTextureCoord.y > 1.0))\n discard;\n\n vec4 tile = texture2D(uLayer, vTextureCoord);\n\n if (tile == c_one4)\n discard;\n\n float flipFlags = floor(tile.w * 255.0);\n\n // GLSL ES 2.0 doesn't have bitwise flags...\n // int isFlippedAD = (flipFlags & Flag_FlippedAntiDiagonal) >> 1;\n // int isFlippedY = (flipFlags & Flag_FlippedVertical) >> 2;\n // int isFlippedX = (flipFlags & Flag_FlippedHorizontal) >> 3;\n\n int imgIndex = int(floor(tile.z * 255.0));\n vec2 tileSize = getTilesetTileSize(imgIndex);\n vec2 tileOffset = getTilesetTileOffset(imgIndex);\n\n vec2 flipVec = vec2(hasFlag(flipFlags, Flag_FlippedHorizontal), hasFlag(flipFlags, Flag_FlippedVertical));\n\n vec2 tileCoord = floor(tile.xy * 255.0);\n\n // tileOffset.x is 'spacing', tileOffset.y is 'margin'\n tileCoord.x = (tileCoord.x * tileSize.x) + (tileCoord.x * tileOffset.x) + tileOffset.y;\n tileCoord.y = (tileCoord.y * tileSize.y) + (tileCoord.y * tileOffset.x) + tileOffset.y;\n\n vec2 offsetInTile = mod(vPixelCoord, tileSize);\n vec2 offsetInTileFlipped = abs((tileSize * flipVec) - offsetInTile);\n\n // if isFlippedAD is set, this will flip the x/y coords\n if (hasFlag(flipFlags, Flag_FlippedAntiDiagonal) == 1.0)\n {\n float x = offsetInTileFlipped.x;\n offsetInTileFlipped.x = offsetInTileFlipped.y;\n offsetInTileFlipped.y = x;\n }\n\n vec4 color = getColor(imgIndex, tileCoord + offsetInTileFlipped);\n\n gl_FragColor = vec4(color.rgb, color.a * uAlpha);\n}\n";
var imagelayerVS = "precision highp float;\n\nattribute vec2 aPosition;\nattribute vec2 aTexture;\n\nuniform float uInverseTileScale;\n\nuniform vec2 uOffset;\nuniform vec2 uSize;\nuniform vec2 uViewportSize;\n// uniform mat3 uProjection;\n\nvarying vec2 vTextureCoord;\n\nvoid main()\n{\n // squash from [-1, 1] to [0, 1]\n vec2 position = aPosition;\n position += 1.0;\n position /= 2.0;\n\n // round offset to the nearest multiple of the inverse scale\n // this essentially clamps offset to whole \"pixels\"\n vec2 offset = uOffset + (uInverseTileScale / 2.0);\n offset -= mod(offset, uInverseTileScale);\n\n // modify offset by viewport & size\n offset.x -= uViewportSize.x / 2.0;\n offset.y += (uViewportSize.y / 2.0) - uSize.y;\n\n // calculate this vertex position based on image size and offset\n position *= uSize;\n position += offset;\n\n // project to clip space\n position *= (2.0 / uViewportSize);\n\n vTextureCoord = aTexture;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n";
var imagelayerFS = "precision mediump float;\n\nvarying vec2 vTextureCoord;\n\nuniform sampler2D uSampler;\nuniform float uAlpha;\nuniform vec4 uTransparentColor;\n\nvoid main()\n{\n vec4 color = texture2D(uSampler, vTextureCoord);\n\n if (uTransparentColor.a == 1.0 && uTransparentColor.rgb == color.rgb)\n discard;\n\n gl_FragColor = vec4(color.rgb, color.a * uAlpha);\n}\n";
var GLTilemap = /** @class */ (function () {
function GLTilemap(desc, options) {
if (options === void 0) { options = {}; }
this.desc = desc;
this.gl = null;
this.shaders = null;
this.assetCache = undefined;
this._layers = [];
this._tilesets = [];
this._viewportSize = new Float32Array(2);
this._scaledViewportSize = new Float32Array(2);
this._inverseLayerTileSize = new Float32Array(2);
this._quadVerts = new Float32Array([
//x y u v
-1, -1, 0, 1,
1, -1, 1, 1,
1, 1, 1, 0,
-1, -1, 0, 1,
1, 1, 1, 0,
-1, 1, 0, 0,
]);
this._quadVertBuffer = null;
this._firstTilelayerUniformUpload = true;
this._tileScale = 1;
this._totalTilesetImages = 0;
if (options.assetCache)
this.assetCache = options.assetCache;
this.renderBackgroundColor = typeof options.renderBackgroundColor === 'boolean' ? options.renderBackgroundColor : true;
this.blendMode = {
func: options.blendMode && options.blendMode.func || [WebGLRenderingContext.SRC_ALPHA, WebGLRenderingContext.ONE_MINUS_SRC_ALPHA],
equation: options.blendMode && options.blendMode.equation || WebGLRenderingContext.FUNC_ADD,
};
this._inverseLayerTileSize[0] = 1 / desc.tilewidth;
this._inverseLayerTileSize[1] = 1 / desc.tileheight;
for (var i = 0; i < desc.tilesets.length; ++i) {
var tileset = new GLTileset(desc.tilesets[i], this.assetCache);
this._totalTilesetImages += tileset.images.length;
this._tilesets.push(tileset);
}
this._createInitialLayers(desc.layers, options);
// parse the background color
this._backgroundColor = new Float32Array(4);
if (desc.backgroundcolor)
parseColorStr(desc.backgroundcolor, this._backgroundColor);
// setup the different buffers
this._tilesetIndices = new Int32Array(this._totalTilesetImages);
this._tilesetTileSizeBuffer = new Float32Array(this._totalTilesetImages * 2);
this._tilesetTileOffsetBuffer = new Float32Array(this._totalTilesetImages * 2);
this._inverseTilesetTextureSizeBuffer = new Float32Array(this._totalTilesetImages * 2);
this._buildBufferData();
if (options.gl) {
this.glInitialize(options.gl);
}
}
Object.defineProperty(GLTilemap.prototype, "layers", {
get: function () {
return this._layers;
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "tilesets", {
get: function () {
return this._tilesets;
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "viewportWidth", {
get: function () {
return this._viewportSize[0];
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "viewportHeight", {
get: function () {
return this._viewportSize[1];
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "scaledViewportWidth", {
get: function () {
return this._scaledViewportSize[0];
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "scaledViewportHeight", {
get: function () {
return this._scaledViewportSize[1];
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "repeatTiles", {
set: function (v) {
for (var i = 0; i < this._layers.length; ++i) {
var layer = this._layers[i];
if (layer.type === exports.ELayerType.Tilelayer) {
layer.repeatTiles = false;
}
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(GLTilemap.prototype, "tileScale", {
get: function () {
return this._tileScale;
},
set: function (scale) {
if (this._tileScale != scale) {
this._tileScale = scale;
this._updateViewportSize();
}
},
enumerable: true,
configurable: true
});
GLTilemap.prototype.resizeViewport = function (width, height) {
if (this._viewportSize[0] != width || this._viewportSize[1] != height) {
this._viewportSize[0] = width;
this._viewportSize[1] = height;
this._updateViewportSize();
}
};
GLTilemap.prototype.glInitialize = function (gl) {
this.glTerminate();
this.gl = gl;
this._firstTilelayerUniformUpload = true;
// initialize layers
for (var i = 0; i < this._layers.length; ++i) {
this._layers[i].glInitialize(gl);
}
// initialize tilesets
for (var i = 0; i < this._tilesets.length; ++i) {
this._tilesets[i].glInitialize(gl);
}
// create buffers
this._quadVertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadVertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._quadVerts, gl.STATIC_DRAW);
// create shaders
this._createShaders();
// update viewport uniforms
this._updateViewportSize();
};
GLTilemap.prototype.glTerminate = function () {
if (!this.gl)
return;
var gl = this.gl;
// destroy layers
for (var i = 0; i < this._layers.length; ++i) {
this._layers[i].glTerminate();
}
// destroy tilesets
for (var i = 0; i < this._tilesets.length; ++i) {
this._tilesets[i].glTerminate();
}
// destroy buffers
if (this._quadVertBuffer) {
gl.deleteBuffer(this._quadVertBuffer);
this._quadVertBuffer = null;
}
// destroy shaders
for (var k in this.shaders) {
if (!hasOwnKey(this.shaders, k))
continue;
var shader = this.shaders[k];
gl.deleteProgram(shader.program);
}
this.shaders = null;
this.gl = null;
};
/**
* Updates each layer's animations by the given delta time.
*
* @param dt Delta time in milliseconds to perform an update for.
*/
GLTilemap.prototype.update = function (dt) {
for (var i = 0; i < this.layers.length; ++i) {
var layer = this._layers[i];
if (layer.type === exports.ELayerType.Tilelayer)
layer.update(dt);
}
};
/**
* Draws the tilemap.
*
* @param x The x offset at which to draw the map
* @param y The y offset at which to draw the map
*/
GLTilemap.prototype.draw = function (x, y) {
if (x === void 0) { x = 0; }
if (y === void 0) { y = 0; }
if (!this.gl || !this.shaders)
return;
var gl = this.gl;
gl.enable(gl.BLEND);
gl.blendEquation(this.blendMode.equation);
if (this.blendMode.func.length > 2) {
gl.blendFuncSeparate(this.blendMode.func[0], this.blendMode.func[1], this.blendMode.func[2], this.blendMode.func[3]);
}
else {
gl.blendFunc(this.blendMode.func[0], this.blendMode.func[1]);
}
// Enable attributes, these are the same for all shaders.
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadVertBuffer);
gl.enableVertexAttribArray(GLTilemap._attribIndices.aPosition);
gl.enableVertexAttribArray(GLTilemap._attribIndices.aTexture);
gl.vertexAttribPointer(GLTilemap._attribIndices.aPosition, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(GLTilemap._attribIndices.aTexture, 2, gl.FLOAT, false, 16, 8);
// Draw background
if (this.renderBackgroundColor && this._backgroundColor[3] > 0) {
var bgShader = this.shaders.background;
gl.useProgram(bgShader.program);
gl.uniform4fv(bgShader.uniforms.uColor, this._backgroundColor);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
// Bind tileset textures
var imgIndex = 0;
for (var i = 0; i < this._tilesets.length; ++i) {
var tileset = this._tilesets[i];
for (var t = 0; t < tileset.textures.length; ++t) {
this.gl.activeTexture(gl.TEXTURE1 + imgIndex);
this.gl.bindTexture(this.gl.TEXTURE_2D, tileset.textures[t]);
imgIndex++;
}
}
// Draw each layer of the map
gl.activeTexture(gl.TEXTURE0);
var lastShader = exports.ELayerType.UNKNOWN;
var activeShader = null;
for (var i = 0; i < this._layers.length; ++i) {
var layer = this._layers[i];
var offsetx = layer.desc.offsetx || 0;
var offsety = layer.desc.offsety || 0;
if (!layer.desc.visible)
continue;
if (lastShader != layer.type) {
activeShader = this._bindShader(layer);
lastShader = layer.type;
}
if (!activeShader)
continue;
switch (layer.type) {
case exports.ELayerType.Tilelayer:
layer.uploadUniforms(activeShader);
gl.uniform2f(activeShader.uniforms.uOffset, -offsetx + (x * layer.scrollScaleX), -offsety + (y * layer.scrollScaleY));
break;
case exports.ELayerType.Imagelayer:
layer.uploadUniforms(activeShader);
gl.uniform2f(activeShader.uniforms.uOffset, offsetx + (-x * layer.scrollScaleX), -offsety + (y * layer.scrollScaleY));
break;
// case ELayerType.Objectgroup:
// break;
default:
assertNever(layer);
}
// if (layer.type !== ELayerType.Objectgroup)
{
gl.bindTexture(gl.TEXTURE_2D, layer.texture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
}
};
GLTilemap.prototype.findLayerDesc = function () {
var name = [];
for (var _i = 0; _i < arguments.length; _i++) {
name[_i] = arguments[_i];
}
return this._doFindLayerDesc(this.desc.layers, name, 0);
};
GLTilemap.prototype.createLayer = function () {
var name = [];
for (var _i = 0; _i < arguments.length; _i++) {
name[_i] = arguments[_i];
}
if (name.length === 0)
return false;
var layerDesc = this._doFindLayerDesc(this.desc.layers, name, 0);
if (!layerDesc)
return false;
this.createLayerFromDesc(layerDesc);
return true;
};
GLTilemap.prototype.destroyLayer = function () {
var name = [];
for (var _i = 0; _i < arguments.length; _i++) {
name[_i] = arguments[_i];
}
if (name.length === 0)
return false;
var layerDesc = this._doFindLayerDesc(this.desc.layers, name, 0);
if (!layerDesc)
return false;
return this.destroyLayerFromDesc(layerDesc);
};
GLTilemap.prototype.createLayerFromDesc = function (layer) {
var newLayer = null;
switch (layer.type) {
case 'tilelayer':
newLayer = new GLTilelayer(layer, this.tilesets);
break;
case 'objectgroup':
// newLayer = new GLObjectgroup(layer);
break;
case 'imagelayer':
newLayer = new GLImagelayer(layer, this.assetCache);
break;
case 'group':
for (var i = 0; i < layer.layers.length; ++i) {
this.createLayerFromDesc(layer.layers[i]);
}
break;
default:
return assertNever(layer);
}
if (newLayer) {
this._layers.push(newLayer);
if (this.gl)
newLayer.glInitialize(this.gl);
}
};
GLTilemap.prototype.destroyLayerFromDesc = function (layerDesc) {
for (var i = 0; i < this._layers.length; ++i) {
var layer = this._layers[i];
if (layer.desc === layerDesc) {
layer.glTerminate();
this._layers.splice(1, 1);
return true;
}
}
return false;
};
GLTilemap.prototype._doFindLayerDesc = function (layers, names, nameIndex) {
for (var i = 0; i < layers.length; ++i) {
var layer = layers[i];
if (layer.name === names[nameIndex]) {
if (layer.type === 'group') {
// more names, so try something in this group
if (names.length < nameIndex + 1) {
return this._doFindLayerDesc(layer.layers, names, ++nameIndex);
}
// No more names, return the group.
else {
return layer;
}
}
else {
return layer;
}
}
}
return null;
};
GLTilemap.prototype._bindShader = function (layer) {
var gl = this.gl;
switch (layer.type) {
case exports.ELayerType.Tilelayer:
{
var tileShader = this.shaders.tilelayer;
gl.useProgram(tileShader.program);
// these are static, and only need to be uploaded once.
if (this._firstTilelayerUniformUpload) {
this._firstTilelayerUniformUpload = false;
gl.uniform1i(tileShader.uniforms.uLayer, 0);
gl.uniform2fv(tileShader.uniforms.uInverseLayerTileSize, this._inverseLayerTileSize);
gl.uniform1iv(tileShader.uniforms.uTilesets, this._tilesetIndices);
gl.uniform2fv(tileShader.uniforms.uTilesetTileSize, this._tilesetTileSizeBuffer);
gl.uniform2fv(tileShader.uniforms.uTilesetTileOffset, this._tilesetTileOffsetBuffer);
gl.uniform2fv(tileShader.uniforms.uInverseTilesetTextureSize, this._inverseTilesetTextureSizeBuffer);
}
return tileShader;
}
case exports.ELayer