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