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