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