Tily.CellBuffer = (function(_super) {
"use strict";
Tily.utility.__extends(CellBuffer, _super);
/**
* @callback resolveCellFunction
* @param {Tily.Cell} cell The generated cell.
*/
/**
* @callback rejectCellFunction
* @param {string} reason A string containing the reason that cell generation failed.
*/
/**
* @callback cellFunction
* @param {Tily.CellBuffer} buffer The cell buffer that the resulting cell will belong to.
* @param {number} x The cell x-coordinate.
* @param {number} y The cell y-coordinate.
* @param {resolveCellFunction} resolve A function to call if the cell is generated
* successfully.
* @param {rejectCellFunction} reject A function to call if cell generation failed.
*/
/**
* @typedef CellBufferOptions
* @type {BufferOptions}
* @property {number} [cellWidth=16] The width of each cell measured in tiles.
* @property {number} [cellHeight=16] The height of each cell measured in tiles.
* @property {?number} [minimumX=null] The minimum cell x-coordinate. If this is null, the cell
* buffer will scroll infinitely in the negative-x direction.
* @property {?number} [minimumY=null] The minimum cell y-coordinate. If this is null, the cell
* buffer will scroll infinitely in the negative-x direction.
* @property {?number} [maximumX=null] The maximum cell x-coordinate. If this is null, the cell
* buffer will scroll infinitely in the positive-x direction.
* @property {?number} [maximumY=null] The maximum cell y-coordinate. If this is null, the cell
* buffer will scroll infinitely in the positive-y direction.
* @property {?cellFunction} [cellFunction=null] A function for generating cells.
*/
/**
* Default cell buffer options, used as a fall-back for options passed to the constructor.
* @type {CellBufferOptions}
*/
const _defaultCellBufferOptions = {
cellWidth: 16,
cellHeight: 16,
minimumX: null,
minimumY: null,
maximumX: null,
maximumY: null,
cellFunction: null
};
/**
* A buffer made out of rectangular cells generated by a cell function, used for infinite
* scrolling buffers and procedurally generated buffers.
* @class
* @extends Tily.BufferBase
* @memberof Tily
* @param {CellBufferOptions} [options] An optional options object for configuring the buffer.
*/
function CellBuffer(options) {
_super.call(this, options);
/**
* A cache of loaded cells with hashed cell positions as keys and cell instances as values.
* @type {Object}
*/
this.cellCache = {};
/**
* Options for configuring this cell buffer.
* @type {CellBufferOptions}
*/
this.options = { ..._defaultCellBufferOptions, ...this.options, ...options || {} };
}
/**
* Get a string representation of the specified position for use as a hash.
* @param {Tily.utility.vec2} p The position to hash.
* @returns {string} A hash string for the specified position.
*/
function hash(p) {
return Tily.utility.vec2.toString(p, "_");
}
/**
* @typedef CellBufferTileInfo
* @type {BufferBaseTileInfo}
* @property {Tily.utility.vec2} cell The cell coordinate for the specified tile position.
* @property {string[]} layers The tile layer characters in z-index order at a tile position.
*/
/**
* Get information about the tiles and active tiles at a tile position.
* @name getTileInfo
* @function
* @instance
* @memberof Tily.CellBuffer
* @param {number} x The x-coordinate of the tile position.
* @param {number} y The y-coordinate of the tile position.
* @returns {CellBufferTileInfo} Information about the tiles at the specified position.
*/
CellBuffer.prototype.getTileInfo = function(x, y) {
const tileInfo = _super.prototype.getTileInfo.call(this, x, y),
cell = Tily.utility.vec2.map(
Tily.utility.vec2(x / this.options.cellWidth, y / this.options.cellHeight),
Math.floor
),
cellOffset = Tily.utility.vec2(
x - cell.x * this.options.cellWidth,
y - cell.y * this.options.cellHeight
),
h = hash(cell);
var layers = [];
if (this.cellCache[h]) {
layers = this.cellCache[h].layers.map(i => i.getTile(cellOffset.x, cellOffset.y));
}
tileInfo.cell = cell;
tileInfo.layers = layers;
return tileInfo;
};
/**
* Render this buffer's cells onto the specified context.
* @name draw
* @function
* @instance
* @memberof Tily.CellBuffer
* @param {CanvasRenderingContext2D} context The context to render the buffer onto.
* @param {number} elapsedTime The time elapsed in seconds since the last draw call.
* @param {number} width The width of the canvas in pixels.
* @param {number} height The height of the canvas in pixels.
*/
CellBuffer.prototype.draw = function(context, elapsedTime, width, height) {
this.canvas.width = width;
this.canvas.height = height;
this.context.save();
// this.context.textBaseline = "top";
this.context.clearRect(0, 0, width, height);
// Update transitions
const offset = this.updateTransitions(elapsedTime);
// Clamp camera scale
var lockedAxis = this.options.lockedAxis,
maximumScale = this.options.maximumScale;
// Get cell buffer size
const size = { width: Infinity, height: Infinity };
if (this.options.minimumX && this.options.maximumX) {
size.width = (this.options.maximumX - this.options.minimumX) * this.options.cellWidth;
}
if (this.options.minimumY && this.options.maximumY) {
size.height = (this.options.maximumY - this.options.minimumY) * this.options.cellHeight;
}
// Camera clamping only clamps scale when the cell buffer has a finite dimension
if (this.options.clampCamera) {
maximumScale = Math.min(maximumScale, size.width, size.height);
if (width > height) {
lockedAxis = "x";
} else if (height > width) {
lockedAxis = "y";
}
}
this.scale = Tily.utility.clamp(
this.scale,
Math.max(this.options.minimumScale, 1), // Minimum scale cannot go below 1 tile
maximumScale
);
this.tileSize = (lockedAxis == "y" ? height : width) / this.scale;
this.viewSize.width = width / this.tileSize;
this.viewSize.height = height / this.tileSize;
// Clamp camera offset
if (this.options.clampCamera) {
const centerX = this.viewSize.width * 0.5 - 0.5,
centerY = this.viewSize.height * 0.5 - 0.5;
if (isFinite(size.width)) {
this.offset.x = offset.x = Tily.utility.clamp(offset.x, centerX, size.width - centerX - 1);
}
if (isFinite(size.height)) {
this.offset.y = offset.y = Tily.utility.clamp(offset.y, centerY, size.height - centerY - 1);
}
}
// Translate camera viewport
this.context.translate(
width * 0.5 - offset.x * this.tileSize - this.tileSize * 0.5,
height * 0.5 - offset.y * this.tileSize - this.tileSize * 0.5
);
// Update active tiles map
const halfSize = Tily.utility.vec2(this.viewSize.width * 0.5 + 1, this.viewSize.height * 0.5 + 1),
tl = Tily.utility.vec2.map(Tily.utility.vec2.sub(offset, halfSize), Math.floor),
br = Tily.utility.vec2.map(Tily.utility.vec2.add(offset, halfSize), Math.ceil),
activeTiles = this.updateActiveTilesMap(tl, br);
// Find the top-left and bottom-right cell positions currently in view
const cellSize = Tily.utility.vec2(this.options.cellWidth, this.options.cellHeight),
tlCell = Tily.utility.vec2.map(Tily.utility.vec2.div(tl, cellSize), Math.floor),
brCell = Tily.utility.vec2.map(Tily.utility.vec2.div(br, cellSize), Math.ceil),
cache = this.cellCache,
getResolve = function(x, y) {
return function(cell) {
cache[hash(Tily.utility.vec2(x, y))] = cell;
};
},
getReject = function(x, y) {
return function(reason) {
console.log("Couldn't generate cell (%i, %i): %s", x, y, reason);
};
};
var h = null;
for (let x = tlCell.x; x < brCell.x; x++) {
for (let y = tlCell.y; y < brCell.y; y++) {
h = hash(Tily.utility.vec2(x, y));
// If the cell isn't already in the cell cache and there is a cell function,
// generate a cell and temporarily mark it as loading
if (this.cellCache[h] === undefined) {
this.cellCache[h] = true; // Mark it as currently loading
if (this.options.cellFunction) {
this.options.cellFunction(this, x, y, getResolve(x, y), getReject(x, y));
}
// Otherwise, if the cell isn't currently loading, render the cell
} else if (this.cellCache[h] !== true) {
this.cellCache[h].draw(
this.context,
elapsedTime,
x, y,
this.tileSize,
tl, br,
activeTiles
);
}
}
}
this.context.restore();
context.drawImage(this.canvas, 0, 0);
};
/**
* Serialize this cell buffer and return the serialized JSON data. The cell function will not
* be serialized and will need to be re-attached when the data is deserialized.
* @name serialize
* @function
* @instance
* @memberof Tily.CellBuffer
* @returns {string} This buffer serialized as JSON data.
*/
CellBuffer.prototype.serialize = function() {
const cellCache = {};
for (let i in this.cellCache) {
if (!this.cellCache.hasOwnProperty(i)) { continue; }
cellCache[i] = this.cellCache[i].getData();
}
return JSON.stringify({
cellCache: cellCache,
activeTiles: this.activeTiles.map(i => i.getData()),
options: this.options,
offset: this.offset,
scale: this.scale
});
};
/**
* Deserialize the JSON data into a cell buffer. The cell function will need to be re-attached
* to the resulting cell buffer, as it cannot be serialized.
* @name deserialize
* @function
* @static
* @memberof Tily.CellBuffer
* @param {string} s The JSON data to deserialize.
* @returns {Tily.CellBuffer} The deserialized buffer.
*/
CellBuffer.deserialize = function(s) {
var data = null;
const cellCache = {};
try {
data = JSON.parse(s);
} catch (e) {
console.log("Couldn't deserialize data: %O", e);
return null;
}
for (let i in data.cellCache) {
if (!data.cellCache.hasOwnProperty(i)) { continue; }
cellCache[i] = Tily.Cell.fromData(data.cellCache[i]);
}
const buffer = new Tily.CellBuffer(data.options);
buffer.offset = data.offset;
buffer.scale = data.scale;
buffer.cellCache = cellCache;
buffer.activeTiles = data.activeTiles.map(i => Tily.ActiveTile.fromData(i));
return buffer;
};
return CellBuffer;
}(Tily.BufferBase));