main.js

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

  /**
   * @typedef Size
   * @type {Object}
   * @property {number} width The width.
   * @property {number} height The height.
   */
  /**
   * @callback beforeDrawFunction
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   */
  /**
   * @callback afterDrawFunction
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   */
  /**
   * @typedef TilyOptions
   * @type {Object}
   * @property {?Size} [size=null] The viewport size, or null to use the canvas element
   * dimensions.
   * @property {boolean} [handleResize=true] True if the window resize event should be handled.
   * @property {boolean} [showFPS=false] True if the FPS should be displayed.
   * @property {boolean} [renderLoop=true] True if the render loop should be started
   * automatically.
   * @property {?beforeDrawFunction} [beforeDrawFunction=null] A function that will be called
   * after clearing the context but before drawing the active buffer. In the context of the
   * function, 'this' will point to the Tily instance.
   * @property {?afterDrawFunction} [afterDrawFunction=null] A function that will be called after
   * drawing the active buffer. In the context of the function, 'this' will point to the Tily instance.
   */
  /**
   * Default Tily options, used as a fall-back for options passed to the constructor.
   * @type {TilyOptions}
   */
  const _defaultTilyOptions = {
    size: null,
    handleResize: true,
    showFPS: false,
    renderLoop: true,
    beforeDrawFunction: null,
    afterDrawFunction: null
  };

  /**
   * The main Tily object. Initialises a canvas element and starts drawing tiles.
   * @class
   * @memberof Tily
   * @param {HTMLElement} canvas The canvas element in which to draw tiles.
   * @param {TilyOptions} [options] An optional object containing options for configuring this
   * Tily instance.
   */
  function Main(canvas, options) {
    if (!window) {  // Check that we are in a browser (ie. window exists)
      console.log("Couldn't find window. Tily.Main can only be created in the browser!");
      return
    }
    if (!canvas.getContext) {  // Check for canvas support
      console.log("Canvas not supported!");
      return;
    }

    /**
     * Options for configuring this Tily instance.
     * @type {TilyOptions}
     */
    this.options = { ..._defaultTilyOptions, ...options || {} };

    /**
     * The canvas element in which to draw tiles.
     * @type {HTMLElement}
     */
    this.canvas = canvas;

    /**
     * The context on which to draw tiles.
     * @type {CanvasRenderingContext2D}
     */
    this.context = canvas.getContext("2d");

    /**
     * The viewport width.
     * @type {number}
     */
    this.width = 0;

    /**
     * The viewport height.
     * @type {number}
     */
    this.height = 0;

    /**
     * The buffer currently being rendered.
     * @type {Tily.BufferBase}
     */
    this.activeBuffer = null;

    /**
     * The currently running buffer transition or null if there is no currently running
     * transition.
     * @default null
     * @type {?Tily.BufferTransition}
     */
    this.bufferTransition = null;

    /**
     * A request id returned from window.requestAnimationFrame that uniquely identifies the
     * entry in the callback list for the main render loop.
     * @type {number}
     */
    this.loop = null;

    /**
     * The unix epoch time of the last rendered frame.
     * @type {number}
     */
    this.lastFrameTime = new Date();

    /**
     * The number of frames rendered in the last second.
     * @type {number}
     */
    this.frameCount = 0;

    /**
     * The number of milliseconds elapsed since the last framerate update.
     * @type {number}
     */
    this.frameTime = 0;

    /**
     * The number of frames rendered per second.
     * @type {number}
     */
    this.frameRate = 0;

    // Handle the window resize event to get the current viewport size or use a fixed size
    if (!this.options.size || this.options.handleResize) {
      const self = this;
      function resize() {
        self.canvas.width = self.width = canvas.clientWidth * window.devicePixelRatio;
        self.canvas.height = self.height = canvas.clientHeight * window.devicePixelRatio;
      }
      window.addEventListener("resize", resize, false);
      resize();
    } else {
      this.canvas.width = this.width = this.options.size.width;
      this.canvas.height = this.height = this.options.size.height;
    }

    // Start the render loop
    if (this.options.renderLoop) {
      loop(this);
    }
  }

  /**
   * Perform a single iteration of the render loop.
   * @param {Tily.Main} t The Tily instance being looped.
   */
  function loop(t) {
    const now = new Date(),
      elapsedTime = (now - t.lastFrameTime) / 1000;
    t.lastFrameTime = now;
    t.frameTime += elapsedTime;
    t.frameCount++;
    if (t.frameTime > 1) {
      t.frameRate = t.frameCount;
      t.frameTime = 0;
      t.frameCount = 0;
    }
    t.draw(elapsedTime);
    t.loop = window.requestAnimationFrame(function() { loop(t) });
  }

  /**
   * Render the Tily instance onto the canvas.
   * @name draw
   * @function
   * @instance
   * @memberof Tily.Main
   * @param {number} elapsedTime The time elapsed in seconds since the last draw call.
   */
  Main.prototype.draw = function(elapsedTime) {
    this.context.save();
    this.context.clearRect(0, 0, this.width, this.height);
    if (typeof this.options.beforeDrawFunction == "function") {
      this.options.beforeDrawFunction.call(this, this.canvas, this.context, this.width, this.height, elapsedTime);
    }

    // Draw the active buffer and handle buffer fade transition
    const width = this.width,
      height = this.height;
    if (this.bufferTransition) {
      const alpha = this.bufferTransition.update(elapsedTime);
      if (this.bufferTransition.start) {
        this.context.globalAlpha = 1 - alpha;
        this.bufferTransition.start.draw(this.context, elapsedTime, width, height);
      }
      if (this.bufferTransition.finish) {
        this.context.globalAlpha = alpha;
        this.bufferTransition.finish.draw(this.context, elapsedTime, width, height);
      }
      if (this.bufferTransition.finished) {  // Remove the transition when it has finished
        this.bufferTransition = null;
      }
    } else if (this.activeBuffer) {
      this.context.globalAlpha = 1;
      this.activeBuffer.draw(this.context, elapsedTime, width, height);
    }
    if (typeof this.options.afterDrawFunction == "function") {
      this.options.afterDrawFunction.call(this, this.canvas, this.context, this.width, this.height, elapsedTime);
    }
    this.context.restore();
    if (this.options.showFPS) {
      this.context.save();
      this.context.font = '20px monospace';
      this.context.fillStyle = 'rgba(0, 0, 0, 0.5)';
      this.context.fillRect(this.width - (10 + 80), 10, 80, 30);
      this.context.fillStyle = 'white';
      this.context.textBaseline = 'top';
      this.context.textAlign = 'right';
      this.context.fillText(this.frameRate, this.width - 15, 15);
      this.context.restore();
    }
  };

  /**
   * Activate a new buffer with an optional fade transition from the current buffer.
   * @name activateBuffer
   * @function
   * @instance
   * @memberof Tily.Main
   * @param {Tily.Buffer} buffer The new buffer to activate.
   * @param {TransitionOptions} [options] An optional options object.
   * @returns {Promise} A promise instance for setting a transition finished callback.
   */
  Main.prototype.activateBuffer = function(buffer, options) {
    const transition = new Tily.BufferTransition(this.activeBuffer, buffer, options);
    this.bufferTransition = transition;
    this.activeBuffer = buffer;
    return new Promise(function(resolve, reject) { transition.finishedCallback = resolve; });
  };

  /**
   * @name size
   * @description The size of this Tily element measured in pixels.
   * @instance
   * @memberof Tily.Main
   * @type {Size}
   */
  Object.defineProperty(Main.prototype, "size", {
    get: function() {
      return {
        width: this.width,
        height: this.height
      };
    }
  });
  return Main;
}());