Tily.TileLayer = (function() {
"use strict";
/**
* A layer of tiles displayed in a buffer or a cell.
* @class
* @memberof Tily
* @param {Tily.Buffer|Tily.Cell} container The buffer or cell that this layer belongs to.
*/
function TileLayer(container) {
/**
* The buffer or cell that this layer belongs to.
* @type {Tily.Buffer|Tily.Cell}
*/
this.container = container;
/**
* The font to use for this layer's tiles.
* @default "sans-serif"
* @type {string}
*/
this.font = "sans-serif";
/**
* The colour to use for this layer's tile characters.
* @default "white"
* @type {string}
*/
this.foreground = "white";
/**
* The colour to use for this layer's tile backgrounds. If the string is empty, tile
* backgrounds won't be rendered.
* @default ""
* @type {string}
*/
this.background = "";
/**
* Optional map of foreground colours
* @default null
* @type {string}
*/
this.foregroundMap = null;
/**
* Optional map of background colours
* @default null
* @type {string}
*/
this.backgroundMap = null;
/**
* The opacity of this layer's tiles.
* @default 1
* @type {number}
*/
this.opacity = 1;
/**
* The composite operation to use when drawing this layer.
* @default "source-over"
* @type {string}
*/
this.compositeMode = "source-over";
/**
* Whether or not to clip this layer's tiles at their edges.
* @default false
* @type {boolean}
*/
this.clip = false;
/**
* True if the text in this layer's tiles should be centered.
* @default false
* @type {boolean}
*/
this.centered = false;
/**
* An array of strings for each tile. If any element in this array has length greater than
* 1, the string characters will be rendered on top of each other. If any element is an
* empty string, the tile won't be rendered.
* @type {string[]}
*/
this.tiles = [];
}
/**
* Return the array index from a given position.
* @param {number} x The x-coordinate of the position.
* @param {number} y The y-coordinate of the position.
* @param {number} w The width of this layer.
* @returns {number} An array index.
*/
function index(x, y, w) {
return w * y + x;
}
/**
* Return the position from a given array index.
* @param {number} i The array index.
* @param {number} w The width of this layer.
* @returns {Tily.utility.vec2} A 2d position.
*/
function position(i, w) {
return Tily.utility.vec2(i % w, Math.floor(i / w));
}
/**
* Return information about the region between (x1, y1) and (x2, y2). The second corner must be
* below and to the right of the first corner. Corners will default to the top-left and
* bottom-right corners of the layer if undefined.
* @param {number} x1 The x-coordinate of the top-left corner of the region.
* @param {number} y1 The y-coordinate of the top-left corner of the region.
* @param {number} x2 The x-coordinate of the bottom-right corner of the region.
* @param {number} y2 The y-coordinate of the bottom-right corner of the region.
* @param {number} w The width of this layer.
* @param {number} h The height of this layer.
* @returns {Object} An object containing the start offset into the tiles array, the width and
* height of the region and the gap between row sections.
*/
function region(x1, y1, x2, y2, w, h) {
x1 = x1 || 0;
y1 = y1 || 0;
x2 = x2 || w;
y2 = y2 || h;
// Make sure (x2, y2) is below and to the right of (x1, y1), or at the same position.
x2 = Math.max(x1, x2);
y2 = Math.max(y1, y2);
// Make sure both corners are within layer bounds
x1 = Tily.utility.clamp(x1, 0, w);
y1 = Tily.utility.clamp(y1, 0, h);
x2 = Tily.utility.clamp(x2, 0, w);
y2 = Tily.utility.clamp(y2, 0, h);
const width = x2 - x1;
return {
start: index(x1, y1, w),
width: Math.abs(width),
height: Math.abs(y2 - y1),
gap: w - width
};
}
/**
* Get the characters at the specified tile position, or an empty string if there are no
* characters at this tile position.
* @name getTile
* @function
* @instance
* @memberof Tily.TileLayer
* @param {number} x The x-coordinate of the position.
* @param {number} y The y-coordinate of the position.
* @returns {string} The character or characters at the specified position.
*/
TileLayer.prototype.getTile = function(x, y) {
if (
x >= 0 && x < this.container.size.width &&
y >= 0 && y < this.container.size.height
) {
return this.tiles[index(x, y, this.container.size.width)] || "";
}
return "";
};
TileLayer.prototype.getForeground = function(x, y) {
if (
this.foregroundMap !== null &&
x >= 0 && x < this.container.size.width &&
y >= 0 && y < this.container.size.height
) {
return this.foregroundMap[index(x, y, this.container.size.width)] || this.foreground;
}
return this.foreground;
};
TileLayer.prototype.getBackground = function(x, y) {
if (
this.backgroundMap !== null &&
x >= 0 && x < this.container.size.width &&
y >= 0 && y < this.container.size.height
) {
return this.backgroundMap[index(x, y, this.container.size.width)] || this.background;
}
return this.background;
};
/**
* Set the characters at the specified tile position.
* @name setTile
* @function
* @instance
* @memberof Tily.TileLayer
* @param {number} x The x-coordinate of the position.
* @param {number} y The y-coordinate of the position.
* @param {string} character The character or characters to set.
* @param {string} foreground The foreground colour for this tile, or null to use default
* @param {string} background The background colour for this tile, or null to use default
* @returns {boolean} True if the tile was set successfully.
*/
TileLayer.prototype.setTile = function(x, y, character, foreground = null, background = null) {
if (
x >= 0 && x < this.container.size.width &&
y >= 0 && y < this.container.size.height
) {
this.tiles[index(x, y, this.container.size.width)] = character;
if (foreground !== null) {
if (this.foregroundMap === null) {
this.foregroundMap = [];
}
this.foregroundMap[index(x, y, this.container.size.width)] = foreground;
}
if (background !== null) {
if (this.backgroundMap === null) {
this.backgroundMap = [];
}
this.backgroundMap[index(x, y, this.container.size.width)] = background;
}
return true;
}
return false;
};
/**
* Set the characters for all tiles in a rectangular region. If x2 and y2 are not specified
* then fill from (x1, y1) to the bottom-right corner, and if no coordinates are specified then
* fill the entire layer. Top-left corner is inclusive, bottom-right corner is exclusive.
* @name fill
* @function
* @instance
* @memberof Tily.TileLayer
* @param {string} character The character or characters to set.
* @param {number} [x1] The x-coordinate of the top-left corner of the region.
* @param {number} [y1] The y-coordinate of the top-left corner of the region.
* @param {number} [x2] The x-coordinate of the bottom-right corner of the region.
* @param {number} [y2] The y-coordinate of the bottom-right corner of the region.
*/
TileLayer.prototype.fill = function(character, x1, y1, x2, y2, foreground = null, background = null) {
const r = region(x1, y1, x2, y2, this.container.size.width, this.container.size.height);
for (let i = r.start, y = r.height; y--; i += r.gap) {
for (let x = r.width; x--; i++) {
this.tiles[i] = character;
if (foreground !== null) {
if (this.foregroundMap === null) {
this.foregroundMap = [];
}
this.foregroundMap[i] = foreground;
}
if (background !== null) {
if (this.backgroundMap === null) {
this.backgroundMap = [];
}
this.backgroundMap[i] = background;
}
}
}
};
/**
* Clear all tiles in a rectangular region. If x2 and y2 are not specified then clear from
* (x1, y1) to the bottom-right corner, and if no coordinates are specified then clear the
* entire layer. Top-left corner is inclusive, bottom-right corner is exclusive.
* @name clear
* @function
* @instance
* @memberof Tily.TileLayer
* @param {number} [x1] The x-coordinate of the top-left corner of the region.
* @param {number} [y1] The y-coordinate of the top-left corner of the region.
* @param {number} [x2] The x-coordinate of the bottom-right corner of the region.
* @param {number} [y2] The y-coordinate of the bottom-right corner of the region.
*/
TileLayer.prototype.clear = function(x1, y1, x2, y2) {
const r = region(x1, y1, x2, y2, this.container.size.width, this.container.size.height);
for (let i = r.start, y = r.height; y--; i += r.gap) {
for (let x = r.width; x--; i++) {
this.tiles[i] = "";
if (this.foregroundMap !== null) {
this.foregroundMap[i] = null;
}
if (this.backgroundMap !== null) {
this.backgroundMap[i] = null;
}
}
}
};
/**
* Rearrange the tiles in this layer so they align with the specified width and height.
* @name resize
* @function
* @instance
* @memberof Tily.TileLayer
* @param {number} width The new layer width.
* @param {number} height The new layer height.
*/
TileLayer.prototype.resize = function(width, height) {
if (width == this.container.size.width && height == this.container.size.height) { return; }
const tiles = [], foreground = [], background = [];
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const i = index(x, y, width);
tiles[i] = this.getTile(x, y);
foreground[i] = this.getForeground(x, y);
background[i] = this.getBackground(x, y);
}
}
this.tiles = tiles;
if (this.foregroundMap) {
this.foregroundMap = foreground;
}
if (this.backgroundMap) {
this.backgroundMap = background;
}
};
/**
* Render this layer onto the specified context. If a tile has a string with length greater
* than 1, draw each character of the string on top of each other.
* @name draw
* @function
* @instance
* @memberof Tily.TileLayer
* @param {CanvasRenderingContext2D} context The context to render the layer onto.
* @param {number} tileSize The size of each tile measured in pixels.
* @param {Tily.utility.vec2} tl The top-left tile position currently in view.
* @param {Tily.utility.vec2} br The bottom-right tile position currently in view.
*/
TileLayer.prototype.draw = function(context, tileSize, tl, br) {
if (!this.container || !this.tiles) { return; } // Layer has no container or no tiles
const width = this.container.size.width,
height = this.container.size.height,
r = region(tl.x, tl.y, br.x, br.y, width, height);
var p = null;
context.save();
context.font = (tileSize + 1) + "px " + this.font;
context.globalAlpha = this.opacity;
context.globalCompositeOperation = this.compositeMode;
// Render background tiles if a background colour or background map is defined
if (this.background || this.backgroundMap) {
context.fillStyle = this.background;
for (let i = r.start, y = r.height; y--; i += r.gap) {
for (let x = r.width; x--; i++) {
if (!this.tiles[i]) { continue; }
p = position(i, width);
context.save();
if (this.backgroundMap && this.backgroundMap[i]) {
context.fillStyle = this.backgroundMap[i];
}
context.fillRect(
p.x * tileSize - 0.5,
p.y * tileSize - 0.5,
tileSize + 1,
tileSize + 1
);
context.restore();
}
}
}
// Render foreground characters
let c;
if (this.centered === true) {
c = Tily.utility.vec2.mul(Tily.utility.vec2(0.5, 0.5), tileSize);
context.textAlign = "center";
context.textBaseline = "middle";
} else {
c = Tily.utility.vec2.mul(Tily.utility.vec2(0, 0), tileSize);
context.textAlign = "left";
context.textBaseline = "top";
}
context.fillStyle = this.foreground;
for (let i = r.start, y = r.height; y--; i += r.gap) {
for (let x = r.width; x--; i++) {
if (!this.tiles[i]) { continue; }
p = position(i, width);
context.save();
if (this.foregroundMap && this.foregroundMap[i]) {
context.fillStyle = this.foregroundMap[i];
}
if (this.clip) { // Clip tile boundaries if clipping is enabled
context.rect(p.x * tileSize, p.y * tileSize, tileSize, tileSize);
context.clip();
}
for (let j = 0, length = this.tiles[i].length; j < length; j++) {
context.fillText(this.tiles[i][j], p.x * tileSize + c.x, p.y * tileSize + c.y);
}
context.restore();
}
}
context.restore();
};
/**
* Get serializable data for this tile layer.
* @name getData
* @function
* @instance
* @memberof Tily.TileLayer
* @returns {Object} This tile layer's data.
*/
TileLayer.prototype.getData = function() {
return {
font: this.font,
foreground: this.foreground,
background: this.background,
foregroundMap: this.foregroundMap,
backgroundMap: this.backgroundMap,
opacity: this.opacity,
compositeMode: this.compositeMode,
clip: this.clip,
centered: this.centered,
tiles: this.tiles
};
};
/**
* Create a tile layer from data.
* @name fromData
* @function
* @static
* @memberof Tily.TileLayer
* @param {Tily.Buffer|Tily.Cell} container The buffer or cell that the layer belongs to.
* @param {Object} data Serialized buffer layer data.
* @returns {Tily.TileLayer} A buffer layer created from the provided data.
*/
TileLayer.fromData = function(container, data) {
const layer = new Tily.TileLayer(container);
layer.font = data.font;
layer.foreground = data.foreground;
layer.background = data.background;
layer.foregroundMap = data.foregroundMap;
layer.backgroundMap = data.backgroundMap;
layer.opacity = data.opacity;
layer.compositeMode = data.compositeMode;
layer.clip = data.clip;
layer.centered = data.centered;
layer.tiles = data.tiles;
return layer;
};
return TileLayer;
}());