gl-tiled
Version:
A Tiled editor renderer for WebGL.
236 lines (196 loc) • 6.37 kB
text/typescript
// @if DEBUG
import { ASSERT } from './debug';
// @endif
import { ITileset, ITile } from './tiled/Tileset';
import { loadImage } from './utils/loadImage';
import { IDictionary } from './IDictionary';
import { IAssetCache } from './IAssetCache';
import { IPoint } from './IPoint';
export interface ITileProps
{
coords: IPoint;
imgIndex: number;
flippedX: boolean;
flippedY: boolean;
flippedAD: boolean;
tile?: ITile;
}
/**
* Tileset GID flags, these flags are set on a tile's ID to give it a special property
*
* @property FLAGS
* @static
*/
export enum TilesetFlags {
FlippedAntiDiagonal = 0x20000000,
FlippedVertical = 0x40000000,
FlippedHorizontal = 0x80000000,
All = FlippedHorizontal | FlippedVertical | FlippedAntiDiagonal,
FlippedAntiDiagonalFlag = FlippedAntiDiagonal >> 28,
FlippedVerticalFlag = FlippedVertical >> 28,
FlippedHorizontalFlag = FlippedHorizontal >> 28,
};
export class GLTileset
{
gl: WebGLRenderingContext | null = null;
/** The images in this tileset. */
images: (TexImageSource | null)[] = [];
/** The gl textures in this tileset */
textures: (WebGLTexture | null)[] = [];
private _lidToTileMap: IDictionary<ITile> = {};
constructor(public readonly desc: ITileset, assetCache?: IAssetCache)
{
// load the images
if (this.desc.image)
{
this._addImage(this.desc.image, assetCache);
}
if (this.desc.tiles)
{
for (let i = 0; i < this.desc.tiles.length; ++i)
{
const tile = this.desc.tiles[i];
this._lidToTileMap[tile.id] = tile;
if (tile.image)
{
this._addImage(tile.image, assetCache);
}
}
}
}
/** The last gid in this tileset */
get lastgid(): number
{
return this.desc.firstgid + this.desc.tilecount;
}
/**
* Returns true if the given gid is contained in this tileset
*
* @param gid The global ID of the tile in a map.
*/
containsGid(gid: number): boolean
{
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.
*/
containsLocalId(index: number): boolean
{
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.
*/
getTileLocalId(gid: number): number
{
return (gid & ~TilesetFlags.All) - this.desc.firstgid;
}
/**
* Gathers the properties of a tile
*
* @param gid The global ID of the tile in a map.
*/
getTileProperties(gid: number): ITileProps | null
{
if (!gid)
return null;
const 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 & TilesetFlags.FlippedHorizontal) != 0,
flippedY: (gid & TilesetFlags.FlippedVertical) != 0,
flippedAD: (gid & TilesetFlags.FlippedAntiDiagonal) != 0,
tile: this._lidToTileMap[localId],
};
}
bind(startSlot: number): void
{
// @if DEBUG
ASSERT(!!(this.gl), 'Cannot call `bind` before `glInitialize`.');
// @endif
const gl = this.gl!;
for (let i = 0; i < this.textures.length; ++i)
{
gl.activeTexture(startSlot + i);
gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
}
}
glInitialize(gl: WebGLRenderingContext): void
{
this.glTerminate();
this.gl = gl;
for (let 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);
}
}
}
glTerminate(): void
{
if (!this.gl)
return;
const gl = this.gl;
for (let i = 0; i < this.textures.length; ++i)
{
const tex = this.textures[i];
if (tex)
{
gl.deleteTexture(tex);
}
}
this.textures.length = 0;
this.gl = null;
}
private _addImage(src: string, assets?: IAssetCache): void
{
const imgIndex = this.images.length;
this.images.push(null);
this.textures.push(null);
loadImage(src, assets, (errEvent, img) =>
{
if (!errEvent)
{
this.images[imgIndex] = img;
this._createTexture(imgIndex);
}
});
}
private _createTexture(imgIndex: number): void
{
if (!this.gl)
return;
const gl = this.gl;
const img = this.images[imgIndex];
const 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);
}
}