activetilebase.js

Tily.ActiveTileBase = (function() {
  "use strict";

  /**
   * Implements basic functionality for active tiles and active tile layers.
   * @class
   * @memberof Tily
   */
  function ActiveTileBase() {
    /**
     * The layers in this active tile.
     * @type {Tily.ActiveTileLayer[]}
     */
    this.layers = [];

    /**
     * The animations currently running on this active tile.
     * @type {Tily.Animation[]}
     */
    this.animations = [];

    /**
     * The font to use when rendering this active tile. Set this to null to inherit from the
     * parent object.
     * @default null
     * @type {?string}
     */
    this.font = null;

    /**
     * The font style to use when rendering this active tile. Set this to null to inherit from the
     * parent object.
     * @default null
     * @type {?string}
     */
    this.fontStyle = null;

    /**
     * The font size to use when rendering this active tile. Set this to null to inherit from the
     * parent object.
     * @default null
     * @type {?string}
     */
    this.fontSize = null;

    /**
     * The foreground colour of this active tile. Set this to null to inherit from the parent
     * object.
     * @default null
     * @type {?string}
     */
    this.foreground = null;

    /**
     * The outline width and colour of this active tile. Set this to null to inherit from the parent
     * object.
     * @default null
     * @type {?string}
     */
    this.outline = null;

    /**
     * The shadow width, offset and colour of this active tile. Set this to null to inherit from the parent
     * object.
     * @default null
     * @type {?string}
     */
    this.shadow = null;

    /**
     * The opacity of this active tile. Set this to null to inherit from the parent object.
     * @default null
     * @type {?number}
     */
    this.opacity = null;

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

    /**
     * The offset of this active tile measured in tiles. Set this to null to inherit from the
     * parent object.
     * @default null
     * @type {?Tily.utility.vec2}
     */
    this.offset = null;

    /**
     * The scale of this active tile. Set this to null to inherit from the parent object.
     * @default null
     * @type {?Tily.utility.vec2}
     */
    this.scale = null;

    /**
     * The rotation angle of this active tile measured in radians. Set this to null to inherit
     * from the parent object.
     * @default null
     * @type {?number}
     */
    this.rotation = null;

    /**
     * If the text in this tile should be centered. Set this to null to inherit from the parent object.
     * @default null
     * @type {?boolean}
     */
    this.centered = null;
  }

  /**
   * Define an inherited property on this object's prototype
   * @param {string} name The name of the property that is inherited from the parent object.
   * @param {string} inheritedName The name of the inherited property that will be defined.
   */
  function createInheritedProperty(name, inheritedName) {
    Object.defineProperty(ActiveTileBase.prototype, inheritedName, {
      get: function() {
        if (this[name] !== null) {
          return this[name];
        }
        if (this.parent instanceof Tily.ActiveTile) {
          return this.parent[name];
        }
        return this.parent[inheritedName];
      }
    });
  }

  /**
   * @name inheritedFont
   * @description The font set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("font", "inheritedFont");

  /**
   * @name inheritedFontStyle
   * @description The font style set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("fontStyle", "inheritedFontStyle");

  /**
   * @name inheritedFontSize
   * @description The font size set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("fontSize", "inheritedFontSize");

  /**
   * @name inheritedForeground
   * @description The foreground colour set in this layer or inherited from the parent object if
   * null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("foreground", "inheritedForeground");

  /**
   * @name inheritedOutline
   * @description The outline set in this layer or inherited from the parent object if
   * null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("outline", "inheritedOutline");

  /**
   * @name inheritedShadow
   * @description The shadow set in this layer or inherited from the parent object if
   * null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("shadow", "inheritedShadow");

  /**
   * @name inheritedOpacity
   * @description The opacity set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {number}
   */
  createInheritedProperty("opacity", "inheritedOpacity");

  /**
   * @name inheritedCompositeMode
   * @description The composite operation set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {string}
   */
  createInheritedProperty("compositeMode", "inheritedCompositeMode");

  /**
   * @name inheritedOffset
   * @description The offset set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {Tily.utility.vec2}
   */
  createInheritedProperty("offset", "inheritedOffset");

  /**
   * @name inheritedScale
   * @description The scale set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {Tily.utility.vec2}
   */
  createInheritedProperty("scale", "inheritedScale");

  /**
   * @name inheritedRotation
   * @description The rotation set in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {number}
   */
  createInheritedProperty("rotation", "inheritedRotation");

  /**
   * @name inheritedCentered
   * @description The centering mode in this layer or inherited from the parent object if null.
   * @instance
   * @memberof Tily.ActiveTileBase
   * @type {boolean}
   */
  createInheritedProperty("centered", "inheritedCentered");

  /**
   * Pause all animations below this layer.
   * @name pauseAnimations
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {boolean} [inherit] If true (default) then pass down to child layers
   */
  ActiveTileBase.prototype.pauseAnimations = function(inherit = true) {
    this.animations.forEach(a => a.pause());
    if (inherit) {
      this.layers.forEach(l => l.pauseAnimations(inherit));
    }
  };

  /**
   * Un-pause all animations below this layer.
   * @name runAnimations
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {boolean} [inherit] If true (default) then pass down to child layers
   */
  ActiveTileBase.prototype.runAnimations = function(inherit = true) {
    this.animations.forEach(a => a.run());
    if (inherit) {
      this.layers.forEach(l => l.runAnimations(inherit));
    }
  };

  /**
   * Reset all animations below this layer.
   * @name resetAnimations
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {boolean} [inherit] If true (default) then pass down to child layers
   */
  ActiveTileBase.prototype.resetAnimations = function(inherit = true) {
    this.animations.forEach(a => a.reset());
    if (inherit) {
      this.layers.forEach(l => l.resetAnimations(inherit));
    }
  };

  /**
   * Stop and remove all animations below this layer.
   * @name stopAnimations
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {boolean} [inherit] If true (default) then pass down to child layers
   */
  ActiveTileBase.prototype.stopAnimations = function(inherit = true) {
    this.animations = [];
    if (inherit) {
      this.layers.forEach(l => { l.animations = []; });
    }
  };

  /**
   * Animate this active tile's foreground colour.
   * @name animateForeground
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {string} foreground The target foreground colour.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateForeground = function(foreground, options) {
    const current = Tily.utility.parseColor(this.inheritedForeground),
      target = Tily.utility.parseColor(foreground),
      animation = new Tily.ForegroundAnimation(this, current, target, options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * Animate this active tile's outline.
   * @name animateOutline
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {string} outline The target outline.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateOutline = function(outline, options) {
    const current = this.inheritedOutline,
      target = outline,
      animation = new Tily.OutlineAnimation(this, current, target, options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * Animate this active tile's fshadow.
   * @name animateShadow
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {string} shadow The target shadow.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateShadow = function(shadow, options) {
    const current = this.inheritedShadow,
      target = shadow,
      animation = new Tily.ShadowAnimation(this, current, target, options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * Animate this active tile's opacity.
   * @name animateOpacity
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {number} opacity The target opacity.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateOpacity = function(opacity, options) {
    const current = this.inheritedOpacity,
      animation = new Tily.OpacityAnimation(this, current, opacity, options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * Animate this active tile's scale.
   * @name animateScale
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {number} x The target x-scale.
   * @param {number} y The target y-scale.
   * @param {AnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateScale = function(x, y, options) {
    const current = Tily.utility.vec2(this.inheritedScale),
      scale = Tily.utility.vec2(x, y),
      animation = new Tily.ScaleAnimation(this, current, Tily.utility.vec2(scale), options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * @typedef OffsetAnimationOptions
   * @type {AnimationOptions}
   * @property {boolean} [relative=false] True if the movement should be relative to the current
   * offset.
   */
  /**
   * Animate this active tile's offset.
   * @name animateOffset
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {number} x The x-coordinate of the target offset position.
   * @param {number} y The y-coordinate of the target offset position.
   * @param {OffsetAnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateOffset = function(x, y, options) {
    const current = Tily.utility.vec2(this.inheritedOffset);
    var offset = Tily.utility.vec2(x, y);
    if (options && options.relative === true) {  // Add the current offset if moving relatively
      offset = Tily.utility.vec2.add(current, offset);
    }
    const animation = new Tily.OffsetAnimation(this, current, Tily.utility.vec2(offset), options);
    this.animations.push(animation);
    return new Promise(function(resolve, reject) { animation.finishedCallback = resolve; });
  };

  /**
   * @typedef RotationAnimationOptions
   * @type {AnimationOptions}
   * @property {boolean} [relative=false] True if the rotation should be relative to the current
   * angle.
   * @property {string} [direction=""] The rotation direction. This should be 'cw' for clockwise,
   * 'ccw' for counter-clockwise. If this is not 'cw' or 'ccw', the rotation will be in the
   * direction of the smallest change in angle.
   */
  /**
   * Animate this active tile's rotation.
   * @name animateRotation
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {number} angle The target angle in radians.
   * @param {RotationAnimationOptions} [options] An optional options object.
   */
  ActiveTileBase.prototype.animateRotation = function(angle, options) {
    const current = this.inheritedRotation;
    if (options && options.relative === true) {  // Add the current angle if rotating relatively
      angle += current;
    }
    const animation = new Tily.RotationAnimation(this, current, angle, 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.ActiveTileBase
   * @param {Tily.ActiveTileLayer} layer The layer to add.
   * @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.
   */
  ActiveTileBase.prototype.addLayer = function(layer, z) {
    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.ActiveTileBase
   * @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.ActiveTileLayer} The layer that was removed.
   */
  ActiveTileBase.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 active tile.
   * @name removeAllLayers
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   */
  ActiveTileBase.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.ActiveTileBase
   * @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.
   */
  ActiveTileBase.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;
  };

  /**
   * Handle this active tile's animations.
   * @name draw
   * @function
   * @instance
   * @memberof Tily.ActiveTileBase
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   */
  ActiveTileBase.prototype.draw = function(elapsedTime) {
    for (let i = 0, length = this.animations.length; i < length; i++) {
      this.animations[i].update(elapsedTime);
    }

    // Remove any animations that have finished
    this.animations = this.animations.filter(i => !i.finished);
  };
  return ActiveTileBase;
}());