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