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