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