1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * @class View类是所有可视对象或组件的基类。
  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 创建对象的属性参数。可包含此类所有可写属性。
 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 可视对象的唯一标识符。
 21  * @property {Number} x 可视对象的x轴坐标。默认值为0。
 22  * @property {Number} y 可视对象的y轴坐标。默认值为0。
 23  * @property {Number} width 可视对象的宽度。默认值为0。
 24  * @property {Number} height 可视对象的高度。默认值为0。
 25  * @property {Number} alpha 可视对象的透明度。默认值为1。
 26  * @property {Number} rotation 可视对象的旋转角度。默认值为0。
 27  * @property {Boolean} visible 可视对象是否可见。默认为可见,即true。
 28  * @property {Number} pivotX 可视对象的中心点的x轴坐标。默认值为0。
 29  * @property {Number} pivotY 可视对象的中心点的y轴坐标。默认值为0。
 30  * @property {Number} scaleX 可视对象在x轴上的缩放比例。默认为不缩放,即1。
 31  * @property {Number} scaleY 可视对象在y轴上的缩放比例。默认为不缩放,即1。
 32  * @property {Matrix} transform 可视对象的transform属性,如果设置将忽略x, y, scaleX, scaleY, rotation. pivotX, pivotY 属性。默认null。
 33  * @property {Boolean} pointerEnabled 可视对象是否接受交互事件。默认为接受交互事件,即true。
 34  * @property {Object} background 可视对象的背景样式。可以是CSS颜色值、canvas的gradient或pattern填充。
 35  * @property {Graphics} mask 可视对象的遮罩图形。
 36  * @property {Number} tint 可视对象的附加颜色,默认0xFFFFFF,只支持WebGL模式。
 37  * @property {String|Function} align 可视对象相对于父容器的对齐方式。取值可查看Hilo.align枚举对象。
 38  * @property {Container} parent 可视对象的父容器。只读属性。
 39  * @property {Number} depth 可视对象的深度,也即z轴的序号。只读属性。
 40  * @property {Drawable} drawable 可视对象的可绘制对象。供高级开发使用。
 41  * @property {Array} boundsArea 可视对象的区域顶点数组。格式为:[{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      * 返回可视对象的舞台引用。若对象没有被添加到舞台,则返回null。
 79      * @returns {Stage} 可视对象的舞台引用。
 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      * 返回可视对象缩放后的宽度。
 92      * @returns {Number} 可视对象缩放后的宽度。
 93      */
 94     getScaledWidth: function(){
 95         return this.width * this.scaleX;
 96     },
 97 
 98     /**
 99      * 返回可视对象缩放后的高度。
100      * @returns {Number} 可视对象缩放后的高度。
101      */
102     getScaledHeight: function(){
103         return this.height * this.scaleY;
104     },
105 
106     /**
107      * 添加此对象到父容器。
108      * @param {Container} container 一个容器。
109      * @param {Uint} index 要添加到索引位置。
110      * @returns {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      * 从父容器里删除此对象。
120      * @returns {View} 可视对象本身。
121      */
122     removeFromParent: function(){
123         var parent = this.parent;
124         if(parent) parent.removeChild(this);
125         return this;
126     },
127 
128     /**
129      * 获取可视对象在舞台全局坐标系内的外接矩形以及所有顶点坐标。
130      * @returns {Array} 可视对象的顶点坐标数组vertexs。另vertexs还包含属性:
131      * <ul>
132      * <li><b>x</b> - 可视对象的外接矩形x轴坐标。</li>
133      * <li><b>y</b> - 可视对象的外接矩形y轴坐标。</li>
134      * <li><b>width</b> - 可视对象的外接矩形的宽度。</li>
135      * <li><b>height</b> - 可视对象的外接矩形的高度。</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      * 获取可视对象相对于其某个祖先(默认为最上层容器)的连接矩阵。
170      * @param {View} ancestor 可视对象的相对的祖先容器。
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      * 检测由x和y参数指定的点是否在其外接矩形之内。
265      * @param {Number} x 要检测的点的x轴坐标。
266      * @param {Number} y 要检测的点的y轴坐标。
267      * @param {Boolean} usePolyCollision 是否使用多边形碰撞检测。默认为false。
268      * @returns {Boolean} 点是否在可视对象之内。
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      * 检测object参数指定的对象是否与其相交。
283      * @param {View} object 要检测的可视对象。
284      * @param {Boolean} usePolyCollision 是否使用多边形碰撞检测。默认为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      * 可视对象的基本渲染实现,用于框架内部或高级开发使用。通常应该重写render方法。
300      * @param {Renderer} renderer 渲染器。
301      * @param {Number} delta 渲染时时间偏移量。
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      * 冒泡鼠标事件
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      * 更新可视对象,此方法会在可视对象渲染之前调用。此函数可以返回一个Boolean值。若返回false,则此对象不会渲染。默认值为null。
349      * 限制:如果在此函数中改变了可视对象在其父容器中的层级,当前渲染帧并不会正确渲染,而是在下一渲染帧。可在其父容器的onUpdate方法中来实现。
350      * @type Function
351      * @default null
352      */
353     onUpdate: null,
354 
355     /**
356      * 可视对象的具体渲染逻辑。子类可通过覆盖此方法实现自己的渲染。
357      * @param {Renderer} renderer 渲染器。
358      * @param {Number} delta 渲染时时间偏移量。
359      */
360     render: function(renderer, delta){
361         renderer.draw(this);
362     },
363 
364     /**
365      * 返回可视对象的字符串表示。
366      * @returns {String} 可视对象的字符串表示。
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 })();