activetile.js

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

  /**
   * A tile that can be translated, scaled, rotated and animated.
   * @class
   * @extends Tily.ActiveTileBase
   * @memberof Tily
   * @param {number} [x=0] The initial x-coordinate of this active tile.
   * @param {number} [y=0] The initial y-coordinate of this active tile.
   * @param {number} [zIndex=0] The initial z-index of this active tile.
   */
  function ActiveTile(x, y, zIndex) {
    _super.call(this);

    /**
     * The position of this active tile.
     * @type {Tily.utility.vec2}
     */
    this.position = Tily.utility.vec2(x || 0, y || 0);

    /**
     * The layer ordering of this active tile.
     * @type {number}
     */
    this.zIndex = zIndex || 0;

    /**
     * True if this active tile should clip at the tile edges.
     * @default false
     * @type {boolean}
     */
    this.clip = false;

    /**
     * True if this active tile should wrap around tile edges.
     * @default false
     * @type {boolean}
     */
    this.wrap = false;

    /**
     * True if this active tile should be flipped along the x-axis.
     * @default false
     * @type {boolean}
     */
    this.flip = false;

    /**
     * The font to use when rendering this active tile's layers.
     * @default "sans-serif"
     * @type {string}
     */
    this.font = "sans-serif";

    /**
     * The font style to use when rendering this active tile's layers.
     * @default "normal"
     * @type {string}
     */
    this.fontStyle = "normal";

    /**
     * The font size to use when rendering this active tile's layers. If null, fit characters to the tile.
     * @default null
     * @type {?string}
     */
    this.fontSize = null;

    /**
     * The foreground colour, used for rendering text on this active tile's layers.
     * @default "white"
     * @type {string}
     */
    this.foreground = "white";

    /**
     * The outline width and colour for this active tile's layers.
     * @default null
     * @type {?string}
     */
    this.outline = null;

    /**
     * The shadow amount, offset and colour for this active tile's layers.
     * @default null
     * @type {?string}
     */
    this.shadow = null;

    /**
     * The opacity of this active tile.
     * @default 1
     * @type {number}
     */
    this.opacity = 1;

    /**
     * The composite operation to use when drawing this active tile.
     * @default "source-over"
     * @type {string}
     */
    this.compositeMode = "source-over";

    /**
     * An offset from this active tile's position measured in tiles.
     * @default { x: 0, y: 0 }
     * @type {Tily.utility.vec2}
     */
    this.offset = Tily.utility.vec2();

    /**
     * The scale of this active tile.
     * @default { x: 1, y: 1 }
     * @type {Tily.utility.vec2}
     */
    this.scale = Tily.utility.vec2(1, 1);

    /**
     * The rotation angle of this active tile measured in radians.
     * @default 0
     * @type {number}
     */
    this.rotation = 0;

    /**
     * True if the text in this active tile should be centered.
     * @default false
     * @type {boolean}
     */
    this.centered = false;

    /**
     * True if this active tile should be removed in the next frame.
     * @default false
     * @type {boolean}
     */
    this.destroyed = false;
  }
  /**
   * Animate this active tile's position.
   * @name move
   * @function
   * @instance
   * @memberof Tily.ActiveTile
   * @param {string} direction The movement direction, either 'up', 'down', 'left', 'right'.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTile.prototype.move = function(direction, options) {
    const directions = {
      up: Tily.utility.vec2(0, -1),
      down: Tily.utility.vec2(0, 1),
      left: Tily.utility.vec2(-1, 0),
      right: Tily.utility.vec2(1, 0)
    };
    this.position = Tily.utility.vec2.add(this.position, directions[direction]);
    const animation = new Tily.OffsetAnimation(this, Tily.utility.vec2.mul(directions[direction], -1), Tily.utility.vec2(), options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * Add an active tile layer to this active tile 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.ActiveTile
   * @param {?Tily.ActiveTileLayer} layer The layer to add. If this is null, a new layer will be created.
   * @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.ActiveTileLayer} The layer that was added.
   */
  ActiveTile.prototype.addLayer = function(layer, z) {
    // If no layer is specified, create a new one
    layer = layer || new Tily.ActiveTileLayer(this);

    // Make sure the layer has a reference to the top-level active tile
    layer.activeTile = this;

    // Make sure the layer has a reference to its parent (ie. this active tile)
    layer.parent = this;
    return _super.prototype.addLayer.call(this, layer, z);
  };

  /**
   * Draw layers onto the specified context.
   * @param {Tily.ActiveTileLayer[]} layers The layers to draw.
   * @param {CanvasRenderingContext2D} context The context to draw this active tile onto.
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   * @param {number} tileSize The tile size measured in pixels.
   */
  function drawLayers(layers, context, elapsedTime, tileSize) {
    for (let i = 0, length = layers.length; i < length; i++) {
      layers[i].draw(context, elapsedTime, tileSize);
    }
  }

  /**
   * Render this active tile onto the container context.
   * @name draw
   * @function
   * @instance
   * @memberof Tily.ActiveTile
   * @param {CanvasRenderingContext2D} context The context to draw this active tile onto.
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   * @param {number} tileSize The tile size measured in pixels.
   */
  ActiveTile.prototype.draw = function(context, elapsedTime, tileSize) {
    if (!this.layers) { return; }  // Active tile has no layers so don't render
    _super.prototype.draw.call(this, elapsedTime);
    context.save();
    const fontSize = this.fontSize || ((tileSize + 1) + 'px');
    context.font = `${this.fontStyle} ${fontSize} ${this.font}`;
    context.fillStyle = this.foreground;
    context.globalAlpha = this.opacity;
    context.globalCompositeOperation = this.compositeMode;

    // Outline
    if (this.outline) {
      const { width: outlineWidth, colour: outlineColour } = Tily.utility.outline(this.outline);
      context.lineWidth = Math.floor(outlineWidth * tileSize);
      context.strokeStyle = outlineColour;
    }

    // Shadow
    if (this.shadow) {
      const {
        blur: shadowBlur,
        xOffset: shadowXOffset,
        yOffset: shadowYOffset,
        colour: shadowColour
      } = Tily.utility.shadow(this.shadow);
      context.shadowBlur = shadowBlur * tileSize;
      context.shadowOffsetX = Math.floor(shadowXOffset * tileSize);
      context.shadowOffsetY = Math.floor(shadowYOffset * tileSize);
      context.shadowColor = shadowColour;
    }

    // Clip tile boundaries if clipping is enabled
    context.translate(this.position.x * tileSize - 0.5, this.position.y * tileSize - 0.5);
    if (this.clip) {
      context.rect(0, 0, tileSize + 1, tileSize + 1);
      context.clip();
    }
    context.translate((this.offset.x + 0.5) * tileSize, (this.offset.y + 0.5) * tileSize);
    context.rotate(this.rotation);
    context.scale(this.scale.x * (this.flip ? -1 : 1), this.scale.y);
    drawLayers(this.layers, context, elapsedTime, tileSize);

    // If both clipping and wrapping are enabled, re-draw the layers at inverted offsets
    if (this.clip && this.wrap) {
      const wrapX = tileSize * (this.offset.x > 0 ? -1 : 1) * (this.flip ? -1 : 1),
        wrapY = tileSize * (this.offset.y > 0 ? -1 : 1);
      if (this.offset.x != 0) {
        context.save();
        context.translate(this.offset.x + wrapX, 0);
        drawLayers(this.layers, context, elapsedTime, tileSize);
        context.restore();
      }
      if (this.offset.y != 0) {
        context.save();
        context.translate(0, this.offset.y + wrapY, 0);
        drawLayers(this.layers, context, elapsedTime, tileSize);
        context.restore();
      }
      if (this.offset.x != 0 && this.offset.y != 0) {
        context.save();
        context.translate(this.offset.x + wrapX, this.offset.y + wrapY);
        drawLayers(this.layers, context, elapsedTime, tileSize);
        context.restore();
      }
    }
    context.restore();
  };

  /**
   * Get serializable data for this active tile.
   * @name getData
   * @function
   * @instance
   * @memberof Tily.ActiveTile
   * @returns {Object} This active tile's data.
   */
  ActiveTile.prototype.getData = function() {
    return {
      layers: this.layers.map(i => i.getData()),
      position: this.position,
      zIndex: this.zIndex,
      clip: this.clip,
      wrap: this.wrap,
      flip: this.flip,
      font: this.font,
      fontStyle: this.fontStyle,
      fontSize: this.fontSize,
      foreground: this.foreground,
      outline: this.outline,
      shadow: this.shadow,
      opacity: this.opacity,
      compositeMode: this.compositeMode,
      offset: this.offset,
      scale: this.scale,
      rotation: this.rotation,
      centered: this.centered
    };
  };

  /**
   * Create an active tile from data.
   * @name fromData
   * @function
   * @static
   * @memberof Tily.ActiveTile
   * @param {Object} data Serialized active tile data.
   * @returns {Tily.ActiveTile} An active tile created from the provided data.
   */
  ActiveTile.fromData = function(data) {
    const tile = new Tily.ActiveTile(data.position.x, data.position.y, data.zIndex);
    tile.layers = data.layers.map(i => Tily.ActiveTileLayer.fromData(tile, tile, i));
    tile.clip = data.clip;
    tile.wrap = data.wrap;
    tile.flip = data.flip;
    tile.font = data.font;
    tile.fontStyle = data.fontStyle;
    tile.fontSize = data.fontSize;
    tile.foreground = data.foreground;
    tile.outline = data.outline;
    tile.shadow = data.shadow;
    tile.opacity = data.opacity;
    tile.compositeMode = data.compositeMode;
    tile.offset = data.offset;
    tile.scale = data.scale;
    tile.rotation = data.rotation;
    tile.centered = data.centered;
    return tile;
  };
  return ActiveTile;
}(Tily.ActiveTileBase));