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