Source: core/Stage.js

import Class from './Class';
import Node from './Node';
import version from './version';
import WebGLRenderer from '../renderer/WebGLRenderer';
import Ray from '../math/Ray';
import Vector3 from '../math/Vector3';
import browser from '../utils/browser';
import log from '../utils/log';
import {
    getElementRect
} from '../utils/util';

/**
 * 舞台类
 * @class
 * @extends Node
 * @example
 * const stage = new Hilo3d.Stage({
 *     container:document.body,
 *     width:innerWidth,
 *     height:innerHeight
 * });
 */
const Stage = Class.create(/** @lends Stage.prototype */ {
    Extends: Node,

    isStage: true,
    className: 'Stage',

    /**
     * 渲染器
     * @type {WebGLRenderer}
     */
    renderer: null,

    /**
     * 摄像机
     * @type {Camera}
     */
    camera: null,

    /**
     * 像素密度
     * @type {Number}
     * @default 根据设备自动判断
     */
    pixelRatio: null,

    /**
     * 偏移值
     * @type {Number}
     * @default 0
     */
    offsetX: 0,

    /**
     * 偏移值
     * @type {Number}
     * @default 0
     */
    offsetY: 0,

    /**
     * 舞台宽度
     * @type {Number}
     * @default 0
     */
    width: 0,

    /**
     * 舞台高度
     * @type {Number}
     * @default 0
     */
    height: 0,

    /**
     * canvas
     * @type {HTMLCanvasElement}
     * @default null
     */
    canvas: null,

    /**
     * @constructs
     * @param {Object} [params] 创建对象的属性参数。可包含此类的所有属性,所有属性会透传给 Renderer。
     * @param {HTMLElement} [params.container] stage的容器, 如果有,会把canvas加进container里。
     * @param {HTMLCanvasElement} [params.canvas] stage的canvas,不传会自动创建。
     * @param {Camera} [params.camera] stage的摄像机。
     * @param {number} [params.width=innerWidth] stage的宽,默认网页宽度
     * @param {number} [params.height=innerHeight] stage的高,默认网页高度
     * @param {number} [params.pixelRatio=根据设备自动判断] 像素密度。
     * @param {Color} [params.clearColor=new Color(1, 1, 1, 1)] 背景色。
     * @param {boolean} [params.preferWebGL2=false] 是否优先使用 WebGL2
     * @param {boolean} [params.useFramebuffer=false] 是否使用Framebuffer,有后处理需求时需要。
     * @param {Object} [params.framebufferOption={}] framebufferOption Framebuffer的配置,useFramebuffer为true时生效。
     * @param {boolean} [params.useLogDepth=false] 是否使用对数深度,处理深度冲突。
     * @param {boolean} [params.alpha=false] 是否背景透明。
     * @param {boolean} [params.depth=true] 是否需要深度缓冲区。
     * @param {boolean} [params.stencil=false] 是否需要模版缓冲区。
     * @param {boolean} [params.antialias=true] 是否抗锯齿。
     * @param {boolean} [params.premultipliedAlpha=true] 是否需要 premultipliedAlpha。
     * @param {boolean} [params.preserveDrawingBuffer=false] 是否需要 preserveDrawingBuffer。
     * @param {boolean} [params.failIfMajorPerformanceCaveat=false] 是否需要 failIfMajorPerformanceCaveat。
     * @param {boolean} [params.gameMode=false] 是否开启游戏模式,UC 浏览器专用
     * @param {any} [params.[value:string]] 其它属性
     */
    constructor(params) {
        if (!params.width) {
            params.width = window.innerWidth;
        }

        if (!params.height) {
            params.height = window.innerHeight;
        }

        if (!params.pixelRatio) {
            let pixelRatio = window.devicePixelRatio || 1;
            pixelRatio = Math.min(pixelRatio, 1024 / Math.max(params.width, params.height), 2);
            pixelRatio = Math.max(pixelRatio, 1);
            params.pixelRatio = pixelRatio;
        }

        Stage.superclass.constructor.call(this, params);
        this.initRenderer(params);

        log.log(`Hilo3d version: ${version}`);
    },
    /**
     * 初始化渲染器
     * @private
     * @param  {Object} params
     */
    initRenderer(params) {
        const canvas = this.canvas = this.createCanvas(params);
        this.renderer = new WebGLRenderer(Object.assign(params, {
            domElement: canvas
        }));
        this.resize(this.width, this.height, this.pixelRatio, true);
    },
    /**
     * 生成canvas
     * @private
     * @param  {Object} params
     * @return {HTMLCanvasElement}
     */
    createCanvas(params) {
        let canvas;
        if (params.canvas) {
            canvas = params.canvas;
        } else {
            canvas = document.createElement('canvas');
        }

        if (params.container) {
            params.container.appendChild(canvas);
        }

        return canvas;
    },
    /**
     * 缩放舞台
     * @param  {Number} width 舞台宽
     * @param  {Number} height 舞台高
     * @param  {Number} [pixelRatio=this.pixelRatio] 像素密度
     * @param  {Boolean} [force=false] 是否强制刷新
     * @return {Stage} 舞台本身。链式调用支持。
     */
    resize(width, height, pixelRatio, force) {
        if (pixelRatio === undefined) {
            pixelRatio = this.pixelRatio;
        }

        if (force || this.width !== width || this.height !== height || this.pixelRatio !== pixelRatio) {
            this.width = width;
            this.height = height;
            this.pixelRatio = pixelRatio;
            this.rendererWidth = width * pixelRatio;
            this.rendererHeight = height * pixelRatio;

            const canvas = this.canvas;
            const renderer = this.renderer;

            renderer.resize(this.rendererWidth, this.rendererHeight, force);
            canvas.style.width = this.width + 'px';
            canvas.style.height = this.height + 'px';
            this.updateDomViewport();
        }
        return this;
    },
    /**
     * 设置舞台偏移值
     * @param {Number} x x
     * @param {Number} y y
     * @return {Stage} 舞台本身。链式调用支持。
     */
    setOffset(x, y) {
        if (this.offsetX !== x || this.offsetY !== y) {
            this.offsetX = x;
            this.offsetY = y;

            const pixelRatio = this.pixelRatio;
            this.renderer.setOffset(x * pixelRatio, y * pixelRatio);
        }
        return this;
    },
    /**
     * 改viewport
     * @param  {Number} x      x
     * @param  {Number} y      y
     * @param  {Number} width  width
     * @param  {Number} height height
     * @return {Stage} 舞台本身。链式调用支持。
     */
    viewport(x, y, width, height) {
        this.resize(width, height, this.pixelRatio, true);
        this.setOffset(x, y);
        return this;
    },
    /**
     * 渲染一帧
     * @param  {Number} dt 间隔时间
     * @return {Stage} 舞台本身。链式调用支持。
     */
    tick(dt) {
        this.traverseUpdate(dt);
        if (this.camera) {
            this.renderer.render(this, this.camera, true);
        }
        return this;
    },
    /**
     * 开启/关闭舞台的DOM事件响应。要让舞台上的可视对象响应用户交互,必须先使用此方法开启舞台的相应事件的响应。
     * @param {String|Array} type 要开启/关闭的事件名称或数组。
     * @param {Boolean} enabled 指定开启还是关闭。如果不传此参数,则默认为开启。
     * @return {Stage} 舞台本身。链式调用支持。
     */
    enableDOMEvent(types, enabled = true) {
        const canvas = this.canvas;
        const handler = this._domListener || (this._domListener = (e) => {
            this._onDOMEvent(e);
        });
        types = typeof types === 'string' ? [types] : types;

        types.forEach((type) => {
            if (enabled) {
                canvas.addEventListener(type, handler, false);
            } else {
                canvas.removeEventListener(type, handler);
            }
        });
        return this;
    },
    /**
     * DOM事件处理函数。此方法会把事件调度到事件的坐标点所对应的可视对象。
     * @private
     */
    _onDOMEvent(event) {
        const canvas = this.canvas;
        const target = this._eventTarget;

        const type = event.type;
        const isTouch = type.indexOf('touch') === 0;

        // calculate stageX/stageY
        let posObj = event;
        if (isTouch) {
            const touches = event.touches;
            const changedTouches = event.changedTouches;
            if (touches && touches.length) {
                posObj = touches[0];
            } else if (changedTouches && changedTouches.length) {
                posObj = changedTouches[0];
            }
        }

        const domViewport = this.domViewport || this.updateDomViewport();
        const x = (posObj.pageX || posObj.clientX) - domViewport.left;
        const y = (posObj.pageY || posObj.clientY) - domViewport.top;
        event.stageX = x;
        event.stageY = y;

        // 鼠标事件需要阻止冒泡方法 Prevent bubbling on mouse events.
        event.stopPropagation = function() {
            this._stopPropagationed = true;
        };

        const meshResult = this.getMeshResultAtPoint(x, y, true);
        const obj = meshResult.mesh;
        event.hitPoint = meshResult.point;

        // fire mouseout/touchout event for last event target
        const leave = type === 'mouseout';
        // 当obj和target不同 且obj不是target的子元素时才触发out事件 fire out event when obj and target isn't the same as well as obj is not a child element to target.
        if (target && (target !== obj && (!target.contains || !target.contains(obj)) || leave)) {
            let out = false;
            if (type === 'touchmove') {
                out = 'touchout';
            } else if (type === 'mousemove' || leave || !obj) {
                out = 'mouseout';
            }

            if (out) {
                const outEvent = Object.assign({}, event);
                outEvent.type = out;
                outEvent.eventTarget = target;
                target._fireMouseEvent(outEvent);
            }
            event.lastEventTarget = target;
            this._eventTarget = null;
        }

        // fire event for current view
        if (obj && obj.pointerEnabled && type !== 'mouseout') {
            event.eventTarget = this._eventTarget = obj;
            obj._fireMouseEvent(event);
        }

        // set cursor for current view
        if (!isTouch) {
            let cursor = (obj && obj.pointerEnabled && obj.useHandCursor) ? 'pointer' : '';
            canvas.style.cursor = cursor;
        }

        // fix android: `touchmove` fires only once
        if (browser.android && type === 'touchmove') {
            event.preventDefault();
        }
    },
    /**
     * 更新 DOM viewport
     * @return {Object} DOM viewport, {left, top, right, bottom}
     */
    updateDomViewport() {
        const canvas = this.canvas;
        let domViewport = null;
        if (canvas.parentNode) {
            domViewport = this.domViewport = getElementRect(canvas);
        }
        return domViewport;
    },
    /**
     * 获取指定点的 mesh
     * @param  {Number}  x
     * @param  {Number}  y
     * @param  {Boolean} [eventMode=false]
     * @return {Mesh|null}
     */
    getMeshResultAtPoint(x, y, eventMode = false) {
        const camera = this.camera;
        let ray = this._ray;
        if (!ray) {
            ray = this._ray = new Ray();
        }
        ray.fromCamera(camera, x, y, this.width, this.height);
        const hitResult = this.raycast(ray, true, eventMode);
        if (hitResult) {
            return hitResult[0];
        }

        if (!this._stageResultAtPoint) {
            this._stageResultAtPoint = {
                mesh: this,
                point: new Vector3()
            };
        }

        const point = this._stageResultAtPoint.point;
        point.copy(camera.unprojectVector(point.set(x, y, 0), this.width, this.height));
        return this._stageResultAtPoint;
    },
    /**
     * 释放 WebGL 资源
     * @return {Stage} this
     */
    releaseGLResource() {
        this.renderer.releaseGLResource();
        return this;
    },
    /**
     * 销毁
     * @override
     * @return {Stage} this
     */
    destroy() {
        Stage.superclass.destroy.call(this, this.renderer);
        this.releaseGLResource();
        this.traverse((child) => {
            child.off();
            child.parent = null;
        });
        this.children.length = 0;
        this.renderer.off();

        return this;
    }
});

export default Stage;