      1 /**
      2  * Hilo
      3  * Copyright 2015 alibaba.com
      4  * Licensed under the MIT License
      5  */
      6
      7 /**
      8  * Demo:
      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 Stage is the root of all visual object tree, any visual object will be render only after being added to Stage or any children elements of Stage. Normally, every hilo application start with an stage instance.
     18  * @augments Container
     19  * @param {Object} properties Properties parameters for the object. Includes all writable properties of this class. Some important like:
     20  * <ul>
     21  * <li><b>container</b>:String|HTMLElement - Assign the parent container element of the Stage in the page. It should be a dom container or an id. If this parameter is not given and canvas isn't in the dom tree, you should add the stage vanvas into the dom tree yourself, otherwise Stage will not render. optional.</li>
     22  * <li><b>renderType</b>:String - Renering way: canvas|dom|webgl，default value is canvas, optional.</li>
     23  * <li><b>canvas</b>:String|HTMLCanvasElement|HTMLElement - 指定舞台所对应的画布元素。它是一个canvas或普通的div，也可以传入元素的id。若为canvas，则使用canvas来渲染所有对象，否则使用dom+css来渲染。可选。</li>
     24  * <li><b>width</b>:Number</li> - The width of the Stage, default value is the width of canvas, optional.
     25  * <li><b>height</b>:Number</li> - The height of the Stage, default value is the height of canvas, optional.
     26  * <li><b>paused</b>:Boolean</li> - Whether stop rendering the Stage, default value is false, optional.
     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 The canvas the Stage is related to. It can be a canvas or a div element, readonly!
     38  * @property {Renderer} renderer Stage renderer, readonly!
     39  * @property {Boolean} paused Paused Stage rendering.
     40  * @property {Object} viewport Rendering area of the Stage. Including properties like: left, top, width, height. readonly!
     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      * Add Stage canvas to DOM container. Note: this function overwrite View.addTo function.
    113      * @param {HTMLElement} domElement An dom element.
    114      * @returns {Stage} The Stage Object, chained call supported.
    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      * Invoke tick function and Stage will update and render. Developer may not need to use this funciton.
    126      * @param {Number} delta The time had pass between this tick invoke and last tick invoke.
    127      */
    128     tick: function(delta){
    129         if(!this.paused){
    130             this._render(this.renderer, delta);
    131         }
    132     },
    133
    134     /**
    135      * Turn on/off Stage response to DOM event. To make visual objects on the Stage interactive, use this function to turn on Stage's responses to events.
    136      * @param {String|Array} type The event name or array that need to turn on/off.
    137      * @param {Boolean} enabled Whether turn on or off the response method of stage DOM event. If not provided, default value is true.
    138      * @returns {Stage} The Stage Object, chained call supported.
    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 events handler function. This funciton will invoke events onto the visual object, which is on the position of the coordinate where the events is invoked.
    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      * Update the viewport (rendering area) which Stage show on the page. Invoke this function to update viewport when Stage canvas changes border, margin or padding properties.
    226      * @returns {Object} The visible area of the Stage (the viewport property).
    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      * Resize the Stage.
    238      * @param {Number} width The width of the new Stage.
    239      * @param {Number} height The height of the new Stage.
    240      * @param {Boolean} forceResize Whether forced to resize the Stage, means no matter the size of the Stage, force to change the size to keep Stage, canvas and window act at the same time.
    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