      1 /**
      2  * Hilo
      3  * Copyright 2015 alibaba.com
      4  * Licensed under the MIT License
      5  */
      6
      7 /**
      8  * @class View View is the base class of all display objects
      9  * @mixes EventMixin
     10  * @borrows EventMixin#on as #on
     11  * @borrows EventMixin#off as #off
     12  * @borrows EventMixin#fire as #fire
     13  * @param {Object} properties The properties to create a view object, contains all writeable props of this class
     14  * @module hilo/view/View
     15  * @requires hilo/core/Hilo
     16  * @requires hilo/core/Class
     17  * @requires hilo/event/EventMixin
     18  * @requires hilo/geom/Matrix
     19  * @requires hilo/util/util
     20  * @property {String} id The identifier for the view.
     21  * @property {Number} x The position of the view on the x axis relative to the local coordinates of the parent, default value is 0.
     22  * @property {Number} y The position of the view on the y axis relative to the local coordinates of the parent, default value is 0.
     23  * @property {Number} width The width of the view, default value is 0.
     24  * @property {Number} height The height of the view, default value is 0.
     25  * @property {Number} alpha The opacity of the view, default value is 1.
     26  * @property {Number} rotation The rotation of the view in angles, default value is 0.
     27  * @property {Boolean} visible The visibility of the view. If false the vew will not be drawn, default value is true.
     28  * @property {Number} pivotX Position of the center point on the x axis of the view, default value is 0.
     29  * @property {Number} pivotY Position of the center point on the y axis of the view, default value is 0.
     30  * @property {Number} scaleX The x axis scale factor of the view, default value is 1.
     31  * @property {Number} scaleY The y axis scale factor of the view, default value is 1.
     32  * @property {Matrix} transform The transform of the view.If set the transform, x, y, scaleX, scaleY, rotation, pivotX, pivotY will be ignored.default is null.
     33  * @property {Boolean} pointerEnabled Is the view can receive DOM events, default value is true.
     34  * @property {Object} background The background style to fill the view, can be css color, gradient or pattern of canvas
     35  * @property {Graphics} mask Sets a mask for the view. A mask is an object that limits the visibility of an object to the shape of the mask applied to it. A regular mask must be a Hilo.Graphics object. This allows for much faster masking in canvas as it utilises shape clipping. To remove a mask, set this property to null.
     36  * @property {Number} tint The tint applied to the view，default is 0xFFFFFF.Only support in WebGL mode.
     37  * @property {String|Function} align The alignment of the view, the value must be one of Hilo.align enum.
     38  * @property {Container} parent The parent view of this view, readonly!
     39  * @property {Number} depth The z index of the view, readonly!
     40  * @property {Drawable} drawable The drawable object of the view. Only for advanced develop.
     41  * @property {Array} boundsArea The vertex points of the view, the points are relative to the center point. This is a example: [{x:10, y:10}, {x:20, y:20}].
     42  */
     43 var View = (function(){
     44
     45 return Class.create(/** @lends View.prototype */{
     46     Mixes: EventMixin,
     47     constructor: function(properties){
     48         properties = properties || {};
     49         this.id = this.id || properties.id || Hilo.getUid("View");
     50         util.copy(this, properties, true);
     51     },
     52
     53     tint:0xffffff,
     54     id: null,
     55     x: 0,
     56     y: 0,
     57     width: 0,
     58     height: 0,
     59     alpha: 1,
     60     rotation: 0,
     61     visible: true,
     62     pivotX: 0,
     63     pivotY: 0,
     64     scaleX: 1,
     65     scaleY: 1,
     66     pointerEnabled: true,
     67     background: null,
     68     mask: null,
     69     align: null,
     70     drawable: null,
     71     boundsArea: null,
     72     parent: null,
     73     depth: -1,
     74     transform: null,
     75     blendMode:'source-over',
     76
     77     /**
     78      * Get the stage object of the view. If the view doesn't add to any stage, null will be returned.
     79      * @returns {Stage} The stage object of the view.
     80      */
     81     getStage: function(){
     82         var obj = this, parent;
     83         while(parent = obj.parent) obj = parent;
     84         //NOTE: don't use `instanceof` to prevent circular module requirement.
     85         //But it's not a very reliable way to check it's a stage instance.
     86         if(obj.canvas) return obj;
     87         return null;
     88     },
     89
     90     /**
     91      * Get the scaled width of the view.
     92      * @returns {Number} scaled width of the view.
     93      */
     94     getScaledWidth: function(){
     95         return this.width * this.scaleX;
     96     },
     97
     98     /**
     99      * Get the scaled height of the view.
    100      * @returns {Number} scaled height of the view.
    101      */
    102     getScaledHeight: function(){
    103         return this.height * this.scaleY;
    104     },
    105
    106     /**
    107      * Add current view to a Contaner.
    108      * @param {Container} container Container object.
    109      * @param {Uint} index The index of the view in container.
    110      * @returns {View} Current view.
    111      */
    112     addTo: function(container, index){
    113         if(typeof index === 'number') container.addChildAt(this, index);
    114         else container.addChild(this);
    115         return this;
    116     },
    117
    118     /**
    119      * Remove current view from it's parent container
    120      * @returns {View} Current view.
    121      */
    122     removeFromParent: function(){
    123         var parent = this.parent;
    124         if(parent) parent.removeChild(this);
    125         return this;
    126     },
    127
    128     /**
    129      * Get the bounds of the view as a circumscribed rectangle and all vertex points relative to the coordinates of the stage.
    130      * @returns {Array} The vertex points array, and the array contains the following properties:
    131      * <ul>
    132      * <li><b>x</b> - The position of the view on the x axis relative to the coordinates of the stage.</li>
    133      * <li><b>y</b> - The position of the view on the y axis relative to the coordinates of the stage.</li>
    134      * <li><b>width</b> - The width of circumscribed rectangle of the view.</li>
    135      * <li><b>height</b> - The height of circumscribed rectangle of the view</li>
    136      * </ul>
    137      */
    138     getBounds: function(){
    139         var w = this.width, h = this.height,
    140             mtx = this.getConcatenatedMatrix(),
    141             poly = this.boundsArea || [{x:0, y:0}, {x:w, y:0}, {x:w, y:h}, {x:0, y:h}],
    142             vertexs = [], point, x, y, minX, maxX, minY, maxY;
    143
    144         for(var i = 0, len = poly.length; i < len; i++){
    145             point = mtx.transformPoint(poly[i], true, true);
    146             x = point.x;
    147             y = point.y;
    148
    149             if(i == 0){
    150                 minX = maxX = x;
    151                 minY = maxY = y;
    152             }else{
    153                 if(minX > x) minX = x;
    154                 else if(maxX < x) maxX = x;
    155                 if(minY > y) minY = y;
    156                 else if(maxY < y) maxY = y;
    157             }
    158             vertexs[i] = point;
    159         }
    160
    161         vertexs.x = minX;
    162         vertexs.y = minY;
    163         vertexs.width = maxX - minX;
    164         vertexs.height = maxY - minY;
    165         return vertexs;
    166     },
    167
    168     /**
    169      * Get the matrix that can transform points from current view coordinates to the ancestor container coordinates.
    170      * @param {View} ancestor The ancestor of current view, default value is the top container.
    171      * @private
    172      */
    173     getConcatenatedMatrix: function(ancestor){
    174         var mtx = new Matrix(1, 0, 0, 1, 0, 0);
    175
    176         for(var o = this; o != ancestor && o.parent; o = o.parent){
    177             var cos = 1, sin = 0,
    178                 rotation = o.rotation % 360,
    179                 pivotX = o.pivotX, pivotY = o.pivotY,
    180                 scaleX = o.scaleX, scaleY = o.scaleY,
    181                 transform = o.transform;
    182
    183             if(transform) {
    184                 mtx.concat(transform);
    185             }
    186             else{
    187                 if(rotation){
    188                     var r = rotation * Math.PI / 180;
    189                     cos = Math.cos(r);
    190                     sin = Math.sin(r);
    191                 }
    192
    193                 if(pivotX != 0) mtx.tx -= pivotX;
    194                 if(pivotY != 0) mtx.ty -= pivotY;
    195
    196                 var pos = o.getAlignPosition();
    197                 mtx.concat(cos*scaleX, sin*scaleX, -sin*scaleY, cos*scaleY, pos.x, pos.y);
    198             }
    199
    200         }
    201         return mtx;
    202     },
    203
    204     getAlignPosition: function(){
    205         var parent = this.parent;
    206         var align = this.align;
    207         var x = this.x;
    208         var y = this.y;
    209
    210         if(parent && this.align){
    211             if(typeof align === 'function'){
    212                 return this.align();
    213             }
    214
    215             var w = this.width, h = this.height,
    216                 pw = parent.width, ph = parent.height;
    217             switch(align){
    218                 case 'TL':
    219                     x = 0;
    220                     y = 0;
    221                     break;
    222                 case 'T':
    223                     x = pw - w >> 1;
    224                     y = 0;
    225                     break;
    226                 case 'TR':
    227                     x = pw - w;
    228                     y = 0;
    229                     break;
    230                 case 'L':
    231                     x = 0;
    232                     y = ph - h >> 1;
    233                     break;
    234                 case 'C':
    235                     x = pw - w >> 1;
    236                     y = ph - h >> 1;
    237                     break;
    238                 case 'R':
    239                     x = pw - w;
    240                     y = ph - h >> 1;
    241                     break;
    242                 case 'BL':
    243                     x = 0;
    244                     y = ph - h;
    245                     break;
    246                 case 'B':
    247                     x = pw - w >> 1;
    248                     y = ph - h;
    249                     break;
    250                 case 'BR':
    251                     x = pw - w;
    252                     y = ph - h;
    253                     break;
    254             }
    255         }
    256
    257         return {
    258             x:x,
    259             y:y
    260         };
    261     },
    262
    263     /**
    264      * Determining whether a point is in the circumscribed rectangle of current view.
    265      * @param {Number} x The x axis relative to the stage coordinates.
    266      * @param {Number} y The y axis relative to the stage coordinates.
    267      * @param {Boolean} usePolyCollision Is use polygon collision, default value is false.
    268      * @returns {Boolean} the point is in the circumscribed rectangle of current view.
    269      */
    270     hitTestPoint: function(x, y, usePolyCollision){
    271         var bound = this.getBounds(),
    272             hit = x >= bound.x && x <= bound.x + bound.width &&
    273                   y >= bound.y && y <= bound.y + bound.height;
    274
    275         if(hit && usePolyCollision){
    276             hit = pointInPolygon(x, y, bound);
    277         }
    278         return hit;
    279     },
    280
    281     /**
    282      * Determining whether an object is in the circumscribed rectangle of current view.
    283      * @param {View} object The object need to determining.
    284      * @param {Boolean} usePolyCollision Is use polygon collision, default value is false.
    285      */
    286     hitTestObject: function(object, usePolyCollision){
    287         var b1 = this.getBounds(),
    288             b2 = object.getBounds(),
    289             hit = b1.x <= b2.x + b2.width && b2.x <= b1.x + b1.width &&
    290                   b1.y <= b2.y + b2.height && b2.y <= b1.y + b1.height;
    291
    292         if(hit && usePolyCollision){
    293             hit = polygonCollision(b1, b2);
    294         }
    295         return !!hit;
    296     },
    297
    298     /**
    299      * The method to render current display object. Only for advanced develop.
    300      * @param {Renderer} renderer Renderer object.
    301      * @param {Number} delta The delta time of render.
    302      * @protected
    303      */
    304     _render: function(renderer, delta){
    305         if((!this.onUpdate || this.onUpdate(delta) !== false) && renderer.startDraw(this)){
    306             renderer.transform(this);
    307             this.render(renderer, delta);
    308             renderer.endDraw(this);
    309         }
    310     },
    311     /**
    312      * Mouse event
    313     */
    314     _fireMouseEvent:function(e){
    315         e.eventCurrentTarget = this;
    316         this.fire(e);
    317
    318         // 处理mouseover事件 mouseover不需要阻止冒泡
    319         // handle mouseover event, mouseover needn't stop propagation.
    320         if(e.type == "mousemove"){
    321             if(!this.__mouseOver){
    322                 this.__mouseOver = true;
    323                 var overEvent = util.copy({}, e);
    324                 overEvent.type = "mouseover";
    325                 this.fire(overEvent);
    326             }
    327         }
    328         else if(e.type == "mouseout"){
    329             this.__mouseOver = false;
    330         }
    331
    332         // 向上冒泡
    333         // handle event propagation
    334         var parent = this.parent;
    335         if(!e._stopped && !e._stopPropagationed && parent){
    336             if(e.type == "mouseout" || e.type == "touchout"){
    337                 if(!parent.hitTestPoint(e.stageX, e.stageY, true)){
    338                     parent._fireMouseEvent(e);
    339                 }
    340             }
    341             else{
    342                 parent._fireMouseEvent(e);
    343             }
    344         }
    345     },
    346
    347     /**
    348      * This method will call while the view need update(usually caused by ticker update). This method can return a Boolean value, if return false, the view will not be drawn.
    349      * Limit: If you change the index in it's parent, it will not be drawn correct in current frame but next frame is correct.
    350      * @type Function
    351      * @default null
    352      */
    353     onUpdate: null,
    354
    355     /**
    356      * The render method of current view. The subclass can implement it's own render logic by rewrite this function.
    357      * @param {Renderer} renderer Renderer object.
    358      * @param {Number} delta The delta time of render.
    359      */
    360     render: function(renderer, delta){
    361         renderer.draw(this);
    362     },
    363
    364     /**
    365      * Get a string representing current view.
    366      * @returns {String} string representing current view.
    367      */
    368     toString: function(){
    369         return Hilo.viewToString(this);
    370     }
    371 });
    372
    373 /**
    374  * @private
    375  */
    376 function pointInPolygon(x, y, poly){
    377     var cross = 0, onBorder = false, minX, maxX, minY, maxY;
    378
    379     for(var i = 0, len = poly.length; i < len; i++){
    380         var p1 = poly[i], p2 = poly[(i+1)%len];
    381
    382         if(p1.y == p2.y && y == p1.y){
    383             p1.x > p2.x ? (minX = p2.x, maxX = p1.x) : (minX = p1.x, maxX = p2.x);
    384             if(x >= minX && x <= maxX){
    385                 onBorder = true;
    386                 continue;
    387             }
    388         }
    389
    390         p1.y > p2.y ? (minY = p2.y, maxY = p1.y) : (minY = p1.y, maxY = p2.y);
    391         if(y < minY || y > maxY) continue;
    392
    393         var nx = (y - p1.y)*(p2.x - p1.x) / (p2.y - p1.y) + p1.x;
    394         if(nx > x) cross++;
    395         else if(nx == x) onBorder = true;
    396
    397         //当射线和多边形相交
    398         if(p1.x > x && p1.y == y){
    399             var p0 = poly[(len+i-1)%len];
    400             //当交点的两边在射线两旁
    401             if(p0.y < y && p2.y > y || p0.y > y && p2.y < y){
    402                 cross ++;
    403             }
    404         }
    405     }
    406
    407     return onBorder || (cross % 2 == 1);
    408 }
    409
    410 /**
    411  * @private
    412  */
    413 function polygonCollision(poly1, poly2){
    414     var result = doSATCheck(poly1, poly2, {overlap:-Infinity, normal:{x:0, y:0}});
    415     if(result) return doSATCheck(poly2, poly1, result);
    416     return false;
    417 }
    418
    419 /**
    420  * @private
    421  */
    422 function doSATCheck(poly1, poly2, result){
    423     var len1 = poly1.length, len2 = poly2.length,
    424         currentPoint, nextPoint, distance,
    425         min1, max1, min2, max2, dot, overlap, normal = {x:0, y:0};
    426
    427     for(var i = 0; i < len1; i++){
    428         currentPoint = poly1[i];
    429         nextPoint = poly1[(i < len1-1 ? i+1 : 0)];
    430
    431         normal.x = currentPoint.y - nextPoint.y;
    432         normal.y = nextPoint.x - currentPoint.x;
    433
    434         distance = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
    435         normal.x /= distance;
    436         normal.y /= distance;
    437
    438         min1 = max1 = poly1[0].x * normal.x + poly1[0].y * normal.y;
    439         for(var j = 1; j < len1; j++){
    440             dot = poly1[j].x * normal.x + poly1[j].y * normal.y;
    441             if(dot > max1) max1 = dot;
    442             else if(dot < min1) min1 = dot;
    443         }
    444
    445         min2 = max2 = poly2[0].x * normal.x + poly2[0].y * normal.y;
    446         for(j = 1; j < len2; j++){
    447             dot = poly2[j].x * normal.x + poly2[j].y * normal.y;
    448             if(dot > max2) max2 = dot;
    449             else if(dot < min2) min2 = dot;
    450         }
    451
    452         if(min1 < min2){
    453             overlap = min2 - max1;
    454             normal.x = -normal.x;
    455             normal.y = -normal.y;
    456         }else{
    457             overlap = min1 - max2;
    458         }
    459
    460         if(overlap >= 0){
    461             return false;
    462         }else if(overlap > result.overlap){
    463             result.overlap = overlap;
    464             result.normal.x = normal.x;
    465             result.normal.y = normal.y;
    466         }
    467     }
    468
    469     return result;
    470 }
    471
    472 })();