CanvasImageRender 预设UI之按钮

   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 }
Utils.js

 

   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 = new CanvasImage().loadImage("view/examples/img/test.png", cir),
 444     pi2 = Math.PI*2;
 445 
 446     for(let k = 0, ca; k < 3000; k++){
 447         ca = cir.list[k] = new CanvasImage(ciA);
 448         ca.pos(UTILS.random(0, 10000), UTILS.random(0, 10000));
 449         ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2));
 450     }
 451     
 452     //为什么要用 initList() ? 因为for循环中是直接把ca加入到了列表, 没对ca做任何初始化操作;
 453     //如果全用.add() 就不需要在调用 initList(); 但 .add() 方法不适合一次性添加多个 CanvasImage;
 454     cir.initList(); 
 455 
 456 */
 457 class CanvasImageRender{
 458 
 459     static emptyArrA = []
 460     static emptyArrB = []
 461     static paramCon = {alpha: true}
 462 
 463     static defaultStyles = {
 464         filter: "none",
 465         globalAlpha: 1,
 466         globalCompositeOperation: "source-over",
 467         imageSmoothingEnabled: true,
 468         miterLimit: 10,
 469         font: "12px SimSun, Songti SC",
 470         textAlign: "left",
 471         textBaseline: "top",
 472         lineCap: "butt",
 473         lineJoin: "miter",
 474         lineDashOffset: 0,
 475         lineWidth: 1,
 476         shadowColor: "rgba(0, 0, 0, 0)",
 477         shadowBlur: 0,
 478         shadowOffsetX: 0,
 479         shadowOffsetY: 0,
 480         fillStyle: "rgba(0,0,0,0.6)",
 481         strokeStyle: "#ffffff",
 482     }
 483 
 484     static setDefaultStyles(context){
 485         let k = "", styles = CanvasImageRender.defaultStyles;
 486         for(k in styles){
 487             if(context[k] !== styles[k]) context[k] = styles[k];
 488         }
 489     }
 490 
 491     static getContext(canvas, className, id){
 492         if(CanvasImageRender.isCanvas(canvas) === false) canvas = document.createElement("canvas");
 493         const context = canvas.getContext("2d", CanvasImageRender.paramCon);
 494 
 495         if(typeof className === "string") canvas.className = className;
 496         if(typeof id === "string") canvas.setAttribute('id', id);
 497         
 498         return context;
 499     }
 500 
 501     static downloadImage(func){
 502         const input = document.createElement("input");
 503         input.type = "file";
 504         input.multiple = "multiple";
 505         input.accept = ".png, .jpg, .jpeg, .bmp, .gif";
 506     
 507         input.onchange = e => {
 508             if(e.target.files.length === 0) return;
 509             const fr = new FileReader();
 510             fr.onloadend = e1 => {
 511                 const img = new Image();
 512                 img.onload = () => func(img);
 513                 img.src = e1.target.result;
 514             }
 515 
 516             fr.readAsDataURL(e.target.files[0]);
 517         }
 518         
 519         input.click();
 520     }
 521 
 522     static isCanvasImage(img){ //OffscreenCanvas:ImageBitmap; CSSImageValue:
 523 
 524         return ImageBitmap["prototype"]["isPrototypeOf"](img) || 
 525         HTMLImageElement["prototype"]["isPrototypeOf"](img) || 
 526         HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 
 527         CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 
 528         HTMLVideoElement["prototype"]["isPrototypeOf"](img);
 529         
 530     }
 531 
 532     static isCanvas(canvas){
 533         
 534         return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas);
 535 
 536     }
 537 
 538     #canvasImageEvent = new CanvasImageEvent();
 539     #eventDispatcher = new EventDispatcher();
 540     #box = new Box();
 541     #length = 0;
 542     get box(){return this.#box;}
 543     get length(){return this.#length;}
 544     
 545     #maxSize = null;
 546     get maxSize(){return this.#maxSize;}
 547 
 548     constructor(option = {}){
 549         this.list = [];
 550         this.context = CanvasImageRender.getContext(option.canvas, option.className, option.id);
 551         this.domElement = this.context.canvas;
 552 
 553         let _x = this.#box.x, _y = this.#box.y;
 554         Object.defineProperties(this.#box, {
 555 
 556             x: {
 557                 get: () => {return _x;},
 558                 set: v => {
 559                     if(v < 0 || this.#maxSize === null) v = 0;
 560                     else if(v + this.#box.w > this.#maxSize.x) v = this.#maxSize.x - this.#box.w;
 561                     _x = v;
 562                 }
 563             },
 564 
 565             y: {
 566                 get: () => {return _y;},
 567                 set: v => {
 568                     if(v < 0 || this.#maxSize === null) v = 0;
 569                     else if(v + this.#box.h > this.#maxSize.y) v = this.#maxSize.y - this.#box.h;
 570                     _y = v;
 571                 }
 572             },
 573 
 574         });
 575 
 576         //event
 577         this.#canvasImageEvent.initEvent(this);
 578         const eventParam = {target: this}, 
 579         eventParam1 = {target: this, value: null}
 580         this.#eventDispatcher
 581         .customEvents("pos", eventParam)
 582         .customEvents("initList", eventParam)
 583         .customEvents("size", eventParam)
 584         .customEvents("add", eventParam1)
 585         .customEvents("remove", eventParam1);
 586 
 587         //scroll
 588         if(option.scroll === true){
 589             this.#maxSize = new Point();
 590             this.scrollSize = UTILS.isNumber(option.scrollSize) ? option.scrollSize : 6;
 591 
 592             switch(option.scrollVisible){
 593                 case "visible": 
 594                 case "auto": 
 595                 this.scrollVisible = option.scrollVisible;
 596                 break;
 597 
 598                 default: 
 599                 this.scrollVisible = "";
 600                 break;
 601             }
 602     
 603             switch(option.scrollEventType){
 604                 case "default": 
 605                 this._createScrollEventPC();
 606                 break;
 607 
 608                 case "touch": 
 609                 this._createScrollEventMobile(option.inertia, option.inertiaLife);
 610                 break;
 611 
 612                 default: 
 613                 break;
 614             }
 615         }
 616 
 617         this.size(option.width, option.height);
 618     }
 619 
 620     addEvent(ca, eventName, callback){
 621         if(ca) this.#canvasImageEvent.add(ca, eventName, callback);
 622         else this.#eventDispatcher.register(eventName, callback);
 623     }
 624 
 625     removeEvent(ca, eventName, callback){
 626         if(ca) this.#canvasImageEvent.remove(ca, eventName, callback);
 627         else this.#eventDispatcher.deleteEvent(eventName, callback);
 628     }
 629 
 630     isDraw(ca){
 631         return ca["visible"] === true && ca["image"] !== null && this["box"]["intersectsBox"](ca["box"]);
 632     }
 633 
 634     pos(x = 0, y = 0){
 635         this.domElement.style.left = x + "px";
 636         this.domElement.style.top = y + "px";
 637         this.#eventDispatcher.trigger("pos");
 638         return this;
 639     }
 640 
 641     size(w = 1, h = 1){
 642         this.domElement.width = w;
 643         this.domElement.height = h;
 644         CanvasImageRender.setDefaultStyles(this.context);
 645         this.box.size(w, h);
 646         this.#eventDispatcher.trigger("size");
 647         return this;
 648     }
 649 
 650     add(ca){
 651         if(CanvasImage.prototype.isPrototypeOf(ca) && !this.list.includes(ca)){
 652             const len = this.list.length;
 653             
 654             if(this.list[ca.index] === undefined){
 655                 ca.index = len;
 656                 this.list.push(ca);
 657             }
 658 
 659             else{
 660                 const arr = this.list.splice(ca.index);
 661                 this.list.push(ca);
 662                 for(let k = 0, c = arr.length; k < c; k++){
 663                     this.list.push(arr[k]);
 664                     arr[k].index++;
 665                 }
 666             }
 667 
 668             if(this.#maxSize !== null) this._bindScroll(ca);
 669             this.#length++;
 670             this.#eventDispatcher.trigger("add", param => param.value = ca);
 671         }
 672         
 673         return ca;
 674     }
 675 
 676     remove(ca){
 677         var i = ca.index;
 678 
 679         if(this.list[i] !== ca) i = this.list.indexOf(ca);
 680 
 681         if(i !== -1){
 682             this.list.splice(i, 1);
 683             for(let k = i, len = this.list.length; k < len; k++) this.list[k].index -= 1;
 684             if(this.#maxSize !== null) this._unbindScroll(ca);
 685             this.#length--;
 686             this.#eventDispatcher.trigger("remove", param => param.value = ca);
 687         }
 688 
 689         return ca;
 690     }
 691 
 692     render(parentElem = document.body){
 693         this.redraw();
 694         if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement);
 695         return this;
 696     }
 697 
 698     initList(i){
 699         if(i === undefined || i < 0) i = 0;
 700         this.#length = this.list.length;
 701 
 702         for(let k = i; k < this.#length; k++) this.list[k].index = k;
 703 
 704         if(this.#maxSize !== null){
 705             this._resetMaxSizeX();
 706             this._resetMaxSizeY();
 707         }
 708         
 709         this.#eventDispatcher.trigger("initList");
 710         return this;
 711     }
 712 
 713     clear(){
 714 
 715         this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']);
 716 
 717     }
 718 
 719     draw(){
 720         const len = this["list"]["length"];
 721         for(let k = 0, ca; k < len; k++){
 722             ca = this["list"][k];
 723             if(this["isDraw"](ca) === true) this["_draw"](ca);
 724         }
 725         
 726         if(this.#maxSize !== null && this.scrollVisible !== "") this._drawScroll();
 727     }
 728 
 729     clearTarget(ca){
 730         this["_computeOverlap"](ca);
 731         ca["overlap"]["draw"] = false;
 732         this["_drawTarget"](ca);
 733     }
 734 
 735     drawTarget(ca){
 736         this["_computeOverlap"](ca);
 737         this["_drawTarget"](ca);
 738     }
 739 
 740     redraw(){
 741         this.clear();
 742         this.draw();
 743     }
 744 
 745 
 746     //限内部使用
 747     _computeOverlap(tar){ //暂不支持带有旋转或缩放的 CanvasImage (如遇到将忽略)
 748         //1 如果检索到与box相交的ca时, 合并其box执行以下步骤: 
 749         //2 检索 已检索过的 并且 没有相交的ca
 750         //3 如果存在相交的ca就合并box并继续第 2 步; 如果不存在继续第 1 步
 751         if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false};
 752         const _list = CanvasImageRender.emptyArrA, list = CanvasImageRender.emptyArrB, len = this.list.length, box = tar["overlap"]["box"]["copy"](tar["box"]);
 753 
 754         for(let k = 0, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){
 755             tar = this["list"][k];
 756             if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false};
 757             
 758             if(this.isDraw(tar) === false){
 759                 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false;
 760                 continue;
 761             }
 762 
 763             if(box["intersectsBox"](tar["box"]) === true){
 764                 if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true;
 765                 box["expand"](tar["box"]);
 766                 loop = true;
 767 
 768                 while(loop === true){
 769                     b["length"] = 0;
 770                     loop = false;
 771                     c = _c;
 772 
 773                     for(i = 0; i < c; i++){
 774                         tar = a[i];
 775 
 776                         if(box["intersectsBox"](tar["box"]) === true){
 777                             if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true;
 778                             box["expand"](tar["box"]);
 779                             loop = true; _c--;
 780                         }
 781 
 782                         else b.push(tar);
 783                         
 784                     }
 785 
 786                     a = a === _list ? list : _list;
 787                     b = b === _list ? list : _list;
 788 
 789                 }
 790                 
 791             }
 792 
 793             else{
 794                 _c++;
 795                 a["push"](tar);
 796                 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false;
 797             }
 798 
 799         }
 800         
 801         _list.length = list.length = 0;
 802     }
 803 
 804     _drawImage(ca, ox = 0, oy = 0){
 805         if(ca["opacity"] === 1){
 806             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 807             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);
 808         }
 809 
 810         else{
 811             this["context"]["globalAlpha"] = ca["opacity"];
 812             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 813             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);
 814             this["context"]["globalAlpha"] = 1;
 815         }
 816     }
 817 
 818     _draw(ca){
 819         if(ca.rotate === null) this._drawImage(ca);
 820         else{
 821             const cx = ca.rotate.origin.x + ca.box.x - this.#box.x, 
 822             cy = ca.rotate.origin.y + ca.box.y - this.#box.y;
 823             this.context.translate(cx, cy);
 824             this.context.rotate(ca.rotate.angle);
 825             this._drawImage(ca, -cx, -cy);
 826             this.context.rotate(-ca.rotate.angle);
 827             this.context.translate(-cx, -cy);
 828         }
 829     }
 830     
 831     _drawTarget(ca){
 832         const len = this["list"]["length"];
 833         this["context"]["clearRect"](ca["overlap"]["box"]["x"], ca["overlap"]["box"]["y"], ca["overlap"]["box"]["w"], ca["overlap"]["box"]["h"]);
 834 
 835         for(let k = 0; k < len; k++){
 836             ca = this["list"][k];
 837             if(ca["overlap"]["draw"] === true) this._draw(ca);
 838         }
 839 
 840         if(this.#maxSize !== null && this.scrollVisible !== "") this._drawScroll();
 841     }
 842 
 843     //关于scroll
 844     _drawScroll(){
 845         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.x > this.#box.w)){
 846             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);
 847         }
 848 
 849         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.y > this.#box.h)){
 850             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);
 851         }
 852     }
 853 
 854     _createScrollEventPC(){
 855         var dPos = -1;
 856 
 857         const setTop = (event, top) => {
 858             this.#box.y = top / this.box.h * this.#maxSize.y;
 859             this.redraw();
 860         },
 861 
 862         setLeft = (event, left) => {
 863             this.#box.x = left / this.box.w * this.#maxSize.x;
 864             this.redraw();
 865         },
 866         
 867         onMoveTop = event => {
 868             setTop(event, event.offsetY - dPos);
 869         },
 870 
 871         onMoveLeft = event => {
 872             setLeft(event, event.offsetX - dPos);
 873         },
 874 
 875         onUpTop = event => {
 876             document.body.removeEventListener('pointermove', onMoveTop);
 877             document.body.removeEventListener('pointerup', onUpTop);
 878             if(event !== null) onMoveTop(event);
 879         },
 880 
 881         onUpLeft = event => {
 882             document.body.removeEventListener('pointermove', onMoveLeft);
 883             document.body.removeEventListener('pointerup', onUpLeft);
 884             if(event !== null) onMoveLeft(event);
 885         },
 886 
 887         scope = this, _box = this.#box,
 888 
 889         //伪 CanvasImage, 是 CanvasImageEvent 使用的必须属性
 890         boxX = new Box(),
 891         eventTargetX = {
 892             get index(){return scope.length+1;},
 893             get visible(){return true;},
 894             get box(){return boxX},
 895             get rotate(){return null},
 896         },
 897         
 898         boxY = new Box(),
 899         eventTargetY = {
 900             get index(){return scope.length+2;},
 901             get visible(){return true;},
 902             get box(){return boxY},
 903             get rotate(){return null},
 904         },
 905 
 906         eventTargetWheel = {
 907             get index(){return -1;},
 908             get visible(){return true;},
 909             get box(){return _box},
 910             get rotate(){return null},
 911         }
 912 
 913         Object.defineProperties(boxX, {
 914             x: {get: () => {return _box.x;}},
 915             y: {get: () => {return _box.h - scope.scrollSize + _box.y;}},
 916             w: {get: () => {return _box.w;}},
 917             h: {get: () => {return scope.scrollSize;}},
 918         });
 919 
 920         Object.defineProperties(boxY, {
 921             x: {get: () => {return _box.w - scope.scrollSize + _box.x;}},
 922             y: {get: () => {return _box.y;}},
 923             w: {get: () => {return scope.scrollSize;}},
 924             h: {get: () => {return _box.h;}},
 925         });
 926 
 927         //pc event
 928         this.#canvasImageEvent.add(eventTargetX, "down", event => {
 929             dPos = event.offsetX - this._cursorX();
 930             onUpLeft(null);
 931             document.body.addEventListener("pointermove", onMoveLeft);
 932             document.body.addEventListener("pointerup", onUpLeft);
 933         });
 934 
 935         this.#canvasImageEvent.add(eventTargetY, "down", event => {
 936             dPos = event.offsetY - this._cursorY();
 937             onUpTop(null);
 938             document.body.addEventListener("pointermove", onMoveTop);
 939             document.body.addEventListener("pointerup", onUpTop);
 940         });
 941 
 942         this.#canvasImageEvent.add(eventTargetWheel, "wheel", event => {
 943             if(this.#maxSize.y > this.#box.h){
 944                 dPos = 50 / this.#maxSize.y * this.#box.h;
 945                 setTop(event, this._cursorY() + (event.wheelDelta === 120 ? -dPos : dPos));
 946             }
 947             else if(this.#maxSize.x > this.#box.w){
 948                 dPos = 50 / this.#maxSize.x * this.#box.w;
 949                 setLeft(event, this._cursorX() + (event.wheelDelta === 120 ? -dPos : dPos));
 950             }
 951         });
 952 
 953     }
 954 
 955     _createScrollEventMobile(inertia, inertiaLife){
 956         var px, py, dPos = 0, sPos = new Point(), step = 1, aniamteRun = false, stepA = "", stepB = "", sTime = 0;
 957 
 958         if(inertia === true){
 959             inertiaLife = 1 - ((UTILS.isNumber(inertiaLife) && inertiaLife >= 0 && inertiaLife <= 1) ? inertiaLife : 0.04);
 960             var animate = new AnimateLoop(null),
 961         
 962             inertiaTop = (event, speed) => {
 963                 if(Math.abs(speed) < 1) return;
 964                 
 965                 stepA = speed < 0 ? "-top" : "top";
 966                 if(aniamteRun && stepA === stepB) step += 0.3;
 967                 else{
 968                     step = 1;
 969                     stepB = stepA;
 970                 }
 971                 
 972                 animate.play(() => {
 973                     speed *= inertiaLife;
 974                     setTop(event, this.#box.y + step * 20 * speed);
 975                     if(Math.abs(speed) < 0.001 || this.#box.y <= 0 || this.#box.my >= this.#maxSize.y) animate.pause();
 976                 });
 977             },
 978 
 979             inertiaLeft = (event, speed) => {
 980                 if(Math.abs(speed) < 1) return;
 981                 stepA = speed < 0 ? "-left" : "left";
 982                 if(aniamteRun && stepA === stepB) step += 0.3;
 983                 else{
 984                     step = 1;
 985                     stepB = stepA;
 986                 }
 987                 animate.play(() => {
 988                     speed *= inertiaLife;
 989                     setLeft(event, this.#box.x + step * 20 * speed);
 990                     if(Math.abs(speed) < 0.001 || this.#box.x <= 0 || this.#box.mx >= this.#maxSize.x) animate.pause();
 991                 });
 992             }
 993         }
 994 
 995         const setTop = (event, top) => {
 996             this.#box.y = top;
 997             this.redraw();
 998         },
 999 
1000         setLeft = (event, left) => {
1001             this.#box.x = left;
1002             this.redraw();
1003         },
1004         
1005         onMoveTop = event => {
1006             setTop(event, dPos - event.offsetY);
1007         },
1008 
1009         onMoveLeft = event => {
1010             setLeft(event, dPos - event.offsetX);
1011         },
1012 
1013         onUpTop = event => {
1014             document.body.removeEventListener('pointermove', onMoveTop);
1015             document.body.removeEventListener('pointerup', onUpTop);
1016             if(event !== null){
1017                 onMoveTop(event);
1018                 if(inertia === true) inertiaTop(event, (this.#box.y - sPos.y) / (Date.now() - sTime));
1019             }
1020         },
1021 
1022         onUpLeft = event => {
1023             document.body.removeEventListener('pointermove', onMoveLeft);
1024             document.body.removeEventListener('pointerup', onUpLeft);
1025             if(event !== null){
1026                 onMoveLeft(event);
1027                 if(inertia === true) inertiaLeft(event, (this.#box.x - sPos.x) / (Date.now() - sTime));
1028             }
1029         },
1030 
1031         _box = this.#box,
1032 
1033         eventTarget = {
1034             get index(){return -1;}, //最低优先
1035             get visible(){return true;},
1036             get box(){return _box;},
1037             get rotate(){return null},
1038         },
1039 
1040         a1 = Math.PI / 4, a2 = Math.PI / 2 + a1,
1041 
1042         onUp = event => {
1043             this.domElement.removeEventListener("pointerup", onUp);
1044             this.domElement.removeEventListener('pointermove', onMove);
1045         },
1046 
1047         onMove = event => {
1048             dPos++; if(dPos < 10) return;
1049             onUp(event);
1050             const a = Math.atan2(event.pageY - py, event.pageX - px);
1051             if((a < a2 && a >= a1) || (a < -a1 && a >= -a2)){ //y轴
1052                 if(this.#maxSize.y > this.#box.h){
1053                     dPos = py + sPos.y;
1054                     document.body.addEventListener("pointermove", onMoveTop);
1055                     document.body.addEventListener("pointerup", onUpTop);
1056                 }
1057             }
1058             else{ //x轴
1059                 if(this.#maxSize.x > this.#box.w){
1060                     dPos = px + sPos.x;
1061                     document.body.addEventListener("pointermove", onMoveLeft);
1062                     document.body.addEventListener("pointerup", onUpLeft);
1063                 }
1064             }
1065         }
1066     
1067         this.#canvasImageEvent.add(eventTarget, "down", event => {
1068             px = event.offsetX;
1069             py = event.offsetY;
1070             dPos = 0;
1071             sPos.set(this.#box.x, this.#box.y);
1072             sTime = Date.now();
1073 
1074             if(inertia === true){
1075                 aniamteRun = animate.running;
1076                 animate.pause();
1077             }
1078 
1079             //防止没触发 move 或 up 事件;
1080             onUpLeft(null);
1081             onUpTop(null);
1082             onUp();
1083             
1084             this.domElement.addEventListener("pointermove", onMove);
1085             this.domElement.addEventListener("pointerup", onUp);
1086         });
1087     }
1088 
1089     _bindScroll(ca){
1090         ca.__bindObj.writeBoxX = (nm, om) => {
1091             if(nm > this.#maxSize.x) this.#maxSize.x = nm;
1092             else if(nm < this.#maxSize.x){
1093                 if(om >= this.#maxSize.x) this._resetMaxSizeX();
1094             }
1095         };
1096 
1097         ca.__bindObj.writeBoxY = (nm, om) => {
1098             if(nm > this.#maxSize.y) this.#maxSize.y = nm;
1099             else if(nm < this.#maxSize.y){
1100                 if(om >= this.#maxSize.y) this._resetMaxSizeY();
1101             }
1102         };
1103 
1104         ca.__bindObj.writeBoxX(ca.box.mx);
1105         ca.__bindObj.writeBoxY(ca.box.my);
1106     }
1107 
1108     _unbindScroll(ca){
1109         ca.__bindObj.writeBoxX = null;
1110         ca.__bindObj.writeBoxY = null;
1111         if(ca.box.mx >= this.#maxSize.x) this._resetMaxSizeX();
1112         if(ca.box.my >= this.#maxSize.y) this._resetMaxSizeY();
1113     }
1114 
1115     _resetMaxSizeX(){
1116         this.#maxSize.x = 0;
1117         for(let k = 0, len = this.list.length, m; k < len; k++){
1118             m = this.list[k].box.mx;
1119             if(m > this.#maxSize.x) this.#maxSize.x = m;
1120         }
1121     }
1122 
1123     _resetMaxSizeY(){
1124         this.#maxSize.y = 0;
1125         for(let k = 0, len = this.list.length, m; k < len; k++){
1126             m = this.list[k].box.my;
1127             if(m > this.#maxSize.y) this.#maxSize.y = m;
1128         }
1129     }
1130 
1131     _cursorX(v = this.#box.x){
1132         return v/this.#maxSize.x*this.#box.w;
1133     }
1134 
1135     _cursorY(v = this.#box.y){
1136         return v/this.#maxSize.y*this.#box.h;
1137     }
1138 
1139 }
1140 
1141 
1142 
1143 
1144 /* CanvasImage 
1145 parameter: 
1146     image (构造器会调用一次 .setImage(image) 来处理 image 参数)
1147 
1148 attribute:
1149     opacity: Float;     //透明度; 值0至1之间; 默认1;
1150     visible: Boolean;    //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件)
1151     box: Box;             //.x.y 图像的位置, .w.h 图像的宽高;
1152     rotate: Roate;        //旋转; 默认 null (注意: 旋转的原点是相对于 box.x.y 的)
1153     暂未实现, 先用box.w.h代替缩放//scale: Box;            //缩放; .x.y中心点, .w.h缩放; 默认 null; (注意: 缩放的原点是相对于 box.x.y 的)
1154     x, y: Number;        //this.box 的 .x.y
1155 
1156     //以下属性不建议直接修改
1157     overlap: Object; //CanvasImageRender.computeOverlaps(ca) 方法更新
1158     index: Integer; //CanvasImageRender.index(ca) 修改
1159 
1160     //只读
1161     width, height, image; isRotate, isScale
1162 
1163 method:
1164     setImage(image): this;         //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image);
1165     loadImage(src, onload): this;    //加载并设置图像 (onload 如果是 CanvasImageRender 则加载完后自动调用一次 redraw 或 render 方法);
1166     loadVideo(src, onload, type = "mp4")
1167     pos(x, y): this;             //设置位置; x 可以是: Number, Object{x,y}
1168 
1169 demo:
1170     //CanvasImageRender
1171     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1172     cir.domElement.style = `
1173         position: absolute;
1174         z-index: 9999;
1175         background: rgb(127,127,127);
1176     `;
1177 
1178     //values
1179     const ciA = cir.add(new CanvasImage()).pos(59, 59).load("view/examples/img/test.png", cir);
1180     ciA.opacity = 0.2; //设置透明度
1181 
1182     const ciB = cir.add(new CanvasImage(ciA)).pos(59, 120);
1183     ciB.rotate = new Rotate().toAngle(45); //旋转45度
1184 
1185     //event
1186     const cie = new CanvasImageEvent(cir); //注意: CanvasImage 的缩放和旋转都会影响到 CanvasImageEvent
1187     cie.add(ciB, "click", event => { //ciB 添加 点击事件
1188         console.log("click ciB: ", event);
1189     });
1190 
1191 
1192     //这个用于搭载H5视频 video
1193     cir.add(new CanvasImage())
1194 
1195     .loadVideo("view/examples/video/test.mp4", ci => {
1196         
1197         //同比例缩放视频
1198         const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100});
1199         ci.box.size(newSize.width, newSize.height).center(cir.box);
1200 
1201         //播放按钮
1202         const cic = cir.add(new CanvasImageCustom())
1203         .size(50, 30).text("PLAY", "#fff")
1204         .rect(4).stroke("blue");
1205         cic.box.center(cir.box);
1206 
1207         //动画循环
1208         const animateLoop = new AnimateLoop(() => cir.redraw());
1209 
1210         //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画)
1211         cir.addEvent(cic, "up", () => {
1212             cic.visible = false;
1213             ci.image.play(); // ci.image 其实是一个 video 元素
1214             animateLoop.play(); //播放动画
1215         });
1216         
1217         //把 canvas 添加至 dom 树
1218         cir.render();
1219 
1220     });
1221 
1222 */
1223 class CanvasImage{
1224 
1225     static bindBox(obj, box){
1226         var _x = box.x, _y = box.y, _w = box.w, _h = box.h, oldV;
1227 
1228         Object.defineProperties(box, {
1229 
1230             x: {
1231                 get: () => {return _x;},
1232                 set: v => {
1233                     oldV = _x;
1234                     _x = v;
1235                     if(obj.writeBoxX !== null) obj.writeBoxX(v+_w, oldV+_w);
1236                 }
1237             },
1238 
1239             y: {
1240                 get: () => {return _y;},
1241                 set: v => {
1242                     oldV = _y;
1243                     _y = v;
1244                     if(obj.writeBoxY !== null) obj.writeBoxY(v+_h, oldV+_h);
1245                 }
1246             },
1247 
1248             w: {
1249                 get: () => {return _w;},
1250                 set: v => {
1251                     oldV = _w;
1252                     _w = v;
1253                     if(obj.writeBoxX !== null) obj.writeBoxX(v+_x, oldV+_x);
1254                 }
1255             },
1256 
1257             h: {
1258                 get: () => {return _h;},
1259                 set: v => {
1260                     oldV = _h;
1261                     _h = v;
1262                     if(obj.writeBoxY !== null) obj.writeBoxY(v+_y, oldV+_y);
1263                 }
1264             },
1265 
1266         });
1267     }
1268 
1269     #box = new Box();
1270     #image = null;
1271     #isLoadImage = false;
1272     #__bindObj = {
1273         writeBoxX: null,
1274         writeBoxY: null,
1275     }
1276 
1277     get name(){return this.constructor.name;}
1278     get box(){return this.#box;}
1279     get image(){return this.#image;}
1280     get isScale(){return this.#image.width !== this.box.w || this.#image.height !== this.box.h;}
1281     get width(){return this.#image !== null ? this.#image.width : 0;}
1282     get height(){return this.#image !== null ? this.#image.height : 0;}
1283     get isLoadImage(){return this.#isLoadImage;}
1284     get __bindObj(){return this.#__bindObj;}
1285     get x(){return this.box.x;}
1286     get y(){return this.box.y;}
1287     
1288     constructor(image){
1289         this.opacity = 1;
1290         this.visible = true;
1291         this.rotate = null;
1292         //this.scale = null;
1293 
1294         //以下属性不建议直接修改
1295         this.overlap = null;
1296         this.index = -1;
1297 
1298         CanvasImage.bindBox(this.#__bindObj, this.#box);
1299         this.setImage(image);
1300     }
1301 
1302     pos(x, y){
1303         if(UTILS.isNumber(x)){
1304             this.box.x = x;
1305             this.box.y = y;
1306         }
1307         else if(UTILS.isObject(x)){
1308             this.box.x = x.x;
1309             this.box.y = x.y;
1310         }
1311         return this;
1312     }
1313 
1314     setImage(image){
1315         if(CanvasImageRender.isCanvasImage(image)){
1316             this.box.size(image.width, image.height);
1317             this.#image = image;
1318         }
1319         else if(CanvasImage.prototype.isPrototypeOf(image)){
1320             if(image.isLoadImage){
1321                 if(Array.isArray(image.loadImage_cis)) image.loadImage_cis.push(this);
1322                 else image.loadImage_cis = [this];
1323             }
1324             else this.setImage(image.image);
1325         }
1326         else{
1327             this.box.size(0, 0);
1328             this.#image = null;
1329         }
1330         return this;
1331     }
1332 
1333     loadImage(src, onload){
1334         this.#isLoadImage = true;
1335         const image = new Image();
1336         image.onload = () => this._loadSuccess(image, onload);
1337         image.src = src;
1338         return this;
1339     }
1340 
1341     loadVideo(src, onload, type = "mp4"){
1342         /*    video 加载事件 的顺序
1343             onloadstart
1344             ondurationchange
1345             onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。
1346             onloadeddata
1347             onprogress
1348             oncanplay
1349             oncanplaythrough
1350 
1351             //控制事件:
1352             onended //播放结束
1353             onpause //暂停播放
1354             onplay //开始播放
1355         */
1356         this.#isLoadImage = true;
1357         const video = document.createElement("video"),
1358         source = document.createElement("source");
1359         video.appendChild(source);
1360         source.type = `video/${type}`;
1361 
1362         video.oncanplay = () => {
1363             //video 的 width, height 属性如果不设的话永远都是0
1364             video.width = video.videoWidth;
1365             video.height = video.videoHeight;
1366             this._loadSuccess(video, onload);
1367         };
1368 
1369         source.src = src;
1370         return this;
1371     }
1372 
1373     _loadSuccess(image, onload){
1374         this.setImage(image);
1375         this.#isLoadImage = false;
1376 
1377         if(Array.isArray(this.loadImage_cis)){
1378             this.loadImage_cis.forEach(ci => ci.setImage(image));
1379             delete this.loadImage_cis;
1380         }
1381 
1382         if(typeof onload === "function") onload(this);
1383         else if(CanvasImageRender.prototype.isPrototypeOf(onload)){
1384             if(onload.domElement.parentElement !== null) onload.redraw();
1385             else onload.render();
1386         }
1387     }
1388 
1389     /* setScale(nx = 0, ny = 0, ratio = 1){
1390         this.scale.w = this.#box.w * ratio;
1391         this.scale.h = this.#box.h * ratio;
1392         this.scale.x = nx - ((nx - this.scale.x) * ratio + this.scale.x) + this.scale.x;
1393         this.scale.y = ny - ((ny - this.scale.y) * ratio + this.scale.y) + this.scale.y;
1394         return this;
1395     }
1396 
1397     setScaleToSameScaleFromBox(newWidth = this.#box.w){
1398         if(this.#image === null || this.scale === null) return this;
1399 
1400         const width = this.#image.width, 
1401         height = this.#image.height,
1402         ratio = width / height,
1403         scale = ratio < 1 ? ratio * newWidth / width : newWidth / width;
1404 
1405         this.scale.w = width * scale;
1406         this.scale.h = height * scale;
1407         
1408         return this;
1409     }
1410 
1411     setScaleToCenterFromBox(){
1412         if(this.scale === null) return this;
1413 
1414         this.scale.x = (this.#box.w - this.scale.w) / 2;
1415         this.scale.y = (this.#box.h - this.scale.h) / 2;
1416 
1417         return this;
1418     } */
1419 
1420 }
1421 
1422 
1423 
1424 
1425 /* CanvasImages
1426 
1427 */
1428 class CanvasImages extends CanvasImage{
1429 
1430     #i = -1;
1431     get cursor(){return this.#i;}
1432     set cursor(i){
1433         super.setImage(this.images[i]);
1434         this.#i = this.image !== null ? i : -1;
1435     }
1436 
1437     constructor(images = []){
1438         super(images[0]);
1439         this.images = images;
1440         if(this.image !== null) this.#i = 0;
1441     }
1442 
1443     setImage(image){
1444         super.setImage(image);
1445     
1446         if(this.image !== null && Array.isArray(this.images)){
1447             const i = this.images.indexOf(this.image);
1448             if(i === -1){
1449                 this.#i = this.images.length;
1450                 this.images.push(this.image);
1451             }
1452             else this.#i = i;
1453         }
1454 
1455         return this;
1456     }
1457 
1458     next(){
1459         const len = this.images.length - 1;
1460         if(len !== -1){
1461             if(this.#i < len) this.#i++;
1462             else this.#i = 0;
1463             super.setImage(this.images[this.#i]);
1464         }
1465     }
1466 
1467     loadImages(srcs, onDone, onUpdate){
1468         onUpdate = typeof onUpdate === "function" ? onUpdate : null;
1469         var i = 0, c = srcs.length, img = null, _i = this.images.length;
1470 
1471         const len = srcs.length, 
1472         func = ()=>{
1473             i++; if(onUpdate !== null) onUpdate(this.images, _i);
1474             if(i === c && typeof onDone === "function"){
1475                 this.cursor = 0;
1476                 onDone(this.images, _i, srcs);
1477             }
1478             else _i++;
1479         }
1480 
1481         for(let k = 0, ty = ""; k < len; k++){
1482             ty = typeof srcs[k];
1483             if(ty === "string" || ty === "object"){
1484                 ty = ty === "string" ? srcs[k] : srcs[k].src;
1485                 if(ty !== "" && typeof ty === "string"){
1486                     img = new Image();
1487                     img.onload = func;
1488                     this.images.push(img);
1489                     img.src = ty;
1490                 }
1491                 else c--;
1492             }
1493         }
1494 
1495         return this;
1496     }
1497 
1498 }
1499 
1500 
1501 
1502 
1503 /* CanvasImageCustom
1504 
1505 demo:
1506     //CanvasImageRender
1507     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1508     cir.domElement.style = `
1509         position: absolute;
1510         z-index: 9999;
1511         background: rgb(127,127,127);
1512     `;
1513 
1514     //values
1515     cir.add(new CanvasImageCustom())
1516     .pos(59, 180)
1517     .load("view/examples/img/test.png", cir);
1518 
1519     cir.add(new CanvasImageCustom())
1520     .pos(59, 180)
1521     .text("value", "red")
1522     .rect().stroke()
1523 
1524 */
1525 class CanvasImageCustom extends CanvasImage{
1526 
1527     constructor(canvas){
1528         super(canvas);
1529     }
1530 
1531     toImage(callback){
1532         var image = new Image();
1533         image.src = this.image.toDataURL("image/png");
1534         image.onload = callback;
1535     }
1536 
1537     shear(canvas, dx = 0, dy = 0){
1538         if(CanvasImageRender.isCanvas(canvas) === false){
1539             canvas = document.createElement("canvas");
1540             canvas.width = this.width;
1541             canvas.height = this.height;
1542         }
1543 
1544         canvas.getContext("2d").drawImage(this.image, dx, dy); 
1545 
1546         return canvas;
1547     }
1548 
1549     setImage(image){
1550         if(CanvasImageRender.isCanvas(image)){ //image 如果是画布
1551             super.setImage(image);
1552             this.context = CanvasImageRender.getContext(image);
1553             this.size(image.width, image.height);
1554         }
1555         else{
1556             if(CanvasImageRender.isCanvasImage(image)){ //image 如果是图像
1557                 this.context = CanvasImageRender.getContext();
1558                 super.setImage(this.context.canvas);
1559                 this.size(image.width, image.height);
1560                 this.context.drawImage(image, 0, 0);
1561             }else{ //image 如果是其它对象
1562                 if(image) super.setImage(image);
1563                 else{
1564                     this.context = CanvasImageRender.getContext();
1565                     super.setImage(this.context.canvas);
1566                     this.size(this.width, this.height);
1567                 }
1568             }
1569         }
1570         
1571         return this;
1572     }
1573 
1574     clear(){
1575         this.context.clearRect(0, 0, this.box.w, this.box.h);
1576         return this;
1577     }
1578 
1579     size(w, h){
1580         this.box.size(w, h);
1581         this.image.width = w;
1582         this.image.height = h;
1583         CanvasImageRender.setDefaultStyles(this.context);
1584         this.fontSize = parseFloat(CanvasImageRender.defaultStyles.font);
1585         this.fillStyle = CanvasImageRender.defaultStyles.fillStyle;
1586         this.strokeStyle = CanvasImageRender.defaultStyles.strokeStyle;
1587         this.shadowColor = CanvasImageRender.defaultStyles.shadowColor;
1588         return this;
1589     }
1590 
1591     shadow(shadowColor = "rgba(0,0,0,0)", shadowBlur, shadowOffsetX, shadowOffsetY){
1592         const con = this.context;
1593         if(typeof shadowColor === "string" && this.shadowColor !== shadowColor) this.shadowColor = con.shadowColor = shadowColor;
1594         if(con.shadowBlur !== shadowBlur && typeof shadowBlur === "number") con.shadowBlur = shadowBlur;
1595         if(con.shadowOffsetX !== shadowOffsetX && typeof shadowOffsetX === "number") con.shadowOffsetX = shadowOffsetX;
1596         if(con.shadowOffsetY !== shadowOffsetY && typeof shadowOffsetY === "number") con.shadowOffsetY = shadowOffsetY;
1597         return this;
1598     }
1599 
1600     line(x, y, x1, y1){
1601         this.context.beginPath();
1602         this.context.moveTo(x, y);
1603         this.context.lineTo(x1, y1);
1604         return this;
1605     }
1606 
1607     path(arr, close = false){
1608         const con = this.context;
1609         con.beginPath();
1610         con.moveTo(arr[0], arr[1]);
1611         for(let k = 2, len = arr.length; k < len; k+=2) con.lineTo(arr[k], arr[k+1]);
1612         if(close === true) con.closePath();
1613         return this;
1614     }
1615 
1616     rect(r){
1617         const con = this.context, s = con.lineWidth;
1618         var x = s / 2,
1619             y = s / 2,
1620             w = this.box.w - s,
1621             h = this.box.h - s;
1622 
1623         if(UTILS.isNumber(r) === false || r <= 0){
1624             con.rect(x, y, w, h);
1625             return this;
1626         }
1627 
1628         const _x = x + r, 
1629         _y = y + r, 
1630         mx = x + w, 
1631         my = y + h, 
1632         _mx = mx - r, 
1633         _my = my - r;
1634         
1635         //
1636         con.moveTo(_x, y);
1637         con.lineTo(_mx, y);
1638         con.arcTo(mx, y, mx, _y, r);
1639 
1640         //
1641         con.lineTo(mx, _y);
1642         con.lineTo(mx, _my);
1643         con.arcTo(mx, my, _x, my, r);
1644 
1645         //
1646         con.lineTo(_x, my);
1647         con.lineTo(_mx, my);
1648         con.arcTo(x, my, x, _my, r);
1649 
1650         //
1651         con.lineTo(x, _y);
1652         con.lineTo(x, _my);
1653         con.arcTo(x, y, _x, y, r);
1654 
1655         return this;
1656     }
1657 
1658     stroke(color, lineWidth){
1659         if(typeof color === "string" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color;
1660         if(UTILS.isNumber(lineWidth) && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
1661         this.context.stroke();
1662         return this;
1663     }
1664 
1665     fill(color){
1666         if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1667         this.context.fill();
1668         return this;
1669     }
1670 
1671     text(value, color, fontSize = this.fontSize, x = -1, y = -1){
1672         if(UTILS.isNumber(fontSize) && this.fontSize !== fontSize){
1673             this.fontSize = fontSize;
1674             this.context.font = fontSize+"px SimSun, Songti SC";
1675         }
1676         
1677         const textWidth = this.context.measureText(value).width;
1678         if(textWidth > this.box.w || this.fontSize > this.box.h){
1679             this.size(textWidth+4, this.fontSize+4);
1680             this.fontSize = fontSize;
1681             this.context.font = fontSize+"px SimSun, Songti SC";
1682         }
1683 
1684         if(x === -1) x = (this.box.w - textWidth) / 2;
1685         if(y === -1) y = (this.box.h - this.fontSize) / 2;
1686 
1687         if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1688         this.context.fillText(value, x, y);
1689 
1690         return this;
1691     }
1692 
1693 }
1694 
1695 
1696 
1697 export {
1698     CanvasImageRender,
1699     CanvasImage, 
1700     CanvasImages,
1701     CanvasImageCustom,
1702 }
CanvasImageRender.js

 

  1 "use strict";
  2 
  3 import {
  4     UTILS, 
  5 } from './Utils.js';
  6 
  7 import {
  8     CanvasImages,
  9     CanvasImageCustom,
 10 } from './CanvasImageRender.js';
 11 
 12 var padding = 4;
 13 
 14 const emptyCIC = new CanvasImageCustom();
 15 
 16 function But_0(w, h, v){
 17     if(!UTILS.isNumber(w) || w <= 0 || w === Infinity) w = emptyCIC.context.measureText(v).width+padding+padding;
 18     if(!UTILS.isNumber(h) || h <= 0 || h === Infinity) h = Math.max(emptyCIC.fontSize+padding+padding,Math.round(30/70*w));
 19     
 20     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.1);
 21 
 22     const bgColor = "#b3b3b3", color = "#ffffff", borderColor = "#999999",
 23 
 24     a = emptyCIC
 25     .fill(bgColor)
 26     .text(v, color)
 27     .stroke(borderColor)
 28     .shear(),
 29 
 30     c = emptyCIC
 31     .fill("rgba(0,0,0,0.75)")
 32     .shear(),
 33 
 34     b = emptyCIC.clear()
 35     .fill(bgColor)
 36     .text(v, borderColor)
 37     .stroke(color)
 38     .shear();
 39 
 40     return [a, b, c];
 41 }
 42 
 43 
 44 /* CanvasButton 为画布预设按钮 (防止移动端多指混乱)
 45 parameter: 
 46     option = {
 47         style: 0,
 48         width: 100,
 49         height: 0,
 50         value: "TEST",
 51     }
 52 
 53 attribute:
 54     enable: Bool;     //是否启用;
 55     value: String;     //只读
 56 
 57 method:
 58     bindEvent(cir: CanvasImageRender, callback: Func): this;
 59 
 60 demo:
 61     //button
 62     const cb = cir.add(new CanvasButton({
 63         width: 80,
 64         value: "测试 TEST",
 65     }));
 66 
 67     cb.bindEvent(cir, event => console.log(1));
 68     cb.box.center(cir.box);
 69     console.log(cb);
 70 */
 71 class CanvasButton extends CanvasImages{
 72 
 73     static fontSize(v){
 74         v = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 12;
 75         if(v === emptyCIC.fontSize) return v;
 76         emptyCIC.fontSize = v;
 77         emptyCIC.context.font = v+"px SimSun, Songti SC";
 78         return v;
 79     }
 80 
 81     static padding(v){
 82         padding = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 4;
 83         return padding;
 84     }
 85 
 86     #value = "";
 87     get value(){return this.#value;}
 88 
 89     #enable = true;
 90     get enable(){return this.#enable;}
 91     set enable(v){
 92         if(v === false){
 93             this.cursor = 2;
 94             this.#enable = false;
 95         }
 96         else if(v === true){
 97             this.cursor = 0;
 98             this.#enable = true;
 99         }
100     }
101 
102     constructor(option = {}){
103 
104         const value = option.value !== "" && typeof option.value === "string" ? option.value : "Button";
105 
106         switch(option.style){
107             case 0: 
108             default: 
109             var images = But_0(option.width, option.height, value);
110             break;
111         }
112         
113         super(images);
114         this.#value = value;
115 
116     }
117 
118     bindEvent(cir, callback){
119         callback = typeof callback === "function" ? callback : null;
120         
121         var pointerId = -1;
122 
123         const onup_body = event => {
124             if(event.pointerId !== pointerId) return;
125             document.body.removeEventListener("pointerup", onup_body);
126             this.cursor = 0;
127             cir.redraw();
128         },
129 
130         onup = event => {
131             if(callback !== null && this.#enable && event.pointerId === pointerId) callback(event, this);
132         },
133 
134         ondown = event => {
135             if(event.button !== 0 || this.#enable === false) return;
136             pointerId = event.pointerId;
137             document.body.addEventListener("pointerup", onup_body);
138             this.cursor = 1;
139             cir.redraw();
140         },
141 
142         remove = () => {
143             document.body.removeEventListener("pointerup", onup_body);
144             cir.removeEvent(this, "up", onup);
145             cir.removeEvent(this, "down", ondown);
146             cir.removeEvent(null, "remove", remove);
147         }
148 
149         cir.addEvent(this, "down", ondown);
150         cir.addEvent(this, "up", onup);
151         cir.addEvent(null, "remove", remove);
152     
153         return this;
154     }
155 
156 }
157 
158 
159 
160 
161 export {
162     CanvasButton,
163 }
CanvasButton.js

 

 

使用例子: (按钮的默认样式)

 1    //CanvasImageRender
 2     const cir = new CanvasImageRender({
 3         width: window.innerWidth,   //canvas 的宽高
 4         height: window.innerHeight,
 5     }); 
 6 
 7     //.domElement 是一个 canvas
 8     cir.domElement.style = `
 9         position: absolute;
10         z-index: 9999;
11         background: rgb(127,127,127);
12     `;
13 
14     console.log(cir);
15 
16     //button
17     const cb = cir.add(new CanvasButton({
18         style: 0,           //默认样式
19         width: 80,          //高自动设置, 也可手动设置
20         value: "测试 TEST", //按钮名称
21     }));
22 
23     //绑定事件
24     cb.bindEvent(cir, event => console.log(1));
25 
26     //位置在画布中居中
27     cb.box.center(cir.box);
28 
29     console.log(cb);
30 
31     //canvas 添加至dom树
32     cir.render();

 

结果图:

 

 抬起

 

 

 按下

 

 

 禁用

 

 CanvasButton 被new完后根据参数创建了3张图(每个按钮都会创建三张图), 

 按钮的样式变化其实就是切换这三张图而已, 这些事都是它的父类做的

posted @ 2022-09-10 07:36  鸡儿er  阅读(35)  评论(0编辑  收藏  举报