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