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