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