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