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