buffer.js

Tily.Buffer = (function(_super) {
  "use strict";
  Tily.utility.__extends(Buffer, _super);

  /**
   * A buffer containing tile layers that can be rendered in the Tily canvas.
   * @class
   * @extends Tily.BufferBase
   * @memberof Tily
   * @param {number} width The width of the buffer in tiles.
   * @param {number} height The height of the buffer in tiles.
   * @param {BufferOptions} [options] An optional options object for configuring the buffer.
   */
  function Buffer(width, height, options) {
    _super.call(this, options);

    /**
     * The layers contained in this buffer.
     * @type {Tily.TileLayer[]}
     */
    this.layers = [];
    this.size.width = width;
    this.size.height = height;
  }

  /**
   * Add a layer to this buffer at the specified z-index. If the z-index is undefined, add the
   * layer on top of existing layers, and if the z-index is -1, add the layer below existing
   * layers.
   * @name addLayer
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @param {?Tily.TileLayer} [layer] The layer to add. If null, add an empty new layer.
   * @param {number} [z] The z-index at which to add the layer. If this is -1, the layer will be
   * added below existing layers and if it is undefined the layer will be added above existing
   * layers.
   * @returns {Tily.TileLayer} The layer that was added.
   */
  Buffer.prototype.addLayer = function(layer, z) {
    // If no layer is specified, create a new one
    layer = layer || new Tily.TileLayer(this);

    // Make sure the layer has a reference to this buffer
    layer.container = this;
    if (z === undefined) {
      this.layers.push(layer);
    } else if (z == -1) {
      this.layers.unshift(layer);
    } else {
      this.layers.splice(z, 0, layer);
    }
    return layer;
  };

  /**
   * Remove a layer at the specified z-index. If the z-index is undefined, remove the top layer
   * and if the z-index is -1, remove the bottom layer. The removed layer is returned.
   * @name removeLayer
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @param {number} [z] The z-index of the layer to remove. If this is -1, the bottom layer will
   * be removed and if it is undefined the top layer will be removed.
   * @returns {Tily.TileLayer} The layer that was removed.
   */
  Buffer.prototype.removeLayer = function(z) {
    if (this.layers.length < 1) { return null; }
    if (z === undefined) {
      return this.layers.pop();
    } else if (z == -1) {
      return this.layers.shift();
    }
    return this.layers.splice(z, 1)[0];
  };

  /**
   * Remove all layers from this buffer.
   * @name removeAllLayers
   * @function
   * @instance
   * @memberof Tily.Buffer
   */
  Buffer.prototype.removeAllLayers = function() {
    this.layers = [];
  };

  /**
   * Move a layer from one z-index to another z-index, either an absolute value or relative to
   * the layer's current z-index.
   * @name moveLayer
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @param {number} zFrom The z-index of the layer to move.
   * @param {number} zTo The z-index to move the layer to.
   * @param {boolean} relative If this is true, the layer will be moved relative to it's current
   * z-index.
   * @returns {boolean} True if a layer was moved successfully.
   */
  Buffer.prototype.moveLayer = function(zFrom, zTo, relative) {
    if (this.layers.length < 2) { return false; }
    if (zFrom < 0 || zFrom >= this.layers.length) { return false; }
    const layer = this.layers.splice(zFrom, 1)[0],
      toIndex = Tily.utility.clamp(relative ? zFrom + zTo : zTo, 0, this.layers.length);
    this.layers.splice(toIndex, 0, layer);
    return true;
  };

  /**
   * Resize this buffer's layers.
   * @name resize
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @param {number} width The new width.
   * @param {number} height The new height.
   */
  Buffer.prototype.resize = function(width, height) {
    for (let i = this.layers.length; i--;) {
      this.layers[i].resize(width, height);
    }
    this.size.width = width;
    this.size.height = height;
  };

  /**
   * @typedef BufferTileInfo
   * @type {BufferBaseTileInfo}
   * @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.Buffer
   * @param {number} x The x-coordinate of the tile position.
   * @param {number} y The y-coordinate of the tile position.
   * @returns {BufferTileInfo} Information about the tiles at the specified position.
   */
  Buffer.prototype.getTileInfo = function(x, y) {
    const tileInfo = _super.prototype.getTileInfo.call(this, x, y);
    tileInfo.layers = this.layers.map(i => i.getTile(x, y));
    return tileInfo;
  };

  /**
   * Render this buffer's layers onto the specified context.
   * @name draw
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @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.
   */
  Buffer.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);
    var offset = this.updateTransitions(elapsedTime);

    // Clamp camera scale
    var lockedAxis = this.options.lockedAxis,
      maximumScale = this.options.maximumScale;

    // If clamping is enabled, make sure the maximum scale doesn't exceed the smallest buffer
    // dimension (so that there are no empty edges visible)
    if (this.options.clampCamera) {
      maximumScale = Math.min(maximumScale, this.size.width, this.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;
      this.offset = offset = Tily.utility.vec2(
        Tily.utility.clamp(offset.x, centerX, this.size.width - centerX - 1),
        Tily.utility.clamp(offset.y, centerY, this.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);

    // Render layers and active tiles in z-order
    var j = 0;
    for (let i = 0, length = this.layers.length; i < length; i++) {
      this.layers[i].draw(this.context, this.tileSize, tl, br);

      // Draw active tiles on or below this layer
      while (j < activeTiles.length && activeTiles[j].zIndex < i + 1) {
        activeTiles[j].draw(this.context, elapsedTime, this.tileSize);
        j++;
      }
    }

    // Draw any remaining active tiles (ie. on the top layer)
    while (j < activeTiles.length) {
      activeTiles[j].draw(this.context, elapsedTime, this.tileSize);
      j++;
    }
    this.context.restore();
    context.drawImage(this.canvas, 0, 0);
  };

  /**
   * Get serializable data for this buffer.
   * @name getData
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @returns {Object} This buffer's data.
   */
  Buffer.prototype.getData = function() {
    return {
      layers: this.layers.map(i => i.getData()),
      activeTiles: this.activeTiles.map(i => i.getData()),
      options: this.options,
      size: this.size,
      offset: this.offset,
      scale: this.scale
    };
  };

  /**
   * Create a buffer from data.
   * @name fromData
   * @function
   * @static
   * @memberof Tily.Buffer
   * @param {Object} data Serialized buffer data.
   * @returns {Tily.Buffer} A buffer created from the provided data.
   */
  Buffer.fromData = function(data) {
    const buffer = new Tily.Buffer(data.size.width, data.size.height, data.options);
    buffer.size = data.size;
    buffer.offset = data.offset;
    buffer.scale = data.scale;
    buffer.layers = data.layers.map(i => Tily.TileLayer.fromData(buffer, i));
    buffer.activeTiles = data.activeTiles.map(i => Tily.ActiveTile.fromData(i));
    return buffer;
  };

  /**
   * Serialize this buffer and return the serialized JSON data.
   * @name serialize
   * @function
   * @instance
   * @memberof Tily.Buffer
   * @returns {string} This buffer serialized as JSON data.
   */
  Buffer.prototype.serialize = function() {
    return JSON.stringify(this.getData());
  };

  /**
   * Deserialize the JSON data into a buffer.
   * @name deserialize
   * @function
   * @static
   * @memberof Tily.Buffer
   * @param {string} s The JSON data to deserialize.
   * @returns {Tily.Buffer} The deserialized buffer.
   */
  Buffer.deserialize = function(s) {
    var data = null;
    try {
      data = JSON.parse(s);
    } catch (e) {
      console.log("Couldn't deserialize data: %O", e);
      return null;
    }
    return Tily.Buffer.fromData(data);
  };
  return Buffer;
}(Tily.BufferBase));