Source: tiny/core/Application.js

import EventEmitter from 'eventemitter3';
import TWEEN from '../../libs/tween.js';
import Container from './display/Container';
import { shared } from './ticker';
import { config, defaultConfig } from './settings';
import { RENDERER_TYPE, WIN_SIZE } from './const';
import * as utils from './utils';
import { isMobile as Device } from '../../utils';
import Debug from './Debug';
import Text from './text/Text';
import Transition from '../transitions/Transition';
import CanvasRenderer from './renderers/canvas/CanvasRenderer';
import WebGLRenderer from './renderers/webgl/WebGLRenderer';

let updateKeyId = 0;

/**
 * 故事从这里开始
 *
 * @example
 * // 定义启动参数
 * var config = {
 *   showFPS: true,
 *   renderOptions: {
 *     backgroundColor: 0x2a3145
 *   }
 * };
 * // 新建App
 * var app = new Tiny.Application(config);
 * // 通过 fromImage 实例化精灵
 * var sprite = Tiny.Sprite.fromImage('https://zos.alipayobjects.com/rmsportal/nJBojwdMJfUqpCWvwyoA.png');
 * // 启动
 * app.run(sprite);
 *
 * @class
 * @memberof Tiny
 */
export default class Application extends EventEmitter {
  /**
   * @param {Tiny.config} conf - 启动参数
   */
  constructor(conf) {
    super();

    this._fps = 0;

    //游戏是否已经停止
    this._paused = true;

    this._updatePoll = {};
    this._tickerHandlers = [];

    /**
     * Ticker for doing render updates.
     *
     * @member {Tiny.ticker.Ticker}
     */
    this.ticker = shared;

    /**
     * 所有显示类的根容器
     *
     * @member {Tiny.Container}
     */
    this.camera = new Container();

    /**
     * 舞台对象,用户创建的显示类都会添加到这个对象中
     *
     * @member {Tiny.Container}
     */
    this.stage = new Container();
    this.camera.addChild(this.stage);

    this.setup(Object.assign({}, defaultConfig, conf));

    if (config.showFPS) {
      this._createStatsLabel();
    }

    if (config.debug) {
      this.debug = new Debug(this.camera);
    }

    /**
     * WebGL renderer if available, otherwise CanvasRenderer
     *
     * @member {Tiny.WebGLRenderer|Tiny.CanvasRenderer}
     * @example
     * var renderer = app.renderer
     */
    this.renderer = this.autoDetectRenderer(config.newWidth, config.newHeight, {
      view: this.view,
    });

    WIN_SIZE.width = Math.round(this.renderer.width);
    WIN_SIZE.height = Math.round(this.renderer.height);

    // @version 1.2.0
    if (config.viewTouched) {
      this.view.style['touch-action'] = 'initial';
      this.renderer.plugins.interaction.autoPreventDefault = false;
    }
  }

  /**
   * Render the current camera.
   */
  render() {
    if (config.debug) {
      this.debug && this.debug.render(this.stage);
    }
    this.renderer && this.renderer.render(this.camera);
  }

  /**
   * 停止
   * 注意:该方法已不推荐使用,请直接使用 `pause` 方法
   *
   * @deprecated since version 1.1.7
   */
  stop() {
    this.pause();
  }

  /**
   * 开始
   */
  start() {
    if (!this._paused) {
      return;
    }
    this._paused = false;
    shared.add(Application._tweenUpdateFn);
    shared.start();

    this._tickerHandlers.push(Application._tweenUpdateFn);
  }

  /**
   * 恢复
   *
   * - 恢复暂停的 `Tiny.ticker.shared` 下的所有事件(含主调度)
   * - 恢复暂停的 `Tiny.TWEEN` 动画
   * - 恢复暂停的 `CountDown` 实例
   * - 恢复暂停的 `tinyjs-plugin-audio` 的 Audio 实例
   */
  resume() {
    if (!this._paused) {
      return;
    }
    this._paused = false;
    try {
      shared.start();
      utils.CountDownCache.forEach((cd) => {
        if (!cd.isManualPause()) {
          cd.start();
        }
      });
      TWEEN.resume();
      Tiny.audio.manager.resume();
    } catch (e) {}
  }

  /**
   * 暂停
   *
   * - 暂停 `Tiny.ticker.shared` 下的所有事件(含主调度)
   * - 暂停 `Tiny.TWEEN` 动画
   * - 暂停 `CountDown` 实例
   * - 暂停 `tinyjs-plugin-audio` 的 Audio 实例
   */
  pause() {
    if (this._paused) {
      return;
    }
    this._paused = true;
    try {
      shared.stop();
      utils.CountDownCache.forEach((cd) => {
        cd.pause(true);
      });
      TWEEN.pause();
      Tiny.audio.manager.pause();
    } catch (e) {}
  }

