js画布封装之测试旋转
依赖:
1 "use strict"; 2 3 var __emptyPoint = null, __emptyPointA = null, __emptyContext = null; 4 5 const ColorRefTable = { 6 "aliceblue": "#f0f8ff", 7 "antiquewhite": "#faebd7", 8 "aqua": "#00ffff", 9 "aquamarine": "#7fffd4", 10 "azure": "#f0ffff", 11 "beige": "#f5f5dc", 12 "bisque": "#ffe4c4", 13 "black": "#000000", 14 "blanchedalmond": "#ffebcd", 15 "blue": "#0000ff", 16 "blueviolet": "#8a2be2", 17 "brown": "#a52a2a", 18 "burlywood": "#deb887", 19 "cadetblue": "#5f9ea0", 20 "chartreuse": "#7fff00", 21 "chocolate": "#d2691e", 22 "coral": "#ff7f50", 23 "cornsilk": "#fff8dc", 24 "crimson": "#dc143c", 25 "cyan": "#00ffff", 26 "darkblue": "#00008b", 27 "darkcyan": "#008b8b", 28 "darkgoldenrod": "#b8860b", 29 "darkgray": "#a9a9a9", 30 "darkgreen": "#006400", 31 "darkgrey": "#a9a9a9", 32 "darkkhaki": "#bdb76b", 33 "darkmagenta": "#8b008b", 34 "firebrick": "#b22222", 35 "darkolivegreen": "#556b2f", 36 "darkorange": "#ff8c00", 37 "darkorchid": "#9932cc", 38 "darkred": "#8b0000", 39 "darksalmon": "#e9967a", 40 "darkseagreen": "#8fbc8f", 41 "darkslateblue": "#483d8b", 42 "darkslategray": "#2f4f4f", 43 "darkslategrey": "#2f4f4f", 44 "darkturquoise": "#00ced1", 45 "darkviolet": "#9400d3", 46 "deeppink": "#ff1493", 47 "deepskyblue": "#00bfff", 48 "dimgray": "#696969", 49 "dimgrey": "#696969", 50 "dodgerblue": "#1e90ff", 51 "floralwhite": "#fffaf0", 52 "forestgreen": "#228b22", 53 "fuchsia": "#ff00ff", 54 "gainsboro": "#dcdcdc", 55 "ghostwhite": "#f8f8ff", 56 "gold": "#ffd700", 57 "goldenrod": "#daa520", 58 "gray": "#808080", 59 "green": "#008000", 60 "greenyellow": "#adff2f", 61 "grey": "#808080", 62 "honeydew": "#f0fff0", 63 "hotpink": "#ff69b4", 64 "indianred": "#cd5c5c", 65 "indigo": "#4b0082", 66 "ivory": "#fffff0", 67 "khaki": "#f0e68c", 68 "lavender": "#e6e6fa", 69 "lavenderblush": "#fff0f5", 70 "lawngreen": "#7cfc00", 71 "lemonchiffon": "#fffacd", 72 "lightblue": "#add8e6", 73 "lightcoral": "#f08080", 74 "lightcyan": "#e0ffff", 75 "lightgoldenrodyellow": "#fafad2", 76 "lightgray": "#d3d3d3", 77 "lightgreen": "#90ee90", 78 "lightgrey": "#d3d3d3", 79 "lightpink": "#ffb6c1", 80 "lightsalmon": "#ffa07a", 81 "lightseagreen": "#20b2aa", 82 "lightskyblue": "#87cefa", 83 "lightslategray": "#778899", 84 "lightslategrey": "#778899", 85 "lightsteelblue": "#b0c4de", 86 "lightyellow": "#ffffe0", 87 "lime": "#00ff00", 88 "limegreen": "#32cd32", 89 "linen": "#faf0e6", 90 "magenta": "#ff00ff", 91 "maroon": "#800000", 92 "mediumaquamarine": "#66cdaa", 93 "mediumblue": "#0000cd", 94 "mediumorchid": "#ba55d3", 95 "mediumpurple": "#9370db", 96 "mediumseagreen": "#3cb371", 97 "mediumslateblue": "#7b68ee", 98 "mediumspringgreen": "#00fa9a", 99 "mediumturquoise": "#48d1cc", 100 "mediumvioletred": "#c71585", 101 "midnightblue": "#191970", 102 "mintcream": "#f5fffa", 103 "mistyrose": "#ffe4e1", 104 "moccasin": "#ffe4b5", 105 "navajowhite": "#ffdead", 106 "navy": "#000080", 107 "oldlace": "#fdf5e6", 108 "olive": "#808000", 109 "olivedrab": "#6b8e23", 110 "orange": "#ffa500", 111 "orangered": "#ff4500", 112 "orchid": "#da70d6", 113 "palegoldenrod": "#eee8aa", 114 "palegreen": "#98fb98", 115 "paleturquoise": "#afeeee", 116 "palevioletred": "#db7093", 117 "papayawhip": "#ffefd5", 118 "peachpuff": "#ffdab9", 119 "peru": "#cd853f", 120 "pink": "#ffc0cb", 121 "plum": "#dda0dd", 122 "powderblue": "#b0e0e6", 123 "purple": "#800080", 124 "red": "#ff0000", 125 "rosybrown": "#bc8f8f", 126 "royalblue": "#4169e1", 127 "saddlebrown": "#8b4513", 128 "salmon": "#fa8072", 129 "sandybrown": "#f4a460", 130 "seagreen": "#2e8b57", 131 "seashell": "#fff5ee", 132 "sienna": "#a0522d", 133 "silver": "#c0c0c0", 134 "skyblue": "#87ceeb", 135 "slateblue": "#6a5acd", 136 "slategray": "#708090", 137 "slategrey": "#708090", 138 "snow": "#fffafa", 139 "springgreen": "#00ff7f", 140 "steelblue": "#4682b4", 141 "tan": "#d2b48c", 142 "teal": "#008080", 143 "thistle": "#d8bfd8", 144 "tomato": "#ff6347", 145 "turquoise": "#40e0d0", 146 "violet": "#ee82ee", 147 "wheat": "#f5deb3", 148 "white": "#ffffff", 149 "whitesmoke": "#f5f5f5", 150 "yellow": "#ffff00", 151 "yellowgreen": "#9acd32" 152 }, 153 154 UTILS = { 155 156 get time(){ 157 return Date.now(); 158 }, 159 160 get isMobile(){ 161 return /Android|webOS|iPhone|iPod|BlackBerry|SymbianOS|Windows Phone|iPad/i.test(navigator.userAgent); 162 }, 163 164 get emptyPoint(){ 165 if(__emptyPoint === null) __emptyPoint = new Point(); 166 return __emptyPoint; 167 }, 168 169 get emptyPointA(){ 170 if(__emptyPointA === null) __emptyPointA = new Point(); 171 return __emptyPointA; 172 }, 173 174 get emptyContext(){ 175 if(__emptyContext === null) __emptyContext = document.createElement("canvas").getContext('2d') 176 return __emptyContext; 177 }, 178 179 get colorTable(){ 180 return ColorRefTable; 181 }, 182 183 emptyArray(arr){ 184 return !Array.isArray(arr) || arr.length === 0; 185 }, 186 187 isObject(obj){ 188 189 return obj !== null && typeof obj === "object" && Array.isArray(obj) === false; 190 191 }, 192 193 isNumber(num){ 194 195 return typeof num === "number" && isNaN(num) === false; 196 197 }, 198 199 //获取最后一个点后面的字符(不包含点) 200 getExtension(fileName){ 201 /* let type = "", str = fileName.split('').reverse().join(''); 202 for(let k = 0, len = str.length; k < len; k++){ 203 if(str[k] === ".") break; 204 type += str[k]; 205 } 206 return type.split('').reverse().join(''); */ 207 return fileName.substring(fileName.lastIndexOf(".")+1); 208 }, 209 get getFileType(){ 210 console.warn("现在用 getExtension 替代 getFileType"); 211 return this.getExtension; 212 }, 213 214 getFileName(fileName){ 215 return fileName.substring(0, fileName.lastIndexOf(".")); 216 }, 217 218 //删除 string 所有的空格 219 deleteSpaceAll(str){ 220 const len = str.length; 221 var result = ''; 222 for(let i = 0; i < len; i++){ 223 if(str[i] !== ' ') result += str[i] 224 } 225 226 return result 227 }, 228 229 //str 是否全是中文 230 isChains(str){ 231 return !(/[^\u4E00-\u9FA5]/.test(str)); 232 }, 233 234 //删除 string 两边空格 235 removeSpaceSides(string){ 236 237 return string.replace(/(^\s*)|(\s*$)/g, ""); 238 239 }, 240 241 //var str = "abcd[123]efg"; UTILS.replaceText([..."a{123}b], "{", "}", (str: "123") => {return "-"}) //["a", "-", "b"] 242 replaceText(strs, s, e, f){ 243 for(let i = 0, si = -1, str = ""; i < strs.length; i++){ 244 if(typeof strs[i] !== "string"){ 245 si = -1; 246 str = ""; 247 continue; 248 } 249 250 if(si === -1){ 251 if(strs[i] === s) si = i; 252 continue; 253 } 254 255 if(strs[i] === e){ 256 strs[i] = f(str); 257 i = i-si; 258 strs.splice(si, i <= 0 ? 1 : i); 259 return this.replaceText(strs, s, e, f); 260 } 261 262 str += strs[i]; 263 } 264 265 return strs; 266 }, 267 268 //返回 num 与 num1 之间的随机数 269 random(num, num1){ 270 271 if(num < num1) return Math.random() * (num1 - num) + num; 272 273 else if(num > num1) return Math.random() * (num - num1) + num1; 274 275 else return num; 276 277 }, 278 279 //生成 UUID 280 generateUUID: function (){ 281 const _lut = []; 282 283 for ( let i = 0; i < 256; i ++ ) { 284 285 _lut[ i ] = ( i < 16 ? '0' : '' ) + ( i ).toString( 16 ); 286 287 } 288 289 return function (){ 290 const d0 = Math.random() * 0xffffffff | 0; 291 const d1 = Math.random() * 0xffffffff | 0; 292 const d2 = Math.random() * 0xffffffff | 0; 293 const d3 = Math.random() * 0xffffffff | 0; 294 const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' + 295 _lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' + 296 _lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] + 297 _lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ]; 298 299 return uuid.toLowerCase(); //toLowerCase() 这里展平连接的字符串以节省堆内存空间 300 } 301 }(), 302 303 //欧几里得距离(两点的直线距离) 304 distance(x, y, x1, y1){ 305 306 return Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2)); 307 308 }, 309 310 getSameScale(oldSize, newSize){ 311 if(oldSize.width < oldSize.height && newSize.width < newSize.height){ 312 return newSize.width / oldSize.width; 313 } 314 if(oldSize.width > oldSize.height && newSize.width > newSize.height){ 315 return newSize.height / oldSize.height; 316 } 317 if(oldSize.width / newSize.width < oldSize.height / newSize.height){ 318 return newSize.height / oldSize.height; 319 } 320 const aspect = oldSize.width / oldSize.height; 321 return aspect < 1 ? aspect * newSize.width / oldSize.width : newSize.width / oldSize.width; 322 }, 323 324 /* 把 target 以相等的比例缩放至 result 大小 325 target, result: Object{width, height}; //也可以是img元素 326 */ 327 setSizeToSameScale(target, result = {width: 100, height: 100}){ 328 const scale = this.getSameScale(target, result); 329 result.width = target.width * scale; 330 result.height = target.height * scale; 331 return result; 332 }, 333 334 //小数保留 count 位 335 floatKeep(float, count = 3){ 336 count = Math.pow(10, count); 337 return Math.ceil(float * count) / count; 338 }, 339 340 } 341 342 343 344 345 /* AnimateLoop 动画循环 346 parameter: 347 loopStep: Number; //默认 1000 / 60 (每秒运行60次) 348 onupdate: Function(); //更新回调, 默认 null 349 onUpdateFPS: Function(fps); //如果定义此参数就在每帧计算并传递fps值, 数值越高越好, 默认 null 350 351 attributes: 352 onupdate: Func(); 353 354 //只读: 355 delta: Number; //延迟 356 running: Bool; //是否正在运行 357 358 method: 359 play(onupdate: Func): this; //onupdate 可选 (如果动画循环正在运行那么此方法什么都不会做) 360 stop(): this; 361 update(): undefined; //使用前必须已定义.onupdate 属性 (如果动画循环正在运行那么此方法什么都不会做) 362 363 demo: 364 const animateLoop = new AnimateLoop( 365 () => console.log(animateLoop.delta), 366 fps => console.log(fps) 367 ); 368 animateLoop.play(); 369 */ 370 class AnimateLoop{ 371 372 #nowTime = 0; 373 #oldTime = 0; 374 #id = -1; 375 376 get running(){ 377 return this.#id !== -1; 378 } 379 380 get delta(){ 381 return this.#nowTime - this.#oldTime; 382 } 383 384 constructor(onupdate = null, onUpdateFPS = null, loopStep = 1000 / 60){ 385 if(typeof onUpdateFPS !== "function"){ 386 const animate = () => { 387 this.#nowTime = UTILS.time; 388 this.#id = requestAnimationFrame(animate); 389 if(this.#nowTime - this.#oldTime >= loopStep){ 390 this.onupdate(); 391 this.#oldTime = this.#nowTime; 392 } 393 } 394 395 this._animate = animate; 396 } else { 397 //更新fps 398 let old = UTILS.time, frames = 0; 399 const updateFPS = () => { 400 frames++; 401 if (this.#nowTime >= old + 1000){ 402 onUpdateFPS(Math.floor((frames * 1000) / (this.#nowTime - old))); 403 old = this.#nowTime; 404 frames = 0; 405 } 406 }, 407 408 //动画循环 409 animate = () => { 410 this.#nowTime = UTILS.time; 411 this.#id = requestAnimationFrame(animate); 412 updateFPS(); 413 if(this.#nowTime - this.#oldTime >= loopStep){ 414 this.onupdate(); 415 this.#oldTime = this.#nowTime; 416 } 417 } 418 419 this._animate = animate; 420 } 421 422 this.onupdate = onupdate; 423 } 424 425 stop(){ 426 if(this.#id !== -1){ 427 cancelAnimationFrame(this.#id); 428 this.#id = -1; 429 this.#oldTime = this.#nowTime; 430 } 431 return this; 432 } 433 434 play(onupdate){ 435 if(typeof onupdate === "function") this.onupdate = onupdate; 436 if(this.onupdate !== null && this.#id === -1){ 437 this.#id = requestAnimationFrame(this._animate); 438 this.#oldTime = UTILS.time; 439 } 440 return this; 441 } 442 443 update(){ 444 if(this.#id === -1){ 445 this.onupdate(); 446 } 447 } 448 449 } 450 451 452 453 454 /* TweenCache 455 parameter: 456 origin, end: Object{Number...}, 457 time: Number, //origin 到 end 花费的毫秒时间 458 onend: Function // 459 460 demo: 461 const tweenCache = new TweenCache({x:-50}, {x:100}, null, 3000); 462 const animateLoop = new AnimateLoop(() => tweenCache.update()); 463 464 tweenCache.onend = () => { 465 animateLoop.stop(); 466 tweenCache.reset(); 467 } 468 469 animateLoop.play(); 470 tweenCache.start(); 471 */ 472 class TweenCache{ 473 474 #t = 0; 475 #v = {}; 476 #o = {}; 477 478 constructor(origin, end, time, onend){ 479 this.origin = origin; 480 this.end = end; 481 this.time = time; 482 this.onend = onend; 483 484 for(let v in origin){ 485 this.#v[v] = end[v] - origin[v]; 486 this.#o[v] = origin[v]; 487 } 488 } 489 490 start(){ 491 this.#t = UTILS.time; 492 } 493 494 update(){ 495 var ted = UTILS.time - this.#t; 496 497 if(ted < this.time){ 498 499 ted = ted / this.time; 500 for(let n in this.origin) this.origin[n] = ted * this.#v[n] + this.#o[n]; 501 502 } else { 503 504 for(ted in this.origin) this.origin[ted] = this.end[ted]; 505 if(typeof this.onend === "function") this.onend(); 506 507 } 508 509 } 510 511 } 512 513 514 515 516 /* TweenTarget 517 parameter: 518 v1 = {x: 0}, 519 v2 = {x: 100}, 520 distance = 1, //每次移动的距离 521 onend = null // 522 523 attribute: 524 v1: Object; //起点 525 v2: Object; //终点 526 onend: Function; // 527 528 method: 529 update(): undefined; //一般在动画循环里执行此方法 530 updateAxis(): undefined; //更新v1至v2的方向轴 (初始化时构造器自动调用一次) 531 setDistance(distance: Number): undefined; //设置每次移动的距离 (初始化时构造器自动调用一次) 532 */ 533 class TweenTarget{ 534 535 #distance = 1; 536 #distancePow2 = 1; 537 #axis = {}; 538 get axis(){return this.#axis;} 539 get distance(){return this.#distance;} 540 541 constructor(v1 = {x: 0}, v2 = {x: 100}, distance, onend = null){ 542 this.v1 = v1; 543 this.v2 = v2; 544 this.onend = onend; 545 546 this.setDistance(distance); 547 this.updateAxis(); 548 } 549 550 setDistance(v = 1){ 551 this.#distance = v; 552 this.#distancePow2 = Math.pow(v, 2); 553 } 554 555 updateAxis(){ 556 var n, v, len = 0; 557 558 for(n in this.v1){ 559 v = this.v2[n] - this.v1[n]; 560 len += v * v; 561 this.#axis[n] = v; 562 } 563 564 len = Math.sqrt(len); 565 566 if(len !== 0){ 567 for(n in this.v1) this.#axis[n] *= 1 / len; 568 } 569 } 570 571 update(){ 572 var n, len = 0; 573 574 for(n in this.v1){ 575 len += Math.pow(this.v1[n] - this.v2[n], 2); 576 } 577 578 if(len > this.#distancePow2){ 579 580 for(n in this.v1){ 581 this.v1[n] += this.#axis[n] * this.#distance; 582 } 583 584 } 585 586 else{ 587 588 for(n in this.v1){ 589 this.v1[n] = this.v2[n]; 590 } 591 592 if(this.onend) this.onend(); 593 594 } 595 } 596 597 } 598 599 600 601 602 /* SecurityDoor 安检门 603 作用: 有时候我们要给一个功能加一个启禁用锁, 就比如用一个.enable属性表示此功能是否启用, 604 a, b是两个使用此功能的人; a 需要把此功能禁用(.enable = false), 605 过了一会b也要禁用此功能(.enable = false), 又过了一会a又要在次启用此功能, 606 此时a让此功能启用了(.enable = true), 很显然这违背了b, 对于b来说此功能还在禁用状态, 607 实际并非如b所想, 所以当多个人使用此功能时一个.enable满足不了它们; 608 或许还有其它解决方法就比如让所有使用此功能的人(a,b)加一个属性表示自己是否可以使用此功能, 609 但我更希望用一个专门的数组去装载它们, 而不是在某个使用者身上都添加一个属性; 610 611 parameter: 612 list: Array[]; 613 614 attribute: 615 list: Array; //私有, 默认 null 616 empty: Bool; //只读, 列表是否为空 617 618 method: 619 clear() 620 equals(v) 621 add(v) 622 remove(v) 623 */ 624 class SecurityDoor{ 625 626 #list = null; 627 628 get empty(){ 629 return this.#list === null; 630 } 631 632 constructor(list, onchange){ 633 if(UTILS.emptyArray(list) === false) this.#list = list; 634 this._onchange = typeof onchange === "function" ? onchange : null; 635 } 636 637 add(sign){ 638 if(this.#list !== null){ 639 if(this.#list.includes(sign) === false) this.#list.push(sign); 640 } 641 else{ 642 this.#list = [sign]; 643 if(this._onchange !== null) this._onchange(false); 644 } 645 } 646 647 clear(){ 648 this.#list = null; 649 } 650 651 equals(sign){ 652 return this.#list !== null && this.#list.includes(sign); 653 } 654 655 remove(sign){ 656 if(this.#list !== null){ 657 sign = this.#list.indexOf(sign); 658 if(sign !== -1){ 659 if(this.#list.length > 1) this.#list.splice(sign, 1); 660 else{ 661 this.#list = null; 662 if(this._onchange !== null) this._onchange(true); 663 } 664 } 665 } 666 } 667 668 } 669 670 671 672 673 /* RunningList 674 675 */ 676 class RunningList{ 677 678 #runName = ""; 679 #running = false; 680 #list = []; 681 #disabls = []; 682 get list(){ 683 return this.#list; 684 } 685 686 constructor(runName = 'update'){ 687 this.#runName = runName; 688 } 689 690 clear(){ 691 this.#list.length = 0; 692 this.#disabls.length = 0; 693 } 694 695 add(v){ 696 if(this.#list.includes(v) === false) this.#list.push(v); 697 else{ 698 const i = this.#disabls.indexOf(v); 699 if(i !== -1) this.#disabls.splice(i, 1); 700 } 701 } 702 703 remove(v){ 704 if(this.#running) this.#disabls.push(v); 705 else{ 706 const i = this.#list.indexOf(v); 707 if(i !== -1) this.#list.splice(i, 1); 708 } 709 } 710 711 update(){ 712 var len = this.#list.length; 713 if(len === 0) return; 714 715 var k; 716 this.#running = true; 717 if(this.#runName !== ''){ 718 for(k = 0; k < len; k++) this.#list[k][this.#runName](); 719 }else{ 720 for(k = 0; k < len; k++) this.#list[k](); 721 } 722 this.#running = false; 723 724 var i; 725 len = this.#disabls.length; 726 for(k = 0; k < len; k++){ 727 i = this.#list.indexOf(this.#disabls[k]); 728 if(i !== -1) this.#list.splice(i, 1); 729 } 730 this.#disabls.length = 0; 731 } 732 733 } 734 735 736 737 738 /** Ajax 739 parameter: 740 option = { 741 url: 可选, 默认 '' 742 method: 可选, post 或 get请求, 默认 post 743 asy: 可选, 是否异步执行, 默认 true 744 success: 可选, 成功回调, 默认 null 745 error: 可选, 超时或失败调用, 默认 null 746 change: 可选, 请求状态改变时调用, 默认 null 747 data: 可选, 如果定义则在初始化时自动执行.send(data)方法 748 } 749 750 demo: 751 const data = `email=${email}&password=${password}`, 752 753 //默认 post 请求: 754 ajax = new Ajax({ 755 url: './login', 756 data: data, 757 success: mes => console.log(mes), 758 }); 759 760 //get 请求: 761 ajax.method = "get"; 762 ajax.send(data); 763 */ 764 class Ajax{ 765 766 constructor(option = {}){ 767 this.url = option.url || ""; 768 this.method = option.method || "post"; 769 this.asy = typeof option.asy === "boolean" ? option.asy : true; 770 this.success = option.success || null; 771 this.error = option.error || null; 772 this.change = option.change || null; 773 774 //init XML 775 this.xhr = new XMLHttpRequest(); 776 777 this.xhr.onerror = this.xhr.ontimeout = event => { 778 if(this.error !== null) this.error(event); 779 } 780 781 this.xhr.onreadystatechange = event => { 782 783 if(event.target.readyState === 4 && event.target.status === 200){ 784 785 if(this.success !== null) this.success(event.target.responseText, event); 786 787 } 788 789 else if(this.change !== null) this.change(event); 790 791 } 792 793 if(option.data) this.send(option.data); 794 } 795 796 send(data = ""){ 797 if(this.method === "get"){ 798 this.xhr.open(this.method, this.url+"?"+data, this.asy); 799 this.xhr.send(); 800 } 801 802 else if(this.method === "post"){ 803 this.xhr.open(this.method, this.url, this.asy); 804 this.xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 805 this.xhr.send(data); 806 } 807 } 808 809 } 810 811 812 813 814 /* IndexedDB 本地数据库 815 816 parameter: 817 name: String; //需要打开的数据库名称(如果不存在则会新建一个) 必须 818 done: Function(IndexedDB); //链接数据库成功时的回调 默认 null 819 version: Number; //数据库版本(高版本数据库将覆盖低版本的数据库) 默认 1 820 821 attribute: 822 database: IndexedDB; //链接完成的数据库对象 823 transaction: IDBTransaction; //事务管理(读和写) 824 objectStore: IDBObjectStore; //当前的事务 825 826 method: 827 set(data, key, callback) //添加或更新 828 get(key, callback) //获取 829 delete(key, callback) //删除 830 831 traverse(callback) //遍历 832 getAll(callback) //获取全部 833 clear(callback) //清理所以数据 834 close() //关闭数据库链接 835 836 readOnly: 837 838 static: 839 indexedDB: Object; 840 841 demo: 842 843 new IndexedDB('TEST', db => { 844 845 conosle.log(db); 846 847 }); 848 849 */ 850 class IndexedDB{ 851 852 static indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 853 854 get objectStore(){ //每个事务只能使用一次, 所以每次都需要重新创建 855 return this.database.transaction(this.name, 'readwrite').objectStore(this.name); 856 } 857 858 constructor(name, done = null, version = 1){ 859 860 if(IndexedDB.indexedDB === undefined) return console.error("IndexedDB: 不支持IndexedDB"); 861 862 if(typeof name !== 'string') return console.warn('IndexedDB: 参数错误'); 863 864 this.name = name; 865 this.database = null; 866 867 const request = IndexedDB.indexedDB.open(name, version); 868 869 request.onupgradeneeded = (e)=>{ //数据库不存在 或 版本号不同时 触发 870 if(!this.database) this.database = e.target.result; 871 if(this.database.objectStoreNames.contains(name) === false) this.database.createObjectStore(name); 872 } 873 874 request.onsuccess = (e)=>{ 875 this.database = e.target.result; 876 if(typeof done === 'function') done(this); 877 } 878 879 request.onerror = (e)=>{ 880 console.error(e); 881 } 882 883 } 884 885 close(){ 886 887 return this.database.close(); 888 889 } 890 891 clear(callback){ 892 893 this.objectStore.clear().onsuccess = callback; 894 895 } 896 897 traverse(callback){ 898 899 this.objectStore.openCursor().onsuccess = callback; 900 901 } 902 903 set(data, key = 0, callback){ 904 905 this.objectStore.put(data, key).onsuccess = callback; 906 907 } 908 909 get(key = 0, callback){ 910 911 this.objectStore.get(key).onsuccess = callback; 912 913 } 914 915 del(key = 0, callback){ 916 917 this.objectStore.delete(key).onsuccess = callback; 918 919 } 920 921 getAll(callback){ 922 923 this.objectStore.getAll().onsuccess = callback; 924 925 } 926 927 } 928 929 930 931 932 /* TreeStruct 树结构基类 933 934 attribute: 935 parent: TreeStruct; 936 children: Array[TreeStruct]; 937 938 method: 939 appendChild(v: TreeStruct): v; //v添加到自己的子集 940 removeChild(v: TreeStruct): v; //删除v, 前提v必须是自己的子集 941 export(): Array[Object]; //TreeStruct 转为 可导出的结构, 包括其所有的后代 942 943 getPath(v: TreeStruct): Array[TreeStruct]; //获取自己到v的路径 944 945 traverse(callback: Function): undefined; //迭代自己的每一个后代, 包括自己 946 callback(value: TreeStruct); //如返回 "continue" 则不在迭代其后代(不是结束迭代, 而是只结束当前节点的后代); 947 948 traverseUp(callback): undefined; //向上遍历每一个父, 包括自己 949 callback(value: TreeStruct); //如返回 "break" 立即停止遍历; 950 951 static: 952 import(arr: Array[Object]): TreeStruct; //.export() 返回的 arr 转为 TreeStruct 953 954 */ 955 class TreeStruct{ 956 957 static import(arr){ 958 959 //json = JSON.parse(json); 960 const len = arr.length; 961 962 for(let k = 0, v; k < len; k++){ 963 v = Object.assign(new TreeStruct(), arr[k]); 964 v.parent = arr[arr[k].parent] || null; 965 if(v.parent !== null) v.parent.appendChild(v); 966 arr[k] = v; 967 } 968 969 return arr[0]; 970 971 } 972 973 constructor(){ 974 this.parent = null; 975 this.children = []; 976 } 977 978 getPath(v){ 979 980 var path; 981 982 const pathA = []; 983 this.traverseUp(tar => { 984 if(v === tar){ 985 path = pathA; 986 return "break"; 987 } 988 pathA.push(tar); 989 }); 990 991 if(path) return path; 992 993 const pathB = []; 994 v.traverseUp(tar => { 995 if(this === tar){ 996 path = pathB.reverse(); 997 return "break"; 998 } 999 else{ 1000 let i = pathA.indexOf(tar); 1001 if(i !== -1){ 1002 pathA.splice(i); 1003 pathA.push(tar); 1004 path = pathA.concat(pathB.reverse()); 1005 return "break"; 1006 } 1007 } 1008 pathB.push(tar); 1009 }); 1010 1011 return path; 1012 1013 } 1014 1015 appendChild(v){ 1016 v.parent = this; 1017 if(this.children.includes(v) === false) this.children.push(v); 1018 1019 return v; 1020 } 1021 1022 removeChild(v){ 1023 const i = this.children.indexOf(v); 1024 if(i !== -1) this.children.splice(i, 1); 1025 v.parent = null; 1026 1027 return v; 1028 } 1029 1030 traverse(callback){ 1031 1032 if(callback(this) !== "continue"){ 1033 1034 for(let k = 0, len = this.children.length; k < len; k++){ 1035 1036 this.children[k].traverse(callback); 1037 1038 } 1039 1040 } 1041 1042 } 1043 1044 traverseUp(callback){ 1045 1046 var par = this.parent; 1047 1048 while(par !== null){ 1049 if(callback(par) === "break") return; 1050 par = par.parent; 1051 } 1052 1053 } 1054 1055 export(){ 1056 1057 const result = [], arr = []; 1058 var obj = null; 1059 1060 this.traverse(v => { 1061 obj = Object.assign({}, v); 1062 obj.parent = arr.indexOf(v.parent); 1063 delete obj.children; 1064 result.push(obj); 1065 arr.push(v); 1066 }); 1067 1068 return result; //JSON.stringify(result); 1069 1070 } 1071 1072 } 1073 1074 1075 1076 1077 /* Point 1078 parameter: 1079 x = 0, y = 0; 1080 1081 attribute 1082 x, y: Number; 1083 1084 method: 1085 set(x, y): this; 1086 angle(origin): Number; 1087 copy(point): this; 1088 clone(): Point; 1089 distance(point): Number; //获取欧几里得距离 1090 distanceMHD(point): Number; //获取曼哈顿距离 1091 distanceCompare(point): Number; //获取用于比较的距离(相对于.distance() 效率更高) 1092 equals(point): Bool; //是否恒等 1093 reverse(): this; //取反值 1094 rotate(origin: Object{x,y}, angle): this; //旋转点 1095 normalize(): this; //归一 1096 isClockwise(startPoint): Bool; //startPoint 到自己是否是顺时针旋转 1097 */ 1098 class Point{ 1099 1100 get isPoint(){return true;} 1101 1102 constructor(x = 0, y = 0){ 1103 this.x = x; 1104 this.y = y; 1105 } 1106 1107 set(x = 0, y = 0){ 1108 this.x = x; 1109 this.y = y; 1110 1111 return this; 1112 } 1113 1114 radian(origin){ 1115 1116 return Math.atan2(this.y - origin.y, this.x - origin.x); 1117 1118 } 1119 1120 copy(point){ 1121 1122 this.x = point.x; 1123 this.y = point.y; 1124 return this; 1125 //return Object.assign(this, point); 1126 1127 } 1128 1129 clone(){ 1130 1131 return new this.constructor().copy(this); 1132 //return Object.assign(new this.constructor(), this); 1133 1134 } 1135 1136 distance(point){ 1137 1138 return Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2)); 1139 1140 } 1141 1142 distanceMHD(point){ 1143 1144 return Math.abs(this.x - point.x) + Math.abs(this.y - point.y); 1145 1146 } 1147 1148 distanceCompare(point){ 1149 1150 return Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2); 1151 1152 } 1153 1154 equals(point){ 1155 1156 return point.x === this.x && point.y === this.y; 1157 1158 } 1159 1160 reverse(){ 1161 this.x = -this.x; 1162 this.y = -this.y; 1163 1164 return this; 1165 } 1166 1167 rotate(origin, radian){ 1168 const c = Math.cos(radian), s = Math.sin(radian), 1169 x = this.x - origin.x, y = this.y - origin.y; 1170 1171 this.x = x * c - y * s + origin.x; 1172 this.y = x * s + y * c + origin.y; 1173 1174 return this; 1175 } 1176 1177 normalize(){ 1178 const len = 1 / (Math.sqrt(this.x * this.x + this.y * this.y) || 1); 1179 this.x *= len; 1180 this.y *= len; 1181 1182 return this; 1183 } 1184 1185 isClockwise(startPoint){ 1186 1187 return this.x * startPoint.y - this.y * startPoint.x < 0; 1188 1189 } 1190 1191 floatKeep(count = 3){ 1192 count = Math.pow(10, count); 1193 this.x = Math.ceil(this.x * count) / count; 1194 this.y = Math.ceil(this.y * count) / count; 1195 } 1196 1197 /* add(point){ 1198 this.x += point.x; 1199 this.y += point.y; 1200 return this; 1201 } 1202 1203 addScalar(v){ 1204 this.x += v; 1205 this.y += v; 1206 return this; 1207 } 1208 1209 sub(point){ 1210 this.x -= point.x; 1211 this.y -= point.y; 1212 return this; 1213 } 1214 1215 subScalar(v){ 1216 this.x -= v; 1217 this.y -= v; 1218 return this; 1219 } 1220 1221 multiply(point){ 1222 this.x *= point.x; 1223 this.y *= point.y; 1224 return this; 1225 } 1226 1227 multiplyScalar(v){ 1228 this.x *= v; 1229 this.y *= v; 1230 return this; 1231 } 1232 1233 divide(point){ 1234 this.x /= point.x; 1235 this.y /= point.y; 1236 return this; 1237 } 1238 1239 divideScalar(v){ 1240 this.x /= v; 1241 this.y /= v; 1242 return this; 1243 } */ 1244 1245 } 1246 1247 1248 1249 1250 /* Line 1251 parameter: x, y, x1, y1: Number; 1252 attribute: x, y, x1, y1: Number; 1253 method: 1254 set(x, y, x1, y1): this; 1255 copy(line): this; 1256 clone(): Line; 1257 containsPoint(x, y): Bool; //点是否在线上 1258 intersectPoint(line: Line, point: Point): Point; //如果不相交则返回null, 否则返回交点Point 1259 isIntersect(line): Bool; //this与line是否相交 1260 */ 1261 class Line{ 1262 1263 constructor(x = 0, y = 0, x1 = 0, y1 = 0){ 1264 this.x = x; 1265 this.y = y; 1266 this.x1 = x1; 1267 this.y1 = y1; 1268 } 1269 1270 set(x = 0, y = 0, x1 = 0, y1 = 0){ 1271 this.x = x; 1272 this.y = y; 1273 this.x1 = x1; 1274 this.y1 = y1; 1275 return this; 1276 } 1277 1278 copy(line){ 1279 this.x = line.x; 1280 this.y = line.y; 1281 this.x1 = line.x1; 1282 this.y1 = line.y1; 1283 return this; 1284 //return Object.assign(this, line); 1285 } 1286 1287 clone(){ 1288 return new this.constructor().copy(this); 1289 //return Object.assign(new this.constructor(), this); 1290 } 1291 1292 containsPoint(x, y){ 1293 return (x - this.x) * (x - this.x1) <= 0 && (y - this.y) * (y - this.y1) <= 0; 1294 } 1295 1296 intersectPoint(line, point){ 1297 //解线性方程组, 求线段交点 1298 //如果分母为0则平行或共线, 不相交 1299 var denominator = (this.y1 - this.y) * (line.x1 - line.x) - (this.x - this.x1) * (line.y - line.y1); 1300 if(denominator === 0) return null; 1301 1302 //线段所在直线的交点坐标 (x , y) 1303 const x = ((this.x1 - this.x) * (line.x1 - line.x) * (line.y - this.y) 1304 + (this.y1 - this.y) * (line.x1 - line.x) * this.x 1305 - (line.y1 - line.y) * (this.x1 - this.x) * line.x) / denominator; 1306 1307 const y = -((this.y1 - this.y) * (line.y1 - line.y) * (line.x - this.x) 1308 + (this.x1 - this.x) * (line.y1 - line.y) * this.y 1309 - (line.x1 - line.x) * (this.y1 - this.y) * line.y) / denominator; 1310 1311 //判断交点是否在两条线段上 1312 if(this.containsPoint(x, y) && line.containsPoint(x, y)){ 1313 point.x = x; 1314 point.y = y; 1315 return point; 1316 } 1317 1318 return null; 1319 } 1320 1321 isIntersect(line){ 1322 //快速排斥: 1323 //两个线段为对角线组成的矩形,如果这两个矩形没有重叠的部分,那么两条线段是不可能出现重叠的 1324 1325 //这里的确如此,这一步是判定两矩形是否相交 1326 //1.线段ab的低点低于cd的最高点(可能重合) 1327 //2.cd的最左端小于ab的最右端(可能重合) 1328 //3.cd的最低点低于ab的最高点(加上条件1,两线段在竖直方向上重合) 1329 //4.ab的最左端小于cd的最右端(加上条件2,两直线在水平方向上重合) 1330 //综上4个条件,两条线段组成的矩形是重合的 1331 //特别要注意一个矩形含于另一个矩形之内的情况 1332 if(!(Math.min(this.x,this.x1)<=Math.max(line.x,line.x1) && Math.min(line.y,line.y1)<=Math.max(this.y,this.y1) && Math.min(line.x,line.x1)<=Math.max(this.x,this.x1) && Math.min(this.y,this.y1)<=Math.max(line.y,line.y1))) return false; 1333 1334 //跨立实验: 1335 //如果两条线段相交,那么必须跨立,就是以一条线段为标准,另一条线段的两端点一定在这条线段的两段 1336 //也就是说a b两点在线段cd的两端,c d两点在线段ab的两端 1337 var u=(line.x-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y-this.y), 1338 v = (line.x1-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y1-this.y), 1339 w = (this.x-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y-line.y), 1340 z = (this.x1-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y1-line.y); 1341 1342 return u*v <= 0.00000001 && w*z <= 0.00000001; 1343 } 1344 1345 } 1346 1347 1348 1349 1350 /* Box 矩形 1351 parameter: 1352 x = 0, y = 0, w = 0, h = 0; 1353 1354 attribute: 1355 x,y: Number; 位置 1356 w,h: Number; 大小 1357 1358 只读 1359 mx, my: Number; // 1360 1361 method: 1362 set(x, y, w, h): this; 1363 pos(x, y): this; //设置位置 1364 size(w, h): this; //设置大小 1365 setFromRotate(rotate: Rotate): this; //根据 rotate 旋转自己 1366 setFromShapeRect(shapeRect): this; //ShapeRect 转为 Box (忽略 ShapeRect 的旋转和缩放) 1367 setFromCircle(circle, inner: Bool): this; // 1368 setFromPolygon(polygon, inner = true): this;// 1369 toArray(array: Array, index: Integer): this; 1370 copy(box): this; //复制 1371 clone(): Box; //克隆 1372 center(box): this; //设置位置在box居中 1373 distance(x, y): Number; //左上角原点 与 x,y 的直线距离 1374 distanceFromPoint(x,y, isMax: Bool): Number;//返回点 x,y 距自己四个角的 最大或最小(isMax) 距离 1375 isEmpty(): Boolean; //.w.h是否小于等于零 1376 maxX(): Number; //返回 max x(this.x+this.w); 1377 maxY(): Number; //返回 max y(this.y+this.h); 1378 expand(box): undefined; //扩容; 把box合并到this 1379 equals(box): Boolean; //this与box是否恒等 1380 intersectsBox(box): Boolean; //box与this是否相交(box在this内部也会返回true) 1381 containsPoint(x, y): Boolean; //x,y点是否在this内 1382 containsBox(box): Boolean; //box是否在this内(只是相交返回fasle) 1383 computeOverflow(b: Box, r: Box): undefined; //this相对b超出的部分赋值到r; r的size小于或等于0的话说明完全超出 1384 */ 1385 class Box{ 1386 1387 get mx(){ 1388 return this.x + this.w; 1389 } 1390 1391 get my(){ 1392 return this.y + this.h; 1393 } 1394 1395 get cx(){ 1396 return this.w / 2 + this.x; 1397 } 1398 1399 get cy(){ 1400 return this.h / 2 + this.y; 1401 } 1402 1403 constructor(x = 0, y = 0, w = 0, h = 0){ 1404 //this.set(x, y, w, h); 1405 this.x = x; 1406 this.y = y; 1407 this.w = w; 1408 this.h = h; 1409 } 1410 1411 set(x, y, w, h){ 1412 this.x = x; 1413 this.y = y; 1414 this.w = w; 1415 this.h = h; 1416 return this; 1417 } 1418 1419 pos(x, y){ 1420 this.x = x; 1421 this.y = y; 1422 return this; 1423 } 1424 1425 posSub(box){ 1426 this.x -= box.x; 1427 this.y -= box.y; 1428 return this; 1429 } 1430 1431 posSubScalar(v){ 1432 this.x -= v; 1433 this.y -= v; 1434 return this; 1435 } 1436 1437 size(w, h){ 1438 this.w = w; 1439 this.h = h; 1440 return this; 1441 } 1442 1443 sizeMultiply(box){ 1444 this.w *= box.w; 1445 this.h *= box.h; 1446 return this; 1447 } 1448 1449 sizeMultiplyScalar(v){ 1450 this.w *= v; 1451 this.h *= v; 1452 1453 return this; 1454 } 1455 1456 setFromRotate(rotate){ 1457 var minX = this.x, minY = this.y, maxX = 0, maxY = 0; 1458 const point = UTILS.emptyPoint, 1459 run = function (){ 1460 if(point.x < minX) minX = point.x; 1461 else if(point.x > maxX) maxX = point.x; 1462 if(point.y < minY) minY = point.y; 1463 else if(point.y > maxY) maxY = point.y; 1464 } 1465 1466 point.set(this.x, this.y).rotate(rotate.origin, rotate.radian); run(); 1467 point.set(this.mx, this.y).rotate(rotate.origin, rotate.radian); run(); 1468 point.set(this.mx, this.my).rotate(rotate.origin, rotate.radian); run(); 1469 point.set(this.x, this.my).rotate(rotate.origin, rotate.radian); run(); 1470 1471 this.x = minX; 1472 this.y = minY; 1473 this.w = maxX - minX; 1474 this.h = maxY - minY; 1475 1476 return this; 1477 } 1478 1479 /* setFromShapeRect(shapeRect){ 1480 this.width = shapeRect.width; 1481 this.height = shapeRect.height; 1482 this.x = shapeRect.position.x - this.width / 2; 1483 this.y = shapeRect.position.y - this.height / 2; 1484 return this; 1485 } */ 1486 1487 setFromCircle(circle, inner = true){ 1488 if(inner === true){ 1489 this.x = Math.sin(-135 / 180 * Math.PI) * circle.r + circle.x; 1490 this.y = Math.cos(-135 / 180 * Math.PI) * circle.r + circle.y; 1491 this.w = this.h = Math.sin(135 / 180 * Math.PI) * circle.r + circle.x - this.x; 1492 } 1493 1494 else{ 1495 this.x = circle.x - circle.r; 1496 this.y = circle.y - circle.r; 1497 this.w = this.h = circle.r * 2; 1498 } 1499 return this; 1500 } 1501 1502 setFromPolygon(polygon, inner = true){ 1503 if(inner === true){ 1504 console.warn('Box: 暂不支持第二个参数为true'); 1505 } 1506 1507 else{ 1508 const len = polygon.path.length; 1509 let x = Infinity, y = Infinity, mx = 0, my = 0; 1510 for(let k = 0, v; k < len; k+=2){ 1511 v = polygon.path[k]; 1512 if(v < x) x = v; 1513 else if(v > mx) mx = v; 1514 1515 v = polygon.path[k+1]; 1516 if(v < y) y = v; 1517 else if(v > my) my = v; 1518 1519 } 1520 1521 this.set(x, y, mx - x, my - y); 1522 1523 } 1524 return this; 1525 } 1526 1527 toArray(array, index){ 1528 array[index] = this.x; 1529 array[index+1] = this.y; 1530 array[index+2] = this.w; 1531 array[index+3] = this.h; 1532 1533 return this; 1534 } 1535 1536 copy(box){ 1537 this.x = box.x; 1538 this.y = box.y; 1539 this.w = box.w; 1540 this.h = box.h; 1541 return this; 1542 //return Object.assign(this, box); //this.set(box.x, box.y, box.w, box.h); 1543 } 1544 1545 clone(){ 1546 return new this.constructor().copy(this); 1547 //return Object.assign(new this.constructor(), this); 1548 } 1549 1550 center(box){ 1551 this.x = (box.w - this.w) / 2 + box.x; 1552 this.y = (box.h - this.h) / 2 + box.y; 1553 return this; 1554 } 1555 1556 /* distance(x, y){ 1557 return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); 1558 } */ 1559 1560 distanceFromPoint(x, y, isMax = true){ 1561 x -= this.x; y -= this.y; 1562 const cx = this.w / 2 < x ? (isMax === true ? 0 : this.w) : (isMax === true ? this.w : 0), 1563 cy = this.h / 2 < y ? (isMax === true ? 0 : this.h) : (isMax === true ? this.h : 0); 1564 return Math.sqrt(Math.pow(x - cx, 2) + Math.pow(y - cy, 2)); 1565 } 1566 1567 isEmpty(){ 1568 return this.w <= 0 || this.h <= 0; 1569 } 1570 1571 maxX(){ 1572 return this.x + this.w; 1573 } 1574 1575 maxY(){ 1576 return this.y + this.h; 1577 } 1578 1579 equals(box){ 1580 return this.x === box.x && this.w === box.w && this.y === box.y && this.h === box.h; 1581 } 1582 1583 expand(box){ 1584 var v = Math.min(this.x, box.x); 1585 this.w = Math.max(this.x + this.w - v, box.x + box.w - v); 1586 this.x = v; 1587 1588 v = Math.min(this.y, box.y); 1589 this.h = Math.max(this.y + this.h - v, box.y + box.h - v); 1590 this.y = v; 1591 } 1592 1593 intersectsBox(box){ 1594 return box.x + box.w < this.x || box.x > this.x + this.w || box.y + box.h < this.y || box.y > this.y + this.h ? false : true; 1595 } 1596 1597 containsPoint(x, y){ 1598 return x < this.x || x > this.x + this.w || y < this.y || y > this.y + this.h ? false : true; 1599 } 1600 1601 containsBox(box){ 1602 return this.x <= box.x && box.x + box.w <= this.x + this.w && this.y <= box.y && box.y + box.h <= this.y + this.h; 1603 } 1604 1605 computeOverflow(p, r){ 1606 r["copy"](this); 1607 1608 if(this["x"] < p["x"]){ 1609 r["x"] = p["x"]; 1610 r["w"] -= p["x"] - this["x"]; 1611 } 1612 1613 if(this["y"] < p["y"]){ 1614 r["y"] = p["y"]; 1615 r["h"] -= p["y"] - this["y"]; 1616 } 1617 1618 var m = p["x"] + p["w"]; 1619 if(r["x"] + r["w"] > m) r["w"] = m - r["x"]; 1620 1621 m = p["y"] + p["h"]; 1622 if(r["y"] + r["h"] > m) r["h"] = m - r["y"]; 1623 } 1624 1625 } 1626 1627 1628 1629 1630 /* Circle 圆形 1631 parameter: 1632 attribute: 1633 x,y: Number; 中心点 1634 r: Number; 半径 1635 1636 //只读 1637 r2: Number; //返回直径 r*2 1638 1639 method: 1640 set(x, y, r): this; 1641 pos(x, y): this; 1642 copy(circle: Circle): this; 1643 clone(): Circle; 1644 distance(x, y): Number; 1645 equals(circle: Circle): Bool; 1646 expand(circle: Circle): undefined; //扩容; 把circle合并到this 1647 containsPoint(point: Point): Bool; 1648 intersectsCircle(circle: Circle): Bool; 1649 intersectsBox(box: Box): Bool; 1650 setFromBox(box, inner = true): this; 1651 1652 */ 1653 class Circle{ 1654 1655 get r2(){ 1656 return this.r * 2; 1657 } 1658 1659 constructor(x = 0, y = 0, r = -1){ 1660 //this.set(0, 0, -1); 1661 this.x = x; 1662 this.y = y; 1663 this.r = r; 1664 } 1665 1666 set(x, y, r){ 1667 this.x = x; 1668 this.y = y; 1669 this.r = r; 1670 1671 return this; 1672 } 1673 1674 setFromPoint(point){ 1675 this.x = point.x; 1676 this.y = point.y; 1677 return this; 1678 } 1679 1680 setFromBox(box, inner = true){ 1681 this.x = box.w / 2 + box.x; 1682 this.y = box.h / 2 + box.y; 1683 1684 if(inner === true) this.r = Math.min(box.w, box.h) / 2; 1685 else this.r = Math.sqrt(box.w + box.h); 1686 1687 return this; 1688 } 1689 1690 toArray(array, index){ 1691 array[index] = this.x; 1692 array[index+1] = this.y; 1693 array[index+2] = this.r; 1694 1695 return this; 1696 } 1697 1698 pos(x, y){ 1699 this.x = x; 1700 this.y = y; 1701 1702 return this; 1703 } 1704 1705 copy(circle){ 1706 this.r = circle.r; 1707 this.x = circle.x; 1708 this.y = circle.y; 1709 1710 return this; 1711 } 1712 1713 clone(){ 1714 1715 return new this.constructor().copy(this); 1716 1717 } 1718 1719 distance(x, y){ 1720 1721 return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); 1722 1723 } 1724 1725 expand(circle){ 1726 1727 } 1728 1729 equals(circle){ 1730 1731 return circle.x === this.x && circle.y === this.y && circle.r === this.r; 1732 1733 } 1734 1735 containsPoint(point){ 1736 1737 return (Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2) <= Math.pow(this.r, 2)); 1738 1739 } 1740 1741 intersectsCircle(circle){ 1742 1743 return (Math.pow(circle.x - this.x, 2) + Math.pow(circle.y - this.y, 2) <= Math.pow(circle.r + this.r, 2)); 1744 1745 } 1746 1747 intersectsBox(box){ 1748 1749 return (Math.pow(Math.max(box.x, Math.min(box.x + box.w, this.x)) - this.x, 2) + Math.pow(Math.max(box.y, Math.min(box.y + box.h, this.y)) - this.y, 2) <= Math.pow(this.r, 2)); 1750 1751 } 1752 1753 } 1754 1755 1756 1757 1758 /* Polygon 多边形 1759 1760 parameter: 1761 path: Array[x, y]; 1762 1763 attribute: 1764 1765 //只读属性 1766 path: Array[x, y]; 1767 1768 method: 1769 add(x, y): this; //x,y添加至path; 1770 containsPoint(x, y): Bool; //x,y是否在多边形的内部(注意: 在路径上也返回 true) 1771 1772 */ 1773 class Polygon{ 1774 1775 #position = null; 1776 #path2D = null; 1777 1778 get path(){ 1779 1780 return this.#position; 1781 1782 } 1783 1784 constructor(path = []){ 1785 this.#position = path; 1786 1787 this.#path2D = new Path2D(); 1788 1789 var len = path.length; 1790 if(len >= 2){ 1791 if(len % 2 !== 0){ 1792 len -= 1; 1793 path.splice(len, 1); 1794 } 1795 1796 const con = this.#path2D; 1797 con.moveTo(path[0], path[1]); 1798 for(let k = 2; k < len; k+=2) con.lineTo(path[k], path[k+1]); 1799 1800 } 1801 1802 } 1803 1804 add(x, y){ 1805 this.#position.push(x, y); 1806 this.#path2D.lineTo(x, y); 1807 return this; 1808 } 1809 1810 containsPoint(x, y){ 1811 1812 return UTILS.emptyContext.isPointInPath(this.#path2D, x, y); 1813 1814 } 1815 1816 isInPolygon(checkPoint, polygonPoints) { 1817 var counter = 0; 1818 var i; 1819 var xinters; 1820 var p1, p2; 1821 var pointCount = polygonPoints.length; 1822 p1 = polygonPoints[0]; 1823 for (i = 1; i <= pointCount; i++) { 1824 p2 = polygonPoints[i % pointCount]; 1825 if ( 1826 checkPoint[0] > Math.min(p1[0], p2[0]) && 1827 checkPoint[0] <= Math.max(p1[0], p2[0]) 1828 ) { 1829 if (checkPoint[1] <= Math.max(p1[1], p2[1])) { 1830 if (p1[0] != p2[0]) { 1831 xinters = 1832 (checkPoint[0] - p1[0]) * 1833 (p2[1] - p1[1]) / 1834 (p2[0] - p1[0]) + 1835 p1[1]; 1836 if (p1[1] == p2[1] || checkPoint[1] <= xinters) { 1837 counter++; 1838 } 1839 } 1840 } 1841 } 1842 p1 = p2; 1843 } 1844 if (counter % 2 == 0) { 1845 return false; 1846 } else { 1847 return true; 1848 } 1849 } 1850 1851 containsPolygon(polygon){ 1852 const path = polygon.path, len = path.length; 1853 for(let k = 0; k < len; k += 2){ 1854 if(this.containsPoint(path[k], path[k+1]) === false) return false; 1855 } 1856 1857 return true; 1858 } 1859 1860 toPoints(){ 1861 const path = this.path, len = path.length, result = []; 1862 1863 for(let k = 0; k < len; k += 2) result.push(new Point(path[k], path[k+1])); 1864 1865 return result; 1866 } 1867 1868 toLines(){ 1869 const path = this.path, len = path.length, result = []; 1870 1871 for(let k = 0, x = NaN, y; k < len; k += 2){ 1872 1873 if(isNaN(x)){ 1874 x = path[k]; 1875 y = path[k+1]; 1876 continue; 1877 } 1878 1879 const line = new Line(x, y, path[k], path[k+1]); 1880 1881 x = line.x1; 1882 y = line.y1; 1883 1884 result.push(line); 1885 1886 } 1887 1888 return result; 1889 } 1890 1891 merge(polygon){ 1892 1893 const linesA = this.toLines(), linesB = polygon.toLines(), nodes = [], newLines = [], 1894 1895 pointA = new Point(), pointB = new Point(), 1896 1897 forEachNodes = (pathA, pathB, funcA = null, funcB = null) => { 1898 for(let k = 0, lenA = pathA.length, lenB = pathB.length; k < lenA; k++){ 1899 if(funcA !== null) funcA(pathA[k]); 1900 1901 for(let i = 0; i < lenB; i++){ 1902 if(funcB !== null) funcB(pathB[i], pathA[k]); 1903 } 1904 1905 } 1906 } 1907 1908 if(this.containsPolygon(polygon)){console.log('this -> polygon'); 1909 forEachNodes(linesA, linesB, lineA => newLines.push(lineA), (lineB, lineA) => { 1910 if(lineA.intersectPoint(lineB, pointA) === pointA) newLines.push(pointA.clone()); 1911 }); 1912 1913 return newLines; 1914 } 1915 1916 //收集所有的交点 (保存至 line) 1917 forEachNodes(linesA, linesB, lineA => lineA.nodes = [], (lineB, lineA) => { 1918 if(lineB.nodes === undefined) lineB.nodes = []; 1919 if(lineA.intersectPoint(lineB, pointA) === pointA){ 1920 const node = { 1921 lineA: lineA, 1922 lineB: lineB, 1923 point: pointA.clone(), 1924 disA: pointA.distanceCompare(pointB.set(lineA.x, lineA.y)), 1925 disB: pointA.distanceCompare(pointB.set(lineB.x, lineB.y)), 1926 } 1927 lineA.nodes.push(node); 1928 lineB.nodes.push(node); 1929 nodes.push(node); 1930 } 1931 }); 1932 1933 //交点以原点为目标排序 1934 for(let k = 0, sotr = function (a,b){return a.disA - b.disA;}, countA = linesA.length; k < countA; k++) linesA[k].nodes.sort(sotr); 1935 for(let k = 0, sotr = function (a,b){return a.disB - b.disB;}, countB = linesB.length; k < countB; k++) linesB[k].nodes.sort(sotr); 1936 1937 var _loopTypeA, _loopTypeB; 1938 const result_loop = { 1939 lines: null, 1940 loopType: '', 1941 line: null, 1942 count: 0, 1943 indexed: 0, 1944 }, 1945 1946 //遍历某条线 1947 loop = (lines, index, loopType) => { 1948 const length = lines.length, indexed = lines.length, model = lines === linesA ? polygon : this; 1949 1950 var line, i = 1; 1951 while(true){ 1952 if(loopType === 'next') index = index === length - 1 ? 0 : index + 1; 1953 else if(loopType === 'back') index = index === 0 ? length - 1 : index - 1; 1954 line = lines[index]; 1955 1956 result_loop.count = line.nodes.length; 1957 if(result_loop.count !== 0){ 1958 result_loop.lines = lines; 1959 result_loop.loopType = loopType; 1960 result_loop.line = line; 1961 result_loop.indexed = index; 1962 if(loopType === 'next') addLine(line, model); 1963 1964 return result_loop; 1965 } 1966 1967 addLine(line, model); 1968 if(indexed === i++) break; 1969 1970 } 1971 1972 }, 1973 1974 //更新或创建交点的索引 1975 setNodeIndex = (lines, index, loopType) => { 1976 const line = lines[index], count = line.nodes.length; 1977 if(loopType === undefined) loopType = lines === linesA ? _loopTypeA : _loopTypeB; 1978 1979 if(loopType === undefined) return; 1980 1981 if(line.nodeIndex === undefined){ 1982 line.nodeIndex = loopType === 'next' ? 0 : count - 1; 1983 line.nodeState = count === 1 ? 'end' : 'start'; 1984 1985 } 1986 1987 else{ 1988 if(line.nodeState === 'end' || line.nodeState === ''){ 1989 line.nodeState = ''; 1990 return; 1991 } 1992 1993 if(loopType === 'next'){ 1994 line.nodeIndex += 1; 1995 1996 if(line.nodeIndex === count - 1) line.nodeState = 'end'; 1997 else line.nodeState = 'run'; 1998 } 1999 2000 else if(loopType === 'back'){ 2001 line.nodeIndex -= 1; 2002 2003 if(line.nodeIndex === 0) line.nodeState = 'end'; 2004 else line.nodeState = 'run'; 2005 } 2006 2007 } 2008 2009 }, 2010 2011 //只有在跳线的时候才执行此方法, 如果跳线的话: 某条线上的交点必然在两端; 2012 getLoopType = (lines, index, nodePoint) => { 2013 const line = lines[index], lineNext = lines[index === lines.length - 1 ? 0 : index + 1], 2014 2015 model = lines === linesA ? polygon : this, 2016 isLineBack = newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false, 2017 isLineNext = newLines.includes(lineNext) === false && model.containsPoint(lineNext.x, lineNext.y) === false; 2018 2019 if(isLineBack && isLineNext){ 2020 const len = line.nodes.length; 2021 if(len >= 2){ 2022 if(line.nodes[len - 1].point.equals(nodePoint)) return 'next'; 2023 else if(line.nodes[0].point.equals(nodePoint)) return 'back'; 2024 } 2025 2026 else console.warn('路径复杂', line); 2027 2028 } 2029 2030 else if(isLineNext){ 2031 return 'next'; 2032 } 2033 2034 else if(isLineBack){ 2035 return 'back'; 2036 } 2037 2038 return ''; 2039 }, 2040 2041 //添加线至新的形状数组 2042 addLine = (line, model) => { 2043 //if(newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); 2044 if(line.nodes.length === 0) newLines.push(line); 2045 else if(model.containsPoint(line.x, line.y) === false) newLines.push(line); 2046 2047 }, 2048 2049 //处理拥有交点的线 2050 computeNodes = v => { 2051 if(v === undefined || v.count === 0) return; 2052 2053 setNodeIndex(v.lines, v.indexed, v.loopType); 2054 2055 //添加交点 2056 const node = v.line.nodes[v.line.nodeIndex]; 2057 if(newLines.includes(node.point) === false) newLines.push(node.point); 2058 else return; 2059 2060 var lines = v.lines === linesA ? linesB : linesA, 2061 line = lines === linesA ? node.lineA : node.lineB, 2062 index = lines.indexOf(line); 2063 2064 setNodeIndex(lines, index); 2065 2066 //选择交点状态 2067 var nodeState = line.nodeState !== undefined ? line.nodeState : v.line.nodeState; 2068 if((v.count <= 2 && line.nodes.length > 2) || (v.count > 2 && line.nodes.length <= 2)){ 2069 if(line.nodeState === 'start'){ 2070 const backLine = v.loopType === 'next' ? v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1] : v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1]; 2071 2072 if(newLines.includes(backLine) && backLine.nodes.length === 0){ 2073 nodeState = 'run'; 2074 } 2075 2076 } 2077 else if(line.nodeState === 'end'){ 2078 const nextLine = v.loopType === 'next' ? v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1] : v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1]; 2079 const model = v.lines === linesA ? polygon : this; 2080 if(model.containsPoint(nextLine.x, nextLine.y) === false && nextLine.nodes.length === 0){ 2081 nodeState = 'run'; 2082 } 2083 2084 } 2085 } 2086 2087 switch(nodeState){ 2088 2089 //不跳线 2090 case 'run': 2091 if(v.loopType === 'back') addLine(v.line, v.lines === linesA ? polygon : this); 2092 return computeNodes(loop(v.lines, v.indexed, v.loopType)); 2093 2094 //跳线 2095 case 'start': 2096 case 'end': 2097 const loopType = getLoopType(lines, index, node.point); 2098 if(loopType !== ''){ 2099 if(lines === linesA) _loopTypeA = loopType; 2100 else _loopTypeB = loopType; 2101 if(loopType === 'back') addLine(line, v.lines === linesA ? this : polygon); 2102 return computeNodes(loop(lines, index, loopType)); 2103 } 2104 break; 2105 2106 } 2107 2108 } 2109 2110 //获取介入点 2111 var startLine = null; 2112 for(let k = 0, len = nodes.length, node; k < len; k++){ 2113 node = nodes[k]; 2114 if(node.lineA.nodes.length !== 0){ 2115 startLine = node.lineA; 2116 if(node.lineA.nodes.length === 1 && node.lineB.nodes.length > 1){ 2117 startLine = node.lineB.nodes[0].lineB; 2118 result_loop.lines = linesB; 2119 result_loop.loopType = _loopTypeB = 'next'; 2120 } 2121 else{ 2122 result_loop.lines = linesA; 2123 result_loop.loopType = _loopTypeA = 'next'; 2124 } 2125 result_loop.line = startLine; 2126 result_loop.count = startLine.nodes.length; 2127 result_loop.indexed = result_loop.lines.indexOf(startLine); 2128 break; 2129 } 2130 } 2131 2132 if(startLine === null){ 2133 console.warn('Polygon: 找不到介入点, 终止了合并'); 2134 return newLines; 2135 } 2136 2137 computeNodes(result_loop); 2138 2139 return newLines; 2140 } 2141 2142 } 2143 2144 2145 2146 2147 /* RGBColor 2148 parameter: 2149 r, g, b 2150 2151 method: 2152 set(r, g, b: Number): this; //rgb: 0 - 255; 第一个参数可以为 css color 2153 setFormHex(hex: Number): this; // 2154 setFormHSV(h, s, v: Number): this; //h:0-360; s,v:0-100; 颜色, 明度, 暗度 2155 setFormString(str: String): Number; //str: 英文|css color; 返回的是透明度 (如果为 rgba 则返回a; 否则总是返回1) 2156 2157 copy(v: RGBColor): this; 2158 clone(): RGBColor; 2159 2160 getHex(): Number; 2161 getHexString(): String; 2162 getHSV(result: Object{h, s, v}): result; //result: 默认是一个新的Object 2163 getRGBA(alpha: Number): String; //alpha: 0 - 1; 默认 1 2164 getStyle() //.getRGBA()别名 2165 2166 stringToColor(str: String): String; //str 转为 css color; 如果str不是color格式则返回 "" 2167 2168 */ 2169 class RGBColor{ 2170 2171 get isRGBColor(){return true;} 2172 2173 constructor(r = 255, g = 255, b = 255){ 2174 this.r = r; 2175 this.g = g; 2176 this.b = b; 2177 } 2178 2179 copy(v){ 2180 this.r = v.r; 2181 this.g = v.g; 2182 this.b = v.b; 2183 return this; 2184 } 2185 2186 clone(){ 2187 return new this.constructor().copy(this); 2188 } 2189 2190 set(r, g, b){ 2191 if(typeof r !== "string"){ 2192 this.r = UTILS.isNumber(r) ? r : 255; 2193 this.g = UTILS.isNumber(g) ? g : 255; 2194 this.b = UTILS.isNumber(b) ? b : 255; 2195 } 2196 2197 else this.setFormString(r); 2198 2199 return this; 2200 } 2201 2202 setFormHex(hex){ 2203 hex = Math.floor( hex ); 2204 2205 this.r = hex >> 16 & 255; 2206 this.g = hex >> 8 & 255; 2207 this.b = hex & 255; 2208 return this; 2209 } 2210 2211 setFormHSV(h, s, v){ 2212 h = h >= 360 ? 0 : h; 2213 var s=s/100; 2214 var v=v/100; 2215 var h1=Math.floor(h/60) % 6; 2216 var f=h/60-h1; 2217 var p=v*(1-s); 2218 var q=v*(1-f*s); 2219 var t=v*(1-(1-f)*s); 2220 var r,g,b; 2221 switch(h1){ 2222 case 0: 2223 r=v; 2224 g=t; 2225 b=p; 2226 break; 2227 case 1: 2228 r=q; 2229 g=v; 2230 b=p; 2231 break; 2232 case 2: 2233 r=p; 2234 g=v; 2235 b=t; 2236 break; 2237 case 3: 2238 r=p; 2239 g=q; 2240 b=v; 2241 break; 2242 case 4: 2243 r=t; 2244 g=p; 2245 b=v; 2246 break; 2247 case 5: 2248 r=v; 2249 g=p; 2250 b=q; 2251 break; 2252 } 2253 2254 this.r = Math.round(r*255); 2255 this.g = Math.round(g*255); 2256 this.b = Math.round(b*255); 2257 return this; 2258 } 2259 2260 setFormString(color){ 2261 if(typeof color !== "string") return 1; 2262 var _color = this.stringToColor(color); 2263 2264 if(_color[0] === "#"){ 2265 const len = _color.length; 2266 if(len === 4){ 2267 _color = _color.slice(1); 2268 this.setFormHex(parseInt("0x"+_color + "" + _color)); 2269 } 2270 else if(len === 7) this.setFormHex(parseInt("0x"+_color.slice(1))); 2271 } 2272 2273 else if(_color[0] === "r" && _color[1] === "g" && _color[2] === "b"){ 2274 const arr = []; 2275 for(let k = 0, len = _color.length, v = "", is = false; k < len; k++){ 2276 2277 if(is === true){ 2278 if(_color[k] === "," || _color[k] === ")"){ 2279 arr.push(parseFloat(v)); 2280 v = ""; 2281 } 2282 else v += _color[k]; 2283 2284 } 2285 2286 else if(_color[k] === "(") is = true; 2287 2288 } 2289 2290 this.set(arr[0], arr[1], arr[2]); 2291 return arr[3] === undefined ? 1 : arr[3]; 2292 } 2293 2294 return 1; 2295 } 2296 2297 getHex(){ 2298 2299 return Math.max( 0, Math.min( 255, this.r ) ) << 16 ^ Math.max( 0, Math.min( 255, this.g ) ) << 8 ^ Math.max( 0, Math.min( 255, this.b ) ) << 0; 2300 2301 } 2302 2303 getHexString(){ 2304 2305 return '#' + ( '000000' + this.getHex().toString( 16 ) ).slice( - 6 ); 2306 2307 } 2308 2309 getHSV(result){ 2310 result = result || {} 2311 var r=this.r/255; 2312 var g=this.g/255; 2313 var b=this.b/255; 2314 var h,s,v; 2315 var min=Math.min(r,g,b); 2316 var max=v=Math.max(r,g,b); 2317 var l=(min+max)/2; 2318 var difference = max-min; 2319 2320 if(max==min){ 2321 h=0; 2322 }else{ 2323 switch(max){ 2324 case r: h=(g-b)/difference+(g < b ? 6 : 0);break; 2325 case g: h=2.0+(b-r)/difference;break; 2326 case b: h=4.0+(r-g)/difference;break; 2327 } 2328 h=Math.round(h*60); 2329 } 2330 if(max==0){ 2331 s=0; 2332 }else{ 2333 s=1-min/max; 2334 } 2335 s=Math.round(s*100); 2336 v=Math.round(v*100); 2337 result.h = h; 2338 result.s = s; 2339 result.v = v; 2340 return result; 2341 } 2342 2343 getStyle(){ 2344 return this.getRGBA(1); 2345 } 2346 2347 getRGBA(alpha = 1){ 2348 return `rgba(${this.r},${this.g},${this.b},${alpha})`; 2349 } 2350 2351 stringToColor(str){ 2352 var _color = ""; 2353 for(let k = 0, len = str.length; k < len; k++){ 2354 if(str[k] === " ") continue; 2355 _color += str[k]; 2356 } 2357 2358 if(_color[0] === "#" || (_color[0] === "r" && _color[1] === "g" && _color[2] === "b")) return _color; 2359 2360 return UTILS.colorTable[_color] || ""; 2361 } 2362 2363 } 2364 2365 2366 2367 2368 /* Timer 定时器 2369 2370 parameter: 2371 func: Function; //定时器运行时的回调; 默认 null, 如果定义构造器将自动调用一次.restart()方法 2372 speed: Number; //延迟多少毫秒执行一次 func; 默认 3000; 2373 step: Integer; //执行多少次: 延迟speed毫秒执行一次 func; 默认 Infinity; 2374 2375 attribute: 2376 func, speed, step; //这些属性可以随时更改; 2377 2378 method: 2379 start(func, speed): this; //启动定时器 (如果定时器正在运行则什么都不会做) 2380 stop(): undefined; //停止定时器 2381 2382 demo: 2383 //每 3000 毫秒 打印一次 timer.number 2384 new Timer(timer => { 2385 console.log(timer.number); 2386 }, 3000); 2387 2388 */ 2389 class Timer{ 2390 2391 #i = 0; 2392 #id = -1; 2393 2394 get running(){ 2395 return this.#id !== -1; 2396 } 2397 2398 constructor(func = null, speed = 3000, step = Infinity, isStart = true){ 2399 this.func = func; 2400 this.speed = speed; 2401 this.step = step; 2402 this.onend = null; 2403 2404 const animate = () => { 2405 this.#i++; 2406 this.func(this); 2407 if(this.#id !== -1){ 2408 if(this.#i < this.step) this.#id = setTimeout(animate, this.speed); 2409 else{ 2410 this.#id = -1; 2411 if(this.onend !== null) this.onend(this); 2412 } 2413 } 2414 } 2415 2416 this._animate = animate; 2417 if(isStart === true && typeof this.func === "function"){ 2418 this.#id = setTimeout(this._animate, this.speed); 2419 this.#i = 0; 2420 } 2421 } 2422 2423 restart(){ 2424 if(this.#id !== -1) clearTimeout(this.#id); 2425 this.#id = setTimeout(this._animate, this.speed); 2426 this.#i = 0; 2427 } 2428 2429 start(func, speed){ 2430 if(this.#id === -1){ 2431 if(typeof func === 'function') this.func = func; 2432 if(UTILS.isNumber(speed) === true) this.speed = speed; 2433 this.#id = setTimeout(this._animate, this.speed); 2434 this.#i = 0; 2435 } 2436 } 2437 2438 stop(){ 2439 if(this.#id !== -1){ 2440 clearTimeout(this.#id); 2441 this.#id = -1; 2442 } 2443 } 2444 2445 } 2446 2447 2448 2449 2450 /* SeekPath A*寻路 2451 parameter: 2452 option: Object{ 2453 angle: Number, //8 || 16 2454 timeout: Number, //单位为毫秒 2455 size: Number, //每格的宽高 2456 lenX, lenY: Number, //长度 2457 disables: Array[0||1], 2458 heights: Array[Number], 2459 path: Array[], //存放寻路结果 默认创建一个空数组 2460 } 2461 2462 attribute: 2463 size: Number; //每个索引的大小 2464 lenX: Number; //最大长度x (设置此属性时, 你需要重新.initMap(heights);) 2465 lenY: Number; //最大长度y (设置此属性时, 你需要重新.initMap(heights);) 2466 2467 //此属性已废弃 range: Box; //本次的搜索范围, 默认: 0,0,lenX,lenY 2468 angle: Number; //8四方向 或 16八方向 默认 16 2469 timeout: Number; //超时毫秒 默认 500 2470 mapRange: Box; //地图box 2471 //此属性已废弃(run方法不在检测相邻的高) maxHeight: Number; //相邻可走的最大高 默认 6 2472 2473 //只读属性 2474 success: Bool; //只读; 本次搜索是否成功找到终点; (如果为false说明.run()返回的是 距终点最近的路径; 超时也会被判定为false) 2475 path: Array[node]; //存放.run()返回的路径 2476 map: Map; //地图的缓存数据 2477 2478 method: 2479 initMap(heights: Array[Number]): undefiend; //初始化类时自动调用一次; heights:如果你的场景存在高请定义此参数 2480 run(x, y, x1, y1: Number): Array[x, y, z]; //参数索引坐标 2481 getDots(x, y, a, r): Array[ix, iy]; //获取周围的点 x,y, a:8|16, r:存放结果数组 2482 getLegalPoints(ix, iy, count, result = []): Array[node]; //获取 ix, iy 周围 合法的, 相邻的 count 个点 2483 2484 demo: 2485 const sp = new SeekPath({ 2486 angle: 16, 2487 timeout: 500, 2488 size: 10, 2489 lenX: 1000, 2490 lenY: 1000, 2491 }), 2492 2493 path = sp.run(0, 0, 1000, 1000); 2494 2495 console.log(sp); 2496 */ 2497 class SeekPath{ 2498 2499 static _open = [] 2500 static _dots = [] //.run() .getLegalPoints() 2501 static dots4 = []; //._check() 2502 static _sort = function (a, b){return a["f"] - b["f"];} 2503 2504 #map = null; 2505 #path = null; 2506 #success = true; 2507 #halfX = 50; 2508 #halfY = 50; 2509 2510 #size = 10; 2511 #lenX = 10; 2512 #lenY = 10; 2513 2514 constructor(option = {}){ 2515 this.angle = (option.angle === 8 || option.angle === 16) ? option.angle : 16; //8四方向 或 16八方向 2516 this.timeout = option.timeout || 500; //超时毫秒 2517 //this.maxHeight = option.maxHeight || 6; 2518 this.mapRange = new Box(); 2519 this.size = option.size || 10; 2520 this.lenX = option.lenX || 10; 2521 this.lenY = option.lenY || 10; 2522 this.#path = Array.isArray(option.path) ? option.path : []; 2523 this.initMap(option.disable, option.height); 2524 option = undefined; 2525 } 2526 2527 //this.#map[x][y] = Object{height: 此点的高,默认0, is: 此点是否可走,默认1(disable)}; 2528 get map(){ 2529 return this.#map; 2530 } 2531 2532 //this.#path = Array[x,y,z] 2533 get path(){ 2534 return this.#path; 2535 } 2536 2537 get success(){ 2538 return this.#success; 2539 } 2540 2541 get size(){ 2542 return this.#size; 2543 } 2544 2545 set size(v){ 2546 this.#size = v; 2547 v = v / 2; 2548 this.#halfX = v * this.#lenX; 2549 this.#halfY = v * this.#lenY; 2550 } 2551 2552 get lenX(){ 2553 return this.#lenX; 2554 } 2555 2556 set lenX(v){ 2557 this.#lenX = v; 2558 v = this.#size / 2; 2559 this.#halfX = v * this.#lenX; 2560 this.#halfY = v * this.#lenY; 2561 } 2562 2563 get lenY(){ 2564 return this.#lenY; 2565 } 2566 2567 set lenY(v){ 2568 this.#lenY = v; 2569 v = this.#size / 2; 2570 this.#halfX = v * this.#lenX; 2571 this.#halfY = v * this.#lenY; 2572 } 2573 2574 getNode(sx, sy){ 2575 sx = this.#map[Math.floor((this.#halfX + sx) / this.#size)]; 2576 if(sx !== undefined) return sx[Math.floor((this.#halfY + sy) / this.#size)]; 2577 } 2578 2579 node(ix, iy){ 2580 ix = this.#map[ix]; 2581 if(ix !== undefined) return ix[iy]; 2582 } 2583 2584 toScene(n, v){ //n = "x|y" 2585 //n = n === "y" ? "lenY" : "lenX"; 2586 if(n === "x") return v * this.#size - this.#halfX; 2587 return v * this.#size - this.#halfY; 2588 } 2589 2590 toIndex(n, v){ 2591 //n = n === "y" ? "lenY" : "lenX"; 2592 if(n === "x") return Math.floor((this.#halfX + v) / this.#size); 2593 return Math.floor((this.#halfY + v) / this.#size); 2594 } 2595 2596 initMap(disable, height){ 2597 2598 disable = Array.isArray(disable) === true ? disable : null; 2599 height = Array.isArray(height) === true ? height : null; 2600 2601 const lenX = this.lenX, lenY = this.lenY; 2602 var getHeight = (ix, iy) => { 2603 if(height === null) return 0; 2604 ix = height[ix * lenY + iy]; 2605 if(ix === undefined) return 0; 2606 return ix; 2607 }, 2608 getDisable = (ix, iy) => { 2609 if(disable === null) return 1; 2610 ix = disable[ix * lenY + iy]; 2611 if(ix === undefined) return 0; 2612 return ix; 2613 }, 2614 2615 map = []//new Map(); 2616 2617 for(let x = 0, y, m; x < lenX; x++){ 2618 m = []//new Map(); 2619 for(y = 0; y < lenY; y++) m[y] = {x:x, y:y, height:getHeight(x, y), is:getDisable(x, y), g:0, h:0, f:0, p:null, id:""}//m.set(y, {x:x, y:y, height:getHeight(x, y), g:0, h:0, f:0, p:null, id:""}); 2620 map[x] = m;//map.set(x, m); 2621 } 2622 2623 this.#map = map; 2624 this._id = -1; 2625 this._updateID(); 2626 this.mapRange.set(0, 0, this.#lenX-1, this.#lenY-1); 2627 2628 map = disable = height = getHeight = undefined; 2629 2630 } 2631 2632 getLegalPoints(ix, iy, count, result = []){ //不包括 ix, iy 2633 const sTime = UTILS.time, _dots = SeekPath._dots; 2634 result.length = 0; 2635 result[0] = this.#map[ix][iy]; 2636 count += 1; 2637 2638 while(result.length < count){ 2639 for(let k = 0, i, n, d, len = result.length; k < len; k++){ 2640 n = result[k]; 2641 this.getDots(n.x, n.y, this.angle, _dots); 2642 for(i = 0; i < this.angle; i += 2){ 2643 d = this.#map[_dots[i]][_dots[i+1]]; 2644 if(d.is === 1 && this.mapRange.containsPoint(d.x, d.y) && !result.includes(d)){ 2645 if(Math.abs(n.x - d.x) + Math.abs(n.y - d.y) === 2 && this._check(n, d)){ 2646 result.push(d); 2647 } 2648 } 2649 } 2650 } 2651 2652 if(UTILS.time - sTime >= this.timeout) break; 2653 } 2654 2655 result.splice(0, 1); 2656 return result; 2657 } 2658 2659 getLinePoints(now, next, count, result = []){ //不包括 now 2660 if(count % 2 !== 0) count += 1; 2661 2662 const len = count / 2, angle90 = 90/180*Math.PI; 2663 2664 var i, ix, iy, n, nn = next, is = false; 2665 2666 UTILS.emptyPoint.set(now.x, now.y).rotate(next, angle90); 2667 var disX = UTILS.emptyPoint.x - next.x, 2668 disY = UTILS.emptyPoint.y - next.y; 2669 2670 for(i = 0; i < len; i++){ 2671 if(is){ 2672 result[len-1-i] = nn; 2673 continue; 2674 } 2675 ix = disX + disX * i + next.x; 2676 iy = disY + disY * i + next.y; 2677 2678 n = this.#map[ix][iy]; 2679 if(n.is === 1) nn = n; 2680 else is = true; 2681 result[len-1-i] = nn; 2682 } 2683 2684 //result[len] = next; 2685 is = false; 2686 nn = next; 2687 2688 UTILS.emptyPoint.set(now.x, now.y).rotate(next, -angle90); 2689 disX = UTILS.emptyPoint.x - next.x, 2690 disY = UTILS.emptyPoint.y - next.y; 2691 2692 for(i = 0; i < len; i++){ 2693 if(is){ 2694 result[len+i] = nn; 2695 continue; 2696 } 2697 ix = disX + disX * i + next.x; 2698 iy = disY + disY * i + next.y; 2699 2700 n = this.#map[ix][iy]; 2701 if(n.is === 1) nn = n; 2702 else is = true; 2703 result[len+i] = nn; 2704 } 2705 2706 return result; 2707 } 2708 2709 getDots(x, y, a, r = []){ //获取周围的点 x,y, a:8|16, r:存放结果数组 2710 r.length = 0; 2711 const x_1 = x-1, x1 = x+1, y_1 = y-1, y1 = y+1; 2712 if(a === 16) r.push(x_1, y_1, x, y_1, x1, y_1, x_1, y, x1, y, x_1, y1, x, y1, x1, y1); 2713 else r.push(x, y_1, x, y1, x_1, y, x1, y); 2714 } 2715 2716 getDisMHD(nodeA, nodeB){ 2717 return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y); 2718 } 2719 2720 _updateID(){ //更新标记 2721 this._id++; 2722 this._openID = "o_"+this._id; 2723 this._closeID = "c_"+this._id; 2724 } 2725 2726 _check(dotA, dotB){ //检测 a 是否能到 b 2727 //获取 dotB 周围的4个点 并 遍历这4个点 2728 this.getDots(dotB.x, dotB.y, 8, SeekPath.dots4); 2729 for(let k = 0, x, y; k < 8; k += 2){ 2730 x = SeekPath.dots4[k]; 2731 y = SeekPath.dots4[k+1]; 2732 if(this.mapRange.containsPoint(x, y) === false) continue; 2733 2734 //找出 dotA 与 dotB 相交的两个点: 2735 if(Math.abs(dotA.x - x) + Math.abs(dotA.y - y) === 1){ 2736 //如果其中一个交点是不可走的则 dotA 到 dotB 不可走, 既返回 false 2737 if(this.#map[x][y].is === 0) return false; 2738 } 2739 2740 } 2741 2742 return true; 2743 } 2744 2745 run(x, y, x1, y1, path = this.#path){ 2746 path.length = 0; 2747 if(this.#map === null || this.mapRange.containsPoint(x, y) === false) return path; 2748 2749 var _n = this.#map[x][y]; 2750 if(_n.is === 0) return path; 2751 2752 const _sort = SeekPath._sort, 2753 _open = SeekPath._open, 2754 _dots = SeekPath._dots, 2755 time = Date.now(); 2756 2757 //var isDot = true, 2758 var suc = _n, k, mhd, g, h, f, _d; 2759 2760 _n.g = 0; 2761 _n.h = _n.h = Math.abs(x1 - x) * 10 + Math.abs(y1 - y) * 10; 2762 _n.f = _n.h; 2763 _n.p = null; 2764 this._updateID(); 2765 _n.id = this._openID; 2766 _open.push(_n); 2767 2768 while(_open.length !== 0){ 2769 if(Date.now() - time > this.timeout) break; 2770 2771 _open.sort(_sort); 2772 _n = _open.shift(); 2773 if(_n.x === x1 && _n.y === y1){ 2774 suc = _n; 2775 break; 2776 } 2777 2778 if(suc.h > _n.h) suc = _n; 2779 _n.id = this._closeID; 2780 this.getDots(_n.x, _n.y, this.angle, _dots); 2781 2782 for(k = 0; k < this.angle; k += 2){ 2783 2784 _d = this.#map[_dots[k]][_dots[k+1]]; 2785 if(_d.id === this._closeID || _d.is === 0 || this.mapRange.containsPoint(_d.x, _d.y) === false) continue; 2786 2787 mhd = Math["abs"](_n["x"] - _d.x) + Math["abs"](_n["y"] - _d.y); 2788 g = _n["g"] + (mhd === 1 ? 10 : 14); 2789 h = Math["abs"](x1 - _d.x) * 10 + Math["abs"](y1 - _d.y) * 10; 2790 f = g + h; 2791 2792 if(_d.id !== this._openID){ 2793 //如果是斜角和8方向: 2794 if(mhd !== 1 && this.angle === 16){ 2795 if(this._check(_n, _d)){ 2796 _d.g = g; 2797 _d.h = h; 2798 _d.f = f; 2799 _d.p = _n; 2800 _d.id = this._openID; 2801 _open.push(_d); 2802 } 2803 }else{ 2804 _d.g = g; 2805 _d.h = h; 2806 _d.f = f; 2807 _d.p = _n; 2808 _d.id = this._openID; 2809 _open.push(_d); 2810 } 2811 } 2812 2813 else if(g < _d.g){ 2814 _d.g = g; 2815 _d.f = g + _d.h; 2816 _d.p = _n; 2817 } 2818 2819 } 2820 } 2821 2822 this.#success = suc === _n; 2823 2824 while(suc !== null){ 2825 path.unshift(suc); //0为起始点,length-1为结束点 2826 //path.unshift(this.toScene("x", suc["x"]), suc["height"], this.toScene("y", suc["y"])); 2827 suc = suc["p"]; 2828 } 2829 2830 _open.length = _dots.length = 0; 2831 2832 return path; 2833 } 2834 2835 } 2836 2837 2838 2839 2840 /* TweenValue (从 原点 以规定的时间到达 终点) 2841 2842 parameter: origin, end, time, onUpdate, onEnd; 2843 2844 attribute: 2845 origin: Object; //原点(起点) 2846 end: Object; //终点 2847 time: Number; //origin 到 end 花费的时间 2848 onUpdate: Function; //更新回调; 一个回调参数 origin; 默认null; 2849 onEnd: Function; //结束回调; 没有回调参数; 默认null; (如果返回的是"restart"将不从队列删除, 你可以在onEnd中更新.end不间断的继续补间) 2850 2851 method: 2852 reset(origin, end: Object): undefined; //更换 .origin, .end; 它会清除其它对象的缓存属性 2853 reverse(): undefined; //this.end 复制 this.origin 的原始值 2854 update(): undefined; //Tween 通过此方法统一更新 TweenValue 2855 2856 demo: 2857 //init Tween: 2858 const tween = new Tween(), 2859 animate = function (){ 2860 requestAnimationFrame(animate); 2861 tween.update(); 2862 } 2863 2864 //init TweenValue: 2865 const v1 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v)); 2866 2867 animate(); 2868 tween.start(v1); 2869 2870 //缓动 2871 const end = 100; 2872 var step, left = 0; 2873 new Timer(timer => { 2874 step = (end - left) / 10; 2875 left += step; 2876 if(Math.ceil(left) === end) timer.stop(); 2877 }, 1000); 2878 */ 2879 class TweenValue{ 2880 2881 constructor(origin = {}, end = {}, time = 500, onEnd = null, onUpdate = null, onStart = null){ 2882 this.origin = origin; 2883 this.end = end; 2884 this.time = time; 2885 2886 this.onUpdate = onUpdate; 2887 this.onEnd = onEnd; 2888 this.onStart = onStart; 2889 2890 //以下属性不能直接设置 2891 this._r = null; 2892 this._t = 0; 2893 this._v = Object.create(null); 2894 } 2895 2896 _start(){ 2897 var v = ""; 2898 for(v in this.origin) this._v[v] = this.origin[v]; 2899 if(this.onStart !== null) this.onStart(this); 2900 this._t = Date.now(); 2901 //this.update(); 2902 } 2903 2904 reset(origin, end){ 2905 this.origin = origin; 2906 this.end = end; 2907 this._v = Object.create(null); 2908 } 2909 2910 reverse(){ 2911 var n = ""; 2912 for(n in this.origin) this.end[n] = this._v[n]; 2913 } 2914 2915 update(){ 2916 2917 if(this["_r"] !== null){ 2918 2919 var ted = Date["now"]() - this["_t"]; 2920 2921 if(ted >= this["time"]){ 2922 2923 for(ted in this["origin"]) this["origin"][ted] = this["end"][ted]; 2924 2925 if(this["onEnd"] !== null){ 2926 2927 if(this["onEnd"](this) === "restart"){ 2928 if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); 2929 this["_start"](); 2930 } 2931 2932 else this["_r"]["stop"](this); 2933 2934 } 2935 2936 else this["_r"]["stop"](this); 2937 2938 } 2939 2940 else{ 2941 ted = ted / this["time"]; 2942 let n = ""; 2943 for(n in this["origin"]) this["origin"][n] = ted * (this["end"][n] - this["_v"][n]) + this["_v"][n]; 2944 if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); 2945 } 2946 2947 } 2948 2949 } 2950 2951 } 2952 2953 2954 2955 2956 /* Tween 动画补间 (TweenValue 的root, 可以管理多个TweenValue) 2957 2958 parameter: 2959 attribute: 2960 method: 2961 start(value: TweenValue): undefined; 2962 stop(value: TweenValue): undefined; 2963 2964 static: 2965 Value: TweenValue; 2966 2967 demo: 2968 //init Tween: 2969 const tween = new Tween(), 2970 animate = function (){ 2971 requestAnimationFrame(animate); 2972 tween.update(); 2973 } 2974 2975 //init TweenValue: 2976 const v2 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v), v => { 2977 v2.reverse(); //v2.end 复制起始值 2978 return "restart"; //返回"restart"表示不删除队列, 需要继续补间 2979 }); 2980 2981 animate(); 2982 tween.start(v2); 2983 2984 */ 2985 class Tween extends RunningList{ 2986 2987 static Value = TweenValue; 2988 2989 constructor(){ 2990 super(); 2991 } 2992 2993 start(value){ 2994 this.add(value); 2995 value._r = this; 2996 value._start(); 2997 } 2998 2999 stop(value){ 3000 this.remove(value); 3001 value._r = null; 3002 } 3003 3004 } 3005 3006 3007 3008 3009 /* EventDispatcher 自定义事件管理器 3010 parameter: 3011 attribute: 3012 3013 method: 3014 clearEvents(eventName): undefined; //清除eventName列表, 如果 eventName 未定义清除所有事件 3015 customEvents(eventName, eventParam): this; //创建自定义事件 eventParam 可选 默认 undefined 3016 getParam(eventName): eventParam; 3017 trigger(eventName, callback): undefined; //触发 (callback: 可选) 3018 register(eventName, callback): undefined; // 3019 deleteEvent(eventName, callback): undefined; // 3020 3021 demo: 3022 const eventDispatcher = new EventDispatcher(); 3023 eventDispatcher.customEvents("test", {name: "test"}); 3024 3025 eventDispatcher.register("test", eventParam => { 3026 console.log(eventParam) //Object{name: "test"} 3027 }); 3028 3029 eventDispatcher.trigger("test"); 3030 3031 */ 3032 class EventDispatcher{ 3033 3034 constructor(){ 3035 this._eventsList = {}; 3036 this.__eventsList = []; 3037 this.__trigger = ""; 3038 } 3039 3040 clearEvents(eventName){ 3041 3042 //if(this.__trigger === eventName) return console.warn("EventDispatcher: 清除事件失败"); 3043 if(this._eventsList[eventName] !== undefined) this._eventsList[eventName].func = [] 3044 3045 else this._eventsList = {} 3046 3047 } 3048 3049 customEvents(eventName, eventParam){ 3050 //if(typeof eventName !== "string") return console.warn("EventDispatcher: 注册自定义事件失败"); 3051 if(this._eventsList[eventName] !== undefined) return console.warn("EventDispatcher: "+eventName+" 已存在"); 3052 //eventParam = eventParam || {} 3053 //if(eventParam.type === undefined) eventParam.type = eventName; 3054 this._eventsList[eventName] = {func: [], param: eventParam} 3055 return this; 3056 } 3057 3058 getParam(eventName){ 3059 return this._eventsList[eventName]["param"]; 3060 } 3061 3062 trigger(eventName, callback){ 3063 //if(this._eventsList[eventName] === undefined) return; 3064 3065 const obj = this._eventsList[eventName]; 3066 var k, len = obj.func.length; 3067 3068 if(len !== 0){ 3069 if(typeof callback === "function") callback(obj["param"]); //更新参数(eventParam) 3070 3071 //触发过程(如果触发过程中删除事件, 不仅 len 没有变, 而且还会漏掉一个key, 所以在触发过程中删除的事件要特殊处理) 3072 this.__trigger = eventName; 3073 for(k = 0; k < len; k++) obj["func"][k](obj["param"]); 3074 this.__trigger = ""; 3075 //触发过程结束 3076 3077 //删除在触发过程中要删除的事件 3078 len = this.__eventsList.length; 3079 for(k = 0; k < len; k++) this.deleteEvent(eventName, this.__eventsList[k]); 3080 this.__eventsList.length = 0; 3081 3082 } 3083 3084 } 3085 3086 register(eventName, callback){ 3087 if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); 3088 const obj = this._eventsList[eventName]; 3089 //if(obj.func.includes(callback) === false) obj.func.push(callback); 3090 //else console.warn("EventDispatcher: 回调函数重复"); 3091 obj.func.push(callback); 3092 } 3093 3094 deleteEvent(eventName, callback){ 3095 if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); 3096 3097 if(this.__trigger === eventName) this.__eventsList.push(callback); 3098 else{ 3099 const obj = this._eventsList[eventName], i = obj.func.indexOf(callback); 3100 if(i !== -1) obj.func.splice(i, 1); 3101 } 3102 3103 } 3104 3105 } 3106 3107 3108 3109 3110 export { 3111 UTILS, 3112 TweenCache, 3113 AnimateLoop, 3114 Ajax, 3115 IndexedDB, 3116 TreeStruct, 3117 Point, 3118 Line, 3119 Box, 3120 Circle, 3121 Polygon, 3122 RGBColor, 3123 Timer, 3124 SeekPath, 3125 RunningList, 3126 TweenValue, 3127 Tween, 3128 TweenTarget, 3129 EventDispatcher, 3130 SecurityDoor, 3131 }
1 "use strict"; 2 import { 3 UTILS, 4 Box, 5 EventDispatcher, 6 Point, 7 AnimateLoop, 8 TreeStruct, 9 Timer, 10 } from './Utils.js'; 11 12 /* Touch 事件 13 touchstart 14 当用户在触摸平面上放置了一个触点时触发。事件的目标 element 将是触点位置上的那个目标 element 15 16 touchend 17 当一个触点被用户从触摸平面上移除(即用户的一个手指或手写笔离开触摸平面)时触发。当触点移出触摸平面的边界时也将触发。例如用户将手指划出屏幕边缘。 18 事件的目标 element 与触发 touchstart 事件的目标 element 相同,即使 touchend 事件触发时,触点已经移出了该 element 。 19 已经被从触摸平面上移除的触点,可以在 changedTouches 属性定义的 TouchList 中找到。 20 21 touchmove 22 当用户在触摸平面上移动触点时触发。事件的目标 element 和触发 touchstart 事件的目标 element 相同,即使当 touchmove 事件触发时,触点已经移出了该 element 。 23 当触点的半径、旋转角度以及压力大小发生变化时,也将触发此事件。 24 注意: 不同浏览器上 touchmove 事件的触发频率并不相同。这个触发频率还和硬件设备的性能有关。因此决不能让程序的运作依赖于某个特定的触发频率。 25 26 touchcancel 27 当触点由于某些原因被中断时触发。有几种可能的原因如下(具体的原因根据不同的设备和浏览器有所不同): 28 由于某个事件出现而取消了触摸:例如触摸过程被弹窗打断。 29 触点离开了文档窗口,而进入了浏览器的界面元素、插件或者其他外部内容区域。 30 当用户产生的触点个数超过了设备支持的个数,从而导致 TouchList 中最早的 Touch 对象被取消。 31 32 33 TouchEvent.changedTouches 34 这个 TouchList 对象列出了和这个触摸事件对应的 Touch 对象。 35 对于 touchstart 事件,这个 TouchList 对象列出在此次事件中新增加的触点。 36 对于 touchmove 事件,列出和上一次事件相比较,发生了变化的触点。 37 对于 touchend 事件,changedTouches 是已经从触摸面的离开的触点的集合(也就是说,手指已经离开了屏幕/触摸面)。 38 39 TouchEvent.targetTouches 40 targetTouches 是一个只读的 TouchList 列表,包含仍与触摸面接触的所有触摸点的 Touch 对象。touchstart (en-US)事件触发在哪个element内,它就是当前目标元素。 41 42 TouchEvent.touches 43 一个 TouchList,其会列出所有当前在与触摸表面接触的 Touch 对象,不管触摸点是否已经改变或其目标元素是在处于 touchstart 阶段。 44 45 1 event.changedTouches 上一次的触点列表 46 1 event.targetTouches 某个元素的当前触点列表 47 1 event.touches 屏幕上所有的当前触点列表 48 5 touches: Array[Object{ 49 clientX, clientY 50 pageX, pageY 51 screenX, screenY 52 radiusX, radiusY 53 force 54 identifier 55 rotationAngle 56 target 57 }] 58 59 */ 60 61 const ElementUtils = { 62 63 getRect(elem){ 64 return elem.getBoundingClientRect(); 65 }, 66 67 downloadFile(blob, fileName){ 68 const link = document.createElement("a"); 69 link.href = URL.createObjectURL(blob); 70 link.download = fileName; 71 link.click(); 72 }, 73 74 loadFileJSON(callback){ 75 const input = document.createElement("input"); 76 input.type = "file"; 77 input.accept = ".json"; 78 79 input.onchange = a => { 80 if(a.target.files.length === 0) return; 81 const fr = new FileReader(); 82 fr.onloadend = b => callback(b.target.result); 83 fr.readAsText(a.target.files[0]); //fr.readAsDataURL(a.target.files[0]); 84 } 85 86 input.click(); 87 }, 88 89 createCanvas(w = 1, h = 1, className = ""){ 90 const canvas = document.createElement("canvas"); 91 canvas.width = w; 92 canvas.height = h; 93 canvas.className = className; 94 return canvas; 95 }, 96 97 createContext(w = 1, h = 1, alpha = true){ 98 const canvas = document.createElement("canvas"), 99 context = canvas.getContext("2d", {alpha: alpha}); 100 canvas.width = w; 101 canvas.height = h; 102 return context; 103 }, 104 105 createElem(tagName, className = "", textContent = ""){ 106 const elem = document.createElement(tagName); 107 elem.className = className; 108 elem.textContent = textContent; 109 return elem; 110 }, 111 112 createInput(type, className = ""){ 113 const input = document.createElement("input"); 114 input.type = type; 115 input.className = className; 116 return input; 117 }, 118 119 appendChilds(parentElem, ...nodes){ 120 const msgContainer = document.createDocumentFragment(); 121 for(let k = 0, len = nodes.length; k < len; k++) msgContainer.appendChild(nodes[k]); 122 parentElem.appendChild(msgContainer); 123 }, 124 125 removeChild(elem){ 126 if(elem.parentElement) elem.parentElement.removeChild(elem); 127 }, 128 129 rotate(elem, a, o = "center"){ 130 elem.style.transformOrigin = o; 131 elem.style.transform = `rotate(${a}deg)`; 132 }, 133 134 bindButton(elem, callback, ondown = null){ 135 var timeout = 0; 136 137 const param = {offsetX: 0, offsetY: 0}, 138 139 onUp = event => { 140 elem.removeEventListener('pointerup', onUp); 141 if(Date.now() - timeout < 300) callback(event, param); 142 }, 143 144 onDown = event => { 145 timeout = Date.now(); 146 param.offsetX = event.offsetX; 147 param.offsetY = event.offsetY; 148 if(ondown !== null) ondown(event, param); 149 elem.removeEventListener('pointerup', onUp); 150 elem.addEventListener('pointerup', onUp); 151 } 152 153 elem.addEventListener('pointerdown', onDown); 154 155 return function (){ 156 elem.removeEventListener('pointerup', onUp); 157 elem.removeEventListener('pointerdown', onDown); 158 } 159 }, 160 161 bindMobileButton(elem, callback, ondown = null){ 162 var timeout = 0, rect; 163 164 const param = {offsetX: 0, offsetY: 0}, 165 166 onUp = event => { 167 event.preventDefault(); 168 if(Date.now() - timeout < 300) callback(event, param); 169 }, 170 171 onDown = event => { 172 event.preventDefault(); 173 timeout = Date.now(); 174 rect = elem.getBoundingClientRect(); 175 param.offsetX = event.targetTouches[0].pageX - rect.x; 176 param.offsetY = event.targetTouches[0].pageY - rect.y; 177 if(ondown !== null) ondown(event, param); 178 } 179 180 elem.addEventListener('touchend', onUp); 181 elem.addEventListener('touchstart', onDown); 182 183 return function (){ 184 elem.removeEventListener('touchend', onUp); 185 elem.removeEventListener('touchstart', onDown); 186 } 187 }, 188 189 } 190 191 192 function gradientColor(gradient, colors, close = false){ 193 if(UTILS.emptyArray(colors)) return; 194 const len = colors.length; 195 if(close === false){ 196 for(let k = 0, _len = len - 1; k < len; k++) gradient.addColorStop(k / _len, colors[k]); 197 }else{ 198 for(let k = 0; k < len; k++) gradient.addColorStop(k / len, colors[k]); 199 gradient.addColorStop(1, colors[0]); 200 } 201 } 202 203 function gradientColorSymme(gradient, colors){ 204 if(Array.isArray(colors) === true){ 205 206 const len = Math.round(colors.length/2), count = len * 2; 207 208 for(let k = 0; k < len; k++){ 209 gradient.addColorStop(k / count, colors[k]); 210 } 211 212 for(let k = len, i = len; k >= 0; k--, i++){ 213 gradient.addColorStop(i / count, colors[k]); 214 } 215 216 } 217 } 218 219 /* CanvasElementEvent domElement 绑定 移动 或 桌面 down, move, up 事件 (使两端的事件触发逻辑和参数保持一致) 220 parameter: null 221 attributes: null 222 method: 223 initEvent(domElement, list, box): function; 224 initEventMobile(domElement, list, box): function; 225 domElement: HTMLCanvasElement 226 list: Array[CanvasEventTarget] 227 box: Box 228 function: 删除绑定的事件 229 */ 230 class CanvasElementEvent{ 231 232 constructor(){ 233 234 } 235 236 initEvent(domElement, list, box){ 237 const ondown = event => { 238 const sTime = UTILS.time; 239 240 let i, ci, len = list.length; 241 //为什么不用event.offsetX, 而是 rect, 用rect是为了鼠标即使移出了目标dom的范围其参数值也是有效的 242 const targets = [], rect = domElement.getBoundingClientRect(), 243 _offsetX = event.pageX - rect.x, 244 _offsetY = event.pageY - rect.y, 245 offsetX = _offsetX + box.x, 246 offsetY = _offsetY + box.y; 247 248 for(i = 0; i < len; i++){ 249 ci = list[i]; 250 if(ci.visible === false) continue; 251 if(ci.position === ""){ 252 if(ci.box.containsPoint(offsetX, offsetY) === false) continue; 253 } else if(ci.position === "fixed"){ 254 if(ci.box.containsPoint(_offsetX, _offsetY) === false) continue; 255 } 256 if(targets.length === 0) targets[0] = ci; 257 else{ 258 if(ci.index === targets[0].index) targets.push(ci); 259 else if(ci.index > targets[0].index){ 260 targets.length = 0; 261 targets[0] = ci; 262 } 263 } 264 } 265 266 len = targets.length; 267 268 if(len !== 0){ 269 270 const info = {targets: targets, target: null, offsetX: offsetX, offsetY: offsetY, delta: 0}, 271 272 onmove = event => { 273 info.offsetX = event.pageX - rect.x + box.x, 274 info.offsetY = event.pageY - rect.y + box.y; 275 info.delta = UTILS.time - sTime; 276 for(i = 0; i < len; i++){ 277 info.target = targets[i]; 278 targets[i].trigger("move", info, event); 279 } 280 }, 281 282 onup = event => { 283 domElement.releasePointerCapture(event.pointerId); 284 domElement.removeEventListener("pointerup", onup); 285 domElement.removeEventListener("pointermove", onmove); 286 info.delta = UTILS.time - sTime; 287 for(i = 0; i < len; i++){ 288 info.target = targets[i]; 289 targets[i].trigger("up", info, event); 290 } 291 } 292 293 domElement.setPointerCapture(event.pointerId); 294 domElement.addEventListener("pointerup", onup); 295 domElement.addEventListener("pointermove", onmove); 296 for(i = 0; i < len; i++){ 297 info.target = targets[i]; 298 targets[i].trigger("down", info, event); 299 } 300 301 } 302 303 } 304 305 domElement.addEventListener("pointerdown", ondown); 306 return function (){ 307 domElement.removeEventListener("pointerdown", ondown); 308 } 309 } 310 311 initEventMobile(domElement, list, box){ 312 313 const ondown = event => { 314 const sTime = UTILS.time; 315 event.preventDefault(); 316 let i, ci, len = list.length; 317 318 const targets = [], rect = domElement.getBoundingClientRect(), 319 _offsetX = event.targetTouches[0].pageX - rect.x, 320 _offsetY = event.targetTouches[0].pageY - rect.y, 321 offsetX = _offsetX + box.x, 322 offsetY = _offsetY + box.y; 323 324 for(i = 0; i < len; i++){ 325 ci = list[i]; 326 if(ci.visible === false) continue; 327 if(ci.position === ""){ 328 if(ci.box.containsPoint(offsetX, offsetY) === false) continue; 329 } else if(ci.position === "fixed"){ 330 if(ci.box.containsPoint(_offsetX, _offsetY) === false) continue; 331 } 332 if(targets.length === 0) targets[0] = ci; 333 else{ 334 if(ci.index === targets[0].index) targets.push(ci); 335 else if(ci.index > targets[0].index){ 336 targets.length = 0; 337 targets[0] = ci; 338 } 339 } 340 } 341 342 len = targets.length; 343 344 if(len !== 0){ 345 346 const info = {targets: targets, target: null, offsetX: offsetX, offsetY: offsetY, delta: 0}, 347 348 onmove = event => { 349 info.offsetX = event.targetTouches[0].pageX - rect.x + box.x; 350 info.offsetY = event.targetTouches[0].pageY - rect.y + box.y; 351 info.delta = UTILS.time - sTime; 352 for(i = 0; i < len; i++){ 353 info.target = targets[i]; 354 targets[i].trigger("move", event); 355 } 356 }, 357 358 onup = event => { 359 domElement.removeEventListener("touchmove", onmove); 360 domElement.removeEventListener("touchend", onup); 361 domElement.removeEventListener("touchcancel", onup); 362 info.delta = UTILS.time - sTime; 363 for(i = 0; i < len; i++){ 364 info.target = targets[i]; 365 targets[i].trigger("up", info, event) 366 } 367 } 368 369 domElement.addEventListener("touchcancel", onup); 370 domElement.addEventListener("touchend", onup); 371 domElement.addEventListener("touchmove", onmove); 372 for(i = 0; i < len; i++){ 373 info.target = targets[i]; 374 targets[i].trigger("down", info, event); 375 } 376 377 } 378 379 } 380 381 domElement.addEventListener("touchstart", ondown); 382 return function (){ 383 domElement.removeEventListener("touchstart", ondown); 384 } 385 386 } 387 388 } 389 390 391 /* CanvasEvent 支持 移动 和 桌面 端 392 parameter: 393 canvasImageRender: CanvasImageDraw; 394 395 method: 396 initEvent(domElement, list, box): Function; 397 initEventMobile(domElement, list, box): Function; 398 399 demo: 400 const cid = new CanvasImageDraw({openEvent: true, width: 100, height: 100}); 401 const button = new CanvasTextView("BUTTON", {padding: 4, margin: 0, isDisable: true}); 402 cid.append(button).render(); 403 404 button.addEventListener("up", (info, event) => { 405 if(info.delta < 300) console.log("up", info, event); 406 }); 407 button.addEventListener("down", (info, event) => console.log("down", info, event)); 408 */ 409 class CanvasEvent extends CanvasElementEvent{ 410 411 constructor(canvasImageRender){ 412 super(); 413 414 if(UTILS.isMobile){ 415 this.exitEventFunc = this.initEventMobile(canvasImageRender.domElement, canvasImageRender.list, canvasImageRender.box); 416 }else{ 417 this.exitEventFunc = this.initEvent(canvasImageRender.domElement, canvasImageRender.list, canvasImageRender.box); 418 } 419 420 } 421 422 } 423 424 425 /* CanvasPath2D CanvasImage.path2D 426 parameter: 427 drawType = "stroke", drawStyle = null, drawBefore = false 428 429 attributes: 430 drawBefore: Bool; //是否在 CanvasImage 之前绘制 431 drawType: String; //填充路径或描绘路径 可能值: 默认 stroke | fill 432 drawStyle: Object; //canvas.context的属性 433 434 method: 435 line(line: Line): undefined; //注意: 参数属性值都是相对于CanvasImage的位置, 参数为引用, 既当参数的属性发生改变时下一次绘制也会生效 436 path(path2D: Path2D): undefined; // 437 438 demo: 439 const test = new CanvasImageCustom().size(100, 100).pos(100, 100).rect().fill("#664466"); 440 test.path2D = new CanvasPath2D("stroke", {strokeStyle: "blue", lineWidth: 4}); 441 442 const path2D = new Path2D(); 443 path2D.roundRect(12, 12, 40, 40, 10); //圆角矩形 444 test.path2D.path(path2D); 445 446 //test.path2D.line(new Line(4, 4, 4, 150000)); 447 448 */ 449 class CanvasPath2D{ 450 451 static emptySVGMatrix = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix(); 452 static resetSVGMatrix(svgMatrix = CanvasPath2D.emptySVGMatrix){ 453 svgMatrix.a = svgMatrix.d = 1; 454 svgMatrix.b = svgMatrix.c = 455 svgMatrix.e = svgMatrix.f = 0; 456 } 457 458 #pathType = ""; 459 get pathType(){ 460 return this.#pathType; 461 } 462 463 #value = null; 464 get value(){ 465 return this.#value; 466 } 467 468 constructor(drawType = "stroke", drawStyle = {strokeStyle: "#ffffff"}, drawBefore = false){ 469 this.drawBefore = drawBefore; 470 this.drawType = drawType; 471 this.drawStyle = drawStyle; 472 } 473 474 reset(){ 475 this.#pathType = ""; 476 this.#value = null; 477 } 478 479 line(line){ 480 this.#pathType = "line"; 481 this.#value = line; 482 } 483 484 path(path2D){ 485 this.#pathType = "path2D"; 486 this.#value = path2D; 487 } 488 489 _draw(con, x, y, w, h){ 490 con.save(); 491 con.beginPath(); 492 con.rect(x, y, w, h); 493 con.clip(); 494 con.translate(x, y); 495 switch(this.pathType){ 496 case "line": 497 con.beginPath(); 498 con.moveTo(this.value.x, this.value.y); 499 con.lineTo(this.value.x1, this.value.y1); 500 if(this.drawStyle === null) con.stroke(); 501 else{ 502 con.save(); 503 let n = ""; 504 for(n in this.drawStyle){ 505 if(this.drawStyle[n] !== con[n]) con[n] = this.drawStyle[n]; 506 } 507 con.stroke(); 508 con.restore(); 509 } 510 break; 511 512 case "path2D": 513 if(this.drawStyle === null) con[this.drawType](this.value); 514 else{ 515 let n = ""; 516 for(n in this.drawStyle){ 517 if(this.drawStyle[n] !== con[n]) con[n] = this.drawStyle[n]; 518 } 519 con[this.drawType](this.value); 520 } 521 break; 522 } 523 con.restore(); 524 } 525 526 } 527 528 529 /* CanvasImageDraw 530 parameter: 531 option = { 532 drawType //默认 3 533 alpha //默认 true 534 className //默认 "" 535 openEvent //默认 false, 是否启用dom事件(如果为true使ci绑定的事件将生效) 536 width, height: Number || objcet: HTMLCanvasElement, CanvasImageCustom 537 } 538 539 attribute: 540 domElement: HTMLCanvasElement; 541 context: CanvasRenderingContext2D; 542 list: Array[CanvasImage] 543 drawType: Number; //怎么去绘制列表, 默认 3 544 0: 纯净模式(遍历列表: context.drawImage(CI.image, CI.x, CI.y)) 545 1: 应用CIR内置属性 546 2: 应用CI内置属性 547 3: 1 + 2 548 549 method: 550 append(...ci: CanvasImage): this; //追加多个ci到列表 551 size(w, h: Number): this; //设置box和canvas的宽高 552 render(parentElem: HTMLElement): this; //重绘所有的 CanvasImage 并把 canvas 添加至dom树 553 redraw(): undefined; //重绘所有的 CanvasImage 554 redrawCI(ci: CanvasImage): undefined; //重绘一个 CanvasImage 555 exit(): undefined; //不在使用此类应调用此方法清除相关缓存并从dom树删除画布; 556 sortCIIndex(): undefined; //所有ci按其 index 属性值从小到大排序一次(视图的层级, 可以绑定 "beforeDraw" 事件, 达到自动更新) 557 558 sortCIPosEquals(list, option): this; //平铺排序(假设list里的所有CanvasImage的大小都一样) 559 list: Array[CanvasImage]; //默认 this.list 560 option: Object{ 561 disX, disY: Number, //CanvasImage 之间的间距,默认 0 562 sx, sy: Number, //开始位置, 默认 0 563 size: Object{w,h}, //如果定义就计算并在其上设置所占的宽高 (x,y 为option.sx.sy) 564 lenX: Number, //x轴最多排多少个 默认 1 565 } 566 567 sortCIPos(list, option): this; //平铺排序 (此排序相对于 .sortCIPosEquals() 较慢) 568 list: Array[CanvasImage]; //默认 this.list 569 option: Object{ 570 disX, disY: Number, //CanvasImage 之间的间距,默认 0 571 sx, sy: Number, //开始位置, 默认 0 572 size: Object{w,h}, //如果定义就计算并在其上设置所占的宽高 (x,y 为option.sx.sy) 573 lineHeight: String, //可能值: top, middle, bottom; 默认 top 574 } 575 576 initEventDispatcher(): this; //初始化自定义事件 (如果不需要这些事件可以不用初始化) 577 支持的事件名: 578 beforeDraw: eventParam{target: CanvasImageDraw} 579 afterDraw: eventParam{target: CanvasImageDraw} 580 boxX: eventParam{target: CanvasImageDraw} 581 boxY: eventParam{target: CanvasImageDraw} 582 size: eventParam{target: CanvasImageDraw} 583 exit: eventParam{target: CanvasImageDraw} 584 append: eventParam{target: Array[CanvasImage]} 585 add: eventParam{target: CanvasImage} 586 remove: eventParam{target: CanvasImage} 587 588 createCircleDiffusion(t, mr, fillStyle): Function; //点击画布时播放向外扩散的圆 589 t: Number; //扩散至最大半径的时间, 默认200 590 mr: Number; //扩散圆的最大半径, 默认25 591 fillStyle: String; //填充扩散圆的主要颜色; 默认"rgba(0,244,255,0.5)" 592 Function //返回值, 用于退出此程序的函数 593 594 demo: 595 //用 new Image() 加载20万个图片直接崩了 (用canvas就稍微有点卡) 596 //如果用 CanvasImageCustom 去绘制形状或文字一样会崩 597 598 const cir = new CanvasImageDraw({width: 600, height: 300}), 599 cis = new CanvasImageScroll(cir, {scrollSize: 4}); 600 cir.domElement.style = ` 601 background: rgb(127,127,127); 602 `; 603 604 const img = new CanvasImage().loadImage(`${RootDir}examples/img/Stuffs/1.png`, () => { 605 const option = { 606 disX: 10, disY: 10, //CanvasImage 之间的间距,默认 0 607 sx: 0, sy: 0, //开始位置, 默认 0 608 size: {}, //如果定义就在其上设置结束时的矩形 609 lineHeight: "", //可能值: top, middle, bottom; 默认 top 610 lenX: Math.floor(cir.box.w / 50), 611 } 612 613 //测试排序 614 for(let i = 0; i < 20; i++){ 615 console.time("test"); 616 //cir.sortCIPos(null, option); 617 cir.sortCIPosEquals(null, option); 618 console.timeEnd("test"); 619 } 620 621 cir.render(); 622 console.log(cir, cis, option.size); 623 }); 624 625 for(let i = 0; i < 200000; i++){ 626 const ci = new CanvasImage(img); 627 cis.bindScroll(ci); //cis 能够监听此ci 628 cis.changeCIAdd(ci); //cis 初始化此ci 629 cir.list[i] = ci; //cir 能够绘制此ci 630 } 631 */ 632 class CanvasImageDraw{ 633 634 static arrSort = function (a, b){return a["index"] - b["index"];} 635 static paramCon = {alpha: true} 636 637 static defaultStyles = { 638 filter: "none", 639 globalAlpha: 1, 640 globalCompositeOperation: "source-over", 641 imageSmoothingEnabled: false, //是否启用平滑处理图像 642 miterLimit: 10, 643 font: "12px SimSun, Songti SC", 644 textAlign: "left", 645 textBaseline: "top", 646 lineCap: "butt", 647 lineJoin: "miter", 648 lineDashOffset: 0, 649 lineWidth: 1, 650 shadowColor: "rgba(0, 0, 0, 0)", 651 shadowBlur: 0, 652 shadowOffsetX: 0, 653 shadowOffsetY: 0, 654 fillStyle: "#ffffff", 655 strokeStyle: "#666666", 656 } 657 658 static setDefaultStyles(context){ 659 const styles = CanvasImageDraw.defaultStyles; 660 for(let k in styles){ 661 if(context[k] !== styles[k]) context[k] = styles[k]; 662 } 663 } 664 665 static getContext(canvas, className, alpha = true){ 666 if(CanvasImageDraw.isCanvas(canvas) === false) canvas = document.createElement("canvas"); 667 CanvasImageDraw.paramCon.alpha = alpha; 668 const context = canvas.getContext("2d", CanvasImageDraw.paramCon); 669 670 if(typeof className === "string") canvas.className = className; 671 //if(typeof id === "string") canvas.setAttribute('id', id); 672 673 return context; 674 } 675 676 static isCanvasImage(img){ //OffscreenCanvas: ImageBitmap; 677 678 return ImageBitmap["prototype"]["isPrototypeOf"](img) || 679 HTMLImageElement["prototype"]["isPrototypeOf"](img) || 680 HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 681 CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 682 HTMLVideoElement["prototype"]["isPrototypeOf"](img); 683 684 } 685 686 static isCanvas(canvas){ 687 return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas); 688 } 689 690 #cded = null; 691 692 #eventDispatcher = null; 693 get eventDispatcher(){return this.#eventDispatcher;} 694 695 #pointEV = new Point(); 696 #boxV = new Box(); 697 #box = null; 698 get box(){return this.#box;} 699 700 constructor(option = {}){ 701 this.list = []; 702 this.drawType = option.drawType === undefined ? 3 : option.drawType; 703 704 if(UTILS.isObject(option.object) === false){ 705 706 this.#box = new Box(); 707 this.context = CanvasImageDraw.getContext(null, option.className, option.alpha); 708 this.domElement = this.context.canvas; 709 this.size(option.width, option.height); 710 711 } else { 712 713 if(CanvasImageDraw.isCanvas(option.object) === true){ 714 this.#box = new Box(); 715 this.context = CanvasImageDraw.getContext(option.object, option.className, option.alpha); 716 this.domElement = this.context.canvas; 717 this.size(option.object.width, option.object.height); 718 } 719 720 else if(CanvasImageCustom.prototype.isPrototypeOf(option.object) === true){ 721 this.#box = option.object.box; 722 this.context = option.object.context; 723 this.domElement = option.object.image; 724 } 725 726 else{ 727 this.#box = new Box(); 728 this.context = CanvasImageDraw.getContext(null, option.className, option.alpha); 729 this.domElement = this.context.canvas; 730 this.size(option.width, option.height); 731 } 732 733 } 734 735 if(option.openEvent === true) this.#cded = new CanvasEvent(this); 736 } 737 738 createCircleDiffusion(t = 200, mr = 25, fillStyle = "rgba(0,244,255,0.5)"){ 739 var x = 0, y = 0, st = 0, r = 0; 740 const PI2 = Math.PI*2, context = this.context, oldFillStyle = context.fillStyle, 741 colors = [ 742 "rgba(0,0,0,0)", 743 fillStyle, 744 "rgba(0,0,0,0)", 745 ], 746 747 animateLoop = new AnimateLoop(() => { 748 if(r <= mr){ 749 this.redraw(); 750 r = (UTILS.time - st) / t * mr; 751 context.beginPath(); 752 context.arc(x, y, r, 0, PI2); 753 const radialGradient = context.createRadialGradient(x, y, 0, x, y, r); 754 gradientColorSymme(radialGradient, colors); 755 context.fillStyle = radialGradient; 756 context.fill(); 757 } 758 else onstop(); 759 }), 760 761 onstop = () => { 762 context.fillStyle = oldFillStyle; 763 animateLoop.stop(); 764 this.redraw(); 765 }, 766 767 ondown = event => { 768 x = event.offsetX; 769 y = event.offsetY; 770 st = UTILS.time; 771 r = 0; 772 animateLoop.play(); 773 } 774 775 this.domElement.addEventListener("pointerdown", ondown); 776 return function (){ 777 onstop(); 778 this.domElement.removeEventListener("pointerdown", ondown); 779 } 780 } 781 782 initEventDispatcher(){ 783 this.#eventDispatcher = new EventDispatcher(); 784 const eventParam = {target: this} 785 this.#eventDispatcher. 786 customEvents("beforeDraw", eventParam) 787 .customEvents("afterDraw", eventParam) 788 .customEvents("beforeDrawTarget", eventParam) 789 .customEvents("afterDrawTarget", eventParam) 790 .customEvents("boxX", eventParam) 791 .customEvents("boxY", eventParam) 792 .customEvents("size", eventParam) 793 .customEvents("exit", eventParam) 794 .customEvents("append", eventParam) 795 .customEvents("add", eventParam) 796 .customEvents("remove", eventParam); 797 798 let _x = this.#box.x, _y = this.#box.y; 799 Object.defineProperties(this.#box, { 800 801 x: { 802 get: () => {return _x;}, 803 set: v => { 804 if(v !== _x && isNaN(v) === false){ 805 _x = v; 806 this.#eventDispatcher.trigger("boxX"); 807 } 808 } 809 }, 810 811 y: { 812 get: () => {return _y;}, 813 set: v => { 814 if(v !== _y && isNaN(v) === false){ 815 _y = v; 816 this.#eventDispatcher.trigger("boxY"); 817 } 818 } 819 }, 820 821 }); 822 823 return this; 824 } 825 826 sortCIIndex(){ 827 this.list.sort(CanvasImageDraw.arrSort); 828 } 829 830 sortCIPosEquals(list, option = {}){ 831 if(Array.isArray(list) === false) list = this.list; 832 if(list.length === 0) return; 833 834 const lenX = option.lenX || 1, 835 disX = option.disX || 0, 836 disY = option.disY || 0, 837 x = option.sx || 0, y = option.sy || 0, 838 w = list[0].w, h = list[0].h; 839 840 for(let i = 0, ix, iy; i < list.length; i++){ 841 ix = i % lenX; 842 iy = Math.floor(i / lenX); 843 list[i].box.pos(ix * w + ix * disX + x, iy * h + iy * disY + y); 844 } 845 846 if(option.size !== undefined){ 847 option.size.w = w * lenX + disX * lenX - disX + x; 848 const lenY = Math.ceil(list.length / lenX); 849 option.size.h = h * lenY + disY * lenY - disY + y; 850 } 851 852 return this; 853 } 854 855 sortCIPos(list, option = {}){ 856 if(Array.isArray(list) === false) list = this.list; 857 const len = list.length; 858 if(len === 0) return; 859 860 const mw = option.width || this.box.w, 861 indexs = option.lineHeight === "middle" || option.lineHeight === "bottom" ? [] : null, //[sIndex, length, mHeight] 862 sx = option.sx || 0, 863 sy = option.sy || 0, 864 disX = option.disX || 0, 865 disY = option.disY || 0, 866 rect = option.size || null; 867 if(rect !== null){ 868 rect.w = list[0].box.mx; 869 rect.h = list[0].box.my; 870 } 871 872 var y = sy, x = sx, w = 0, h = list[0].h; 873 for(let i = 0; i < len; i++){ 874 if(indexs !== null && indexs.length % 3 === 0) indexs.push(i); 875 w = list[i].w; 876 if(x + w + disX > mw){ 877 if(indexs !== null) indexs.push(i, h, i); 878 x = w + sx + disX; 879 y += h + disY; 880 h = list[i].h; 881 list[i].box.pos(sx, y); 882 } else { 883 list[i].box.pos(x, y); 884 x += w + disX; 885 h = Math.max(list[i].h, h); 886 if(rect !== null){ 887 rect.w = Math.max(list[i].box.mx, rect.w); 888 rect.h = Math.max(list[i].box.my, rect.h); 889 } 890 } 891 } 892 893 if(indexs !== null){ 894 if(indexs.length % 3 === 1) indexs.push(len, h); 895 switch(option.lineHeight){ 896 case "middle": 897 for(let i = 0; i < indexs.length; i += 3){ 898 for(let k = indexs[i]; k < indexs[i + 1]; k++) list[k].box.y += (indexs[i + 2] - list[k].h) / 2; 899 } 900 break; 901 case "bottom": 902 for(let i = 0; i < indexs.length; i += 3){ 903 for(let k = indexs[i]; k < indexs[i + 1]; k++) list[k].box.y += indexs[i + 2] - list[k].h; 904 } 905 break; 906 } 907 } 908 909 if(rect !== null){ 910 rect.w -= sx; 911 rect.h -= sx; 912 } 913 914 return this; 915 } 916 917 size(w, h){ 918 switch(typeof w) { 919 case "number": 920 this.box.size(w, h); 921 break; 922 case "object": 923 this.box.size(w.width||w.w||this.box.w, w.height||w.h||this.box.h); 924 break; 925 } 926 927 this.domElement.width = this.box.w; 928 this.domElement.height = this.box.h; 929 CanvasImageDraw.setDefaultStyles(this.context); 930 931 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("size"); 932 933 return this; 934 } 935 936 render(parentElem = document.body){ 937 this.redraw(); 938 if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement); 939 return this; 940 } 941 942 exit(){ 943 if(this.domElement.parentElement) this.domElement.parentElement.removeChild(this.domElement); 944 945 if(this.#cded !== null){ 946 if(typeof this.#cded.exitEventFunc === "function") this.#cded.exitEventFunc(); 947 this.#cded = null; 948 } 949 950 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("exit", param => param.value = ca); 951 952 this.list.length = 0; 953 } 954 955 append(...cis){ 956 const len = this.list.length; 957 for(let i = 0; i < cis.length; i++) this.list[i + len] = cis[i]; 958 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("append", param => param.target = cis); 959 return this; 960 } 961 962 add(ci){ 963 if(this.list.includes(ci) === false){ 964 this.list.push(ci); 965 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("add", param => param.target = ci); 966 } 967 return ci; 968 } 969 970 remove(ci){ 971 const i = this.list.indexOf(ci); 972 if(i !== -1){ 973 this.list.splice(i, 1); 974 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("remove", param => param.target = ci); 975 } 976 return ci; 977 } 978 979 isDraw(ca){ 980 switch(ca.position){ 981 case "fixed": 982 if(this.#boxV.equals(this.box) === false) this.#boxV.set(0, 0, this.box.w, this.box.h); 983 return ca["visible"] === true && ca["image"] !== null && this.#boxV["intersectsBox"](ca["box"]); 984 985 default: 986 return ca["visible"] === true && ca["image"] !== null && this["box"]["intersectsBox"](ca["box"]); 987 } 988 } 989 990 redraw(){ 991 this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']); 992 switch(this.drawType){ 993 case 0: 994 for(let k = 0, ci; k < this.list.length; k++){ 995 ci = this.list[k]; 996 if(ci["image"] !== null && this["box"]["intersectsBox"](ci["box"])) this.context.drawImage(ci.image, ci.x, ci.y); 997 } 998 return; 999 1000 case 1: 1001 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw"); 1002 for(let k = 0, ci; k < this.list.length; k++){ 1003 ci = this.list[k]; 1004 if(ci["image"] !== null && this["box"]["intersectsBox"](ci["box"])) this.context.drawImage(ci.image, ci.x, ci.y); 1005 } 1006 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw"); 1007 return; 1008 1009 case 2: 1010 for(let k = 0, ci; k < this.list.length; k++){ 1011 ci = this.list[k]; 1012 if(this.isDraw(ci) === true) this._draw(ci); 1013 } 1014 return; 1015 1016 case 3: 1017 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw"); 1018 for(let k = 0, ci; k < this.list.length; k++){ 1019 ci = this.list[k]; 1020 if(this.isDraw(ci) === true) this._draw(ci); 1021 } 1022 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw"); 1023 return; 1024 } 1025 } 1026 1027 redrawCI(ci){ 1028 if(this.isDraw(ci) === true){ 1029 this['context']['clearRect'](ci.box.x, ci.box.y, ci.box.w, ci.box.h); 1030 this._draw(ci); 1031 } 1032 } 1033 1034 redrawTarget(box){ 1035 if(CanvasImage["prototype"]["isPrototypeOf"](box) === true) box = box.box; 1036 1037 const _list = [], list = [], len = this.list.length; 1038 1039 for(let k = 0, tar, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){ 1040 tar = this["list"][k]; 1041 1042 if(this.isDraw(tar) === false) continue; 1043 1044 if(box["intersectsBox"](tar["box"]) === true){ 1045 tar["__overlap"] = true; 1046 box["expand"](tar["box"]); 1047 loop = true; 1048 1049 while(loop === true){ 1050 b["length"] = 0; 1051 loop = false; 1052 c = _c; 1053 1054 for(i = 0; i < c; i++){ 1055 tar = a[i]; 1056 1057 if(box["intersectsBox"](tar["box"]) === true){ 1058 tar["__overlap"] = true; 1059 box["expand"](tar["box"]); 1060 loop = true; _c--; 1061 } 1062 1063 else b.push(tar); 1064 1065 } 1066 1067 a = a === _list ? list : _list; 1068 b = b === _list ? list : _list; 1069 1070 } 1071 1072 } 1073 1074 else{ 1075 _c++; 1076 a["push"](tar); 1077 tar["__overlap"] = false; 1078 } 1079 } 1080 1081 this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']); 1082 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw"); 1083 for(let k = 0, ci; k < this.list.length; k++){ 1084 ci = this.list[k]; 1085 if(ci["__overlap"] === true) this._draw(ci); 1086 } 1087 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw"); 1088 } 1089 1090 _draw(ca){ 1091 const con = this.context; 1092 ca.trigger("beforeDraw", con, ca.position === "fixed" ? this.#pointEV.set(0, 0) : this.#pointEV.set(this.#box.x, this.#box.y)); 1093 1094 const x = ca.x - this.#pointEV.x, y = ca.y - this.#pointEV.y; 1095 if(ca.opacity !== con.globalAlpha) con.globalAlpha = ca.opacity; 1096 1097 if(ca.path2D !== null && ca.path2D.drawBefore === true) ca.path2D._draw(con, x, y, ca.w, ca.h); 1098 1099 if(ca.w === ca.width && ca.h === ca.height) con.drawImage(ca.image, x, y); 1100 else con.drawImage(ca.image, x, y, ca.w, ca.h); 1101 1102 if(ca.path2D !== null && ca.path2D.drawBefore === false) ca.path2D._draw(con, x, y, ca.w, ca.h); 1103 1104 ca.trigger("afterDraw", con, this.#pointEV); 1105 } 1106 1107 } 1108 1109 1110 /* CanvasImageScroll 画布滚动条(CanvasImageDraw.box.xy的控制器) 1111 parameter: 1112 cid: CanvasImageDraw, 1113 option: Object{ 1114 scrollVisible //滚动条的显示; 可能值: 默认"visible" || "" || "auto"; //注意: auto 只支持移动端由事件驱动视图的显示 1115 scrollSize //滚动条的宽或高; 默认 10; (如果想隐藏滚动条最好 scrollVisible: "", 而不是把此属性设为0) 1116 1117 scrollEventType //可能的值: "default", "touch" || ""; 默认 根据自己所处的环境自动选择创建 1118 inertia //是否启用移动端滚动轴的惯性; 默认 true; 1119 inertiaLife //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04; 1120 1121 domElement: //dom事件绑定的目标; 默认 cid.domElement 1122 fillStyle 1123 strokeStyle 1124 } 1125 1126 attribute: 1127 enable: Bool; //是否启用; 默认 true 1128 1129 //只读: 1130 maxSize: Point; //最大宽高 1131 cursorX: Number; //相对于视口宽的滚动条的 left (CanvasImageDraw.box.w) 1132 cursorY: Number; 1133 scrollLeft: Number; //相对于最大宽的滚动条的 left (CanvasImageDraw.box.x) 1134 scrollTop: Number; 1135 1136 method: 1137 unbindScroll(ci): undefined; //ci不在使用调用(ci 如果用 CanvasImageDraw.remove 方法从列表中删除的或 CanvasImageDraw 不在使用则无需调用) 1138 unbindEvent(): undefined; //如果不在使用调用 1139 resetMaxSizeX(): undefined; //重新计算最大边界x 1140 resetMaxSizeX(): undefined; //重新计算最大边界y 1141 1142 demo: 1143 手动添加(指不用 CanvasImageDraw 给的方法加入到其队列): 1144 const cid = new CanvasImageDraw(); 1145 const cis = new CanvasImageScroll(); 1146 1147 //Array.push 1148 cid.list.push(ci); 1149 cis.bindScroll(ci); 1150 cis.changeCIAdd(ci); 1151 1152 //Array.splice 1153 ci = cid.list.splice(0, 1)[0]; 1154 cis.unbindScroll(ci); 1155 cis.changeCIDel(ci); 1156 1157 这样做也可以顺利更新最大边界与游标 1158 */ 1159 class CanvasImageScroll{ 1160 1161 #cid = null; 1162 #maxSize = new Point(); 1163 get maxSize(){return this.#maxSize;} 1164 get cursorX(){return this.#cid.box.x/this.#maxSize.x*this.#cid.box.w;} 1165 get cursorY(){return this.#cid.box.y/this.#maxSize.y*this.#cid.box.h;} 1166 get scrollLeft(){return this.#cid.box.x;} 1167 get scrollTop(){return this.#cid.box.y;} 1168 1169 constructor(cid, option = {}){ 1170 if(!cid.eventDispatcher) cid.initEventDispatcher(); 1171 1172 this.#cid = cid; 1173 this.scrollSize = UTILS.isNumber(option.scrollSize) ? option.scrollSize : 10; 1174 this.fillStyle = option.fillStyle || "#ffffff"; 1175 this.strokeStyle = option.strokeStyle || "#666666"; 1176 this.enable = true; 1177 1178 switch(option.scrollVisible){ 1179 case "auto": 1180 case "": 1181 this.scrollVisible = option.scrollVisible; 1182 break; 1183 1184 case "visible": 1185 default: 1186 this.scrollVisible = "visible"; 1187 break; 1188 } 1189 1190 switch(option.scrollEventType){ 1191 case "touch": 1192 this.__unbindEvent = this.createScrollEventMobile(option.domElement || cid.domElement, option.inertia, option.inertiaLife); 1193 break; 1194 1195 case "default": 1196 this.__unbindEvent = this.createScrollEventPC(option.domElement || cid.domElement); 1197 break; 1198 1199 default: 1200 if(UTILS.isMobile) this.__unbindEvent = this.createScrollEventMobile(option.domElement || cid.domElement, option.inertia, option.inertiaLife); 1201 else this.__unbindEvent = this.createScrollEventPC(option.domElement || cid.domElement); 1202 break; 1203 } 1204 1205 const cirED = cid.eventDispatcher; 1206 cirED.register("boxX", () => this.changeCursorX()); 1207 cirED.register("boxY", () => this.changeCursorY()); 1208 1209 cirED.register("append", event => { 1210 const list = event.target; 1211 var x = 0, y = 0; 1212 for(let k = 0, len = list.length, ci, m; k < len; k++){ 1213 ci = list[k]; 1214 this.bindScroll(ci); 1215 if(ci.visible === true){ 1216 m = ci.box.mx; 1217 if(m > x) x = m; 1218 m = ci.box.my; 1219 if(m > y) y = m; 1220 } 1221 } 1222 if(x > this.#maxSize.x){ 1223 this.#maxSize.x = x; 1224 this.changeCursorX(); 1225 } 1226 if(y > this.#maxSize.y){ 1227 this.#maxSize.y = y; 1228 this.changeCursorY(); 1229 } 1230 }); 1231 1232 cirED.register("add", event => { 1233 this.bindScroll(event.target); 1234 this.changeCIAdd(event.target); 1235 }); 1236 1237 cirED.register("remove", event => { 1238 this.unbindScroll(event.target); 1239 this.changeCIDel(event.target); 1240 }); 1241 1242 if(this.scrollVisible === "visible"){ 1243 cirED.register("afterDraw", () => { 1244 this.drawScrollX(); 1245 this.drawScrollY(); 1246 }); 1247 } 1248 1249 if(cid.list.length !== 0) this.bindScrolls(); 1250 } 1251 1252 changeCursorX(){ 1253 if(this.#cid.box.x < 0 || this.#cid.box.w >= this.#maxSize.x) this.#cid.box.x = 0; 1254 else if(this.#cid.box.mx > this.#maxSize.x) this.#cid.box.x = this.#maxSize.x - this.#cid.box.w; 1255 } 1256 1257 changeCursorY(){ 1258 if(this.#cid.box.y < 0 || this.#cid.box.h >= this.#maxSize.y) this.#cid.box.y = 0; 1259 else if(this.#cid.box.my > this.#maxSize.y) this.#cid.box.y = this.#maxSize.y - this.#cid.box.h; 1260 } 1261 1262 changeCIAdd(ci){ 1263 if(ci.visible === false) return; 1264 var v = ci.box.mx; 1265 if(v > this.#maxSize.x){ 1266 this.#maxSize.x = v; 1267 this.changeCursorX(); 1268 } 1269 v = ci.box.my; 1270 if(v > this.#maxSize.y){ 1271 this.#maxSize.y = v; 1272 this.changeCursorY(); 1273 } 1274 } 1275 1276 changeCIDel(ci){ 1277 if(ci.visible === false) return; 1278 if(ci.box.mx >= this.#maxSize.x){ 1279 this.resetMaxSizeX(); 1280 this.changeCursorX(); 1281 } 1282 if(ci.box.my >= this.#maxSize.y){ 1283 this.resetMaxSizeY(); 1284 this.changeCursorY(); 1285 } 1286 } 1287 1288 createScrollEventPC(domElement){ 1289 var dPos = -1, rect; 1290 1291 const box = this.#cid.box, cursorBox = new Box(), 1292 1293 al_draw = new Timer(() => this.#cid.redraw(), 1000/30, 1, false), 1294 1295 setTop = top => { 1296 box.y = top / box.h * this.#maxSize.y; 1297 if(al_draw.running === false) this.#cid.redraw(); 1298 al_draw.start(); 1299 }, 1300 1301 setLeft = left => { 1302 box.x = left / box.w * this.#maxSize.x; 1303 if(al_draw.running === false) this.#cid.redraw(); 1304 al_draw.start(); 1305 }, 1306 1307 onMoveTop = event => { 1308 if(this.enable === false) return onUpTop(); 1309 setTop(event.clientY - rect.y - dPos); 1310 }, 1311 1312 onMoveLeft = event => { 1313 if(this.enable === false) return onUpLeft(); 1314 setLeft(event.clientX - rect.x - dPos); 1315 }, 1316 1317 onUpTop = () => { 1318 document.body.removeEventListener('pointermove', onMoveTop); 1319 document.body.removeEventListener('pointerup', onUpTop); 1320 }, 1321 1322 onUpLeft = () => { 1323 document.body.removeEventListener('pointermove', onMoveLeft); 1324 document.body.removeEventListener('pointerup', onUpLeft); 1325 }, 1326 1327 ondown = event => { 1328 onUpTop(); 1329 onUpLeft(); 1330 if(this.enable === false) return; 1331 rect = domElement.getBoundingClientRect(); 1332 cursorBox.set( 1333 this.cursorX, 1334 this.#cid.box.h - this.scrollSize, 1335 this.#maxSize.x <= this.#cid.box.w ? this.#cid.box.w : this.#cid.box.w / this.#maxSize.x * this.#cid.box.w, 1336 this.scrollSize 1337 ); 1338 if(cursorBox.w < 10) cursorBox.w = 10; 1339 if(cursorBox.containsPoint(event.offsetX, event.offsetY)){ 1340 document.body.addEventListener("pointermove", onMoveLeft); 1341 document.body.addEventListener("pointerup", onUpLeft); 1342 dPos = event.offsetX - this.cursorX; 1343 } else { 1344 cursorBox.set( 1345 this.#cid.box.w - this.scrollSize, 1346 this.cursorY, 1347 this.scrollSize, 1348 this.#maxSize.y <= this.#cid.box.h ? this.#cid.box.h : this.#cid.box.h / this.#maxSize.y * this.#cid.box.h 1349 ); 1350 if(cursorBox.h < 10) cursorBox.h = 10; 1351 if(cursorBox.containsPoint(event.offsetX, event.offsetY)){ 1352 document.body.addEventListener("pointermove", onMoveTop); 1353 document.body.addEventListener("pointerup", onUpTop); 1354 dPos = event.offsetY - this.cursorY; 1355 } 1356 } 1357 }, 1358 1359 onwheel = event => { 1360 if(this.enable === false) return; 1361 if(this.#maxSize.y > box.h){ 1362 dPos = 50 / this.#maxSize.y * box.h; 1363 setTop(this.cursorY + (event.wheelDelta === 120 ? -dPos : dPos)); 1364 } else if(this.#maxSize.x > box.w){ 1365 dPos = 50 / this.#maxSize.x * box.w; 1366 setLeft(this.cursorX + (event.wheelDelta === 120 ? -dPos : dPos)); 1367 } 1368 } 1369 1370 domElement.addEventListener("pointerdown", ondown); 1371 domElement.addEventListener("mousewheel", onwheel); 1372 1373 return function (){ 1374 onUpTop(); 1375 onUpLeft(); 1376 domElement.removeEventListener("pointerdown", ondown); 1377 domElement.removeEventListener("mousewheel", onwheel); 1378 } 1379 } 1380 1381 createScrollEventMobile(domElement, inertia = true, inertiaLife = 0.04){ 1382 var sTime = 0, dis = "", sx = 0, sy = 0; 1383 const box = this.#cid.box, 1384 al_draw = new Timer(() => this.#cid.redraw(), 1000/30, 1, false); 1385 1386 if(inertia === true){ 1387 inertiaLife = 1 - inertiaLife; 1388 var inertiaAnimate = new AnimateLoop(null, null, 1000/30), 1389 step = 1, aniamteRun = false, stepA = "", stepB = "", _sx = 0, _sy = 0, 1390 1391 inertiaTop = speed => { 1392 if(Math.abs(speed) < 0.7) return; 1393 stepA = speed < 0 ? "-top" : "top"; 1394 if(aniamteRun && stepA === stepB) step += 0.3; 1395 else{ 1396 step = 1; 1397 stepB = stepA; 1398 } 1399 inertiaAnimate.play(() => { 1400 speed *= inertiaLife; 1401 box.y += step * 20 * speed; 1402 this.#cid.redraw(); 1403 if(Math.abs(speed) < 0.001 || box.y <= 0 || box.my >= this.#maxSize.y) inertiaAnimate.stop(); 1404 else { 1405 if(this.scrollVisible === "auto"){ 1406 if(this.#maxSize.x > this.#cid.box.w) this.drawScrollX(); 1407 if(this.#maxSize.y > this.#cid.box.h) this.drawScrollY(); 1408 } 1409 } 1410 }); 1411 }, 1412 1413 inertiaLeft = speed => { 1414 if(Math.abs(speed) < 0.7) return; 1415 stepA = speed < 0 ? "-left" : "left"; 1416 if(aniamteRun && stepA === stepB) step += 0.3; 1417 else{ 1418 step = 1; 1419 stepB = stepA; 1420 } 1421 inertiaAnimate.play(() => { 1422 speed *= inertiaLife; 1423 box.x += step * 20 * speed; 1424 this.#cid.redraw(); 1425 if(Math.abs(speed) < 0.001 || box.x <= 0 || box.mx >= this.#maxSize.x) inertiaAnimate.stop(); 1426 else { 1427 if(this.scrollVisible === "auto"){ 1428 if(this.#maxSize.x > this.#cid.box.w) this.drawScrollX(); 1429 if(this.#maxSize.y > this.#cid.box.h) this.drawScrollY(); 1430 } 1431 } 1432 }); 1433 } 1434 } 1435 1436 const update = event => { 1437 if(dis === "x") box.x = sx - event.targetTouches[0].pageX; 1438 else if(dis === "y") box.y = sy - event.targetTouches[0].pageY; 1439 if(al_draw.running === false) this.#cid.redraw(); 1440 al_draw.start(); 1441 if(this.scrollVisible === "auto"){ 1442 if(this.#maxSize.x > this.#cid.box.w) this.drawScrollX(); 1443 if(this.#maxSize.y > this.#cid.box.h) this.drawScrollY(); 1444 } 1445 }, 1446 1447 onup = () => { 1448 if(inertia === true){ 1449 if(dis === "x") inertiaLeft((box.x - _sx) / (UTILS.time - sTime)); 1450 else if(dis === "y") inertiaTop((box.y - _sy) / (UTILS.time - sTime)); 1451 else this.#cid.redraw(); 1452 } 1453 else this.#cid.redraw(); 1454 }, 1455 1456 onmove = event => { 1457 if(this.enable === false) return; 1458 if(dis !== "") return update(event); 1459 if(UTILS.time - sTime < 60) return; 1460 1461 if(Math.abs(event.targetTouches[0].pageX - sx) > Math.abs(event.targetTouches[0].pageY - sy) && this.#maxSize.x > box.w){ 1462 sx = event.targetTouches[0].pageX + box.x; 1463 dis = "x"; 1464 } else if(this.#maxSize.y > box.h){ 1465 sy = event.targetTouches[0].pageY + box.y; 1466 dis = "y"; 1467 } 1468 1469 if(inertia === true){ 1470 sTime = UTILS.time; 1471 _sx = box.x; 1472 _sy = box.y; 1473 } 1474 }, 1475 1476 ondown = event => { 1477 event.preventDefault(); 1478 if(this.enable === false) return; 1479 if(this.#maxSize.x <= box.w && this.#maxSize.y <= box.h) return; 1480 sx = event.targetTouches[0].pageX; 1481 sy = event.targetTouches[0].pageY; 1482 sTime = UTILS.time; 1483 dis = ""; 1484 if(inertia === true){ 1485 aniamteRun = inertiaAnimate.running; 1486 inertiaAnimate.stop(); 1487 } 1488 1489 if(this.scrollVisible === "auto"){ 1490 if(this.#maxSize.x > this.#cid.box.w) this.drawScrollX(); 1491 if(this.#maxSize.y > this.#cid.box.h) this.drawScrollY(); 1492 } 1493 } 1494 1495 domElement.addEventListener("touchend", onup); 1496 domElement.addEventListener("touchmove", onmove); 1497 domElement.addEventListener("touchstart", ondown); 1498 1499 return function (){ 1500 domElement.removeEventListener("touchend", onup); 1501 domElement.removeEventListener("touchmove", onmove); 1502 domElement.removeEventListener("touchstart", ondown); 1503 } 1504 } 1505 1506 unbindEvent(){ 1507 if(typeof this.__unbindEvent === "function"){ 1508 this.__unbindEvent(); 1509 delete this.__unbindEvent; 1510 } 1511 } 1512 1513 resetMaxSizeX(){ 1514 var v = 0; 1515 for(let k = 0, len = this.#cid.list.length, ci, m; k < len; k++){ 1516 ci = this.#cid.list[k]; 1517 if(ci.visible === true){ 1518 m = ci.box.mx; 1519 if(m > v) v = m; 1520 } 1521 } 1522 if(v !== this.#maxSize.x){ 1523 this.#maxSize.x = v; 1524 this.changeCursorX(); 1525 } 1526 } 1527 1528 resetMaxSizeY(){ 1529 var v = 0; 1530 const list = this.#cid.list, len = list.length; 1531 for(let k = 0, len = this.#cid.list.length, ci, m; k < len; k++){ 1532 ci = this.#cid.list[k]; 1533 if(ci.visible === true){ 1534 m = ci.box.my; 1535 if(m > v) v = m; 1536 } 1537 } 1538 if(v !== this.#maxSize.y){ 1539 this.#maxSize.y = v; 1540 this.changeCursorY(); 1541 } 1542 } 1543 1544 unbindScroll(ci){ 1545 var x = ci.box.x; 1546 delete ci.box.x; 1547 ci.box.x = x; 1548 1549 x = ci.box.y; 1550 delete ci.box.y; 1551 ci.box.y = x; 1552 1553 x = ci.box.w; 1554 delete ci.box.w; 1555 ci.box.w = x; 1556 1557 x = ci.box.h; 1558 delete ci.box.h; 1559 ci.box.h = x; 1560 1561 x = ci.visible; 1562 delete ci.visible; 1563 ci.visible = x; 1564 } 1565 1566 bindScroll(ci){ 1567 const box = ci.box; 1568 var _visible = typeof ci.visible === "boolean" ? ci.visible : true, 1569 _x = box.x, _y = box.y, _w = box.w, _h = box.h, nm, om; 1570 1571 const writeBoxX = () => { 1572 if(nm > this.#maxSize.x){ 1573 this.#maxSize.x = nm; 1574 this.changeCursorX(); 1575 } else if(nm < this.#maxSize.x){ 1576 if(om >= this.#maxSize.x) this.resetMaxSizeX(); 1577 } 1578 }, 1579 1580 writeBoxY = () => { 1581 if(nm > this.#maxSize.y){ 1582 this.#maxSize.y = nm; 1583 this.changeCursorY(); 1584 } else if(nm < this.#maxSize.y){ 1585 if(om >= this.#maxSize.y) this.resetMaxSizeY(); 1586 } 1587 }; 1588 1589 Object.defineProperties(box, { 1590 1591 x: { 1592 get: () => {return _x;}, 1593 set: v => { 1594 if(_visible){ 1595 om = _x+_w; 1596 _x = v; 1597 nm = v+_w; 1598 writeBoxX(); 1599 }else{ 1600 _x = v; 1601 } 1602 } 1603 }, 1604 1605 y: { 1606 get: () => {return _y;}, 1607 set: v => { 1608 if(_visible){ 1609 om = _y+_h; 1610 _y = v; 1611 nm = v+_h; 1612 writeBoxY(); 1613 }else{ 1614 _y = v; 1615 } 1616 } 1617 }, 1618 1619 w: { 1620 get: () => {return _w;}, 1621 set: v => { 1622 if(_visible){ 1623 om = _w+_x; 1624 _w = v; 1625 nm = v+_x; 1626 writeBoxX(); 1627 }else{ 1628 _w = v; 1629 } 1630 } 1631 }, 1632 1633 h: { 1634 get: () => {return _h;}, 1635 set: v => { 1636 if(_visible){ 1637 om = _h+_y; 1638 _h = v; 1639 nm = v+_y; 1640 writeBoxY(); 1641 }else{ 1642 _h = v; 1643 } 1644 } 1645 }, 1646 1647 }); 1648 1649 Object.defineProperties(ci, { 1650 1651 visible: { 1652 get: () => {return _visible;}, 1653 set: v => { 1654 if(v === true){ 1655 _visible = true; 1656 this.changeCIAdd(ci); 1657 } 1658 else if(v === false){ 1659 this.changeCIDel(ci); 1660 _visible = false; 1661 } 1662 } 1663 }, 1664 1665 }); 1666 } 1667 1668 drawScrollX(){ 1669 const x = this.cursorX, 1670 y = this.#cid.box.h - this.scrollSize, 1671 w = this.#maxSize.x <= this.#cid.box.w ? this.#cid.box.w : this.#cid.box.w / this.#maxSize.x * this.#cid.box.w, 1672 h = this.scrollSize, 1673 con = this.#cid.context, fillStyle = con.fillStyle, strokeStyle = con.strokeStyle; 1674 if(fillStyle !== this.fillStyle) con.fillStyle = this.fillStyle; 1675 if(strokeStyle !== this.strokeStyle) con.strokeStyle = this.strokeStyle; 1676 con.fillRect(x,y,w,h >= 10 ? h : 10); 1677 con.strokeRect(x,y,w,h); 1678 if(fillStyle !== con.fillStyle) con.fillStyle = fillStyle; 1679 if(strokeStyle !== con.strokeStyle) con.strokeStyle = strokeStyle; 1680 } 1681 1682 drawScrollY(){ 1683 const x = this.#cid.box.w - this.scrollSize, 1684 y = this.cursorY, 1685 w = this.scrollSize, 1686 h = this.#maxSize.y <= this.#cid.box.h ? this.#cid.box.h : this.#cid.box.h / this.#maxSize.y * this.#cid.box.h, 1687 con = this.#cid.context, fillStyle = con.fillStyle, strokeStyle = con.strokeStyle; 1688 if(fillStyle !== this.fillStyle) con.fillStyle = this.fillStyle; 1689 if(strokeStyle !== this.strokeStyle) con.strokeStyle = this.strokeStyle; 1690 con.fillRect(x,y,w,h >= 10 ? h : 10); 1691 con.strokeRect(x,y,w,h); 1692 if(fillStyle !== con.fillStyle) con.fillStyle = fillStyle; 1693 if(strokeStyle !== con.strokeStyle) con.strokeStyle = strokeStyle; 1694 } 1695 1696 } 1697 1698 1699 /* CanvasEventTarget 1700 parameter: 1701 box: Box; //默认创建一个新的Box 1702 1703 attribute: 1704 box: Box; //.x.y 相对于画布的位置, .w.h 图像的宽高; 1705 visible: Boolean; //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件, scroll也忽略此ci) 1706 index: Integer; //层级(不必唯一) -Infinity 最底层; Infinity 最上层 1707 position: String; //定位 可能值: 默认"", "fixed" (如果为 fixed 其绘制的位置将无视滚动条, 如果存在的话) 1708 1709 method: 1710 addEventListener(eventName, callback): undefined; //添加事件 1711 removeEventListener(eventName, callback): undefined; //删除事件 1712 clearEventListener(eventName): undefined; //删除 eventName 栏的所有回调, 如果eventName未定义则清空所有回调 1713 getEventListener(eventName): Array; //返回某个事件监听器 1714 trigger(eventName, info = null, event = null): undefined; //CanvasEvent 使用此方法触发事件回调 1715 1716 eventNames: 1717 down(info, event) //按下 1718 move(info, event) //移动(按下触发才有效) 1719 up(info, event) //抬起(按下触发才有效) 1720 info: Object{targets, target, offsetX, offsetY, delta}; //此参数的属性兼容于两端 1721 event: event; // 1722 1723 beforeDraw(context, point) //绘制之前 1724 afterDraw(context, point) //绘制之后 1725 context: CanvasRender2D; //当前绘制画布的上下文 1726 point: Point; //CanvasImageDraw.box.xy(滚动条游标的坐标) (如果 .position 为 "fixed", 此对象的值总是 0) 1727 */ 1728 class CanvasEventTarget{ 1729 1730 #eventObject = { 1731 down: null, //Array[function] 1732 move: null, 1733 up: null, 1734 beforeDraw: null, 1735 afterDraw: null, 1736 } 1737 1738 constructor(box = new Box()){ 1739 this.box = box; 1740 this.visible = true; 1741 this.index = 0; 1742 this.position = ""; 1743 } 1744 1745 trigger(eventName, info = null, event = null){ 1746 if(this.#eventObject[eventName] !== null){ 1747 const arr = this.#eventObject[eventName]; 1748 for(let i = 0, len = arr.length; i < len; i++) arr[i](info, event); 1749 } 1750 } 1751 1752 addEventListener(eventName, callback){ 1753 if(this.#eventObject[eventName] === undefined || typeof callback !== "function") return; 1754 if(this.#eventObject[eventName] === null) this.#eventObject[eventName] = [callback]; 1755 else{ 1756 const i = this.#eventObject[eventName].indexOf(callback); 1757 if(i === -1) this.#eventObject[eventName].push(callback); 1758 else this.#eventObject[eventName][i] = callback; 1759 } 1760 } 1761 1762 removeEventListener(eventName, callback){ 1763 if(Array.isArray(this.#eventObject[eventName]) === false) return; 1764 const i = this.#eventObject[eventName].indexOf(callback); 1765 if(i !== -1) this.#eventObject[eventName].splice(i, 1); 1766 if(this.#eventObject[eventName].length === 0) this.#eventObject[eventName] = null; 1767 } 1768 1769 clearEventListener(eventName){ 1770 if(this.#eventObject[eventName] !== undefined) this.#eventObject[eventName] = null; 1771 else{ 1772 for(let n in this.#eventObject) this.#eventObject[n] = null; 1773 } 1774 } 1775 1776 getEventListener(eventName){ 1777 return this.#eventObject[eventName]; 1778 } 1779 1780 } 1781 1782 1783 /* CanvasImage 1784 parameter: 1785 image (构造器会调用一次 .setImage(image) 来处理 image 参数) 1786 1787 attribute: 1788 image: CanvasImage; //目标图像, 默认为null; 1789 opacity: Float; //透明度; 值0至1之间; 默认1; 1790 path2D: CanvasPath2D; //此属性一般用于动态绘制某个形状 1791 loadingImage: Bool; //只读, ci是否正在加载图片或视频 1792 x,y,w,h: Number; //只读, 返回 this.box.xywh 属性值 1793 width, height: Number; //只读, 返回 image 的宽高; 如果未定义就返回 0 1794 1795 method: 1796 pos(x, y): this; //设置位置; x 可以是: Number, Object{x,y} 1797 setImage(image): this; //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image); 1798 1799 loadImage(src, onload): this; //加载并设置图像 (onload 如果是 CanvasImageDraw 则加载完后自动调用一次 redraw 或 render 方法); 1800 loadVideo(src, onload, type = "mp4") //与 .loadImage() 类似 1801 1802 setScaleX(s, x): undefined; //缩放x(注意: 是设置ci的box属性实现的缩放) 1803 setScaleY(s, y): undefined; //缩放y 1804 s为必须, s小于1为缩小, 大于1放大; 1805 x默认为0, x是box的局部位置, 如果是居中缩放x应为: this.w/2, 1806 1807 createRotate(angle, cx , cy: Number): Object; //旋转(注意: 用 beforeDraw, afterDraw 事件设置ci新的绘制位置实现的旋转) 1808 angle: 旋转弧度, 默认0 1809 cx, cy: 旋转中心点(局部), 默认 this.w/2, this.h/2 1810 Object: { 1811 set(angle, cx , cy: Number): undefined; // 1812 offset(cx , cy: Number): undefined; //更新旋转中心点(局部) 1813 angle(angle: Number): undefined; //更新旋转弧度 1814 1815 bind(): undefined; //初始化时自动调用一次此方法 1816 unbind(): undefined; //解除绑定(如果ci不在使用则不需要调用此方法) 1817 } 1818 1819 setPath2DToCircleDiffusion(x,y,option): function; //圆形扩散特效(.path2D 实现的) 1820 x, y: Number; //扩散原点 (相对于画布原点,event.offsetX,event.offsetY) 1821 option: Object{ 1822 cir: CanvasImageDraw; //必须; 1823 animateLoop: AnimateLoop; //默认一个新的 AnimateLoop 1824 t: Number; //持续毫秒时间, 默认 200 1825 mr: Number; //扩散的最大半径, 默认 覆盖整个box: this.box.distanceFromPoint(x, y, true) 1826 path2D: CanvasPath2D; //圆的样式, 默认: new CanvasPath2D("fill", {fillStyle: "rgba(255,255,255,0.2)"}) 1827 } 1828 1829 demo: 1830 //CanvasImageDraw 1831 const cid = new CanvasImageDraw({width: WORLD.width, height: WORLD.height}); 1832 1833 //图片 1834 const ciA = cid.add(new CanvasImage()).pos(59, 59).load("view/img/test.png", cid); 1835 1836 //视频 1837 cid.add(new CanvasImage()) 1838 .loadVideo("view/examples/video/test.mp4", ci => { 1839 1840 //同比例缩放视频 1841 const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100}); 1842 ci.box.size(newSize.width, newSize.height).center(cid.box); 1843 1844 //播放按钮 1845 const cic = cid.add(new CanvasImageCustom()) 1846 .size(50, 30).text("PLAY", "#fff") 1847 .rect(4).stroke("blue"); 1848 cic.box.center(cid.box); 1849 1850 //动画循环 1851 const animateLoop = new AnimateLoop(() => cid.redraw()); 1852 1853 //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画) 1854 cid.addEvent(cic, "up", () => { 1855 cic.visible = false; 1856 ci.image.play(); // ci.image 其实是一个 video 元素 1857 animateLoop.play(); //播放动画 1858 }); 1859 1860 //把 canvas 添加至 dom 树 1861 cid.render(); 1862 1863 }); 1864 1865 //鼠标位置缩放图片例子: (target: CanvasImage) 1866 cie.add(target, "wheel", event => { 1867 1868 const scale = target.scaleX + event.wheelDelta * 0.001, 1869 1870 //offsetX,offsetY 是鼠标到 element 的距离, 现在把它们转为 target 的局部距离 1871 localPositionX = event.offsetX - target.x, 1872 localPositionY = event.offsetY - target.y; 1873 1874 //x,y缩放至 scale 1875 target.setScaleX(scale, localPositionX); 1876 target.setScaleY(scale, localPositionY); 1877 1878 //重绘画布 1879 cid.redraw(); 1880 1881 }); 1882 */ 1883 class CanvasImage extends CanvasEventTarget{ 1884 1885 #loadingImage = false; 1886 get loadingImage(){return this.#loadingImage;} 1887 get x(){return this.box.x;} 1888 get y(){return this.box.y;} 1889 get w(){return this.box.w;} 1890 get h(){return this.box.h;} 1891 get width(){return this.image === null ? 0 : this.image.width;} 1892 get height(){return this.image === null ? 0 : this.image.height;} 1893 get className(){return this.constructor.name;} 1894 1895 constructor(image){ 1896 super(); 1897 this.image = null; 1898 this.opacity = 1; 1899 this.path2D = null; 1900 1901 this.setImage(image); 1902 } 1903 1904 pos(x, y){ 1905 switch(typeof x) { 1906 case "number": 1907 this.box.x = x; 1908 this.box.y = y; 1909 break; 1910 1911 case "object": 1912 this.box.x = x.x || this.box.x; 1913 this.box.y = x.y || this.box.y; 1914 break; 1915 } 1916 1917 return this; 1918 } 1919 1920 setImage(image){ 1921 //如果是image 1922 if(CanvasImageDraw.isCanvasImage(image)){ 1923 this.box.size(image.width, image.height); 1924 this.image = image; 1925 } 1926 //如果是 CanvasImage 1927 else if(CanvasImage.prototype.isPrototypeOf(image)){ 1928 if(image.loadingImage){ 1929 if(Array.isArray(image.__setImageList)) image.__setImageList.push(this); 1930 else image.__setImageList = [this]; 1931 } 1932 else this.setImage(image.image); 1933 } 1934 //忽略此次操作 1935 else{ 1936 this.box.size(0, 0); 1937 this.image = null; 1938 } 1939 return this; 1940 } 1941 1942 setScaleX(s, x = 0){ 1943 const oldVal = this.box.w; 1944 this.box.w = this.width * s; 1945 this.box.x += x - this.box.w / oldVal * x; 1946 } 1947 1948 setScaleY(s, y = 0){ 1949 const oldVal = this.box.h; 1950 this.box.h = this.height * s; 1951 this.box.y += y - this.box.h / oldVal * y; 1952 } 1953 1954 loadImage(src, onload){ 1955 this.#loadingImage = true; 1956 const image = new Image(); 1957 image.onload = () => this._loadSuccess(image, onload); 1958 image.src = src; 1959 return this; 1960 } 1961 1962 loadVideo(src, onload, type = "mp4"){ 1963 /* video 加载事件 的顺序 1964 onloadstart 1965 ondurationchange 1966 onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。 1967 onloadeddata 1968 onprogress 1969 oncanplay 1970 oncanplaythrough 1971 1972 //控制事件: 1973 onended //播放结束 1974 onpause //暂停播放 1975 onplay //开始播放 1976 */ 1977 this.#loadingImage = true; 1978 const video = document.createElement("video"), 1979 source = document.createElement("source"); 1980 video.appendChild(source); 1981 source.type = `video/${type}`; 1982 1983 video.oncanplay = () => { 1984 //video 的 width, height 属性如果不设的话永远都是0 1985 video.width = video.videoWidth; 1986 video.height = video.videoHeight; 1987 this._loadSuccess(video, onload); 1988 }; 1989 1990 source.src = src; 1991 return this; 1992 } 1993 1994 createRotate(angle = 0, cx = this.w/2, cy = this.h/2){ 1995 const scope = this; var nx, ny; 1996 1997 function beforeDraw(con, point){ 1998 nx = cx + scope.x - point.x; 1999 ny = cy + scope.y - point.y; 2000 2001 con.translate(nx, ny); 2002 con.rotate(angle); 2003 2004 //ci 减去translate的nx,ny 和 scroll的point 2005 point.set(nx + point.x, ny + point.y); 2006 2007 //在此之后 cid 会这样操作: 2008 //x = ci.x - point.x 2009 //y = ci.y - point.y 2010 //con.drawImage(ci.image, x, y); 2011 //触发 afterDraw 事件 2012 } 2013 2014 function afterDraw(con){ 2015 con.rotate(-angle); 2016 con.translate(-nx, -ny); 2017 } 2018 2019 this.addEventListener("beforeDraw", beforeDraw); 2020 this.addEventListener("afterDraw", afterDraw); 2021 2022 return { 2023 set(x, y, a){ 2024 cx = x; 2025 cy = y; 2026 angle = a; 2027 }, 2028 2029 offset(x, y){ 2030 cx = x; 2031 cy = y; 2032 }, 2033 2034 angle(v){ 2035 angle = v; 2036 }, 2037 2038 bind(){ 2039 scope.addEventListener("beforeDraw", beforeDraw); 2040 scope.addEventListener("afterDraw", afterDraw); 2041 }, 2042 2043 unbind(){ 2044 scope.removeEventListener("beforeDraw", beforeDraw); 2045 scope.removeEventListener("afterDraw", afterDraw); 2046 } 2047 } 2048 } 2049 2050 setPath2DToCircleDiffusion(x, y, option = {}){ 2051 const pi2 = Math.PI*2, p2d = new Path2D(), oldPath2D = this.path2D; 2052 this.path2D = option.path2D || new CanvasPath2D("fill", {fillStyle: "rgba(255,255,255,0.2)"}); 2053 this.path2D.path(p2d); 2054 2055 const t = option.t || 200, 2056 mr = option.mr || this.box.distanceFromPoint(x, y, true), 2057 cid = option.cir, animateLoop = option.animateLoop || new AnimateLoop(null, null, 1000 / 30); 2058 2059 var r = 0, 2060 st = Date.now(); 2061 2062 x -= this.x; 2063 y -= this.y; 2064 2065 animateLoop.play(() => { 2066 if(r <= mr){ 2067 r = (Date.now() - st) / t * mr; 2068 p2d.arc(x, y, r, 0, pi2, false); 2069 } else { 2070 animateLoop.stop(); 2071 this.path2D = oldPath2D; 2072 } 2073 2074 cid.redraw(); 2075 }); 2076 2077 return () => { 2078 animateLoop.stop(); 2079 this.path2D = oldPath2D; 2080 } 2081 } 2082 2083 _loadSuccess(image, onload){ 2084 this.setImage(image); 2085 2086 this.#loadingImage = false; 2087 if(Array.isArray(this.__setImageList)){ 2088 this.__setImageList.forEach(ci => ci.setImage(image)); 2089 delete this.__setImageList; 2090 } 2091 2092 if(typeof onload === "function") onload(this); 2093 else if(CanvasImageDraw.prototype.isPrototypeOf(onload)){ 2094 if(onload.domElement.parentElement !== null) onload.redraw(); 2095 else onload.render(); 2096 } 2097 } 2098 2099 } 2100 2101 2102 /* CanvasImageCustom 2103 parameter: 2104 canvas || image || undefined 2105 value: Path2D || undefined //如果未定义,初始化时创建一个Path2D,如果是其它值则不创建(例如 null,new Path2D()) 2106 2107 attribute: 2108 value: Path2D; //更换新的 value, 类似 context.beginPath(); 2109 2110 method: 2111 //以下是操作 画布 的方法 2112 cloneCanvas(canvas, dx = 0, dy = 0) 2113 clear(): this; 2114 size(w, h: Number): this; 2115 stroke(color: strokeColor, lineWidth: Number): this; 2116 fill(color: fillColor): this; 2117 2118 //以下是操作 Path2D 的方法 2119 line(x, y, x1, y1: Number): this; 2120 path(arr: Array[x,y], close: Bool): this; 2121 rect(round, lineWidth: Number): this; 2122 strokeRect(color: strokeColor, round, lineWidth: Number): this; 2123 fillRect(color: fillColor, round, lineWidth: Number): this; 2124 2125 //文字 2126 setFontSize(fontSize): this; 2127 getTextWidth(text): Number; 2128 fillText(text, color, x): this; //填充文字,x 如果未定义则居中显示 (y 默认为居中) 2129 fillTextWrap(value, option): this; //填充文字或图像,可以自动换行 2130 value: string || Array[...string] 2131 option: Object{ 2132 color, //context.fillStyle, 默认 this.fillStyle 2133 padding, //内边距, 可能的值: Number || Object{top,right,bottom,left:Number}, 默认 0 2134 distance, //文字之间的间距, 可能的值: Number || Object{x,y:Number}, 默认 0 2135 width, //如果定义则内部调用.size(w, h)方法 2136 height, //必须定义width才有效, 默认 text排序后占的高 2137 ePos: Object{x, y}, //如果定义就在其上设置结束时的位置 2138 } 2139 2140 demo: 2141 const cic = new CanvasImageCustom(null, null).size(100, 100); 2142 const cid = new CanvasImageDraw({object: cic}).render(); 2143 cic.fillTextWrap("abcdefg"); 2144 */ 2145 class CanvasImageCustom extends CanvasImage{ 2146 2147 constructor(canvas, value){ 2148 super(canvas); 2149 this.value = value === undefined ? new Path2D() : null; 2150 this.fillStyle = CanvasImageDraw.defaultStyles.fillStyle; 2151 this.strokeStyle = CanvasImageDraw.defaultStyles.strokeStyle; 2152 this.lineWidth = CanvasImageDraw.defaultStyles.lineWidth; 2153 this.fontSize = parseFloat(CanvasImageDraw.defaultStyles.font); 2154 } 2155 2156 cloneCanvas(canvas, dx = 0, dy = 0){ 2157 if(CanvasImageDraw.isCanvas(canvas) === false){ 2158 canvas = document.createElement("canvas"); 2159 canvas.width = this.width; 2160 canvas.height = this.height; 2161 } 2162 2163 canvas.getContext("2d").drawImage(this.image, dx, dy); 2164 2165 return canvas; 2166 } 2167 2168 setImage(image){ 2169 if(CanvasImageDraw.isCanvas(image)){ //image 如果是画布 2170 super.setImage(image); 2171 this.context = CanvasImageDraw.getContext(image); 2172 this.size(image.width, image.height); 2173 }else{ 2174 if(CanvasImageDraw.isCanvasImage(image)){ //image 如果是图像 2175 this.context = CanvasImageDraw.getContext(); 2176 super.setImage(this.context.canvas); 2177 this.size(image.width, image.height); 2178 this.context.drawImage(image, 0, 0); 2179 }else{ //image 如果是其它对象 2180 this.context = CanvasImageDraw.getContext(); 2181 super.setImage(this.context.canvas); 2182 this.size(this.width, this.height); 2183 } 2184 } 2185 2186 return this; 2187 } 2188 2189 clear(){ 2190 this.context.clearRect(0, 0, this.width, this.height); //this.context.clearRect(0, 0, this.box.w, this.box.h); 2191 return this; 2192 } 2193 2194 size(w, h){ 2195 switch(typeof w) { 2196 case "number": 2197 this.box.size(w, h); 2198 break; 2199 case "object": 2200 this.box.size(w.width||w.w||this.box.w, w.height||w.h||this.box.h); 2201 break; 2202 } 2203 2204 this.image.width = this.box.w; 2205 this.image.height = this.box.h; 2206 CanvasImageDraw.setDefaultStyles(this.context); 2207 2208 if(this.context.fillStyle !== this.fillStyle) this.context.fillStyle = this.fillStyle; // 2209 if(this.context.strokeStyle !== this.strokeStyle) this.context.strokeStyle = this.strokeStyle; // 2210 if(this.context.lineWidth !== this.lineWidth) this.context.lineWidth = this.lineWidth; // 2211 if(parseFloat(this.context.font) !== this.fontSize) this.context.font = this.fontSize+"px SimSun, Songti SC"; 2212 2213 return this; 2214 } 2215 2216 stroke(color = this.strokeStyle, lineWidth = this.lineWidth){ 2217 if(color !== "" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color; 2218 if(UTILS.isNumber(lineWidth) && this.lineWidth !== lineWidth) this.lineWidth = this.context.lineWidth = lineWidth; 2219 this.context.stroke(this.value); 2220 return this; 2221 } 2222 2223 fill(color = this.fillStyle){ 2224 if(color !== "" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color; 2225 this.context.fill(this.value); 2226 return this; 2227 } 2228 2229 line(x, y, x1, y1){ 2230 this.value.moveTo(x, y); 2231 this.value.lineTo(x1, y1); 2232 return this; 2233 } 2234 2235 path(arr, close = false){ 2236 this.value.moveTo(arr[0], arr[1]); 2237 for(let k = 2, len = arr.length; k < len; k += 2) this.value.lineTo(arr[k], arr[k+1]); 2238 if(close === true) this.value.closePath(); 2239 return this; 2240 } 2241 2242 rect(round = 4, lineWidth = this.lineWidth){ 2243 if(UTILS.isNumber(lineWidth) && this.lineWidth !== lineWidth) this.lineWidth = this.context.lineWidth = lineWidth; 2244 const l_2 = lineWidth / 2; 2245 this.value.roundRect(l_2, l_2, this.box.w - lineWidth, this.box.h - lineWidth, round); 2246 return this; 2247 } 2248 2249 strokeRect(color = this.strokeStyle, round = 4, lineWidth = this.lineWidth){ 2250 if(color !== "" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color; 2251 this.rect(round, lineWidth); 2252 this.context.stroke(this.value); 2253 return this; 2254 } 2255 2256 fillRect(color = this.fillStyle, round = 4){ 2257 if(color !== "" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color; 2258 this.rect(round); 2259 this.context.fill(this.value); 2260 return this; 2261 } 2262 2263 setFontSize(fontSize = this.fontSize){ 2264 if(UTILS.isNumber(fontSize) && fontSize > 1 && this.fontSize !== fontSize){ 2265 this.fontSize = fontSize; 2266 this.context.font = fontSize+"px SimSun, Songti SC"; 2267 } 2268 return this; 2269 } 2270 2271 getTextWidth(text){ 2272 return this.context.measureText(text).width; 2273 } 2274 2275 fillText(value = "empty", color = this.fillStyle, x){ 2276 if(color !== "" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color; 2277 if(x === undefined){ 2278 const w = this.context.measureText(value).width; 2279 x = w < this.box.w ? (this.box.w - w) / 2 : 0; 2280 } 2281 this.context.fillText(value, x, (this.box.h - this.fontSize) / 2); 2282 return this; 2283 } 2284 2285 fillTextWrap(value, option = {}){ 2286 if(value.length === 0) return this; 2287 if(option.color !== undefined && this.fillStyle !== option.color) this.fillStyle = this.context.fillStyle = option.color; 2288 2289 const con = this.context, pos = [], 2290 mw = option.width || this.box.w, 2291 2292 padIsObj = UTILS.isObject(option.padding), 2293 padT = padIsObj ? padIsObj.top : option.padding || 0, 2294 padR = padIsObj ? padIsObj.right : option.padding || 0, 2295 padB = padIsObj ? padIsObj.bottom : option.padding || 0, 2296 padL = padIsObj ? padIsObj.left : option.padding || 0, 2297 2298 disIsObj = UTILS.isObject(option.distance), 2299 disX = disIsObj ? disIsObj.x : option.distance || 0, 2300 disY = disIsObj ? disIsObj.y : option.distance || 0; 2301 2302 var y = padT, x = padL; 2303 for(let i = 0, w; i < value.length; i++){ 2304 w = con.measureText(value[i]).width; 2305 if(x + w + disX + padR > mw){ 2306 x = w + padL + disX; 2307 y += this.fontSize + disY; 2308 pos.push(padL, y); 2309 } else { 2310 pos.push(x, y); 2311 x += w + disX; 2312 } 2313 } 2314 2315 if(option.width !== undefined) this.size(option.width, option.height || (y + this.fontSize + padB)); 2316 for(let i = 0; i < pos.length; i += 2) con.fillText(value[i / 2], pos[i], pos[i+1]); 2317 2318 if(option.ePos !== undefined){ 2319 option.ePos.x = x; 2320 option.ePos.y = y; 2321 } 2322 2323 return this; 2324 } 2325 2326 } 2327 2328 2329 /* CanvasImages 2330 parameter: 2331 images: Array[image] 2332 2333 attribute: 2334 images: Array[image] 2335 cursor: Number; 2336 2337 method: 2338 next(): undefined; 2339 loadImages(urls, onDone, onUpdate): CanvasImages; 2340 urls: Array[String||Object{url||src:String}]; 2341 onDone, onUpdate: Function; 2342 2343 demo: 2344 const cis = new CanvasImages([imgA, imgB]); 2345 cis.next(); 2346 */ 2347 class CanvasImages extends CanvasImage{ 2348 2349 #i = -1; 2350 get cursor(){return this.#i;} 2351 set cursor(i){this.set(i);} 2352 2353 constructor(images = []){ 2354 super(images[0]); 2355 this.images = images; 2356 if(this.image !== null) this.#i = 0; 2357 } 2358 2359 set(i){ 2360 super.setImage(this.images[i]); 2361 this.#i = this.image !== null ? i : -1; 2362 } 2363 2364 next(){ 2365 const len = this.images.length - 1; 2366 if(len !== -1){ 2367 this.#i = this.#i < len ? this.#i++ : 0; 2368 this.image = this.images[this.#i] || null; //super.setImage(this.images[this.#i]); 2369 } 2370 } 2371 2372 setImage(image){ 2373 super.setImage(image); 2374 2375 if(this.image !== null && Array.isArray(this.images)){ 2376 const i = this.images.indexOf(this.image); 2377 if(i === -1){ 2378 this.#i = this.images.length; 2379 this.images.push(this.image); 2380 } 2381 else this.#i = i; 2382 } 2383 2384 return this; 2385 } 2386 2387 loadImages(srcs, onDone, onUpdate){ 2388 onUpdate = typeof onUpdate === "function" ? onUpdate : null; 2389 var i = 0, c = srcs.length, img = null, _i = this.images.length; 2390 2391 const len = srcs.length, 2392 func = ()=>{ 2393 i++; if(onUpdate !== null) onUpdate(this.images, _i); 2394 if(i === c && typeof onDone === "function"){ 2395 this.cursor = 0; 2396 onDone(this.images, _i, srcs); 2397 } 2398 else _i++; 2399 } 2400 2401 for(let k = 0, ty = ""; k < len; k++){ 2402 ty = typeof srcs[k]; 2403 if(ty === "string" || ty === "object"){ 2404 ty = ty === "string" ? srcs[k] : srcs[k].src || srcs[k].url; 2405 if(ty !== "" && typeof ty === "string"){ 2406 img = new Image(); 2407 img.onload = func; 2408 this.images.push(img); 2409 img.src = ty; 2410 } 2411 else c--; 2412 } 2413 } 2414 2415 return this; 2416 } 2417 2418 } 2419 2420 2421 const emptyCIC = new CanvasImageCustom(null, null); 2422 2423 2424 /* ProgressBar 进度条 2425 option: Object{ 2426 min 2427 max 2428 width //默认 value 的宽 2429 height //默认 textSize 2430 cursorSize 2431 backgroundColor //默认 #000000 2432 borderSize 2433 borderColor 2434 borderRadius 2435 } 2436 */ 2437 class CanvasProgressBar{ 2438 2439 #background = new CanvasImage(); 2440 #cursor = new CanvasImage(); 2441 #box = new Box(); 2442 get box(){return this.#box;} 2443 2444 constructor(option = {}){ 2445 const width = option.width || 100, height = option.height || 4, 2446 borderRadius = option.borderRadius || 0, 2447 cursorSize = option.cursorSize || 8, 2448 borderColor = option.borderColor || "#ffffff"; 2449 2450 emptyCIC.value = new Path2D(); 2451 const backgroundImage = emptyCIC.size(width, height) 2452 .rect(option.borderSize || 0, borderRadius) 2453 .fill(option.backgroundColor || "#000000") 2454 .stroke(borderColor, borderRadius) 2455 .cloneCanvas(); 2456 2457 emptyCIC.value = new Path2D(); 2458 const cursorImage = emptyCIC.size(cursorSize, cursorSize) 2459 .fillRect(borderColor, cursorSize/2) 2460 .cloneCanvas(); 2461 2462 this.#background.setImage(backgroundImage); 2463 this.#cursor.setImage(cursorImage); 2464 2465 if(width > height){ 2466 this.#box.size(width, Math.max(cursorSize, height)); 2467 } else { 2468 this.#box.size(Math.max(cursorSize, width), height); 2469 } 2470 2471 this.min = option.min || 0; 2472 this.max = option.max || 100; 2473 } 2474 2475 setCursor(v){ //v 为 min 与 max 之间 2476 const s = v / (this.max - this.min); 2477 if(this.#box.w > this.#box.h){ 2478 this.#cursor.box.x = (this.box.w - this.#cursor.w) * s + this.box.x; 2479 } else { 2480 this.#cursor.box.y = (this.box.h - this.#cursor.h) * s + this.box.y; 2481 } 2482 } 2483 2484 visible(v){ 2485 this.#background.visible = this.#cursor.visible = v; 2486 } 2487 2488 pos(x, y){ 2489 if(this.#box.w > this.#box.h){ 2490 this.#cursor.pos(this.#cursor.x - this.#box.x + x, y - (this.#cursor.h - this.#background.h) / 2); 2491 } else { 2492 this.#cursor.pos(x - (this.#cursor.w - this.#background.w) / 2, this.#cursor.y - this.#box.y + y); 2493 } 2494 2495 this.#box.pos(x, y); 2496 this.#background.pos(x, y); 2497 } 2498 2499 addToList(arr){ 2500 arr.push(this.#background, this.#cursor); 2501 } 2502 2503 removeToList(arr){ 2504 const i = arr.indexOf(this.#background); 2505 if(i !== -1) arr.splice(i, 2); 2506 } 2507 2508 bindEvent(cir, onchange){ 2509 var sx = 0, ox = 0; 2510 this.#cursor.addEventListener("down", v =>{ 2511 if(this.#box.w > this.#box.h){ 2512 sx = v.offsetX - this.#cursor.x; 2513 } else { 2514 sx = v.offsetY - this.#cursor.y; 2515 } 2516 }); 2517 2518 this.#cursor.addEventListener("move", v => { 2519 if(this.#box.w > this.#box.h){ 2520 const mx = this.#box.mx, nx = v.offsetX - sx; 2521 if(nx + this.#cursor.w > mx) this.#cursor.box.x = mx - this.#cursor.w; 2522 else if(nx < this.#box.x) this.#cursor.box.x = this.#box.x; 2523 else this.#cursor.box.x = nx; 2524 if(ox !== this.#cursor.box.x){ 2525 onchange((this.#cursor.x - this.#box.x) / (this.#box.w - this.#cursor.w) * (this.max - this.min)); 2526 ox = this.#cursor.box.x; 2527 } 2528 } else { 2529 const my = this.#box.my, ny = v.offsetY - sx; 2530 if(ny + this.#cursor.h > my) this.#cursor.box.y = my - this.#cursor.h; 2531 else if(ny < this.#box.y) this.#cursor.box.y = this.#box.y; 2532 else this.#cursor.box.y = ny; 2533 if(ox !== this.#cursor.box.y){ 2534 onchange((this.#cursor.y - this.#box.y) / (this.#box.h - this.#cursor.h) * (this.max - this.min)); 2535 ox = this.#cursor.box.y; 2536 } 2537 } 2538 cir.redraw(); 2539 }); 2540 } 2541 2542 } 2543 2544 2545 /* CanvasTextView 2546 value: String; //默认 "Button"; 2547 option: Object{ 2548 width //默认 value 的宽 2549 height //默认 textSize 2550 2551 margin //border以外的间隔 默认 0 2552 padding //border以内的间隔 默认 0 2553 2554 backgroundColor //默认 #000000 2555 2556 textSize //默认 12 2557 textColor //默认 #ffffff 2558 2559 borderColor //默认 #000000 2560 borderSize //默认 0 2561 borderRadius //默认 0 2562 2563 disableColor //如果定义就创建一个禁用样式的图片 2564 selectColor //如果定义就创建一个选取样式的图片 2565 } 2566 2567 disableStyle(): undefined; //切换为禁用样式 2568 selectStyle(): undefined; //切换选取样式 2569 defaultStyle(): undefined; //切换默认样式 2570 */ 2571 class CanvasTextView extends CanvasImage{ 2572 2573 #selectImage = null; 2574 #disableImage = null; 2575 #image = null; 2576 2577 #value = ""; 2578 get value(){return this.#value;} 2579 2580 constructor(value, option = {}){ 2581 value = value !== "" && typeof value === "string" ? value : "Button"; 2582 2583 //option 2584 const textColor = option.textColor !== undefined ? option.textColor : "#ffffff", 2585 textSize = UTILS.isNumber(option.textSize) ? option.textSize : 12; 2586 emptyCIC.setFontSize(textSize); 2587 2588 const textWidth = emptyCIC.context.measureText(value).width, 2589 width = UTILS.isNumber(option.width) ? option.width : textWidth, 2590 height = UTILS.isNumber(option.height) ? option.height : textSize, 2591 2592 padding = UTILS.isNumber(option.padding) ? option.padding * 2 : 0, 2593 margin = UTILS.isNumber(option.margin) ? option.margin * 2 : 0, 2594 2595 bgc = option.backgroundColor !== undefined ? option.backgroundColor : "#000000", 2596 2597 borderColor = option.borderColor !== undefined ? option.borderColor : textColor, 2598 borderSize = UTILS.isNumber(option.borderSize) ? option.borderSize : 0, 2599 borderRadius = UTILS.isNumber(option.borderRadius) ? option.borderRadius : 0; 2600 2601 //image 2602 emptyCIC.value = new Path2D(); 2603 emptyCIC.size(width + padding + borderSize, height + padding + borderSize) 2604 .rect(borderRadius, borderSize).fill(bgc) 2605 .fillText(value, textColor, textWidth < emptyCIC.w ? (emptyCIC.box.w - textWidth) / 2 : 0) 2606 if(borderSize > 0) emptyCIC.stroke(borderColor, borderSize); 2607 2608 //margin 2609 const context = CanvasImageDraw.getContext(), canvas = context.canvas; 2610 canvas.width = emptyCIC.box.w + margin; 2611 canvas.height = emptyCIC.box.h + margin; 2612 context.drawImage(emptyCIC.image, margin, margin); 2613 2614 //disableColor 2615 var disableImage = null; 2616 if(option.disableColor !== undefined){ 2617 const context = CanvasImageDraw.getContext(); 2618 disableImage = context.canvas; 2619 disableImage.width = canvas.width; 2620 disableImage.height = canvas.height; 2621 context.drawImage(emptyCIC.fill(option.disableColor).image, margin, margin); 2622 } 2623 2624 //selectColor 2625 var selectImage = null; 2626 if(option.selectColor !== undefined){ 2627 const context = CanvasImageDraw.getContext(); 2628 selectImage = context.canvas; 2629 selectImage.width = canvas.width; 2630 selectImage.height = canvas.height; 2631 emptyCIC.clear().context.drawImage(canvas, 0, 0); 2632 context.drawImage(emptyCIC.fill(option.selectColor).image, margin, margin); 2633 } 2634 2635 super(canvas); 2636 this.#disableImage = disableImage; 2637 this.#selectImage = selectImage; 2638 this.#value = value; 2639 this.#image = this.image; 2640 } 2641 2642 disableStyle(){ 2643 this.image = this.#disableImage; 2644 } 2645 2646 selectStyle(){ 2647 this.image = this.#selectImage; 2648 } 2649 2650 defaultStyle(){ 2651 this.image = this.#image; 2652 } 2653 2654 } 2655 2656 /* CanvasTreeList extends TreeStruct 树结构展示对象 2657 parameter: 2658 object: Object, 2659 info: Object{ 2660 name_attributesName: String, //object[name_attributesName] 2661 iamges_attributesName: String, 2662 iamges: Object{ 2663 object[iamges_attributesName]: Image 2664 }, 2665 }, 2666 option: Object, //参见 CanvasIconMenu 的 option 2667 2668 attribute: 2669 objectView: CanvasIconMenu; //object 展示视图(文字) 2670 childIcon: CanvasImages; //子视图隐藏开关(三角形图标) 2671 childView: CanvasImageCustom; //子视图 (线) 2672 2673 //此属性内部自动更新, 表示自己子视图的高 2674 //为什么不直接用 childView.box.h 代替? 2675 //因为如果cir存在scroll则box的属性将会变成一个"状态机", 2676 //所以就是为了不让它高频率的更新box的属性 2677 childViewHeight: Number; 2678 2679 //只读: 2680 object: Object; // 2681 visible: Bool; //自己是否已经显示 2682 root: CanvasTreeList; //向上遍历查找自己的root 2683 rect: Box; //new一个新的box并根据自己和所有子更新此box的值 2684 box: Box; //返回 CanvasIconMenu.box 2685 2686 method: 2687 bindEvent(cir: CanvasImageDraw, root: CanvasTreeList): this; //绑定默认事件 2688 unbindEvent(): undefined; //解绑默认事件 (如果此类不在使用的话可以不用解除事件) 2689 2690 addToList(arr: Array); //添加到数组, arr一般是 CanvasImageDraw.list 渲染队列 2691 removeToList(arr: Array); //从数组删除 2692 2693 visibleChild(root): undefined; //显示 (如果自己为隐藏状态则会想查找已显示的ctl,并从ctl展开至自己为止) 2694 hiddenChild(root): undefined; //隐藏 2695 2696 getByObject(object): CanvasTreeList; //查找 如果不存在返回 undefined; 2697 setName(name: String, arr: Array); //销毁已有的.objectView, 重新创建.objectView 2698 pos(x, y: Number): this; //此方法是专门为 root 设的; 如果你让不是root的root调用root的方法的话 也行 2699 2700 //删除自己和所有的下级 例子: 2701 removeChild(cir){ 2702 const i = cir.list.length; 2703 this.traverse(v => { 2704 v.unbindEvent(); //删除事件 2705 v.removeToList(cir.list); //删除视图 2706 }); 2707 2708 //删除结构 2709 const parent = this.parent; 2710 parent.removeChild(this); 2711 2712 //结尾 2713 parent.visibleChild(); //如果已知 root 就戴上, 否则它会自己寻找root 2714 cir.redraw(); 2715 } 2716 2717 demo: 2718 const info = { 2719 name_attributesName: "CTL_name", 2720 iamges_attributesName: "className", 2721 images: { 2722 CanvasImage: emptyCIC.size(20, 20).rect().fill("blue").shear(), 2723 CanvasImages: emptyCIC.size(20, 20).rect().stroke("blue").shear(), 2724 }, 2725 } 2726 2727 const cir = new CanvasImageDraw({width: innerWidth, height: innerHeight}); 2728 const cie = new CanvasImageEvent(cir); 2729 2730 const onclick = function (event) { 2731 ctl_root.traverse(ctl => console.log(ctl.visible, ctl.childViewHeight)) 2732 } 2733 2734 const ctl_root = new CanvasTreeList(new CanvasImage(), info) 2735 .addToList(cir.list) 2736 .bindEvent(cir, cie, onclick); 2737 2738 ctl_root.appendChild(new CanvasTreeList(new CanvasImages(), info)) 2739 .addToList(cir.list) 2740 .bindEvent(cir, cie, onclick); 2741 2742 ctl_root.pos(10, 10).visibleChild(); 2743 cir.render(); 2744 */ 2745 class CanvasTreeList extends TreeStruct{ 2746 2747 static info = null; 2748 2749 static childLineStyle = { 2750 strokeStyle: "#ffffff", 2751 lineWidth: 2, 2752 } 2753 2754 #info = null; 2755 #option = null; 2756 #object = null; 2757 get object(){ 2758 return this.#object; 2759 } 2760 2761 #visible = false; 2762 get visible(){ 2763 return this.#visible; 2764 } 2765 2766 get root(){ 2767 var par = this; 2768 while(true){ 2769 if(par.parent === null) break; 2770 par = par.parent; 2771 } 2772 return par; 2773 } 2774 2775 get rect(){ 2776 var maxX = this.objectView.background.box.mx, _x; 2777 this.traverse(ctl => { 2778 if(ctl.visible === true){ 2779 _x = ctl.objectView.background.box.mx; 2780 maxX = Math.max(_x, maxX); 2781 } 2782 }); 2783 2784 const masksOffset = this.objectView.masksOffset, x = this.childIcon.box.x - masksOffset; 2785 2786 return new Box(x, this.childIcon.box.y - masksOffset, maxX - x, this.childViewHeight + this.objectView.background.box.h); 2787 } 2788 2789 get box(){ 2790 return this.objectView.box; 2791 } 2792 2793 constructor(object, info = CanvasTreeList.info, option = Object.assign({}, CanvasIconMenu.option)){ 2794 if(UTILS.isObject(object) === false) return console.warn("[CanvasTreeList] 参数错误: ", object); 2795 super(); 2796 this.#object = object; 2797 this.#info = info; 2798 this.#option = option; 2799 2800 //this.objectView 2801 this.setName(); 2802 2803 //this.childIcon 2804 const size = this.objectView.contentHeight, x = (size - option.textSize) / 2, s = x + option.textSize; 2805 this.childIcon = new CanvasImages([ 2806 emptyCIC.size(size, size).rect(option.borderRadius, option.borderSize).fill(option.backgroundColor) 2807 .path([x, x, x, s, s, size/2], true).fill(option.textColor).cloneCanvas(), 2808 emptyCIC.size(size, size).rect(option.borderRadius, option.borderSize).fill(option.backgroundColor) 2809 .path([x, x, s, x, size/2, s], true).fill(option.textColor).cloneCanvas(), 2810 ]); 2811 2812 //this.childView 2813 this.childView = new CanvasImageCustom().size(1,1); 2814 this.childView.box.size(CanvasTreeList.childLineStyle.lineWidth || 1, 0); 2815 this.childView.path2D = new CanvasPath2D("stroke", CanvasTreeList.childLineStyle, true); 2816 this.childView.path2D.line(new Line()); 2817 this.childViewHeight = 0; 2818 2819 //visible 2820 this.childIcon.cursor = 0; 2821 this.childIcon.visible = 2822 this.objectView.background.visible = 2823 this.objectView.icons.visible = 2824 this.objectView.masks.visible = 2825 this.childView.visible = false; 2826 } 2827 2828 getByObject(object){ 2829 if(UTILS.isObject(object) === false) return; 2830 var result; 2831 this.traverse(ctl => { 2832 if(result !== undefined) return "continue"; 2833 if(ctl.object === object){ 2834 result = ctl; 2835 return "continue"; 2836 } 2837 }); 2838 return result; 2839 } 2840 2841 setName(name, arr, cis){ 2842 var objectView; 2843 2844 if(UTILS.isObject(this.#info)){ 2845 const objectType = this.#object[this.#info.iamges_attributesName]; 2846 2847 if(typeof name === "string" && name !== ""){ 2848 this.#object[this.#info.name_attributesName] = name; 2849 }else if(typeof this.#object[this.#info.name_attributesName] === "string" && this.#object[this.#info.name_attributesName] !== ""){ 2850 name = this.#object[this.#info.name_attributesName]; 2851 }else{ 2852 name = objectType; 2853 } 2854 2855 objectView = new CanvasIconMenu(this.#info.images[objectType], name, this.#option); 2856 } 2857 2858 else objectView = new CanvasIconMenu(null, "", this.#option); 2859 2860 //补遮罩层样式 2861 objectView.createMasksImages(); 2862 2863 //const scope = this; 2864 //Object.defineProperty(objectView.background, "scope", {get (){return scope;}}); 2865 2866 //更改渲染队列 2867 if(CanvasIconMenu.prototype.isPrototypeOf(this.objectView)){ 2868 if(Array.isArray(arr)){ 2869 const i = arr.indexOf(this.objectView.background); 2870 if(i !== -1){ 2871 //scroll 2872 if(CanvasImageScroll.prototype.isPrototypeOf(cis)){ 2873 cis.bindScroll(objectView.background); 2874 cis.bindScroll(objectView.icons); 2875 cis.bindScroll(objectView.masks); 2876 cis.resetMaxSizeX(); 2877 cis.resetMaxSizeY(); 2878 } 2879 2880 arr[i] = objectView.background; 2881 arr[i+1] = objectView.icons; 2882 arr[i+2] = objectView.masks; 2883 2884 objectView.background.index = i; 2885 objectView.icons.index = i+1; 2886 objectView.masks.index = i+2; 2887 2888 objectView.background.visible = this.objectView.background.visible; 2889 objectView.icons.visible = this.objectView.icons.visible; 2890 objectView.masks.visible = this.objectView.masks.visible; 2891 2892 objectView.icons.cursor = this.objectView.icons.cursor; 2893 objectView.masks.cursor = this.objectView.masks.cursor; 2894 2895 objectView.pos(this.objectView.box.x, this.objectView.box.y); 2896 2897 this.objectView = objectView; 2898 2899 }else console.warn("[CanvasTreeList] setName: 修改失败, 无法找到目标"); 2900 }else console.warn("[CanvasTreeList] setName: 第二个参数为Array"); 2901 } 2902 2903 else this.objectView = objectView; 2904 } 2905 2906 pos(x, y){ 2907 if(this.#visible === false){ 2908 this.childIcon.cursor = 1; 2909 this.childIcon.visible = 2910 this.childView.visible = true; 2911 2912 this.#visible = 2913 this.objectView.background.visible = 2914 this.objectView.icons.visible = 2915 this.objectView.masks.visible = true; 2916 } 2917 2918 const masksOffset = this.objectView.masksOffset; 2919 this.childIcon.pos(masksOffset+x, masksOffset+y); 2920 this.objectView.pos(this.childIcon.box.mx, this.childIcon.box.y - masksOffset); 2921 this.childView.box.pos(this.childIcon.box.x + this.childIcon.box.w / 2, this.objectView.background.box.my); 2922 return this; 2923 } 2924 2925 addToList(arr){ 2926 this.traverse(ctl => { 2927 arr.push(ctl.childIcon, ctl.objectView.background, ctl.objectView.icons, ctl.objectView.masks, ctl.childView); 2928 }); 2929 2930 return this; 2931 } 2932 2933 removeToList(arr){ 2934 this.traverse(ctl => { 2935 const i = arr.indexOf(ctl.childIcon); 2936 if(i !== -1) arr.splice(i, 5); 2937 }); 2938 } 2939 2940 visibleChild(root = this.root){ 2941 this.childIcon.cursor = 1; 2942 if(root !== this){ 2943 this.childIcon.visible = 2944 this.childView.visible = this.children.length === 0 ? false : true; 2945 } 2946 2947 if(this.#visible){ 2948 this._visibleChild(); 2949 root.childViewHeight = 0; 2950 root._updateSizeChild(); 2951 root._updatePositionChild(); 2952 root.childView.box.h = root.childView.path2D.value.y1 = root.childViewHeight; 2953 }else{ 2954 if(root === this) return console.warn("[CanvasTreeList] visibleChild: 展开失败, 请使用 root.pos() 方法初始化root"); 2955 let _ctl = root; 2956 this.traverseUp(ctl => { 2957 if(ctl.visible){ 2958 _ctl = ctl; 2959 return "break"; 2960 } 2961 if(ctl.childIcon.cursor !== 1) ctl.childIcon.cursor = 1; 2962 }); 2963 _ctl.visibleChild(root); 2964 } 2965 } 2966 2967 hiddenChild(root = this.root){ 2968 if(this.#visible){ 2969 this.childIcon.cursor = 0; 2970 this._hiddenChild(); 2971 root.childViewHeight = 0; 2972 root._updateSizeChild(); 2973 root._updatePositionChild(); 2974 root.childView.box.h = root.childView.path2D.value.y1 = root.childViewHeight; 2975 } 2976 } 2977 2978 //修改: 包括自己所有的子 2979 bindEvent(cir, root){ 2980 if(CanvasTreeList.prototype.isPrototypeOf(root) === false || root.parent !== null) console.warn("CanvasTreeList: 请显示声明 root"); 2981 2982 const onup = info => { 2983 if(info.delta < 600){ 2984 if(this.childIcon.cursor === 0) this.visibleChild(root); 2985 else if(this.childIcon.cursor === 1) this.hiddenChild(root); 2986 cir.redraw(); 2987 } 2988 } 2989 2990 this.childIcon.addEventListener("up", onup); 2991 this.__unbindEvent = () => { 2992 this.childIcon.removeEventListener("up", onup); 2993 } 2994 } 2995 2996 unbindEvent(){ 2997 unbindEvent.call(this); 2998 } 2999 3000 //以下属性限内部使用 3001 _visible(){ 3002 if(this.#visible === false){ 3003 this.childIcon.visible = 3004 this.childView.visible = this.children.length === 0 ? false : true; 3005 3006 this.#visible = 3007 this.objectView.background.visible = 3008 this.objectView.icons.visible = 3009 this.objectView.masks.visible = true; 3010 } 3011 } 3012 3013 _visibleChild(){ 3014 for(let k = 0, len = this.children.length, child; k < len; k++){ 3015 child = this.children[k]; 3016 child._visible(); 3017 if(child.childIcon.cursor === 1) child._visibleChild(); 3018 } 3019 } 3020 3021 _hidden(){ 3022 if(this.#visible === true){ 3023 this.#visible = 3024 this.childIcon.visible = 3025 this.objectView.background.visible = 3026 this.objectView.icons.visible = 3027 this.objectView.masks.visible = 3028 this.childView.visible = false; 3029 } 3030 } 3031 3032 _hiddenChild(){ 3033 for(let k = 0, len = this.children.length, child; k < len; k++){ 3034 child = this.children[k]; 3035 child._hidden(); 3036 child._hiddenChild(); 3037 } 3038 } 3039 3040 _updateSize(){ 3041 const itemHeight = this.objectView.background.box.h; 3042 var par = this.parent; 3043 while(par !== null){ 3044 par.childViewHeight += itemHeight; 3045 par.childView.path2D.value.y1 = par.childViewHeight; 3046 par = par.parent; 3047 } 3048 } 3049 3050 _updateSizeChild(){ 3051 for(let k = 0, len = this.children.length, child; k < len; k++){ 3052 child = this.children[k]; 3053 child.childView.path2D.value.y1 = child.childViewHeight = 0; 3054 if(child.visible){ 3055 child._updateSize(); 3056 child._updateSizeChild(); 3057 } 3058 } 3059 } 3060 3061 _updatePosition(outCTL){ 3062 const masksOffset = this.parent.objectView.masksOffset, 3063 mx_parent = this.parent.childIcon.box.mx, 3064 my_outCTL = outCTL !== undefined ? outCTL.childView.box.y+outCTL.childViewHeight : this.parent.objectView.background.box.my; 3065 3066 //childIcon 3067 this.childIcon.pos( 3068 mx_parent + masksOffset, 3069 my_outCTL + masksOffset 3070 ); 3071 3072 //objectView 3073 this.objectView.pos( 3074 this.childIcon.visible === true ? this.childIcon.box.mx : mx_parent, 3075 my_outCTL 3076 ); 3077 3078 //childView 3079 this.childView.box.pos(mx_parent + this.childIcon.box.w / 2, this.objectView.background.box.my); 3080 this.childView.box.h = this.childViewHeight; 3081 } 3082 3083 _updatePositionChild(){ 3084 for(let k = 0, len = this.children.length, outChild, child; k < len; k++){ 3085 child = this.children[k]; 3086 if(child.visible){ 3087 child._updatePosition(outChild); 3088 child._updatePositionChild(); 3089 outChild = child; 3090 } 3091 } 3092 } 3093 3094 } 3095 3096 3097 export { 3098 ElementUtils, 3099 CanvasPath2D, 3100 CanvasEvent, 3101 CanvasImageScroll, 3102 CanvasImageDraw, 3103 CanvasImage, 3104 CanvasImages, 3105 CanvasImageCustom, 3106 CanvasProgressBar, 3107 CanvasTextView, 3108 CanvasTreeList, 3109 }
开始:
1 <!DOCTYPE html> 2 <html lang = "zh-cn"> 3 4 <head> 5 <title>testCanvas</title> 6 <meta charset = "utf-8" /> 7 <style> 8 *{ 9 margin:0; 10 padding: 0; 11 } 12 body{ 13 overflow: hidden; 14 } 15 </style> 16 </head> 17 18 <body> 19 20 <script src = "./view/js/main.js" type = "module"></script> 21 22 </body> 23 24 </html>
main.js
1 import { CanvasImage, CanvasImageDraw, CanvasImageScroll } from "./ElementUtils.js"; 2 import { UTILS } from "./Utils.js"; 3 4 5 const PI2 = Math.PI * 2; 6 7 /* 测试 canvas 旋转 8 9 */ 10 function main(){ 11 12 //CanvasImage 的渲染器 13 const cid = new CanvasImageDraw({width: innerWidth, height: innerHeight}), 14 15 //渲染器的滚动条(此类暂时还很简陋,支持移动和桌面端) 16 cis = new CanvasImageScroll(cid, {scrollSize: 8}), 17 18 //加载图片 19 image = new CanvasImage().loadImage("./view/img/31.jpg", () => { 20 21 //随机旋转所有的 CanvasImage 22 //因为创建 CanvasImage 时其图片没有加载完 23 //无法获取其宽高, 而旋转的中心点(局部位置)需要知道宽高才行 24 //所以要等这张图片加载完后在创建旋转 25 //所以在这里创建旋转 26 for(let i = 0; i < cid.list.length; i++) cid.list[i].createRotate(UTILS.random(0, PI2)); 27 28 //sortCIPosEquals: ci的位置排序 29 cid.sortCIPosEquals(null, { 30 disX: 10, //ci之间的间距 31 disY: 10, 32 lenX: 5, //x轴最多5个数 33 }); 34 35 //render 把画布添加至dom树 36 cid.render(document.body); 37 38 }); 39 40 //批量创建 CanvasImage 41 for(let i = 0; i < 1000; i++){ 42 //内部用.setImage(image) 方法处理参数, 43 //此方法有个特性就是可以监听其它CanvasImage加载完图片时自动设置自己的图片, 前提时参数必须时 CanvasImage 44 //所以 image 变量只是当作一个加载图片的工具而已,没其它用处 45 //如果传的是其它类实例也有其它效果 46 const ci = new CanvasImage(image); 47 48 //cis.bindScroll(ci: CanvasImage) 方法可以使滚动条(cis)能够监听ci的位置和ci.visible属性的变化来更新滚动条 49 //cis.changeCIAdd(ci: CanvasImage) 方法根据ci主动更新一次滚动条 50 cis.bindScroll(ci); 51 cis.changeCIAdd(ci); 52 53 //加入到渲染器(cid)队列 54 cid.list[i] = ci; 55 } 56 57 console.log(cid, cis); 58 } 59 60 main();