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;
}
}