  /**
   * 是否暂停中
   *
   * @return {boolean}
   */
  isPaused() {
    return this._paused;
  }

  /**
   *
   * @private
   * @param {number} [width] - the width of the renderers view
   * @param {number} [height] - the height of the renderers view
   * @param {Tiny.RENDER_OPTIONS} [options] - The optional renderer parameters
   * @return {Tiny.WebGLRenderer|Tiny.CanvasRenderer} - Returns WebGL renderer if available, otherwise CanvasRenderer
   */
  autoDetectRenderer(width = 320, height = 568, options = {}) {
    Object.assign(options, config.renderOptions);

    if (config.renderType !== RENDERER_TYPE.CANVAS) {
      if (utils.isWebGLSupported()) {
        // console.log('WebGLRenderer');
        return new WebGLRenderer(width, height, options);
      }
    }
    // console.log('CanvasRenderer');
    return new CanvasRenderer(width, height, options);
  }

  /**
   * 启动某个场景
   *
   * @param {Tiny.DisplayObject} startScene
   */
  run(startScene) {
    this.replaceScene(startScene);

    if (config.autoRender) {
      this.mainLoop();
    } else {
      //手动渲染一次
      this.render();
    }
  }

  /**
   * 切换场景,如果你想切换下一个场景,使用 replaceScene,还可以使用转场动画
   *
   * @example
   * var app = new Tiny.Application({..});
   * app.replaceScene(scene, 'SlideInR', 800);
   *
   * @param {Tiny.DisplayObject} scene - 场景对象
   * @param {string} [transition] - 转场动画的字符,比如:Fade、MoveIn等。更多参照 {@link Tiny.Transition}
   * @param {number} [duration] - 转场动画时长(单位:ms)
   */
  replaceScene(scene, transition, duration) {
    if (transition) {
      const instance = new Transition(this.stage, scene, duration);
      const trans = transition;
      Array.prototype.splice.call(arguments, 0, 3);
      instance[trans](arguments);
    } else {
      this.stage.removeChildren();
      this.stage.addChild(scene);
    }
    this._currentScene = scene;
  }

  /**
   * @private
   */
  mainLoop() {
    const renderHandler = function(t) {
      this.render();

      for (const key in this._updatePoll) {
        if (this._updatePoll.hasOwnProperty(key)) {
          this._updatePoll[key].call(this, t);
        }
      }
    };

    shared.add(renderHandler, this);

    let __lastTime = 0;
    let __accumDt = 0;
    let __fpsOverstepTimes = 0;
    const fpsLabelHandler = function(t) {
      __accumDt++;
      const currentTime = window.performance.now();
      if (currentTime - __lastTime >= 1000) {
        const fps = __accumDt;
        this._fps = fps;
        // @version 1.2.3 @2018-01-03 启动参数 showFPF 为 false 时,也应该更新 fps,保证 getCurrentFPS 方法能在应用启动后的任何时候都能拿到当前帧率
        this._label && (this._label.text = `SPF: ${(1 / fps).toFixed(3)}\nFPS: ${fps.toFixed(1)}`);
        __accumDt = 0;
        __lastTime = currentTime;

        // 帧率检查,iOS 13.*节能模式下,raf 达到90Hz
        if (fps > defaultConfig.fps + 5) {
          __fpsOverstepTimes++;
          if (__fpsOverstepTimes >= 3) {
            this.emit('fps-overstep', fps);
          }
        } else {
          __fpsOverstepTimes = 0;
        }
      }
    };

    shared.add(fpsLabelHandler, this);

    this._tickerHandlers.push(renderHandler, fpsLabelHandler);
    // Start the rendering
    this.start();
  }

  static _tweenUpdateFn() {
    TWEEN.update();
  }

  /**
   * 游戏的主调度
   *
   * @example
   * var app = new Tiny.Application();
   * var fn = function() {
   *  console.log('update.');
   * }
   *
   * app.onUpdate(fn);
   *
   * @param {function} fn
   * @param {boolean} [force] - 是否强制覆盖方法池中同一个方法
   */
  onUpdate(fn, force = false) {
    const key = fn._tiny_update_key || (fn._tiny_update_key = ++updateKeyId);

    if (!this._updatePoll[key] || force) {
      this._updatePoll[key] = fn;
    }
  }

  /**
   * 移除主调度中的某个方法
   *
   * @example
   * var app = new Tiny.Application();
   * var fn = function() {
   *  console.log('update.');
   * }
   *
   * app.onUpdate(fn);
   *
   * // 5秒后移除fn
   * Tiny.ticker.shared.countDown({
   *  duration: 1e3,
   *  times: 5,
   *  complete: function () {
   *    app.offUpdate(fn);
   *  }
   * });
   *
   * @version 1.0.2
   * @param fn
   */
  offUpdate(fn) {
    const key = fn._tiny_update_key;

    key && (delete this._updatePoll[key]);
  }

