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