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