  /**
   *
   * @private
   * @param conf
   */
  setup(conf) {
    Object.assign(config, conf);

    if (!navigator.isCanvasPlus && (Device.tablet || Device.phone)) {
      //style设置
      const fontStyle = document.createElement('style');
      fontStyle.type = 'text/css';
      document.body.appendChild(fontStyle);
      fontStyle.textContent = 'body,canvas,div{ -moz-user-select: none;-webkit-user-select: none;-ms-user-select: none;-khtml-user-select: none;-webkit-tap-highlight-color:rgba(0,0,0,0);}';
    }

    let view;
    if (config.canvasId instanceof HTMLElement) {
      view = config.canvasId;
    } else {
      view = document.getElementById(config.canvasId);
      if (!view) {
        view = document.createElement('canvas');
        view.setAttribute('tabindex', 99);
        view.id = config.canvasId;
        view.style.outline = 'none';
        document.body.appendChild(view);
      }
    }

    /**
     * 就是那个用于渲染故事的普通 `<canvas>` 画布对象
     *
     * @property view
     * @type {HTMLCanvasElement}
     */
    this.view = view;
    this.resize();
  }

  /**
   * @private
   */
  resize() {
    let multiplier;
    const winH = config.fixSize ? config.height : window.innerHeight;
    const winW = config.fixSize ? config.width : window.innerWidth;
    let cWidth = config.fixSize ? (config.width || winW) : config.referWidth;
    let radio = +config.orientation ? winH / cWidth : winW / cWidth;
    const isFullScreen = (config.width === winW && config.height === winH);
    isFullScreen && (radio = 1);
    const width = config.width * radio || winW;
    const height = config.height * radio || winH;
    let cHeight = cWidth * (+config.orientation ? width / height : height / width);

    if (+config.orientation) {
      const w = cWidth;
      cWidth = cHeight;
      cHeight = w;
    } else {
      // 竖屏
    }

    cWidth = cWidth * config.dpi;
    cHeight = cHeight * config.dpi;

    multiplier = Math.min((height / cHeight), (width / cWidth));
    multiplier = Number(multiplier.toFixed(4));

    config.renderOptions.resolution = Number((1 / multiplier).toFixed(4));
    config.renderOptions.autoResize = true;
    config.newWidth = Math.round(cWidth * multiplier);
    config.newHeight = Math.round(cHeight * multiplier);
    this.stage.setScale(multiplier);
  }

  /**
   *
   * @method _createStatsLabel
   * @return {HTMLElement}
   * @private
   */
  _createStatsLabel() {
    this._label = new Text('SPF: -\nFPS: -', {
      fontSize: 18,
      fontFamily: 'Helvetica',
      fill: '#ffffff',
      stroke: '#666666',
      strokeThickness: 0.2,
    });

    this._label.position.set(10, config.newHeight - this._label.height - 10);
    this.camera.addChild(this._label);
  }

  /**
   * 获取当前场景
   *
   * @version 1.1.7
   * @return {Tiny.DisplayObject}
   */
  getCurrentScene() {
    return this._currentScene;
  }

  /**
   * 获取当前帧率
   *
   * @version 1.1.7
   * @return {number}
   */
  getCurrentFPS() {
    return this._fps;
  }

  /**
   * 设置或获取已设置的帧率
   *
   * @version 1.1.7
   * @member {number}
   */
  static get FPS() {
    return config.fps;
  }

  static set FPS(fps) {
    config.fps = fps;
  }

  /**
   * Destroy and don't use after this.
   *
   * @param {boolean} [removeView=false] - Automatically remove canvas from DOM.
   * @param {object|boolean} [stageOptions] - Options parameter. A boolean will act as if all options have been set to that value
   * @param {boolean} [stageOptions.children=false] - if set to true, all the children will have their destroy method called as well. 'stageOptions' will be passed on to those calls.
   * @param {boolean} [stageOptions.texture=false] - Only used for child Sprites if stageOptions.children is set to true. Should it destroy the texture of the child sprite
   * @param {boolean} [stageOptions.baseTexture=false] - Only used for child Sprites if stageOptions.children is set to true. Should it destroy the base texture of the child sprite
   */
  destroy(removeView, stageOptions) {
    TWEEN.removeAll();
    this._tickerHandlers.forEach(item => {
      this.ticker.remove(item, this);
    });
    this.ticker.stop();

    this.stage.destroy(stageOptions);
    this.stage = null;

    this.camera.destroy(stageOptions);
    this.camera = null;

    this.renderer.destroy(removeView);
    this.renderer = null;
  }
}
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:25 GMT+0800 (CST)