js canvas2D 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 TreeStruct, 11 } from './Utils.js'; 12 13 14 15 16 /* CanvasImageRender (渲染 CanvasImage) 17 18 关于canvas的优化: 19 离屏渲染(预处理图像的 缩放, 旋转, 剪裁, 等) 20 避免浮点坐标(用Math.floor()函数对所有的坐标点取整, 浏览器为了达到抗锯齿的效果会做额外的运算) 21 如果可能请关闭透明度 (CanvasImageRender.paramCon.alpha = false) 22 23 关于渲染 CanvasImage 24 尽量让box.w和box.h与image的宽高保持一致, 否则在渲染时会缩放image 25 如果你不在使用这些属性请将它们设为默认值: .opacity 默认1, .rotate 默认null 26 如果多个 CanvasImage 的image是一样的应该: a.loadImage(src); b.setImage(a); c.setImage(a); 27 28 ImageData: 29 length: w * h * 4 30 r: 0 - 255 31 g: 0 - 255 32 b: 0 - 255 33 a: 0 - 255 Math.round(255 * a) 34 35 遍历: 36 const data = context.getImageData(0, 0, canvas.width, canvas.height).data; 37 for(let k = 0, x, y, r, g, b, a, i; k < len; k++){ 38 x = k % width; 39 y = Math.floor(k / width); 40 41 i = k*4; 42 r = data[i] 43 g = data[i+1] 44 b = data[i+2] 45 a = data[i+3] 46 47 console.log(x, y, r, g, b, a, i); 48 } 49 50 parameter: 51 option = { 52 canvas //默认 新的canvas 53 width, height //默认 1 54 className, id //默认 "" 55 } 56 57 attribute: 58 59 method: 60 size(w, h: Number): this; //设置box和canvas的宽高 61 render(parentElem: HTMLElement): this; //canvas添加至dom树 62 initList(i): this; //初始化列表 63 add(ci: CanvasImage): ci; //添加至列表并初始化ci 64 remove(ci: CanvasImage): ci; //从列表中删除ci 65 redraw(): undefined; //重绘在 this.box 内或相交的 CanvasImage 66 67 redrawTarget(ca: CanvasImage): undefined; 68 //工作原理: 收集与ca重叠的ca; 重绘这些重叠的ca, 前提是ca的box没发生改变; (缩放或旋转ca的box都会发生改变) 69 例子: 70 ca.opacity = 0.6; 71 cir.redrawTarget(ca); 72 73 ca.visible = false; 74 cir.redrawTarget(ca); 75 76 cir.remove(ca); 77 cir.redrawTarget(ca); 78 79 cir.add(ca); 80 cir.redrawTarget(ca); 81 82 ca.setImage(newImage) //假设newImage与ca的box一样大 83 cir.redrawTarget(ca); 84 85 initEventDispatcher(): this; //初始化自定义事件 (如果你不需要这些事件可以不初始化) 86 支持的事件名: 87 beforeDraw 88 afterDraw 89 beforeDrawTarget 90 afterDrawTarget 91 boxX 92 boxY 93 initList 94 size 95 add 96 remove 97 98 demo: 99 const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height}) 100 101 .initEventDispatcher(), //初始化 CanvasImageRender 内部的自定义事件 102 103 cie = new CanvasImageEvent(cir), //启用dom事件 104 105 cis = new CanvasImageScroll(cir.initEventDispatcher(), cie, {scrollEventType: "touch"}), //启用滚动条 106 107 ci = new CanvasImage().loadImage("view/examples/img/test.png", cir); //图片加载完后更新一次画布 108 109 for(let k = 0, pi2 = Math.PI*2, ca; k < 1000000; k++){ 110 ca = cir.list[k] = new CanvasImage(ci); //添加至渲染列表 111 ca.pos(UTILS.random(0, 1000000), UTILS.random(0, 1000000)); //随机位置 112 //ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2)); //随机旋转 113 } 114 115 //为什么要用 initList() ? 因为for循环中是直接把ca加入到了列表, 没对ca做任何初始化操作; 116 //如果全用.add() 就不需要在调用 initList(); 但 .add() 方法不适合一次性添加多个 CanvasImage; 117 cir.initList(); 118 119 */ 120 class CanvasImageRender{ 121 122 static emptyArrA = [] 123 static emptyArrB = [] 124 static paramCon = {alpha: true} 125 126 static defaultStyles = { 127 filter: "none", 128 globalAlpha: 1, 129 globalCompositeOperation: "source-over", 130 imageSmoothingEnabled: true, 131 miterLimit: 10, 132 font: "12px SimSun, Songti SC", 133 textAlign: "left", 134 textBaseline: "top", 135 lineCap: "butt", 136 lineJoin: "miter", 137 lineDashOffset: 0, 138 lineWidth: 1, 139 shadowColor: "rgba(0, 0, 0, 0)", 140 shadowBlur: 0, 141 shadowOffsetX: 0, 142 shadowOffsetY: 0, 143 fillStyle: "rgba(50,50,50,0.4)", 144 strokeStyle: "rgba(210,210,210,0.6)", 145 } 146 147 static setDefaultStyles(context){ 148 let k = "", styles = CanvasImageRender.defaultStyles; 149 for(k in styles){ 150 if(context[k] !== styles[k]) context[k] = styles[k]; 151 } 152 } 153 154 static getContext(canvas, className, id){ 155 if(CanvasImageRender.isCanvas(canvas) === false) canvas = document.createElement("canvas"); 156 const context = canvas.getContext("2d", CanvasImageRender.paramCon); 157 158 if(typeof className === "string") canvas.className = className; 159 if(typeof id === "string") canvas.setAttribute('id', id); 160 161 return context; 162 } 163 164 static downloadImage(func){ 165 const input = document.createElement("input"); 166 input.type = "file"; 167 input.multiple = "multiple"; 168 input.accept = ".png, .jpg, .jpeg, .bmp, .gif"; 169 170 input.onchange = e => { 171 if(e.target.files.length === 0) return; 172 const fr = new FileReader(); 173 fr.onloadend = e1 => { 174 const img = new Image(); 175 img.onload = () => func(img); 176 img.src = e1.target.result; 177 } 178 179 fr.readAsDataURL(e.target.files[0]); 180 } 181 182 input.click(); 183 } 184 185 static isCanvasImage(img){ //OffscreenCanvas:ImageBitmap; CSSImageValue: 186 187 return ImageBitmap["prototype"]["isPrototypeOf"](img) || 188 HTMLImageElement["prototype"]["isPrototypeOf"](img) || 189 HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 190 CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 191 HTMLVideoElement["prototype"]["isPrototypeOf"](img); 192 193 } 194 195 static isCanvas(canvas){ 196 return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas); 197 } 198 199 #box = new Box(); 200 get box(){return this.#box;} 201 202 #eventDispatcher = null; 203 get eventDispatcher(){return this.#eventDispatcher;} 204 205 constructor(option = {}){ 206 this.list = []; 207 this.context = CanvasImageRender.getContext(option.canvas, option.className, option.id); 208 this.domElement = this.context.canvas; 209 this.size(option.width, option.height); 210 } 211 212 initEventDispatcher(){ 213 this.#eventDispatcher = new EventDispatcher(); 214 const eventParam = {target: this}, 215 eventParam1 = {target: this, value: null}; 216 this.#eventDispatcher. 217 customEvents("beforeDraw", eventParam) 218 .customEvents("afterDraw", eventParam) 219 .customEvents("beforeDrawTarget", eventParam) 220 .customEvents("afterDrawTarget", eventParam) 221 .customEvents("boxX", eventParam) 222 .customEvents("boxY", eventParam) 223 .customEvents("initList", {target: this, index: 0}) 224 .customEvents("size", eventParam) 225 .customEvents("add", eventParam1) 226 .customEvents("remove", eventParam1); 227 228 let _x = this.#box.x, _y = this.#box.y; 229 Object.defineProperties(this.#box, { 230 231 x: { 232 get: () => {return _x;}, 233 set: v => { 234 _x = v; 235 this.#eventDispatcher.trigger("boxX"); 236 } 237 }, 238 239 y: { 240 get: () => {return _y;}, 241 set: v => { 242 _y = v; 243 this.#eventDispatcher.trigger("boxY"); 244 } 245 }, 246 247 }); 248 249 return this; 250 } 251 252 strokeBox(box){ 253 this.context.strokeRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h); 254 } 255 256 fillBox(box){ 257 this.context.fillRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h); 258 } 259 260 clearBox(box){ 261 this['context']['clearRect'](box.x, box.y, box.w, box.h); 262 } 263 264 isDraw(ca){ 265 return ca["visible"] === true && ca["image"] !== null && this["box"]["intersectsBox"](ca["box"]); 266 } 267 268 size(w = 1, h = 1){ 269 this.domElement.width = w; 270 this.domElement.height = h; 271 CanvasImageRender.setDefaultStyles(this.context); 272 this.box.size(w, h); 273 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("size"); 274 return this; 275 } 276 277 append(ca){ 278 this.add(ca); 279 return this; 280 } 281 282 add(ca){ 283 if(CanvasImage.prototype.isPrototypeOf(ca) && !this.list.includes(ca)){ 284 const len = this.list.length; 285 286 if(this.list[ca.index] === undefined){ 287 ca.index = len; 288 this.list.push(ca); 289 } 290 291 else{ 292 const arr = this.list.splice(ca.index); 293 this.list.push(ca); 294 for(let k = 0, c = arr.length; k < c; k++){ 295 this.list.push(arr[k]); 296 arr[k].index++; 297 } 298 } 299 300 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("add", param => param.value = ca); 301 } 302 303 return ca; 304 } 305 306 remove(ca){ 307 var i = ca.index; 308 309 if(this.list[i] !== ca) i = this.list.indexOf(ca); 310 311 if(i !== -1){ 312 this.list.splice(i, 1); 313 for(let k = i, len = this.list.length; k < len; k++) this.list[k].index -= 1; 314 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("remove", param => param.value = ca); 315 } 316 317 return ca; 318 } 319 320 render(parentElem = document.body){ 321 this.redraw(); 322 if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement); 323 return this; 324 } 325 326 initList(i){ 327 if(i === undefined || i < 0) i = 0; 328 const len = this.list.length; 329 for(let k = i; k < len; k++) this.list[k].index = k; 330 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("initList", param => param.index = i); 331 return this; 332 } 333 334 clear(){ 335 this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']); 336 } 337 338 draw(){ 339 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw"); 340 const len = this["list"]["length"]; 341 for(let k = 0, ca; k < len; k++){ 342 ca = this["list"][k]; 343 if(this["isDraw"](ca) === true) this["_draw"](ca); 344 } 345 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw"); 346 } 347 348 redrawTarget(ca){ 349 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDrawTarget"); 350 this["_computeOverlap"](ca); 351 this["_drawTarget"](ca); 352 if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDrawTarget"); 353 } 354 355 redraw(){ 356 this.clear(); 357 this.draw(); 358 } 359 360 //限内部使用 361 _computeOverlap(tar){ //暂不支持带有旋转的 CanvasImage (如遇到将忽略) 362 //1 如果检索到与box相交的ca时, 合并其box执行以下步骤: 363 //2 检索 已检索过的 并且 没有相交的ca 364 //3 如果存在相交的ca就合并box并继续第 2 步; 如果不存在继续第 1 步 365 if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false}; 366 const _list = CanvasImageRender.emptyArrA, list = CanvasImageRender.emptyArrB, 367 len = this.list.length, box = tar["overlap"]["box"]["copy"](tar["box"]); 368 369 for(let k = 0, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){ 370 tar = this["list"][k]; 371 if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false}; 372 373 if(this.isDraw(tar) === false){ 374 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false; 375 continue; 376 } 377 378 if(box["intersectsBox"](tar["box"]) === true){ 379 if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true; 380 box["expand"](tar["box"]); 381 loop = true; 382 383 while(loop === true){ 384 b["length"] = 0; 385 loop = false; 386 c = _c; 387 388 for(i = 0; i < c; i++){ 389 tar = a[i]; 390 391 if(box["intersectsBox"](tar["box"]) === true){ 392 if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true; 393 box["expand"](tar["box"]); 394 loop = true; _c--; 395 } 396 397 else b.push(tar); 398 399 } 400 401 a = a === _list ? list : _list; 402 b = b === _list ? list : _list; 403 404 } 405 406 } 407 408 else{ 409 _c++; 410 a["push"](tar); 411 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false; 412 } 413 414 } 415 416 _list.length = list.length = 0; 417 } 418 419 _drawImage(ca, ox = 0, oy = 0){ 420 if(ca["opacity"] === 1){ 421 if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy); 422 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); 423 } 424 425 else{ 426 this["context"]["globalAlpha"] = ca["opacity"]; 427 if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy); 428 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); 429 this["context"]["globalAlpha"] = 1; 430 } 431 } 432 433 _draw(ca){ 434 if(ca.rotate === null) this._drawImage(ca); 435 else{ 436 const cx = ca.rotate.origin.x + ca.box.x - this.#box.x, 437 cy = ca.rotate.origin.y + ca.box.y - this.#box.y; 438 this.context.translate(cx, cy); 439 this.context.rotate(ca.rotate.angle); 440 this._drawImage(ca, -cx, -cy); 441 this.context.rotate(-ca.rotate.angle); 442 this.context.translate(-cx, -cy); 443 } 444 } 445 446 _drawTarget(ca){ 447 const len = this["list"]["length"]; 448 449 this["context"]["clearRect"]( 450 ca["overlap"]["box"]["x"], 451 ca["overlap"]["box"]["y"], 452 ca["overlap"]["box"]["w"], 453 ca["overlap"]["box"]["h"]); 454 455 for(let k = 0; k < len; k++){ 456 ca = this["list"][k]; 457 if(ca["overlap"]["draw"] === true) this._draw(ca); 458 } 459 } 460 461 } 462 463 464 465 466 /* CanvasImageEvent (CanvasImageRender 的dom事件) 467 ca 必须的属性: visible, box, rotate, index 468 ca 必须的方法: 没有; 469 470 parameter: 471 domElement: CanvasImageRender.domElement; //必须 472 box: CanvasImageRender.box; //必须 473 ||或者第一个参数为 CanvasImageRender 474 475 与.initEvent(domElement, box)参数一样 476 477 attribute: 478 domElement 479 box: Box; 480 481 method: 482 add(ca: CanvasImage, eventName: String, callback: Function): ca; //ca添加事件 483 remove(ca: CanvasImage, eventName: String, callback: Function): ca; //ca删除事件 484 eventName: 可能的值为 CanvasImageEvent.canvasEventsList 的属性名 485 callback: 参数 event, ca 486 487 clear(ca: CanvasImage, eventName: String): ca; //ca 必须; eventName 可选, 如果未定义则清空ca的所有事件; 488 disposeEvent(eventName): this; //.initEvent(domElement, box)调用一次此方法 489 initEvent(domElement, box): this; //每次更换 domElement, box 后应调用此方法; (CanvasAnimateEvent初始化时自动调用一次) 490 setScale(x, y: Number): undefiend; // 491 492 eventName: 493 (如果注册了 "out"|"over" 事件, 在弃用时(CanvasImageRender 不在使用): 494 必需要调用.disposeEvent(eventName)方法清理注册的dom事件, 495 因为这两个事件用的是 pointermove 而不是 onpointermove); 496 497 down //鼠标左右键按下 498 move //鼠标左右键移动 499 up //鼠标左右键抬起 500 501 click //鼠标左键 502 wheel //鼠标滚动轮 503 out //移出 504 over //移入 505 506 demo: 507 const car = new CanvasImageRender({width: 100, height: 100}), 508 cae = new CanvasImageEvent(car), 509 ca = car.add(new CanvasImage(image)); 510 511 //ca添加点击事件 512 cae.add(ca, 'click', (event, target) => console.log(event, target)); 513 514 car.render(); 515 516 */ 517 class CanvasImageEvent{ 518 519 static bind(obj, is){ 520 /* const _eventList = {} 521 Object.defineProperty(obj, "_eventList", { 522 get(){return _eventList;} 523 }); */ 524 obj._eventList = {}; 525 if(is === true){ 526 let k, evns = CanvasImageEvent.canvasEventsList; 527 for(k in evns) obj._eventList[k] = []; 528 } 529 530 } 531 532 static canvasEventsList = { 533 down: "onpointerdown", 534 move: "onpointermove", 535 up: "onpointerup", 536 click: "onclick", 537 wheel: "onmousewheel", 538 out: "pointermove", //移出 539 over: "pointermove", //移入 540 } 541 542 static isEventName(eventName){ 543 544 return CanvasImageEvent.canvasEventsList[eventName] !== undefined; 545 546 } 547 548 static emptyBox = new Box(); 549 static emptyRotate = new Rotate(); 550 551 constructor(domElement, box){ 552 this._running = ""; 553 this._delList = []; 554 CanvasImageEvent.bind(this); 555 this.initEvent(domElement, box); 556 } 557 558 initEvent(domElement, box){ 559 this.disposeEvent(); 560 561 if(CanvasImageRender.prototype.isPrototypeOf(domElement)){ 562 this.domElement = domElement.domElement; 563 this.box = domElement.box; 564 } 565 566 else{ 567 this.domElement = domElement; 568 this.box = box; 569 } 570 571 if(this._eventList !== undefined){ 572 for(let evn in this._eventList){ 573 if(this._eventList[evn] !== undefined) this._createEvent(evn); 574 } 575 576 } 577 578 return this; 579 } 580 581 add(ca, eventName, callback){ 582 if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName); 583 if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误"); 584 585 this._add(ca, eventName); 586 this._addCA(ca, eventName, callback); 587 588 return ca; 589 } 590 591 remove(ca, eventName, callback){ 592 if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName); 593 if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误"); 594 if(this._running !== eventName){ 595 this._remove(ca, eventName); 596 this._removeCA(ca, eventName, callback); 597 } 598 599 else this._delList.push(ca, eventName, callback); 600 601 return ca; 602 } 603 604 disposeEvent(eventName){ 605 if(eventName === "over" || eventName === "out"){ 606 607 if(typeof this["_"+eventName] === "function"){ 608 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList[eventName], this["_"+eventName]); 609 delete this["_"+eventName]; 610 } 611 612 } 613 614 else{ 615 616 if(typeof this["_over"] === "function"){ 617 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["over"], this["_over"]); 618 delete this["_over"]; 619 } 620 621 if(typeof this["_out"] === "function"){ 622 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["out"], this["_out"]); 623 delete this["_out"]; 624 } 625 626 } 627 628 return this; 629 } 630 631 clear(ca, eventName){ 632 if(eventName === undefined){ 633 var k; for(k in this._eventList){ 634 this._remove(ca, k); 635 } 636 637 if(ca._eventList !== undefined) delete ca._eventList; //CanvasImageEvent.bind(ca, true); 638 639 } 640 641 else if(CanvasImageEvent.canvasEventsList[eventName] !== undefined){ 642 this._remove(ca, eventName); 643 644 if(ca._eventList !== undefined) ca._eventList[eventName].length = 0; 645 646 } 647 648 return ca; 649 } 650 651 _addCA(ca, eventName, callback){ 652 if(ca._eventList === undefined) CanvasImageEvent.bind(ca); 653 if(ca._eventList[eventName] === undefined) ca._eventList[eventName] = []; 654 ca._eventList[eventName].push(callback); 655 656 } 657 658 _removeCA(ca, eventName, callback){ 659 if(ca._eventList !== undefined && ca._eventList[eventName] !== undefined){ 660 for(let k = 0, len = ca._eventList[eventName].length; k < len; k++){ 661 if(ca._eventList[eventName][k] === callback){ 662 ca._eventList[eventName].splice(k, 1); 663 break; 664 } 665 } 666 } 667 668 } 669 670 _add(ca, eventName){ 671 if(this._eventList[eventName] === undefined){ 672 this._eventList[eventName] = []; 673 this._createEvent(eventName); 674 } 675 676 //if(this._eventList[eventName].includes(ca) === false) this._eventList[eventName].push(ca); 677 this._eventList[eventName].push(ca); 678 } 679 680 _remove(ca, eventName){ 681 if(this._eventList[eventName] !== undefined){ 682 let key = this._eventList[eventName].indexOf(ca); 683 if(key !== -1) this._eventList[eventName].splice(key, 1); 684 if(key === 0){ 685 if(eventName == "over" || eventName === "out") this.disposeEvent(eventName); 686 else this.domElement[CanvasImageEvent.canvasEventsList[eventName]] = null; 687 delete this._eventList[eventName]; 688 } 689 690 } 691 692 } 693 694 _createEvent(evn){ 695 var k, len, ca, arr, tar = null, oldTar = null, _run = null, offsetX, offsetY; 696 697 const _box = CanvasImageEvent.emptyBox, _rotate = CanvasImageEvent.emptyRotate, 698 699 run = event => { 700 len = this["_eventList"][evn].length; 701 if(len === 0) return; 702 703 offsetX = event.offsetX + this.box.x; 704 offsetY = event.offsetY + this.box.y; 705 706 tar = null; 707 for(k = 0; k < len; k++){ 708 ca = this["_eventList"][evn][k]; 709 _box.copy(ca.box); 710 711 //计算旋转后的box 712 if(ca.rotate !== null) _box.setFromRotate(_rotate.set(_box.x+ca.rotate.origin.x, _box.y+ca.rotate.origin.y, ca.rotate.angle)); 713 714 if(ca["visible"] === true && _box.containsPoint(offsetX, offsetY)){ 715 716 if(tar === null || tar["index"] < ca["index"]) tar = ca; 717 718 } 719 720 } 721 722 if(_run !== null) _run(); 723 if(tar !== null){ 724 this._running = evn; 725 arr = tar["_eventList"][evn]; 726 len = arr.length; 727 for(k = 0; k < len; k++) arr[k](event, tar); 728 729 tar = null; 730 731 len = this._delList.length; 732 for(k = 0; k < len; k += 3){ 733 this._remove(this._delList[k], this._delList[k+1]); 734 this._removeCA(this._delList[k], this._delList[k+1], this._delList[k+2]); 735 } 736 this._running = ""; 737 this._delList.length = 0; 738 } 739 740 } 741 742 if(evn === "over" || evn === "out"){ 743 this.domElement.addEventListener(CanvasImageEvent.canvasEventsList[evn], run); 744 this["_"+evn] = run; 745 if(evn === "over"){ 746 _run = ()=>{ 747 if(tar !== null){ 748 if(oldTar !== null){ 749 if(oldTar !== tar) oldTar = tar; 750 else tar = null; 751 } 752 else oldTar = tar; 753 } 754 else if(oldTar !== null) oldTar = null; 755 756 } 757 758 } 759 760 else{ 761 let _tar = null; 762 _run = ()=>{ 763 if(tar !== null){ 764 if(oldTar !== null){ 765 if(oldTar !== tar){ 766 _tar = tar; 767 tar = oldTar; 768 oldTar = _tar; 769 } 770 else tar = null; 771 } 772 else{ 773 oldTar = tar; 774 tar = null; 775 } 776 } 777 else if(oldTar !== null){ 778 tar = oldTar; 779 oldTar = null; 780 } 781 782 } 783 784 } 785 786 /* _run = ()=>{ 787 if(tar !== null){ 788 if(oldTar !== null){ 789 790 if(oldTar !== tar){ 791 if(evn === "over") oldTar = tar; 792 else{ 793 let _tar = tar; 794 tar = oldTar; 795 oldTar = _tar; 796 } 797 } 798 799 else tar = null; 800 801 } 802 803 else{ 804 oldTar = tar; 805 if(evn === "out") tar = null; 806 807 } 808 809 } 810 811 else{ 812 if(oldTar !== null){ 813 if(evn === "out") tar = oldTar; 814 oldTar = null; 815 } 816 817 } 818 819 } */ 820 821 } 822 823 else this.domElement[CanvasImageEvent.canvasEventsList[evn]] = run; 824 825 } 826 827 } 828 829 830 831 832 /* CanvasImageScroll (CanvasImageRender 的滚动条) 833 parameter: 834 cir: CanvasImageRender, 835 cie: CanvasImageEvent, 836 option: Object{ 837 domEventTarget: //dom事件的绑定目标html元素; 默认 cir.domElement 838 scrollSize //滚动轴的宽或高; 默认 10; (如果想隐藏滚动条最好用 scrollVisible, 而不是把此属性设为0) 839 scrollVisible //滚动轴的显示类型; 可能值: 默认"auto" || "visible" || ""; 840 scrollEventType //可能的值: 默认"default", "touch" || ""; 841 inertia //是否启用移动端滚动轴的惯性; 默认 true; 842 inertiaLife //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04; 843 } 844 845 */ 846 class CanvasImageScroll{ 847 848 static unbindScroll(box){ 849 var _x = box.x, _y = box.y, _w = box.w, _h = box.h; 850 Object.defineProperties(box, { 851 852 x: { 853 get(){return _x;}, 854 set(v){_x = v;} 855 }, 856 857 y: { 858 get(){return _y;}, 859 set(v){_y = v;} 860 }, 861 862 w: { 863 get(){return _w;}, 864 set(v){_w = v;} 865 }, 866 867 h: { 868 get(){return _h;}, 869 set(v){_h = v;} 870 }, 871 872 }); 873 } 874 875 #cir = null; 876 #scrollViewBoxX = new Box(); 877 #scrollViewBoxY = new Box(); 878 #maxSize = new Point(); 879 get maxSize(){return this.#maxSize;} 880 881 constructor(cir, cie, option = {}){ 882 this.#cir = cir; 883 this.scrollSize = UTILS.isNumber(option.scrollSize) ? option.scrollSize : 10; 884 885 cir.drawTarget = ca => { 886 if(this.scrollVisible !== ""){ 887 cir["_computeOverlap"](ca); 888 ca["overlap"]["box"]["x"] -= cir.box.x; 889 ca["overlap"]["box"]["y"] -= cir.box.y; 890 891 this.updateScrollViewBox(); 892 893 if(this.#scrollViewBoxX.intersectsBox(ca["overlap"]["box"]) || this.#scrollViewBoxY.intersectsBox(ca["overlap"]["box"])){ 894 cir.clearBox(this.#scrollViewBoxX); 895 cir.clearBox(this.#scrollViewBoxY); 896 cir["_drawTarget"](ca); 897 this.drawScroll(); 898 } 899 900 else cir["_drawTarget"](ca); 901 } 902 else cir.drawTarget(ca); 903 return cir; 904 } 905 906 const oninitList = event => { 907 const len = cir.list.length; 908 for(let k = event ? event.index : 0; k < len; k++) this.bindScroll(cir.list[k].box); 909 this.resetMaxSizeX(); 910 this.resetMaxSizeY(); 911 } 912 913 switch(option.scrollVisible){ 914 case "visible": 915 this.scrollVisible = option.scrollVisible; 916 break; 917 918 case "": 919 this.scrollVisible = ""; 920 break; 921 922 case "auto": 923 default: 924 this.scrollVisible = "auto"; 925 break; 926 } 927 928 switch(option.scrollEventType){ 929 case "touch": 930 this.createScrollEventMobile(cie, option.domEventTarget || cir.domElement, option.inertia, option.inertiaLife); 931 break; 932 933 case "": 934 break; 935 936 case "default": 937 default: 938 this.createScrollEventPC(cie); 939 break; 940 } 941 942 if(EventDispatcher.prototype.isPrototypeOf(cir.eventDispatcher)){ 943 const box = this.#cir.box; 944 945 cir.eventDispatcher.register("boxX", ()=>{ 946 if(box.x < 0) box.x = 0; 947 else if(box.mx > this.#maxSize.x) box.x = this.#maxSize.x - box.w; 948 }); 949 cir.eventDispatcher.register("boxY", ()=>{ 950 if(box.y < 0) box.y = 0; 951 else if(box.my > this.#maxSize.y) box.y = this.#maxSize.y - box.h; 952 }); 953 954 cir.eventDispatcher.register("add", event => { 955 const ciBox = event.value.box; 956 this.bindScroll(ciBox); 957 ciBox.w = ciBox.w; 958 ciBox.h = ciBox.h; 959 }); 960 cir.eventDispatcher.register("remove", event => { 961 const ciBox = event.value.box; 962 if(ciBox.mx >= this.#maxSize.x) this.resetMaxSizeX(); 963 if(ciBox.my >= this.#maxSize.y) this.resetMaxSizeY(); 964 CanvasImageScroll.unbindScroll(ciBox); 965 }); 966 967 cir.eventDispatcher.register("initList", oninitList); 968 969 cir.eventDispatcher.register("afterDraw", () => { 970 if(this.scrollVisible !== ""){ 971 this.updateScrollViewBox(); 972 this.drawScroll(); 973 } 974 }); 975 976 } 977 978 else console.warn("CanvasImageScroll: 请定义 eventDispatcher"); 979 980 if(cir.list.length !== 0) oninitList(); 981 } 982 983 updateScrollViewBox(){ 984 this.#scrollViewBoxX.x = this.cursorX(); 985 this.#scrollViewBoxX.y = this.#cir.box.h - this.scrollSize; 986 this.#scrollViewBoxX.w = this.#maxSize.x <= this.#cir.box.w ? this.#cir.box.w : this.#cir.box.w / this.#maxSize.x * this.#cir.box.w; 987 this.#scrollViewBoxX.h = this.scrollSize; 988 989 this.#scrollViewBoxY.x = this.#cir.box.w - this.scrollSize; 990 this.#scrollViewBoxY.y = this.cursorY(); 991 this.#scrollViewBoxY.w = this.scrollSize; 992 this.#scrollViewBoxY.h = this.#maxSize.y <= this.#cir.box.h ? this.#cir.box.h : this.#cir.box.h / this.#maxSize.y * this.#cir.box.h; 993 } 994 995 drawScroll(){ 996 if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.x > this.#cir.box.w)){ 997 this.#cir.context.fillRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h); 998 this.#cir.context.strokeRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h); 999 } 1000 1001 if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.y > this.#cir.box.h)){ 1002 this.#cir.context.fillRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h); 1003 this.#cir.context.strokeRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h); 1004 } 1005 } 1006 1007 createScrollEventPC(cie){ 1008 var dPos = -1; 1009 1010 const _box = this.#cir.box, scope = this, cir = this.#cir, 1011 1012 setTop = (event, top) => { 1013 _box.y = top / _box.h * this.#maxSize.y; 1014 this.#cir.redraw(); 1015 }, 1016 1017 setLeft = (event, left) => { 1018 _box.x = left / _box.w * this.#maxSize.x; 1019 this.#cir.redraw(); 1020 }, 1021 1022 onMoveTop = event => { 1023 setTop(event, event.offsetY - dPos); 1024 }, 1025 1026 onMoveLeft = event => { 1027 setLeft(event, event.offsetX - dPos); 1028 }, 1029 1030 onUpTop = event => { 1031 document.body.removeEventListener('pointermove', onMoveTop); 1032 document.body.removeEventListener('pointerup', onUpTop); 1033 if(event !== null) onMoveTop(event); 1034 }, 1035 1036 onUpLeft = event => { 1037 document.body.removeEventListener('pointermove', onMoveLeft); 1038 document.body.removeEventListener('pointerup', onUpLeft); 1039 if(event !== null) onMoveLeft(event); 1040 }, 1041 1042 //伪 CanvasImage, 是 CanvasImageEvent 使用的必须属性 1043 boxX = new Box(), 1044 eventTargetX = { 1045 get index(){return Infinity;}, 1046 get visible(){return true;}, 1047 get box(){return boxX}, 1048 get rotate(){return null}, 1049 }, 1050 1051 boxY = new Box(), 1052 eventTargetY = { 1053 get index(){return Infinity;}, 1054 get visible(){return true;}, 1055 get box(){return boxY}, 1056 get rotate(){return null}, 1057 }, 1058 1059 eventTargetWheel = { 1060 get index(){return -1;}, 1061 get visible(){return true;}, 1062 get box(){return _box}, 1063 get rotate(){return null}, 1064 } 1065 1066 Object.defineProperties(boxX, { 1067 x: {get: () => {return _box.x;}}, 1068 y: {get: () => {return _box.h - scope.scrollSize + _box.y;}}, 1069 w: {get: () => {return _box.w;}}, 1070 h: {get: () => {return scope.scrollSize;}}, 1071 }); 1072 1073 Object.defineProperties(boxY, { 1074 x: {get: () => {return _box.w - scope.scrollSize + _box.x;}}, 1075 y: {get: () => {return _box.y;}}, 1076 w: {get: () => {return scope.scrollSize;}}, 1077 h: {get: () => {return _box.h;}}, 1078 }); 1079 1080 //pc event 1081 cie.add(eventTargetX, "down", event => { 1082 dPos = event.offsetX - this.cursorX(); 1083 onUpLeft(null); 1084 document.body.addEventListener("pointermove", onMoveLeft); 1085 document.body.addEventListener("pointerup", onUpLeft); 1086 }); 1087 1088 cie.add(eventTargetY, "down", event => { 1089 dPos = event.offsetY - this.cursorY(); 1090 onUpTop(null); 1091 document.body.addEventListener("pointermove", onMoveTop); 1092 document.body.addEventListener("pointerup", onUpTop); 1093 }); 1094 1095 cie.add(eventTargetWheel, "wheel", event => { 1096 if(this.#maxSize.y > _box.h){ 1097 dPos = 50 / this.#maxSize.y * _box.h; 1098 setTop(event, this.cursorY() + (event.wheelDelta === 120 ? -dPos : dPos)); 1099 } 1100 else if(this.#maxSize.x > _box.w){ 1101 dPos = 50 / this.#maxSize.x * _box.w; 1102 setLeft(event, this.cursorX() + (event.wheelDelta === 120 ? -dPos : dPos)); 1103 } 1104 }); 1105 1106 } 1107 1108 createScrollEventMobile(cie, domElement, inertia = true, inertiaLife){ 1109 var px, py, dPos = 0, sPos = new Point(), step = 1, aniamteRun = false, stepA = "", stepB = "", sTime = 0; 1110 1111 const _box = this.#cir.box; 1112 1113 if(inertia === true){ 1114 inertiaLife = 1 - ((UTILS.isNumber(inertiaLife) && inertiaLife >= 0 && inertiaLife <= 1) ? inertiaLife : 0.04); 1115 var inertiaAnimate = new AnimateLoop(null), 1116 1117 inertiaTop = (event, speed) => { 1118 if(Math.abs(speed) < 1) return; 1119 1120 stepA = speed < 0 ? "-top" : "top"; 1121 if(aniamteRun && stepA === stepB) step += 0.3; 1122 else{ 1123 step = 1; 1124 stepB = stepA; 1125 } 1126 1127 inertiaAnimate.play(() => { 1128 speed *= inertiaLife; 1129 setTop(event, _box.y + step * 20 * speed); 1130 if(Math.abs(speed) < 0.001 || _box.y <= 0 || _box.my >= this.#maxSize.y) inertiaAnimate.stop(); 1131 }); 1132 }, 1133 1134 inertiaLeft = (event, speed) => { 1135 if(Math.abs(speed) < 1) return; 1136 stepA = speed < 0 ? "-left" : "left"; 1137 if(aniamteRun && stepA === stepB) step += 0.3; 1138 else{ 1139 step = 1; 1140 stepB = stepA; 1141 } 1142 inertiaAnimate.play(() => { 1143 speed *= inertiaLife; 1144 setLeft(event, _box.x + step * 20 * speed); 1145 if(Math.abs(speed) < 0.001 || _box.x <= 0 || _box.mx >= this.#maxSize.x) inertiaAnimate.stop(); 1146 }); 1147 } 1148 } 1149 1150 const setTop = (event, top) => { 1151 _box.y = top; 1152 this.#cir.redraw(); 1153 }, 1154 1155 setLeft = (event, left) => { 1156 _box.x = left; 1157 this.#cir.redraw(); 1158 }, 1159 1160 onMoveTop = event => { 1161 setTop(event, dPos - event.offsetY); 1162 }, 1163 1164 onMoveLeft = event => { 1165 setLeft(event, dPos - event.offsetX); 1166 }, 1167 1168 onUpTop = event => { 1169 document.body.removeEventListener('pointermove', onMoveTop); 1170 document.body.removeEventListener('pointerup', onUpTop); 1171 if(event !== null){ 1172 onMoveTop(event); 1173 if(inertia === true) inertiaTop(event, (_box.y - sPos.y) / (Date.now() - sTime)); 1174 } 1175 }, 1176 1177 onUpLeft = event => { 1178 document.body.removeEventListener('pointermove', onMoveLeft); 1179 document.body.removeEventListener('pointerup', onUpLeft); 1180 if(event !== null){ 1181 onMoveLeft(event); 1182 if(inertia === true) inertiaLeft(event, (_box.x - sPos.x) / (Date.now() - sTime)); 1183 } 1184 }, 1185 1186 eventTarget = { 1187 get index(){return -1;}, //最低优先 1188 get visible(){return true;}, 1189 get box(){return _box;}, 1190 get rotate(){return null}, 1191 }, 1192 1193 a1 = Math.PI / 4, a2 = Math.PI / 2 + a1, 1194 1195 onUp = event => { 1196 domElement.removeEventListener("pointerup", onUp); 1197 domElement.removeEventListener('pointermove', onMove); 1198 }, 1199 1200 onMove = event => { 1201 dPos++; if(dPos < 4) return; 1202 onUp(event); 1203 const a = Math.atan2(event.pageY - py, event.pageX - px); 1204 if((a < a2 && a >= a1) || (a < -a1 && a >= -a2)){ //y轴 1205 if(this.#maxSize.y > _box.h){ 1206 dPos = py + sPos.y; 1207 document.body.addEventListener("pointermove", onMoveTop); 1208 document.body.addEventListener("pointerup", onUpTop); 1209 } 1210 } 1211 else{ //x轴 1212 if(this.#maxSize.x > _box.w){ 1213 dPos = px + sPos.x; 1214 document.body.addEventListener("pointermove", onMoveLeft); 1215 document.body.addEventListener("pointerup", onUpLeft); 1216 } 1217 } 1218 } 1219 1220 cie.add(eventTarget, "down", event => { 1221 px = event.offsetX; 1222 py = event.offsetY; 1223 dPos = 0; 1224 sPos.set(_box.x, _box.y); 1225 sTime = Date.now(); 1226 1227 if(inertia === true){ 1228 aniamteRun = inertiaAnimate.running; 1229 inertiaAnimate.stop(); 1230 } 1231 1232 //防止没触发 move 或 up 事件; 1233 onUpLeft(null); 1234 onUpTop(null); 1235 onUp(); 1236 1237 domElement.addEventListener("pointermove", onMove); 1238 domElement.addEventListener("pointerup", onUp); 1239 }); 1240 } 1241 1242 bindScroll(box){ 1243 var _x = box.x, _y = box.y, _w = box.w, _h = box.h, nm, om; 1244 1245 const writeBoxX = () => { 1246 if(nm > this.#maxSize.x) this.#maxSize.x = nm; 1247 else if(nm < this.#maxSize.x){ 1248 if(om >= this.#maxSize.x) this.resetMaxSizeX(); 1249 } 1250 }, 1251 1252 writeBoxY = () => { 1253 if(nm > this.#maxSize.y) this.#maxSize.y = nm; 1254 else if(nm < this.#maxSize.y){ 1255 if(om >= this.#maxSize.y) this.resetMaxSizeY(); 1256 } 1257 }; 1258 1259 Object.defineProperties(box, { 1260 1261 x: { 1262 get: () => {return _x;}, 1263 set: v => { 1264 om = _x+_w; 1265 _x = v; 1266 nm = v+_w; 1267 writeBoxX(); 1268 } 1269 }, 1270 1271 y: { 1272 get: () => {return _y;}, 1273 set: v => { 1274 om = _y+_h; 1275 _y = v; 1276 nm = v+_h; 1277 writeBoxY(); 1278 } 1279 }, 1280 1281 w: { 1282 get: () => {return _w;}, 1283 set: v => { 1284 om = _w+_x; 1285 _w = v; 1286 nm = v+_x; 1287 writeBoxX(); 1288 } 1289 }, 1290 1291 h: { 1292 get: () => {return _h;}, 1293 set: v => { 1294 om = _h+_y; 1295 _h = v; 1296 nm = v+_y; 1297 writeBoxY(); 1298 } 1299 }, 1300 1301 }); 1302 } 1303 1304 resetMaxSizeX(){ 1305 this.#maxSize.x = 0; 1306 for(let k = 0, len = this.#cir.list.length, m; k < len; k++){ 1307 m = this.#cir.list[k].box.mx; 1308 if(m > this.#maxSize.x) this.#maxSize.x = m; 1309 } 1310 } 1311 1312 resetMaxSizeY(){ 1313 this.#maxSize.y = 0; 1314 for(let k = 0, len = this.#cir.list.length, m; k < len; k++){ 1315 m = this.#cir.list[k].box.my; 1316 if(m > this.#maxSize.y) this.#maxSize.y = m; 1317 } 1318 } 1319 1320 cursorX(v = this.#cir.box.x){ 1321 return v/this.#maxSize.x*this.#cir.box.w; 1322 } 1323 1324 cursorY(v = this.#cir.box.y){ 1325 return v/this.#maxSize.y*this.#cir.box.h; 1326 } 1327 1328 } 1329 1330 1331 1332 1333 /* CanvasImage (CanvasImageRender 的渲染目标) 1334 parameter: 1335 image (构造器会调用一次 .setImage(image) 来处理 image 参数) 1336 1337 attribute: 1338 opacity: Float; //透明度; 值0至1之间; 默认1; 1339 visible: Boolean; //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件) 1340 box: Box; //.x.y 图像的位置, .w.h 图像的宽高; 1341 rotate: Roate; //旋转; 默认 null (注意: 旋转的原点是相对于 box.x.y 的) 1342 暂未实现, 先用box.w.h代替缩放//scale: Box; //缩放; .x.y中心点, .w.h缩放; 默认 null; (注意: 缩放的原点是相对于 box.x.y 的) 1343 x, y: Number; //this.box 的 .x.y 1344 1345 //以下属性不建议直接修改 1346 overlap: Object; //CanvasImageRender.computeOverlaps(ca) 方法更新 1347 index: Integer; //CanvasImageRender.index(ca) 修改 1348 1349 //只读 1350 width, height, image; isRotate, isScale 1351 1352 method: 1353 setImage(image): this; //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image); 1354 loadImage(src, onload): this; //加载并设置图像 (onload 如果是 CanvasImageRender 则加载完后自动调用一次 redraw 或 render 方法); 1355 loadVideo(src, onload, type = "mp4") 1356 pos(x, y): this; //设置位置; x 可以是: Number, Object{x,y} 1357 1358 demo: 1359 //CanvasImageRender 1360 const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height}); 1361 cir.domElement.style = ` 1362 position: absolute; 1363 z-index: 9999; 1364 background: rgb(127,127,127); 1365 `; 1366 1367 //values 1368 const ciA = cir.add(new CanvasImage()).pos(59, 59).load("view/examples/img/test.png", cir); 1369 ciA.opacity = 0.2; //设置透明度 1370 1371 const ciB = cir.add(new CanvasImage(ciA)).pos(59, 120); 1372 ciB.rotate = new Rotate().toAngle(45); //旋转45度 1373 1374 //event 1375 const cie = new CanvasImageEvent(cir); //注意: CanvasImage 的缩放和旋转都会影响到 CanvasImageEvent 1376 cie.add(ciB, "click", event => { //ciB 添加 点击事件 1377 console.log("click ciB: ", event); 1378 }); 1379 1380 1381 //这个用于搭载H5视频 video 1382 cir.add(new CanvasImage()) 1383 1384 .loadVideo("view/examples/video/test.mp4", ci => { 1385 1386 //同比例缩放视频 1387 const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100}); 1388 ci.box.size(newSize.width, newSize.height).center(cir.box); 1389 1390 //播放按钮 1391 const cic = cir.add(new CanvasImageCustom()) 1392 .size(50, 30).text("PLAY", "#fff") 1393 .rect(4).stroke("blue"); 1394 cic.box.center(cir.box); 1395 1396 //动画循环 1397 const animateLoop = new AnimateLoop(() => cir.redraw()); 1398 1399 //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画) 1400 cir.addEvent(cic, "up", () => { 1401 cic.visible = false; 1402 ci.image.play(); // ci.image 其实是一个 video 元素 1403 animateLoop.play(); //播放动画 1404 }); 1405 1406 //把 canvas 添加至 dom 树 1407 cir.render(); 1408 1409 }); 1410 1411 */ 1412 class CanvasImage{ 1413 1414 #box = new Box(); 1415 #image = null; 1416 #isLoadImage = false; 1417 1418 get name(){return this.constructor.name;} 1419 get box(){return this.#box;} 1420 get image(){return this.#image;} 1421 get isScale(){return this.#image.width !== this.box.w || this.#image.height !== this.box.h;} 1422 get isLoadImage(){return this.#isLoadImage;} 1423 get width(){return this.#image !== null ? this.#image.width : 0;} 1424 get height(){return this.#image !== null ? this.#image.height : 0;} 1425 get x(){return this.box.x;} 1426 get y(){return this.box.y;} 1427 1428 constructor(image){ 1429 this.opacity = 1; 1430 this.visible = true; 1431 this.rotate = null; 1432 //this.scale = null; 1433 1434 //以下属性不建议直接修改 1435 this.overlap = null; 1436 this.index = -1; 1437 1438 this.setImage(image); 1439 } 1440 1441 pos(x, y){ 1442 if(UTILS.isNumber(x)){ 1443 this.box.x = x; 1444 this.box.y = y; 1445 } 1446 else if(UTILS.isObject(x)){ 1447 this.box.x = x.x; 1448 this.box.y = x.y; 1449 } 1450 return this; 1451 } 1452 1453 setImage(image){ 1454 if(CanvasImageRender.isCanvasImage(image)){ 1455 this.box.size(image.width, image.height); 1456 this.#image = image; 1457 } 1458 else if(CanvasImage.prototype.isPrototypeOf(image)){ 1459 if(image.isLoadImage){ 1460 if(Array.isArray(image.loadImage_cis)) image.loadImage_cis.push(this); 1461 else image.loadImage_cis = [this]; 1462 } 1463 else this.setImage(image.image); 1464 } 1465 else{ 1466 this.box.size(0, 0); 1467 this.#image = null; 1468 } 1469 return this; 1470 } 1471 1472 loadImage(src, onload){ 1473 this.#isLoadImage = true; 1474 const image = new Image(); 1475 image.onload = () => this._loadSuccess(image, onload); 1476 image.src = src; 1477 return this; 1478 } 1479 1480 loadVideo(src, onload, type = "mp4"){ 1481 /* video 加载事件 的顺序 1482 onloadstart 1483 ondurationchange 1484 onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。 1485 onloadeddata 1486 onprogress 1487 oncanplay 1488 oncanplaythrough 1489 1490 //控制事件: 1491 onended //播放结束 1492 onpause //暂停播放 1493 onplay //开始播放 1494 */ 1495 this.#isLoadImage = true; 1496 const video = document.createElement("video"), 1497 source = document.createElement("source"); 1498 video.appendChild(source); 1499 source.type = `video/${type}`; 1500 1501 video.oncanplay = () => { 1502 //video 的 width, height 属性如果不设的话永远都是0 1503 video.width = video.videoWidth; 1504 video.height = video.videoHeight; 1505 this._loadSuccess(video, onload); 1506 }; 1507 1508 source.src = src; 1509 return this; 1510 } 1511 1512 _loadSuccess(image, onload){ 1513 this.setImage(image); 1514 this.#isLoadImage = false; 1515 1516 if(Array.isArray(this.loadImage_cis)){ 1517 this.loadImage_cis.forEach(ci => ci.setImage(image)); 1518 delete this.loadImage_cis; 1519 } 1520 1521 if(typeof onload === "function") onload(this); 1522 else if(CanvasImageRender.prototype.isPrototypeOf(onload)){ 1523 if(onload.domElement.parentElement !== null) onload.redraw(); 1524 else onload.render(); 1525 } 1526 } 1527 1528 /* setScale(nx = 0, ny = 0, ratio = 1){ 1529 this.scale.w = this.#box.w * ratio; 1530 this.scale.h = this.#box.h * ratio; 1531 this.scale.x = nx - ((nx - this.scale.x) * ratio + this.scale.x) + this.scale.x; 1532 this.scale.y = ny - ((ny - this.scale.y) * ratio + this.scale.y) + this.scale.y; 1533 return this; 1534 } 1535 1536 setScaleToSameScaleFromBox(newWidth = this.#box.w){ 1537 if(this.#image === null || this.scale === null) return this; 1538 1539 const width = this.#image.width, 1540 height = this.#image.height, 1541 ratio = width / height, 1542 scale = ratio < 1 ? ratio * newWidth / width : newWidth / width; 1543 1544 this.scale.w = width * scale; 1545 this.scale.h = height * scale; 1546 1547 return this; 1548 } 1549 1550 setScaleToCenterFromBox(){ 1551 if(this.scale === null) return this; 1552 1553 this.scale.x = (this.#box.w - this.scale.w) / 2; 1554 this.scale.y = (this.#box.h - this.scale.h) / 2; 1555 1556 return this; 1557 } */ 1558 1559 } 1560 1561 1562 1563 1564 /* CanvasImages 1565 1566 */ 1567 class CanvasImages extends CanvasImage{ 1568 1569 #i = -1; 1570 get cursor(){return this.#i;} 1571 set cursor(i){ 1572 super.setImage(this.images[i]); 1573 this.#i = this.image !== null ? i : -1; 1574 } 1575 1576 constructor(images = []){ 1577 super(images[0]); 1578 this.images = images; 1579 if(this.image !== null) this.#i = 0; 1580 } 1581 1582 setImage(image){ 1583 super.setImage(image); 1584 1585 if(this.image !== null && Array.isArray(this.images)){ 1586 const i = this.images.indexOf(this.image); 1587 if(i === -1){ 1588 this.#i = this.images.length; 1589 this.images.push(this.image); 1590 } 1591 else this.#i = i; 1592 } 1593 1594 return this; 1595 } 1596 1597 next(){ 1598 const len = this.images.length - 1; 1599 if(len !== -1){ 1600 if(this.#i < len) this.#i++; 1601 else this.#i = 0; 1602 super.setImage(this.images[this.#i]); 1603 } 1604 } 1605 1606 loadImages(srcs, onDone, onUpdate){ 1607 onUpdate = typeof onUpdate === "function" ? onUpdate : null; 1608 var i = 0, c = srcs.length, img = null, _i = this.images.length; 1609 1610 const len = srcs.length, 1611 func = ()=>{ 1612 i++; if(onUpdate !== null) onUpdate(this.images, _i); 1613 if(i === c && typeof onDone === "function"){ 1614 this.cursor = 0; 1615 onDone(this.images, _i, srcs); 1616 } 1617 else _i++; 1618 } 1619 1620 for(let k = 0, ty = ""; k < len; k++){ 1621 ty = typeof srcs[k]; 1622 if(ty === "string" || ty === "object"){ 1623 ty = ty === "string" ? srcs[k] : srcs[k].src; 1624 if(ty !== "" && typeof ty === "string"){ 1625 img = new Image(); 1626 img.onload = func; 1627 this.images.push(img); 1628 img.src = ty; 1629 } 1630 else c--; 1631 } 1632 } 1633 1634 return this; 1635 } 1636 1637 } 1638 1639 1640 1641 1642 /* CanvasImageCustom 1643 1644 demo: 1645 //CanvasImageRender 1646 const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height}); 1647 cir.domElement.style = ` 1648 position: absolute; 1649 z-index: 9999; 1650 background: rgb(127,127,127); 1651 `; 1652 1653 //values 1654 cir.add(new CanvasImageCustom()) 1655 .pos(59, 180) 1656 .load("view/examples/img/test.png", cir); 1657 1658 cir.add(new CanvasImageCustom()) 1659 .pos(59, 180) 1660 .text("value", "red") 1661 .rect().stroke() 1662 1663 */ 1664 class CanvasImageCustom extends CanvasImage{ 1665 1666 constructor(canvas, autoSize = true){ 1667 super(canvas); 1668 this.autoSize = autoSize; 1669 this.fontSize = parseFloat(CanvasImageRender.defaultStyles.font); 1670 this.fillStyle = CanvasImageRender.defaultStyles.fillStyle; 1671 this.strokeStyle = CanvasImageRender.defaultStyles.strokeStyle; 1672 this.shadowColor = CanvasImageRender.defaultStyles.shadowColor; 1673 } 1674 1675 toImage(callback){ 1676 var image = new Image(); 1677 image.src = this.image.toDataURL("image/png"); 1678 image.onload = callback; 1679 } 1680 1681 shear(canvas, dx = 0, dy = 0){ 1682 if(CanvasImageRender.isCanvas(canvas) === false){ 1683 canvas = document.createElement("canvas"); 1684 canvas.width = this.width; 1685 canvas.height = this.height; 1686 } 1687 1688 canvas.getContext("2d").drawImage(this.image, dx, dy); 1689 1690 return canvas; 1691 } 1692 1693 setImage(image){ 1694 if(CanvasImageRender.isCanvas(image)){ //image 如果是画布 1695 super.setImage(image); 1696 this.context = CanvasImageRender.getContext(image); 1697 this.size(image.width, image.height); 1698 } 1699 else{ 1700 if(CanvasImageRender.isCanvasImage(image)){ //image 如果是图像 1701 this.context = CanvasImageRender.getContext(); 1702 super.setImage(this.context.canvas); 1703 this.size(image.width, image.height); 1704 this.context.drawImage(image, 0, 0); 1705 }else{ //image 如果是其它对象 1706 if(image) super.setImage(image); 1707 else{ 1708 this.context = CanvasImageRender.getContext(); 1709 super.setImage(this.context.canvas); 1710 this.size(this.width, this.height); 1711 } 1712 } 1713 } 1714 1715 return this; 1716 } 1717 1718 clear(){ 1719 this.context.clearRect(0, 0, this.box.w, this.box.h); 1720 return this; 1721 } 1722 1723 size(w, h){ 1724 this.box.size(w, h); 1725 this.image.width = w; 1726 this.image.height = h; 1727 CanvasImageRender.setDefaultStyles(this.context); 1728 1729 if(this.context.font !== parseFloat(this.fontSize)) this.context.font = this.fontSize+"px SimSun, Songti SC";// 1730 if(this.context.fillStyle !== this.fillStyle) this.context.fillStyle = this.fillStyle; // 1731 if(this.context.strokeStyle !== this.strokeStyle) this.context.strokeStyle = this.strokeStyle; // 1732 if(this.context.shadowColor !== this.shadowColor) this.context.shadowColor = this.shadowColor; // 1733 1734 return this; 1735 } 1736 1737 shadow(shadowColor = "rgba(0,0,0,0)", shadowBlur, shadowOffsetX, shadowOffsetY){ 1738 const con = this.context; 1739 if(typeof shadowColor === "string" && this.shadowColor !== shadowColor) this.shadowColor = con.shadowColor = shadowColor; 1740 if(con.shadowBlur !== shadowBlur && typeof shadowBlur === "number") con.shadowBlur = shadowBlur; 1741 if(con.shadowOffsetX !== shadowOffsetX && typeof shadowOffsetX === "number") con.shadowOffsetX = shadowOffsetX; 1742 if(con.shadowOffsetY !== shadowOffsetY && typeof shadowOffsetY === "number") con.shadowOffsetY = shadowOffsetY; 1743 return this; 1744 } 1745 1746 line(x, y, x1, y1){ 1747 this.context.beginPath(); 1748 this.context.moveTo(x, y); 1749 this.context.lineTo(x1, y1); 1750 return this; 1751 } 1752 1753 path(arr, close = false){ 1754 const con = this.context; 1755 con.beginPath(); 1756 con.moveTo(arr[0], arr[1]); 1757 for(let k = 2, len = arr.length; k < len; k+=2) con.lineTo(arr[k], arr[k+1]); 1758 if(close === true) con.closePath(); 1759 return this; 1760 } 1761 1762 rect(r, box = this.box){ 1763 const con = this.context, s = con.lineWidth; 1764 1765 if(box === this.box){ 1766 var x = s / 2, 1767 y = s / 2, 1768 w = box.w - s, 1769 h = box.h - s; 1770 } 1771 1772 else{ 1773 var x = box.x, 1774 y = box.y, 1775 w = box.w, 1776 h = box.h; 1777 } 1778 1779 if(UTILS.isNumber(r) === false || r <= 0){ 1780 con.rect(x, y, w, h); 1781 return this; 1782 } 1783 1784 const _x = x + r, 1785 _y = y + r, 1786 mx = x + w, 1787 my = y + h, 1788 _mx = mx - r, 1789 _my = my - r; 1790 1791 //上 1792 con.moveTo(_x, y); 1793 con.lineTo(_mx, y); 1794 con.arcTo(mx, y, mx, _y, r); 1795 1796 //右 1797 con.lineTo(mx, _y); 1798 con.lineTo(mx, _my); 1799 con.arcTo(mx, my, _x, my, r); 1800 1801 //下 1802 con.lineTo(_x, my); 1803 con.lineTo(_mx, my); 1804 con.arcTo(x, my, x, _my, r); 1805 1806 //左 1807 con.lineTo(x, _y); 1808 con.lineTo(x, _my); 1809 con.arcTo(x, y, _x, y, r); 1810 1811 return this; 1812 } 1813 1814 stroke(color, lineWidth){ 1815 if(typeof color === "string" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color; 1816 if(UTILS.isNumber(lineWidth) && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth; 1817 this.context.stroke(); 1818 return this; 1819 } 1820 1821 fill(color){ 1822 if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color; 1823 this.context.fill(); 1824 return this; 1825 } 1826 1827 text(value, color, fontSize = this.fontSize, x = -1, y = -1){ 1828 if(UTILS.isNumber(fontSize) && fontSize > 1 && this.fontSize !== fontSize){ 1829 this.fontSize = fontSize; 1830 this.context.font = fontSize+"px SimSun, Songti SC"; 1831 } 1832 1833 const textWidth = this.context.measureText(value).width; 1834 if(this.autoSize === true && textWidth > this.box.w || this.fontSize > this.box.h){ 1835 this.size(textWidth+4, this.fontSize+4); 1836 this.fontSize = fontSize; 1837 this.context.font = fontSize+"px SimSun, Songti SC"; 1838 } 1839 1840 if(x === -1) x = (this.box.w - textWidth) / 2; 1841 if(y === -1) y = (this.box.h - this.fontSize) / 2; 1842 1843 if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color; 1844 this.context.fillText(value, x, y); 1845 1846 return this; 1847 } 1848 1849 } 1850 1851 1852 1853 export { 1854 CanvasImageEvent, 1855 CanvasImageScroll, 1856 CanvasImageRender, 1857 CanvasImage, 1858 CanvasImages, 1859 CanvasImageCustom, 1860 }

1 "use strict"; 2 3 import { 4 UTILS, 5 TreeStruct, 6 } from './Utils.js'; 7 8 import { 9 CanvasImageEvent, 10 CanvasImageRender, 11 CanvasImage, 12 CanvasImages, 13 CanvasImageCustom, 14 } from './CanvasImageRender.js'; 15 16 var disableColor = "rgba(0,0,0,0.75)"; 17 18 const emptyCIC = new CanvasImageCustom(null, false), 19 bgColor = {down: "#b3b3b3", up: "#b3b3b3"}, //如果为null不创建背景 20 textColor = {down: "#999999", up: "#ffffff"}, 21 borderColor = {down: "#ffffff", up: "#999999"}, 22 padding = {x: 4, y: 4}; 23 24 function isS(v){ 25 return UTILS.isNumber(v) && v >= 1 && v !== Infinity; 26 } 27 28 function getW(v){ 29 return emptyCIC.context.measureText(v).width+padding.x+padding.x; 30 } 31 32 function getH(w){ 33 return Math.max(emptyCIC.fontSize+padding.y+padding.y, Math.round(0.618*w)); 34 } 35 36 function initCIC(w, h){ 37 emptyCIC.size(w, h).rect(Math.min(w, h) * 0.1); 38 return emptyCIC; 39 } 40 41 function getItem(v, t){ 42 return emptyCIC.fill(bgColor[t]) 43 .text(v, textColor[t]) 44 .stroke(borderColor[t]) 45 .shear(); 46 } 47 48 function but_rect(w, h, v){ 49 if(!isS(w)) w = getW(v); 50 if(!isS(h)) h = getH(w); 51 52 //up 53 initCIC(w, h); 54 const a = getItem(v, "up"), 55 56 //disable 57 c = emptyCIC.fill(disableColor).shear(); 58 59 //down 60 emptyCIC.clear(); 61 const b = getItem(v, "down"); 62 63 return [a, b, c]; 64 } 65 66 function but_circle(w, v){ 67 if(!isS(w)) w = getW(v); 68 69 //up 70 emptyCIC.size(w, w).rect(w / 2); 71 const a = getItem(v, "up"), 72 73 //disable 74 c = emptyCIC.fill(disableColor).shear(); 75 76 //down 77 emptyCIC.clear(); 78 const b = getItem(v, "down"); 79 80 return [a, b, c]; 81 } 82 83 function bool_rect(w, h){ 84 if(!isS(w)) w = getW("✔"); 85 if(!isS(h)) h = w; 86 87 //true 88 initCIC(w, h); 89 const a = getItem("✔", "up"), 90 91 b = emptyCIC.fill(disableColor).shear(); 92 93 emptyCIC.clear(); 94 const c = getItem("✔", "down"); 95 96 //false 97 emptyCIC.clear() 98 const d = getItem("", "up"), 99 100 e = emptyCIC.fill(disableColor).shear(); 101 102 emptyCIC.clear(); 103 const f = getItem("", "down"); 104 105 return [a, b, c, d, e, f]; 106 } 107 108 function bool_fillet(w, h){ 109 const textW = emptyCIC.context.measureText("开").width; 110 if(!isS(w)) w = textW*2+padding.x+padding.x + emptyCIC.fontSize; 111 if(!isS(h)) h = Math.max(emptyCIC.fontSize+padding.y+padding.y, getH(w) / 2) 112 113 emptyCIC.size(w, h).rect(Math.min(w, h) * 0.5); 114 115 const halfW = w / 2, cx = (halfW-textW)/2, 116 117 //true 118 a = emptyCIC 119 .fill(bgColor.up) 120 .text("开", textColor.up, 0, cx) 121 .text("关", textColor.disable, 0, halfW+cx) 122 .stroke(borderColor.up) 123 .shear(), 124 125 b = emptyCIC.fill(disableColor).shear(), 126 127 c = emptyCIC.clear() 128 .fill(bgColor.down) 129 .text("开", textColor.down, 0, cx) 130 .text("关", textColor.down, 0, halfW+cx) 131 .stroke(borderColor.down) 132 .shear(), 133 134 //false 135 d = emptyCIC.clear() 136 .fill(bgColor.up) 137 .text("开", textColor.disable, 0, cx) 138 .text("关", textColor.up, 0, halfW+cx) 139 .stroke(borderColor.up) 140 .shear(), 141 e = emptyCIC.fill(disableColor).shear(), 142 f = c; 143 144 return [a, b, c, d, e, f]; 145 } 146 147 148 class CanvasUI extends CanvasImages{ 149 150 static fontSize(v){ 151 v = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 12; 152 if(v === emptyCIC.fontSize) return v; 153 emptyCIC.fontSize = v; 154 emptyCIC.context.font = v+"px SimSun, Songti SC"; 155 return v; 156 } 157 158 constructor(images){ 159 super(images); 160 } 161 162 } 163 164 165 166 167 /* CanvasButton 为画布预设按钮 (防止移动端多指混乱) 168 parameter: 169 value: String; //按钮名; 默认 "Button" 170 width, height: Number; //默认自适应 171 style: String; //可能值: 默认"rect", "circle" 172 173 attribute: 174 enable: Bool; //是否启用; 175 value: String; //只读 176 177 method: 178 bindEvent(cir: CanvasImageRender, callback: Func, delay: Number): this; 179 //cir: 内部使用cir来绑定或解绑事件和更新自己(cir.redrawTarget(this)); 必须 180 //callback: 默认null 181 //delay: 可以限制每次点击的延迟毫秒时间; 默认0 182 183 demo: 184 //button 185 const cb = cir.add(new CanvasButton({ 186 width: 80, 187 value: "测试 TEST", 188 })); 189 190 cb.bindEvent(cir, event => console.log(1)); 191 cb.box.center(cir.box); 192 console.log(cb); 193 */ 194 class CanvasUIButton extends CanvasUI{ 195 196 #value = ""; 197 get value(){return this.#value;} 198 199 #enable = true; 200 get enable(){return this.#enable;} 201 set enable(v){ 202 if(v === false){ 203 this.cursor = 2; 204 this.#enable = false; 205 } 206 else if(v === true){ 207 this.cursor = 0; 208 this.#enable = true; 209 } 210 } 211 212 constructor(value, width, height, style = "rect"){ 213 214 value = value !== "" && typeof value === "string" ? value : "Button"; 215 216 switch(style){ 217 case "circle": 218 var images = but_circle(width, value); 219 break; 220 221 case "rect": 222 default: 223 var images = but_rect(width, height, value); 224 break; 225 } 226 227 super(images); 228 this.#value = value; 229 230 } 231 232 bindEvent(cir, cie, callback, delay = 0){ 233 if(cir.eventDispatcher === null) return this; 234 callback = typeof callback === "function" ? callback : null; 235 236 var pointerId = -1, isDown = false, isDelay = false, sTime = 0; 237 238 const ondelay = () => { 239 if(this.cursor === 2){ 240 this.cursor = 0; 241 cir.redrawTarget(this); 242 } 243 isDelay = false; 244 }, 245 246 onup_body = event => { 247 if(event.pointerId !== pointerId || this.enable === false) return; 248 document.body.removeEventListener("pointerup", onup_body); 249 isDown = false; 250 if(this.cursor === 1){ 251 this.cursor = 0; 252 if(cir.list.includes(this)) cir.redrawTarget(this); 253 } 254 }, 255 256 onup = event => { 257 if(isDown && callback !== null && this.enable && event.pointerId === pointerId){ 258 const dTime = Date.now() - sTime; 259 if(dTime < delay){ 260 isDelay = true; 261 setTimeout(ondelay, delay - dTime); 262 this.cursor = 2; 263 cir.redrawTarget(this); 264 } 265 266 isDown = false; 267 callback(event, this); 268 } 269 }, 270 271 ondown = event => { 272 if(isDelay || event.button !== 0 || this.enable === false) return; 273 pointerId = event.pointerId; 274 document.body.addEventListener("pointerup", onup_body); 275 this.cursor = 1; 276 isDown = true; 277 sTime = Date.now(); 278 cir.redrawTarget(this); 279 }, 280 281 remove = () => { 282 document.body.removeEventListener("pointerup", onup_body); 283 cir.remove(this, "up", onup); 284 cir.remove(this, "down", ondown); 285 cir.eventDispatcher.deleteEvent("remove", remove); 286 } 287 288 cie.add(this, "down", ondown); 289 cie.add(this, "up", onup); 290 cir.eventDispatcher.register("remove", remove); 291 292 return this; 293 } 294 295 } 296 297 298 /* CanvasUIBool 开关控件 299 parameter: 300 value: Bool; //默认 false 301 width, height: Number; //默认自适应 302 style: 默认 rect 矩形开关, fillet 圆角开关 303 304 attribute: 305 enable: Bool; //是否启用; 306 value: String; //设置控件的值;(注意: 它不会自动更新画布) 307 308 method: 309 bindEvent(cir, callback, delay = 0): this; 310 311 */ 312 class CanvasUIBool extends CanvasUI{ 313 314 #value = false; 315 get value(){return this.#value;} 316 set value(v){ 317 if(this.#enable && typeof v === "boolean"){ 318 this.#value = v; 319 this.cursor = v ? 0 : 3; 320 } 321 } 322 323 #enable = true; 324 get enable(){return this.#enable;} 325 set enable(v){ 326 if(v === false){ 327 this.cursor = this.#value ? 1 : 4; 328 this.#enable = false; 329 } 330 else if(v === true){ 331 this.cursor = this.#value ? 0 : 3; 332 this.#enable = true; 333 } 334 } 335 336 constructor(value, width, height, style = "rect"){ 337 value = typeof value === "boolean" ? value : false; 338 339 switch(style){ 340 case "fillet": 341 var images = bool_fillet(width, height); 342 break; 343 344 case "rect": 345 default: 346 var images = bool_rect(width, height); 347 break; 348 } 349 350 super(images); 351 this.value = value; 352 353 } 354 355 bindEvent(cir, cie, callback, delay = 0){ 356 if(cir.eventDispatcher === null) return this; 357 callback = typeof callback === "function" ? callback : null; 358 359 var pointerId = -1, isDown = false, isDelay = false, sTime = 0; 360 361 const ondelay = () => { 362 if(this.cursor === 1){ 363 this.cursor = 0; 364 cir.redrawTarget(this); 365 } 366 else if(this.cursor === 4){ 367 this.cursor = 3; 368 cir.redrawTarget(this); 369 } 370 isDelay = false; 371 }, 372 373 onup_body = event => { 374 if(event.pointerId !== pointerId || this.enable === false) return; 375 document.body.removeEventListener("pointerup", onup_body); 376 if(this.cursor === 2){ 377 this.cursor = 0; 378 cir.redrawTarget(this); 379 } 380 else if(this.cursor === 5){ 381 this.cursor = 3; 382 cir.redrawTarget(this); 383 } 384 isDown = false; 385 }, 386 387 onup = event => { 388 if(isDown && this.enable && event.pointerId === pointerId){ 389 this.value = !this.#value; 390 391 if(callback !== null){ 392 const dTime = Date.now() - sTime; 393 if(dTime < delay){ 394 isDelay = true; 395 setTimeout(ondelay, delay - dTime); 396 this.cursor = this.#value ? 1 : 4; 397 } 398 callback(event, this); 399 } 400 401 cir.redrawTarget(this); 402 isDown = false; 403 } 404 }, 405 406 ondown = event => { 407 if(isDelay || event.button !== 0 || this.enable === false) return; 408 pointerId = event.pointerId; 409 document.body.addEventListener("pointerup", onup_body); 410 this.cursor = this.cursor === 0 ? 2 : 5; 411 isDown = true; 412 sTime = Date.now(); 413 cir.redrawTarget(this); 414 }, 415 416 remove = () => { 417 document.body.removeEventListener("pointerup", onup_body); 418 cie.remove(this, "up", onup); 419 cie.remove(this, "down", ondown); 420 cir.eventDispatcher.deleteEvent("remove", remove); 421 } 422 423 cie.add(this, "down", ondown); 424 cie.add(this, "up", onup); 425 cir.eventDispatcher.register("remove", remove); 426 427 return this; 428 } 429 430 } 431 432 433 class CanvasUIText extends CanvasUI{ 434 435 constructor(value){ 436 super(); 437 438 } 439 440 } 441 442 class CanvasUINumber extends CanvasUI{ 443 444 constructor(value, min, max, style){ 445 super(); 446 447 } 448 449 } 450 451 class CanvasUIColor extends CanvasUI{ 452 453 } 454 455 456 457 458 /* IconMenu 图标菜单 459 parameter: 460 data: Array[Object{ 461 icon: Image, //默认 null 462 text: String, //默认 "空" 463 func: Func, //默认 null 464 }] 465 466 demo: 467 new CanvasImage().loadImage("./test.png", ci => { 468 const iconMenu = new IconMenu([ 469 { 470 icon: ci.image, 471 text: "StringStringStringStringString", 472 }, 473 { 474 icon: ci.image, 475 text: "String", 476 func: (cisA, cisB, data) => { 477 //cisA: 0:"✘", 1:"✔", 2:"●" 3:">"; .visible 默认 false; 478 //cisB: 0: 禁用, 1: 选取; .visible 默认 false; 479 cisA.next(); 480 cisA.visible = true; 481 }, 482 }, 483 ]); 484 485 iconMenu.domElement.style = ` 486 background: #666; 487 position: absolute; 488 left: 100px; 489 top: 100px; 490 border-radius: 4px; 491 `; 492 493 iconMenu.render().bindEvent(); 494 }); 495 */ 496 class IconMenu extends CanvasImageRender{ 497 498 static iconSize = 16; 499 static selectColor = "rgba(85,146,209,0.4)"; 500 static disableColor = disableColor; 501 static getImages(){ 502 const s = IconMenu.iconSize; 503 emptyCIC.size(s, s); 504 505 //0:"✘", 1:"✔", 2:"●" 3:">", 506 return [ 507 emptyCIC.clear().text("✘", textColor.up).shear(), 508 emptyCIC.clear().text("✔", textColor.up).shear(), 509 emptyCIC.clear().text("●", textColor.up).shear(), 510 emptyCIC.path([1, 1, 1, s-1, s-1, s/2], true).clear().fill(textColor.up).shear(), 511 ] 512 } 513 514 static getMaxWidth(data){ 515 const s = IconMenu.iconSize * 2 + padding.x * 2; 516 var mx = 0, maxX = 0; 517 data.forEach(o => { 518 mx = getW(o.text) + s; 519 if(maxX < mx) maxX = mx; 520 }); 521 return maxX; 522 } 523 524 constructor(data){ 525 super(); 526 if(UTILS.emptyArray(data)) return; 527 528 const mw = IconMenu.getMaxWidth(data), 529 iconSize = IconMenu.iconSize, 530 paddingX = padding.x, 531 paddingY = padding.y; 532 var itemLen = 0; 533 534 const icons = IconMenu.getImages(); 535 536 initCIC(mw - paddingX * 2, iconSize); 537 const mask = [ 538 emptyCIC.clear().fill(IconMenu.disableColor).shear(), 539 emptyCIC.fill(IconMenu.selectColor).shear(), 540 ]; 541 emptyCIC.size(mw, iconSize); 542 data.forEach((o, i) => { 543 emptyCIC.clear(); 544 if(CanvasImageRender.isCanvasImage(o.icon)) emptyCIC.context.drawImage(o.icon, 0, 0, iconSize, iconSize); 545 if(typeof o.text === "string" && o.text !== "") emptyCIC.text(o.text, textColor.up, 0, iconSize+paddingX); 546 const a = new CanvasImage(emptyCIC.shear()).pos(paddingX, emptyCIC.box.h * i + paddingY * i + paddingY), 547 b = new CanvasImages(icons).pos(mw - iconSize - paddingX, a.y), 548 c = new CanvasImages(mask).pos(paddingX, a.y); 549 b.visible = false; 550 c.cursor = 1; 551 c.visible = false; 552 this.list.push(a, b, c); 553 itemLen += 1; 554 }); 555 556 this.initList().size(mw, itemLen * emptyCIC.box.h + paddingY * itemLen + paddingY); 557 this.data = data; 558 this.eanvasImageEvent = null; 559 this.itemLen = itemLen; 560 } 561 562 bindEvent(){ 563 if(this.eanvasImageEvent === null) this.eanvasImageEvent = new CanvasImageEvent(this); 564 565 var pointerId = -1, i = -1, isDown = false, cis = null; 566 567 const _box = this.box, 568 569 eventTarget = { 570 get index(){return Infinity;}, 571 get visible(){return true;}, 572 get box(){return _box;}, 573 get rotate(){return null}, 574 }, 575 576 onup_body = () => { 577 document.body.removeEventListener("pointerup", onup_body); 578 579 if(cis && cis.visible === true && cis.cursor !== 0){ 580 cis.visible = false; 581 this.redrawTarget(cis); 582 cis = null; 583 } 584 }, 585 586 onup = event => { 587 if(isDown && event.pointerId === pointerId && i === Math.floor(event.offsetY/_box.h*this.itemLen) && this.list[i*3+2].cursor !== 0){ 588 const item = this.data[i]; 589 if(UTILS.isObject(item) && typeof item.func === "function") item.func(this.list[i*3+1], cis, item); 590 } 591 592 isDown = false; 593 }, 594 595 ondown = event => { 596 onup_body(); 597 if(event.button !== 0) return; 598 i = Math.floor(event.offsetY/_box.h*this.itemLen); 599 cis = this.list[i*3+2]; 600 if(cis && cis.visible === false && cis.cursor !== 0){ 601 cis.cursor = 1; 602 cis.visible = true; 603 this.redrawTarget(cis); 604 pointerId = event.pointerId; 605 isDown = true; 606 document.body.addEventListener("pointerup", onup_body); 607 } 608 } 609 610 this.eanvasImageEvent.add(eventTarget, "down", ondown); 611 this.eanvasImageEvent.add(eventTarget, "up", onup); 612 return this; 613 } 614 615 } 616 617 618 /* TreeIconMenu 树结构图标菜单 619 620 */ 621 class TreeIconMenu extends TreeStruct{ 622 623 constructor(data){ 624 625 } 626 627 } 628 629 630 631 632 /* Carousel 轮播图 (支持pc和mobile端) 633 634 */ 635 class CanvasCarousel extends CanvasImageRender{ 636 637 constructor(){ 638 super(); 639 640 } 641 642 init(images){ 643 const len = images.length; 644 for(let k = 0; k < len; k++) this.context.drawImage(images[k], k*this.box.w, 0, this.box.w, this.box.h); 645 646 } 647 648 } 649 650 651 652 653 /* CanvasTab 654 655 */ 656 class CanvasTab extends CanvasImageRender{ 657 658 constructor(){ 659 super(); 660 661 } 662 663 } 664 665 666 export { 667 CanvasUI, 668 CanvasUIButton, 669 CanvasUIBool, 670 IconMenu, 671 TreeIconMenu, 672 }
使用例子:
"use strict"; import { CanvasImage } from './CanvasImageRender.js'; import { IconMenu } from './CanvasUI.js'; document.body.style = ` width: ${window.innerWidth}px; height: ${window.innerHeight}px; overflow: hidden; background: #000; `; new CanvasImage().loadImage("./life.png", ci => { const iconMenu = new IconMenu([ { icon: ci.image, text: "String", }, { icon: ci.image, text: "StringStringStringStringString", }, { icon: ci.image, text: "String2", }, { icon: ci.image, text: "String3", func: (cisA, cisB, data) => { console.log(cisA, cisB, data); cisB.cursor = 0; iconMenu.redrawTarget(cisB); } }, { icon: ci.image, text: "String4", func: (cisA, cisB, data) => { console.log(cisA, cisB, data); cisA.visible = true; cisA.next(); } }, ]); iconMenu.domElement.style = ` background: #666; position: absolute; left: 100px; top: 100px; border-radius: 4px; `; iconMenu.render().bindEvent(); })
结果图: