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 })();