      1 /**
      2  * Hilo
      3  * Copyright 2015 alibaba.com
      4  * Licensed under the MIT License
      5  */
      6
      7 /**
      8  * 示例:
      9  * <pre>
     10  * var stage = new Hilo.Stage({
     11  *     renderType:'canvas',
     12  *     container: containerElement,
     13  *     width: 320,
     14  *     height: 480
     15  * });
     16  * </pre>
     17  * @class 舞台是可视对象树的根，可视对象只有添加到舞台或其子对象后才会被渲染出来。创建一个hilo应用一般都是从创建一个stage开始的。
     18  * @augments Container
     19  * @param {Object} properties 创建对象的属性参数。可包含此类所有可写属性。主要有：
     20  * <ul>
     21  * <li><b>container</b>:String|HTMLElement - 指定舞台在页面中的父容器元素。它是一个dom容器或id。若不传入此参数且canvas未被加入到dom树，则需要在舞台创建后手动把舞台画布加入到dom树中，否则舞台不会被渲染。可选。</li>
     22  * <li><b>renderType</b>:String - 指定渲染方式，canvas|dom|webgl，默认canvas。可选。</li>
     23  * <li><b>canvas</b>:String|HTMLCanvasElement|HTMLElement - 指定舞台所对应的画布元素。它是一个canvas或普通的div，也可以传入元素的id。若为canvas，则使用canvas来渲染所有对象，否则使用dom+css来渲染。可选。</li>
     24  * <li><b>width</b>:Number</li> - 指定舞台的宽度。默认为canvas的宽度。可选。
     25  * <li><b>height</b>:Number</li> - 指定舞台的高度。默认为canvas的高度。可选。
     26  * <li><b>paused</b>:Boolean</li> - 指定舞台是否停止渲染。默认为false。可选。
     27  * </ul>
     28  * @module hilo/view/Stage
     29  * @requires hilo/core/Hilo
     30  * @requires hilo/core/Class
     31  * @requires hilo/view/Container
     32  * @requires hilo/renderer/CanvasRenderer
     33  * @requires hilo/renderer/DOMRenderer
     34  * @requires hilo/renderer/WebGLRenderer
     35  * @requires hilo/util/browser
     36  * @requires hilo/util/util
     37  * @property {HTMLCanvasElement|HTMLElement} canvas 舞台所对应的画布。它可以是一个canvas或一个普通的div。只读属性。
     38  * @property {Renderer} renderer 舞台渲染器。只读属性。
     39  * @property {Boolean} paused 指示舞台是否暂停刷新渲染。
     40  * @property {Object} viewport 舞台内容在页面中的渲染区域。包含的属性有：left、top、width、height。只读属性。
     41  */
     42 var Stage = Class.create(/** @lends Stage.prototype */{
     43     Extends: Container,
     44     constructor: function(properties){
     45         properties = properties || {};
     46         this.id = this.id || properties.id || Hilo.getUid('Stage');
     47         Stage.superclass.constructor.call(this, properties);
     48
     49         this._initRenderer(properties);
     50
     51         //init size
     52         var width = this.width, height = this.height,
     53             viewport = this.updateViewport();
     54         if(!properties.width) width = (viewport && viewport.width) || 320;
     55         if(!properties.height) height = (viewport && viewport.height) || 480;
     56         this.resize(width, height, true);
     57     },
     58
     59     canvas: null,
     60     renderer: null,
     61     paused: false,
     62     viewport: null,
     63
     64     /**
     65      * @private
     66      */
     67     _initRenderer: function(properties){
     68         var canvas = properties.canvas;
     69         var container = properties.container;
     70         var renderType = properties.renderType||'canvas';
     71
     72         if(typeof canvas === 'string') canvas = Hilo.getElement(canvas);
     73         if(typeof container === 'string') container = Hilo.getElement(container);
     74
     75         if(!canvas){
     76             var canvasTagName = renderType === 'dom'?'div':'canvas';
     77             canvas = Hilo.createElement(canvasTagName, {
     78                 style: {
     79                     position: 'absolute'
     80                 }
     81             });
     82         }
     83         else if(!canvas.getContext){
     84             renderType = 'dom';
     85         }
     86
     87         this.canvas = canvas;
     88         if(container) container.appendChild(canvas);
     89
     90         var props = {canvas:canvas, stage:this};
     91         switch(renderType){
     92             case 'dom':
     93                 this.renderer = new DOMRenderer(props);
     94                 break;
     95             case 'webgl':
     96                 if(WebGLRenderer.isSupport()){
     97                     this.renderer = new WebGLRenderer(props);
     98                 }
     99                 else{
    100                     this.renderer = new CanvasRenderer(props);
    101                 }
    102                 break;
    103             case 'canvas':
    104 	        /* falls through */
    105             default:
    106                 this.renderer = new CanvasRenderer(props);
    107                 break;
    108         }
    109     },
    110
    111     /**
    112      * 添加舞台画布到DOM容器中。注意：此方法覆盖了View.addTo方法。
    113      * @param {HTMLElement} domElement 一个dom元素。
    114      * @returns {Stage} 舞台本身，可用于链式调用。
    115      */
    116     addTo: function(domElement){
    117         var canvas = this.canvas;
    118         if(canvas.parentNode !== domElement){
    119             domElement.appendChild(canvas);
    120         }
    121         return this;
    122     },
    123
    124     /**
    125      * 调用tick会触发舞台的更新和渲染。开发者一般无需使用此方法。
    126      * @param {Number} delta 调度器当前调度与上次调度tick之间的时间差。
    127      */
    128     tick: function(delta){
    129         if(!this.paused){
    130             this._render(this.renderer, delta);
    131         }
    132     },
    133
    134     /**
    135      * 开启/关闭舞台的DOM事件响应。要让舞台上的可视对象响应用户交互，必须先使用此方法开启舞台的相应事件的响应。
    136      * @param {String|Array} type 要开启/关闭的事件名称或数组。
    137      * @param {Boolean} enabled 指定开启还是关闭。如果不传此参数，则默认为开启。
    138      * @returns {Stage} 舞台本身。链式调用支持。
    139      */
    140     enableDOMEvent: function(types, enabled){
    141         var me = this,
    142             canvas = me.canvas,
    143             handler = me._domListener || (me._domListener = function(e){me._onDOMEvent(e);});
    144
    145         types = typeof types === 'string' ? [types] : types;
    146         enabled = enabled !== false;
    147
    148         for(var i = 0; i < types.length; i++){
    149             var type = types[i];
    150
    151             if(enabled){
    152                 canvas.addEventListener(type, handler, false);
    153             }else{
    154                 canvas.removeEventListener(type, handler);
    155             }
    156         }
    157
    158         return me;
    159     },
    160
    161     /**
    162      * DOM事件处理函数。此方法会把事件调度到事件的坐标点所对应的可视对象。
    163      * @private
    164      */
    165     _onDOMEvent: function(e){
    166         var type = e.type, event = e, isTouch = type.indexOf('touch') == 0;
    167
    168         //calculate stageX/stageY
    169         var posObj = e;
    170         if(isTouch){
    171             var touches = e.touches, changedTouches = e.changedTouches;
    172             posObj = (touches && touches.length) ? touches[0] :
    173                      (changedTouches && changedTouches.length) ? changedTouches[0] : null;
    174         }
    175
    176         var x = posObj.pageX || posObj.clientX, y = posObj.pageY || posObj.clientY,
    177             viewport = this.viewport || this.updateViewport();
    178
    179         event.stageX = x = (x - viewport.left) / this.scaleX;
    180         event.stageY = y = (y - viewport.top) / this.scaleY;
    181
    182         //鼠标事件需要阻止冒泡方法 Prevent bubbling on mouse events.
    183         event.stopPropagation = function(){
    184             this._stopPropagationed = true;
    185         };
    186
    187         var obj = this.getViewAtPoint(x, y, true, false, true)||this,
    188             canvas = this.canvas, target = this._eventTarget;
    189
    190         //fire mouseout/touchout event for last event target
    191         var leave = type === 'mouseout';
    192         //当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.
    193         if(target && (target != obj && (!target.contains || !target.contains(obj))|| leave)){
    194             var out = (type === 'touchmove') ? 'touchout' :
    195                       (type === 'mousemove' || leave || !obj) ? 'mouseout' : null;
    196             if(out) {
    197                 var outEvent = util.copy({}, event);
    198                 outEvent.type = out;
    199                 outEvent.eventTarget = target;
    200                 target._fireMouseEvent(outEvent);
    201             }
    202             event.lastEventTarget = target;
    203             this._eventTarget = null;
    204         }
    205
    206         //fire event for current view
    207         if(obj && obj.pointerEnabled && type !== 'mouseout'){
    208             event.eventTarget = this._eventTarget = obj;
    209             obj._fireMouseEvent(event);
    210         }
    211
    212         //set cursor for current view
    213         if(!isTouch){
    214             var cursor = (obj && obj.pointerEnabled && obj.useHandCursor) ? 'pointer' : '';
    215             canvas.style.cursor = cursor;
    216         }
    217
    218         //fix android: `touchmove` fires only once
    219         if(browser.android && type === 'touchmove'){
    220             e.preventDefault();
    221         }
    222     },
    223
    224     /**
    225      * 更新舞台在页面中的可视区域，即渲染区域。当舞台canvas的样式border、margin、padding等属性更改后，需要调用此方法更新舞台渲染区域。
    226      * @returns {Object} 舞台的可视区域。即viewport属性。
    227      */
    228     updateViewport: function(){
    229         var canvas = this.canvas, viewport = null;
    230         if(canvas.parentNode){
    231             viewport = this.viewport = Hilo.getElementRect(canvas);
    232         }
    233         return viewport;
    234     },
    235
    236     /**
    237      * 改变舞台的大小。
    238      * @param {Number} width 指定舞台新的宽度。
    239      * @param {Number} height 指定舞台新的高度。
    240      * @param {Boolean} forceResize 指定是否强制改变舞台大小，即不管舞台大小是否相同，仍然强制执行改变动作，可确保舞台、画布以及视窗之间的尺寸同步。
    241      */
    242     resize: function(width, height, forceResize){
    243         if(forceResize || this.width !== width || this.height !== height){
    244             this.width = width;
    245             this.height = height;
    246             this.renderer.resize(width, height);
    247             this.updateViewport();
    248         }
    249     }
    250
    251 });
    252