js 2d画布 CanvasImageRender.js

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

 

 

   1 "use strict";
   2 
   3 import {
   4     UTILS, 
   5     Box, 
   6     Rotate,
   7     EventDispatcher,
   8     Point,
   9     AnimateLoop,
  10     RGBColor,
  11 } from './Utils.js';
  12 
  13 
  14 /* CanvasImageEvent (CanvasImageRender 内部用此类管理 dom 事件)
  15 ca 必须的属性: visible, box, rotate, index
  16 ca 必须的方法: 没有;
  17 
  18 parameter:
  19     domElement: CanvasImageRender.domElement; //必须
  20     box: CanvasImageRender.box; //必须
  21     ||或者第一个参数为 CanvasImageRender
  22 
  23     与.initEvent(domElement, box)参数一样
  24 
  25 attribute:
  26     domElement
  27     box: Box;
  28 
  29 method:
  30     add(ca: CanvasImage, eventName: String, callback: Function): ca; //ca添加事件
  31     remove(ca: CanvasImage, eventName: String, callback: Function): ca; //ca删除事件
  32         eventName: 可能的值为 CanvasImageEvent.canvasEventsList 的属性名
  33         callback: 参数 event, ca
  34     
  35     clear(ca: CanvasImage, eventName: String): ca; //ca 必须; eventName 可选, 如果未定义则清空ca的所有事件;
  36     disposeEvent(eventName): this; //.initEvent(domElement, box)调用一次此方法
  37     initEvent(domElement, box): this; //每次更换 domElement, box 后应调用此方法; (CanvasAnimateEvent初始化时自动调用一次)
  38     setScale(x, y: Number): undefiend; //
  39 
  40 eventName:
  41     (如果注册了 "out"|"over" 事件, 在弃用时(CanvasImageRender 不在使用): 
  42     必需要调用.disposeEvent(eventName)方法清理注册的dom事件, 
  43     因为这两个事件用的是 pointermove 而不是 onpointermove);
  44 
  45     down    //鼠标左右键按下
  46     move    //鼠标左右键移动
  47     up        //鼠标左右键抬起
  48 
  49     click    //鼠标左键
  50     wheel    //鼠标滚动轮
  51     out        //移出
  52     over    //移入
  53 
  54 demo:
  55     const car = new CanvasImageRender({width: 100, height: 100}),
  56     cae = new CanvasImageEvent(car),
  57     ca = car.add(new CanvasImage(image));
  58 
  59     //ca添加点击事件
  60     cae.add(ca, 'click', (event, target) => console.log(event, target));
  61 
  62     car.render();
  63 
  64 */
  65 class CanvasImageEvent{
  66 
  67     static bind(obj, is){
  68         /* const _eventList = {}
  69         Object.defineProperty(obj, "_eventList", {
  70             get(){return _eventList;}
  71         }); */
  72         obj._eventList = {};
  73         if(is === true){
  74             let k, evns = CanvasImageEvent.canvasEventsList;
  75             for(k in evns) obj._eventList[k] = [];
  76         }
  77             
  78     }
  79 
  80     static canvasEventsList = {
  81         down: "onpointerdown",
  82         move: "onpointermove",
  83         up: "onpointerup",
  84         click: "onclick",
  85         wheel: "onmousewheel",
  86         out: "pointermove", //移出 
  87         over: "pointermove", //移入
  88     }
  89 
  90     static isEventName(eventName){
  91 
  92         return CanvasImageEvent.canvasEventsList[eventName] !== undefined;
  93 
  94     }
  95     
  96     static emptyBox = new Box();
  97     static emptyRotate = new Rotate();
  98 
  99     constructor(domElement, box){
 100         this._running = "";
 101         this._delList = [];
 102         CanvasImageEvent.bind(this);
 103         this.initEvent(domElement, box);
 104     }
 105 
 106     initEvent(domElement, box){
 107         this.disposeEvent();
 108 
 109         if(CanvasImageRender.prototype.isPrototypeOf(domElement)){
 110             this.domElement = domElement.domElement;
 111             this.box = domElement.box;
 112         }
 113 
 114         else{
 115             this.domElement = domElement;
 116             this.box = box;
 117         }
 118 
 119         if(this._eventList !== undefined){
 120             for(let evn in this._eventList){
 121                 if(this._eventList[evn] !== undefined) this._createEvent(evn);
 122             }
 123 
 124         }
 125         
 126         return this;
 127     }
 128 
 129     add(ca, eventName, callback){
 130         if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName);
 131         if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误");
 132         
 133         this._add(ca, eventName);
 134         this._addCA(ca, eventName, callback);
 135         
 136         return ca;
 137     }
 138     
 139     remove(ca, eventName, callback){
 140         if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName);
 141         if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误");
 142         if(this._running !== eventName){
 143             this._remove(ca, eventName);
 144             this._removeCA(ca, eventName, callback);
 145         }
 146 
 147         else this._delList.push(ca, eventName, callback);
 148 
 149         return ca;
 150     }
 151 
 152     disposeEvent(eventName){
 153         if(eventName === "over" || eventName === "out"){
 154 
 155             if(typeof this["_"+eventName] === "function"){
 156                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList[eventName], this["_"+eventName]);
 157                 delete this["_"+eventName];
 158             }
 159 
 160         }
 161 
 162         else{
 163 
 164             if(typeof this["_over"] === "function"){
 165                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["over"], this["_over"]);
 166                 delete this["_over"];
 167             }
 168 
 169             if(typeof this["_out"] === "function"){
 170                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["out"], this["_out"]);
 171                 delete this["_out"];
 172             }
 173 
 174         }
 175 
 176         return this;
 177     }
 178     
 179     clear(ca, eventName){
 180         if(eventName === undefined){
 181             var k; for(k in this._eventList){
 182                 this._remove(ca, k);
 183             }
 184             
 185             if(ca._eventList !== undefined) delete ca._eventList; //CanvasImageEvent.bind(ca, true);
 186             
 187         }
 188 
 189         else if(CanvasImageEvent.canvasEventsList[eventName] !== undefined){
 190             this._remove(ca, eventName);
 191 
 192             if(ca._eventList !== undefined) ca._eventList[eventName].length = 0;
 193             
 194         }
 195 
 196         return ca;
 197     }
 198 
 199     _addCA(ca, eventName, callback){
 200         if(ca._eventList === undefined) CanvasImageEvent.bind(ca);
 201         if(ca._eventList[eventName] === undefined) ca._eventList[eventName] = [];
 202         ca._eventList[eventName].push(callback);
 203 
 204     }
 205 
 206     _removeCA(ca, eventName, callback){
 207         if(ca._eventList !== undefined && ca._eventList[eventName] !== undefined){
 208             for(let k = 0, len = ca._eventList[eventName].length; k < len; k++){
 209                 if(ca._eventList[eventName][k] === callback){
 210                     ca._eventList[eventName].splice(k, 1);
 211                     break;
 212                 }
 213             }
 214         }
 215 
 216     }
 217 
 218     _add(ca, eventName){
 219         if(this._eventList[eventName] === undefined){
 220             this._eventList[eventName] = [];
 221             this._createEvent(eventName);
 222         }
 223 
 224         //if(this._eventList[eventName].includes(ca) === false) this._eventList[eventName].push(ca);
 225         this._eventList[eventName].push(ca);
 226     }
 227 
 228     _remove(ca, eventName){
 229         if(this._eventList[eventName] !== undefined){
 230             let key = this._eventList[eventName].indexOf(ca);
 231             if(key !== -1) this._eventList[eventName].splice(key, 1);
 232             if(key === 0){
 233                 if(eventName == "over" || eventName === "out") this.disposeEvent(eventName);
 234                 else this.domElement[CanvasImageEvent.canvasEventsList[eventName]] = null;
 235                 delete this._eventList[eventName];
 236             }
 237 
 238         }
 239 
 240     }
 241 
 242     _createEvent(evn){
 243         var k, len, ca, arr, tar = null, oldTar = null, _run = null, offsetX, offsetY;
 244 
 245         const _box = CanvasImageEvent.emptyBox, _rotate = CanvasImageEvent.emptyRotate,
 246 
 247         run = event => {
 248             len = this["_eventList"][evn].length;
 249             if(len === 0) return;
 250 
 251             offsetX = event.offsetX + this.box.x;
 252             offsetY = event.offsetY + this.box.y;
 253 
 254             tar = null;
 255             for(k = 0; k < len; k++){
 256                 ca = this["_eventList"][evn][k];
 257                 _box.copy(ca.box);
 258 
 259                 //计算旋转后的box
 260                 if(ca.rotate !== null) _box.setFromRotate(_rotate.set(_box.x+ca.rotate.origin.x, _box.y+ca.rotate.origin.y, ca.rotate.angle));
 261                 
 262                 if(ca["visible"] === true && _box.containsPoint(offsetX, offsetY)){
 263                     
 264                     if(tar === null || tar["index"] < ca["index"]) tar = ca;
 265                     
 266                 }
 267 
 268             }
 269             
 270             if(_run !== null) _run();
 271             if(tar !== null){
 272                 this._running = evn;
 273                 arr = tar["_eventList"][evn]; 
 274                 len = arr.length;
 275                 for(k = 0; k < len; k++) arr[k](event, tar);
 276                 
 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 
 382 关于canvas的优化:
 383     离屏渲染(预处理图像的 缩放, 旋转, 剪裁, 等)
 384     避免浮点坐标(用Math.floor()函数对所有的坐标点取整, 浏览器为了达到抗锯齿的效果会做额外的运算)
 385     如果可能请关闭透明度 (CanvasImageRender.paramCon.alpha = false)
 386 
 387 关于渲染 CanvasImage
 388     尽量让box.w和box.h与image的宽高保持一致, 否则在渲染时会缩放image
 389     如果你不在使用这些属性请将它们设为默认值: .opacity 默认1, .rotate 默认null
 390     如果多个 CanvasImage 的image是一样的应该: a.loadImage(src); b.setImage(a); c.setImage(a);
 391 
 392 parameter: 
 393     option = {
 394         canvas             //默认 新的canvas
 395         width, height     //默认 1
 396         className, id    //默认 ""
 397 
 398         //一下属性 scroll 为 true 才有效
 399         scroll             //是否启用滚动轴; 默认 false
 400         scrollSize        //滚动轴的宽或高; 默认 6; (如果想隐藏滚动条最好用 scrollVisible, 而不是把此属性设为0)
 401         scrollVisible    //滚动轴的显示类型; 可能值 "auto" || "visible" || 默认"";
 402         eventDomTarget: //dom事件的绑定目标html元素; 默认 this.domElement
 403 
 404         scrollEventType    //可能的值: 默认"", "default", "touch";
 405         inertia            //是否启用移动端滚动轴的惯性; 默认 false
 406         inertiaLife        //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04;
 407 
 408         //
 409         test_redrawTarget: CanvasImage; //用于查看 redrawTarget(ca) 方法的重绘范围
 410     }
 411 
 412 attribute:
 413 
 414 method:
 415     redraw(): undefined; //重绘整个画布
 416 
 417     redrawTarget(ca: CanvasImage): undefined;
 418     //工作原理: 收集与ca重叠的ca; 重绘这些重叠的ca, 前提是ca的box没发生改变; (缩放或旋转ca的box都会发生改变)
 419     例子:
 420         ca.opacity = 0.6;
 421         cir.redrawTarget(ca);
 422 
 423         ca.visible = false;
 424         cir.redrawTarget(ca);
 425 
 426         cir.remove(ca);
 427         cir.redrawTarget(ca);
 428 
 429         cir.add(ca);
 430         cir.redrawTarget(ca);
 431 
 432         ca.setImage(newImage) //假设newImage与ca的box一样大
 433         cir.redrawTarget(ca);
 434 
 435 ImageData:
 436     length: w * h * 4
 437     r: 0 - 255
 438     g: 0 - 255
 439     b: 0 - 255
 440     a: 0 - 255 Math.round(255 * a)
 441 
 442     遍历:
 443     const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
 444     for(let k = 0, x, y, r, g, b, a, i; k < len; k++){
 445         x = k % width;
 446         y = Math.floor(k / width);
 447 
 448         i = k*4;
 449         r = data[i]
 450         g = data[i+1]
 451         b = data[i+2]
 452         a = data[i+3]
 453 
 454         console.log(x, y, r, g, b, a, i);
 455     }
 456     
 457 demo:
 458     //CanvasImageRender
 459     const cir = new CanvasImageRender({
 460         width: WORLD.width, 
 461         height: WORLD.height,
 462 
 463         //一下属性 scroll 为 true 才有效
 464         scroll: true,             //是否启用滚动轴; 默认 false
 465         scrollSize: 10,        //滚动轴的宽或高; 默认 6; (如果想隐藏滚动条最好用 scrollVisible, 而不是把此属性设为0)
 466         scrollVisible: "auto",    //滚动轴的显示类型; 可能值 "auto" || "visible" || 默认"";
 467 
 468         scrollEventType: "touch",    //可能的值: 默认"", "default", "touch";
 469         inertia: true,            //是否启用移动端滚动轴的惯性; 默认 false
 470         inertiaLife: 0.03,        //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04;
 471     });
 472 
 473     cir.domElement.style = `
 474         position: absolute;
 475         z-index: 9999;
 476         background: rgb(127,127,127);
 477     `;
 478 
 479     const ciA = new CanvasImage().loadImage("view/examples/img/test.png", cir),
 480     pi2 = Math.PI*2;
 481 
 482     for(let k = 0, ca; k < 3000; k++){
 483         ca = cir.list[k] = new CanvasImage(ciA);
 484         ca.pos(UTILS.random(0, 10000), UTILS.random(0, 10000));
 485         ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2));
 486     }
 487     
 488     //为什么要用 initList() ? 因为for循环中是直接把ca加入到了列表, 没对ca做任何初始化操作;
 489     //如果全用.add() 就不需要在调用 initList(); 但 .add() 方法不适合一次性添加多个 CanvasImage;
 490     cir.initList(); 
 491 
 492 */
 493 class CanvasImageRender{
 494 
 495     static emptyArrA = []
 496     static emptyArrB = []
 497     static paramCon = {alpha: true}
 498 
 499     static defaultStyles = {
 500         filter: "none",
 501         globalAlpha: 1,
 502         globalCompositeOperation: "source-over",
 503         imageSmoothingEnabled: true,
 504         miterLimit: 10,
 505         font: "12px SimSun, Songti SC",
 506         textAlign: "left",
 507         textBaseline: "top",
 508         lineCap: "butt",
 509         lineJoin: "miter",
 510         lineDashOffset: 0,
 511         lineWidth: 1,
 512         shadowColor: "rgba(0, 0, 0, 0)",
 513         shadowBlur: 0,
 514         shadowOffsetX: 0,
 515         shadowOffsetY: 0,
 516         fillStyle: "rgba(50,50,50,0.4)",
 517         strokeStyle: "rgba(210,210,210,0.6)",
 518     }
 519 
 520     static setDefaultStyles(context){
 521         let k = "", styles = CanvasImageRender.defaultStyles;
 522         for(k in styles){
 523             if(context[k] !== styles[k]) context[k] = styles[k];
 524         }
 525     }
 526 
 527     static getContext(canvas, className, id){
 528         if(CanvasImageRender.isCanvas(canvas) === false) canvas = document.createElement("canvas");
 529         const context = canvas.getContext("2d", CanvasImageRender.paramCon);
 530 
 531         if(typeof className === "string") canvas.className = className;
 532         if(typeof id === "string") canvas.setAttribute('id', id);
 533         
 534         return context;
 535     }
 536 
 537     static downloadImage(func){
 538         const input = document.createElement("input");
 539         input.type = "file";
 540         input.multiple = "multiple";
 541         input.accept = ".png, .jpg, .jpeg, .bmp, .gif";
 542     
 543         input.onchange = e => {
 544             if(e.target.files.length === 0) return;
 545             const fr = new FileReader();
 546             fr.onloadend = e1 => {
 547                 const img = new Image();
 548                 img.onload = () => func(img);
 549                 img.src = e1.target.result;
 550             }
 551 
 552             fr.readAsDataURL(e.target.files[0]);
 553         }
 554         
 555         input.click();
 556     }
 557 
 558     static isCanvasImage(img){ //OffscreenCanvas:ImageBitmap; CSSImageValue:
 559 
 560         return ImageBitmap["prototype"]["isPrototypeOf"](img) || 
 561         HTMLImageElement["prototype"]["isPrototypeOf"](img) || 
 562         HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 
 563         CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 
 564         HTMLVideoElement["prototype"]["isPrototypeOf"](img);
 565         
 566     }
 567 
 568     static isCanvas(canvas){
 569         
 570         return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas);
 571 
 572     }
 573 
 574     static unbindScroll(box){
 575         var _x = box.x, _y = box.y, _w = box.w, _h = box.h;
 576         Object.defineProperties(box, {
 577 
 578             x: {
 579                 get(){return _x;},
 580                 set(v){_x = v;}
 581             },
 582 
 583             y: {
 584                 get(){return _y;},
 585                 set(v){_y = v;}
 586             },
 587 
 588             w: {
 589                 get(){return _w;},
 590                 set(v){_w = v;}
 591             },
 592 
 593             h: {
 594                 get(){return _h;},
 595                 set(v){_h = v;}
 596             },
 597 
 598         });
 599     }
 600     
 601     #canvasImageEvent = null;
 602     #eventDispatcher = null;
 603     #box = new Box();
 604     get canvasImageEvent(){return this.#canvasImageEvent;}
 605     get box(){return this.#box;}
 606     
 607     #scrollViewBoxX = null;
 608     #scrollViewBoxY = null;
 609     #maxSize = null;
 610     get maxSize(){return this.#maxSize;}
 611 
 612     constructor(option = {}){
 613         this.list = [];
 614         this.context = CanvasImageRender.getContext(option.canvas, option.className, option.id);
 615         this.domElement = this.context.canvas;
 616 
 617         //box
 618         let _x = this.#box.x, _y = this.#box.y;
 619         Object.defineProperties(this.#box, {
 620 
 621             x: {
 622                 get: () => {return _x;},
 623                 set: v => {
 624                     if(v < 0 || this.#maxSize === null) v = 0;
 625                     else if(v + this.#box.w > this.#maxSize.x) v = this.#maxSize.x - this.#box.w;
 626                     _x = v;
 627                 }
 628             },
 629 
 630             y: {
 631                 get: () => {return _y;},
 632                 set: v => {
 633                     if(v < 0 || this.#maxSize === null) v = 0;
 634                     else if(v + this.#box.h > this.#maxSize.y) v = this.#maxSize.y - this.#box.h;
 635                     _y = v;
 636                 }
 637             },
 638 
 639         });
 640         
 641         //event 
 642         this.#canvasImageEvent = new CanvasImageEvent(option.eventDomTarget || this.domElement, this.#box);
 643         const eventParam = {target: this}, 
 644         eventParam1 = {target: this, value: null}
 645         this.#eventDispatcher = new EventDispatcher();
 646         this.#eventDispatcher
 647         .customEvents("beforeDraw", eventParam)
 648         .customEvents("afterDraw", eventParam)
 649         .customEvents("beforeDrawTarget", eventParam)
 650         .customEvents("afterDrawTarget", eventParam)
 651         .customEvents("pos", eventParam)
 652         .customEvents("initList", eventParam)
 653         .customEvents("size", eventParam)
 654         .customEvents("add", eventParam1)
 655         .customEvents("remove", eventParam1);
 656 
 657         //scroll
 658         if(option.scroll === true){
 659             this.#maxSize = new Point();
 660             this.#scrollViewBoxX = new Box();
 661             this.#scrollViewBoxY = new Box();
 662             this.scrollSize = UTILS.isNumber(option.scrollSize) ? option.scrollSize : 6;
 663 
 664             switch(option.scrollVisible){
 665                 case "visible": 
 666                 case "auto": 
 667                 this.scrollVisible = option.scrollVisible;
 668                 break;
 669 
 670                 default: 
 671                 this.scrollVisible = "";
 672                 break;
 673             }
 674     
 675             switch(option.scrollEventType){
 676                 case "default": 
 677                 this._createScrollEventPC();
 678                 break;
 679 
 680                 case "touch": 
 681                 this._createScrollEventMobile(this.#canvasImageEvent.domElement, option.inertia, option.inertiaLife);
 682                 break;
 683 
 684                 default: 
 685                 break;
 686             }
 687         }
 688 
 689         //test_redrawTarget_box
 690         if(CanvasImage["prototype"]["isPrototypeOf"](option.test_redrawTarget)){
 691             const tar = option.test_redrawTarget,
 692             rgba = new RGBColor().set("#0089ff").getRGBA(0.4);
 693             
 694             this.addEvent(null, "afterDraw", ()=>{
 695                 this._computeOverlap(tar);
 696                 tar.overlap.box.x -= 1;
 697                 tar.overlap.box.y -= 1;
 698                 tar.overlap.box.w += 2;
 699                 tar.overlap.box.h += 2;
 700                 const ss = this.context.fillStyle;
 701                 this.context.fillStyle = rgba;
 702                 this.fillBox(tar.overlap.box);
 703                 this.context.fillStyle = ss;
 704             });
 705         }
 706 
 707         this.size(option.width, option.height);
 708     }
 709 
 710     strokeBox(box){
 711         this.context.strokeRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h);
 712     }
 713 
 714     fillBox(box){
 715         this.context.fillRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h);
 716     }
 717 
 718     addEvent(ca, eventName, callback){
 719         if(ca) this.#canvasImageEvent.add(ca, eventName, callback);
 720         else this.#eventDispatcher.register(eventName, callback);
 721     }
 722 
 723     removeEvent(ca, eventName, callback){
 724         if(ca) this.#canvasImageEvent.remove(ca, eventName, callback);
 725         else this.#eventDispatcher.deleteEvent(eventName, callback);
 726     }
 727 
 728     isDraw(ca){
 729         return ca["visible"] === true && ca["image"] !== null && this["box"]["intersectsBox"](ca["box"]);
 730     }
 731 
 732     pos(x = 0, y = 0){
 733         this.domElement.style.left = x + "px";
 734         this.domElement.style.top = y + "px";
 735         this.#eventDispatcher.trigger("pos");
 736         return this;
 737     }
 738 
 739     size(w = 1, h = 1){
 740         this.domElement.width = w;
 741         this.domElement.height = h;
 742         CanvasImageRender.setDefaultStyles(this.context);
 743         this.box.size(w, h);
 744         this.#eventDispatcher.trigger("size");
 745         return this;
 746     }
 747 
 748     append(ca){
 749         this.add(ca);
 750         return this;
 751     }
 752 
 753     add(ca){
 754         if(CanvasImage.prototype.isPrototypeOf(ca) && !this.list.includes(ca)){
 755             const len = this.list.length;
 756             
 757             if(this.list[ca.index] === undefined){
 758                 ca.index = len;
 759                 this.list.push(ca);
 760             }
 761 
 762             else{
 763                 const arr = this.list.splice(ca.index);
 764                 this.list.push(ca);
 765                 for(let k = 0, c = arr.length; k < c; k++){
 766                     this.list.push(arr[k]);
 767                     arr[k].index++;
 768                 }
 769             }
 770 
 771             if(this.#maxSize !== null){
 772                 this._bindScroll(ca.box);
 773                 ca.box.w = ca.box.w;
 774                 ca.box.h = ca.box.h;
 775             }
 776 
 777             this.#eventDispatcher.trigger("add", param => param.value = ca);
 778         }
 779         
 780         return ca;
 781     }
 782 
 783     remove(ca){
 784         var i = ca.index;
 785 
 786         if(this.list[i] !== ca) i = this.list.indexOf(ca);
 787 
 788         if(i !== -1){
 789             this.list.splice(i, 1);
 790             for(let k = i, len = this.list.length; k < len; k++) this.list[k].index -= 1;
 791             if(this.#maxSize !== null){
 792                 if(ca.box.mx >= this.#maxSize.x) this._resetMaxSizeX();
 793                 if(ca.box.my >= this.#maxSize.y) this._resetMaxSizeY();
 794                 CanvasImageRender.unbindScroll(ca.box);
 795             }
 796             this.#eventDispatcher.trigger("remove", param => param.value = ca);
 797         }
 798 
 799         return ca;
 800     }
 801 
 802     render(parentElem = document.body){
 803         this.redraw();
 804         if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement);
 805         return this;
 806     }
 807 
 808     initList(i){
 809         if(i === undefined || i < 0) i = 0;
 810         const len = this.list.length;
 811 
 812         if(this.#maxSize === null){
 813             for(let k = i; k < len; k++) this.list[k].index = k;
 814         }
 815 
 816         else{
 817             for(let k = i, ca; k < len; k++){
 818                 ca = this.list[k];
 819                 ca.index = k;
 820                 this._bindScroll(ca.box);
 821             }
 822             this._resetMaxSizeX();
 823             this._resetMaxSizeY();
 824         }
 825         
 826         this.#eventDispatcher.trigger("initList");
 827         return this;
 828     }
 829 
 830     clear(){
 831 
 832         this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']);
 833 
 834     }
 835 
 836     draw(){
 837         this.#eventDispatcher.trigger("beforeDraw");
 838         const len = this["list"]["length"];
 839         for(let k = 0, ca; k < len; k++){
 840             ca = this["list"][k];
 841             if(this["isDraw"](ca) === true) this["_draw"](ca);
 842         }
 843         
 844         if(this.#maxSize !== null && this.scrollVisible !== ""){
 845             this._updateScrollViewBox();
 846             this._drawScroll();
 847         }
 848         this.#eventDispatcher.trigger("afterDraw");
 849     }
 850 
 851     redrawTarget(ca){
 852         this.#eventDispatcher.trigger("beforeDrawTarget");
 853         if(this.#maxSize !== null && this.scrollVisible !== ""){
 854             this["_computeOverlap"](ca);
 855             ca["overlap"]["box"]["x"] -= this.#box.x;
 856             ca["overlap"]["box"]["y"] -= this.#box.y;
 857 
 858             this._updateScrollViewBox();
 859 
 860             if(this.#scrollViewBoxX.intersectsBox(ca["overlap"]["box"]) || this.#scrollViewBoxY.intersectsBox(ca["overlap"]["box"])){
 861                 this['context']['clearRect'](this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h);
 862                 this['context']['clearRect'](this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h);
 863                 this["_drawTarget"](ca);
 864                 this._drawScroll();
 865             }
 866 
 867             else this["_drawTarget"](ca);
 868         }
 869 
 870         else{
 871             this["_computeOverlap"](ca);
 872             this["_drawTarget"](ca);
 873         }
 874         this.#eventDispatcher.trigger("afterDrawTarget");
 875     }
 876 
 877     redraw(){
 878         this.clear();
 879         this.draw();
 880     }
 881 
 882     //限内部使用
 883     _computeOverlap(tar){ //暂不支持带有旋转的 CanvasImage (如遇到将忽略)
 884         //1 如果检索到与box相交的ca时, 合并其box执行以下步骤: 
 885         //2 检索 已检索过的 并且 没有相交的ca
 886         //3 如果存在相交的ca就合并box并继续第 2 步; 如果不存在继续第 1 步
 887         if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false};
 888         const _list = CanvasImageRender.emptyArrA, list = CanvasImageRender.emptyArrB, 
 889         len = this.list.length, box = tar["overlap"]["box"]["copy"](tar["box"]);
 890 
 891         for(let k = 0, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){
 892             tar = this["list"][k];
 893             if(tar["overlap"] === null) tar["overlap"] = {box: new Box(), draw: false};
 894             
 895             if(this.isDraw(tar) === false){
 896                 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false;
 897                 continue;
 898             }
 899 
 900             if(box["intersectsBox"](tar["box"]) === true){
 901                 if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true;
 902                 box["expand"](tar["box"]);
 903                 loop = true;
 904 
 905                 while(loop === true){
 906                     b["length"] = 0;
 907                     loop = false;
 908                     c = _c;
 909 
 910                     for(i = 0; i < c; i++){
 911                         tar = a[i];
 912 
 913                         if(box["intersectsBox"](tar["box"]) === true){
 914                             if(tar["overlap"]["draw"] !== true) tar["overlap"]["draw"] = true;
 915                             box["expand"](tar["box"]);
 916                             loop = true; _c--;
 917                         }
 918 
 919                         else b.push(tar);
 920                         
 921                     }
 922 
 923                     a = a === _list ? list : _list;
 924                     b = b === _list ? list : _list;
 925 
 926                 }
 927                 
 928             }
 929 
 930             else{
 931                 _c++;
 932                 a["push"](tar);
 933                 if(tar["overlap"]["draw"] !== false) tar["overlap"]["draw"] = false;
 934             }
 935 
 936         }
 937         
 938         _list.length = list.length = 0;
 939     }
 940 
 941     _drawImage(ca, ox = 0, oy = 0){
 942         if(ca["opacity"] === 1){
 943             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 944             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);
 945         }
 946 
 947         else{
 948             this["context"]["globalAlpha"] = ca["opacity"];
 949             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 950             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);
 951             this["context"]["globalAlpha"] = 1;
 952         }
 953     }
 954 
 955     _draw(ca){
 956         if(ca.rotate === null) this._drawImage(ca);
 957         else{
 958             const cx = ca.rotate.origin.x + ca.box.x - this.#box.x, 
 959             cy = ca.rotate.origin.y + ca.box.y - this.#box.y;
 960             this.context.translate(cx, cy);
 961             this.context.rotate(ca.rotate.angle);
 962             this._drawImage(ca, -cx, -cy);
 963             this.context.rotate(-ca.rotate.angle);
 964             this.context.translate(-cx, -cy);
 965         }
 966     }
 967     
 968     _drawTarget(ca){
 969         const len = this["list"]["length"];
 970 
 971         this["context"]["clearRect"](
 972             ca["overlap"]["box"]["x"], 
 973             ca["overlap"]["box"]["y"], 
 974             ca["overlap"]["box"]["w"], 
 975             ca["overlap"]["box"]["h"]);
 976 
 977         for(let k = 0; k < len; k++){
 978             ca = this["list"][k];
 979             if(ca["overlap"]["draw"] === true) this._draw(ca);
 980         }
 981     }
 982 
 983     //关于scroll
 984     _drawScroll(){
 985         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.x > this.#box.w)){
 986             this.context.fillRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h);
 987             this.context.strokeRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h);
 988         }
 989 
 990         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.y > this.#box.h)){
 991             this.context.fillRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h);
 992             this.context.strokeRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h);
 993         }
 994     }
 995 
 996     _updateScrollViewBox(){
 997         this.#scrollViewBoxX.x = this._cursorX();
 998         this.#scrollViewBoxX.y = this.#box.h - this.scrollSize;
 999         this.#scrollViewBoxX.w = this.#maxSize.x <= this.#box.w ? this.#box.w : this.#box.w / this.#maxSize.x * this.#box.w;
1000         this.#scrollViewBoxX.h = this.scrollSize;
1001 
1002         this.#scrollViewBoxY.x = this.#box.w - this.scrollSize;
1003         this.#scrollViewBoxY.y = this._cursorY();
1004         this.#scrollViewBoxY.w = this.scrollSize;
1005         this.#scrollViewBoxY.h = this.#maxSize.y <= this.#box.h ? this.#box.h : this.#box.h / this.#maxSize.y * this.#box.h;
1006     }
1007 
1008     _createScrollEventPC(){
1009         var dPos = -1;
1010 
1011         const setTop = (event, top) => {
1012             this.#box.y = top / this.box.h * this.#maxSize.y;
1013             this.redraw();
1014         },
1015 
1016         setLeft = (event, left) => {
1017             this.#box.x = left / this.box.w * this.#maxSize.x;
1018             this.redraw();
1019         },
1020         
1021         onMoveTop = event => {
1022             setTop(event, event.offsetY - dPos);
1023         },
1024 
1025         onMoveLeft = event => {
1026             setLeft(event, event.offsetX - dPos);
1027         },
1028 
1029         onUpTop = event => {
1030             document.body.removeEventListener('pointermove', onMoveTop);
1031             document.body.removeEventListener('pointerup', onUpTop);
1032             if(event !== null) onMoveTop(event);
1033         },
1034 
1035         onUpLeft = event => {
1036             document.body.removeEventListener('pointermove', onMoveLeft);
1037             document.body.removeEventListener('pointerup', onUpLeft);
1038             if(event !== null) onMoveLeft(event);
1039         },
1040 
1041         scope = this, _box = this.#box,
1042 
1043         //伪 CanvasImage, 是 CanvasImageEvent 使用的必须属性
1044         boxX = new Box(),
1045         eventTargetX = {
1046             get index(){return scope.length+1;},
1047             get visible(){return true;},
1048             get box(){return boxX},
1049             get rotate(){return null},
1050         },
1051         
1052         boxY = new Box(),
1053         eventTargetY = {
1054             get index(){return scope.length+2;},
1055             get visible(){return true;},
1056             get box(){return boxY},
1057             get rotate(){return null},
1058         },
1059 
1060         eventTargetWheel = {
1061             get index(){return -1;},
1062             get visible(){return true;},
1063             get box(){return _box},
1064             get rotate(){return null},
1065         }
1066 
1067         Object.defineProperties(boxX, {
1068             x: {get: () => {return _box.x;}},
1069             y: {get: () => {return _box.h - scope.scrollSize + _box.y;}},
1070             w: {get: () => {return _box.w;}},
1071             h: {get: () => {return scope.scrollSize;}},
1072         });
1073 
1074         Object.defineProperties(boxY, {
1075             x: {get: () => {return _box.w - scope.scrollSize + _box.x;}},
1076             y: {get: () => {return _box.y;}},
1077             w: {get: () => {return scope.scrollSize;}},
1078             h: {get: () => {return _box.h;}},
1079         });
1080 
1081         //pc event
1082         this.#canvasImageEvent.add(eventTargetX, "down", event => {
1083             dPos = event.offsetX - this._cursorX();
1084             onUpLeft(null);
1085             document.body.addEventListener("pointermove", onMoveLeft);
1086             document.body.addEventListener("pointerup", onUpLeft);
1087         });
1088 
1089         this.#canvasImageEvent.add(eventTargetY, "down", event => {
1090             dPos = event.offsetY - this._cursorY();
1091             onUpTop(null);
1092             document.body.addEventListener("pointermove", onMoveTop);
1093             document.body.addEventListener("pointerup", onUpTop);
1094         });
1095 
1096         this.#canvasImageEvent.add(eventTargetWheel, "wheel", event => {
1097             if(this.#maxSize.y > this.#box.h){
1098                 dPos = 50 / this.#maxSize.y * this.#box.h;
1099                 setTop(event, this._cursorY() + (event.wheelDelta === 120 ? -dPos : dPos));
1100             }
1101             else if(this.#maxSize.x > this.#box.w){
1102                 dPos = 50 / this.#maxSize.x * this.#box.w;
1103                 setLeft(event, this._cursorX() + (event.wheelDelta === 120 ? -dPos : dPos));
1104             }
1105         });
1106 
1107     }
1108 
1109     _createScrollEventMobile(domElement, inertia, inertiaLife){
1110         var px, py, dPos = 0, sPos = new Point(), step = 1, aniamteRun = false, stepA = "", stepB = "", sTime = 0;
1111 
1112         if(inertia === true){
1113             inertiaLife = 1 - ((UTILS.isNumber(inertiaLife) && inertiaLife >= 0 && inertiaLife <= 1) ? inertiaLife : 0.04);
1114             var inertiaAnimate = new AnimateLoop(null),
1115         
1116             inertiaTop = (event, speed) => {
1117                 if(Math.abs(speed) < 1) return;
1118                 
1119                 stepA = speed < 0 ? "-top" : "top";
1120                 if(aniamteRun && stepA === stepB) step += 0.3;
1121                 else{
1122                     step = 1;
1123                     stepB = stepA;
1124                 }
1125                 
1126                 inertiaAnimate.play(() => {
1127                     speed *= inertiaLife;
1128                     setTop(event, this.#box.y + step * 20 * speed);
1129                     if(Math.abs(speed) < 0.001 || this.#box.y <= 0 || this.#box.my >= this.#maxSize.y) inertiaAnimate.stop();
1130                 });
1131             },
1132 
1133             inertiaLeft = (event, speed) => {
1134                 if(Math.abs(speed) < 1) return;
1135                 stepA = speed < 0 ? "-left" : "left";
1136                 if(aniamteRun && stepA === stepB) step += 0.3;
1137                 else{
1138                     step = 1;
1139                     stepB = stepA;
1140                 }
1141                 inertiaAnimate.play(() => {
1142                     speed *= inertiaLife;
1143                     setLeft(event, this.#box.x + step * 20 * speed);
1144                     if(Math.abs(speed) < 0.001 || this.#box.x <= 0 || this.#box.mx >= this.#maxSize.x) inertiaAnimate.stop();
1145                 });
1146             }
1147         }
1148 
1149         const setTop = (event, top) => {
1150             this.#box.y = top;
1151             this.redraw();
1152         },
1153 
1154         setLeft = (event, left) => {
1155             this.#box.x = left;
1156             this.redraw();
1157         },
1158         
1159         onMoveTop = event => {
1160             setTop(event, dPos - event.offsetY);
1161         },
1162 
1163         onMoveLeft = event => {
1164             setLeft(event, dPos - event.offsetX);
1165         },
1166 
1167         onUpTop = event => {
1168             document.body.removeEventListener('pointermove', onMoveTop);
1169             document.body.removeEventListener('pointerup', onUpTop);
1170             if(event !== null){
1171                 onMoveTop(event);
1172                 if(inertia === true) inertiaTop(event, (this.#box.y - sPos.y) / (Date.now() - sTime));
1173             }
1174         },
1175 
1176         onUpLeft = event => {
1177             document.body.removeEventListener('pointermove', onMoveLeft);
1178             document.body.removeEventListener('pointerup', onUpLeft);
1179             if(event !== null){
1180                 onMoveLeft(event);
1181                 if(inertia === true) inertiaLeft(event, (this.#box.x - sPos.x) / (Date.now() - sTime));
1182             }
1183         },
1184 
1185         _box = this.#box,
1186 
1187         eventTarget = {
1188             get index(){return -1;}, //最低优先
1189             get visible(){return true;},
1190             get box(){return _box;},
1191             get rotate(){return null},
1192         },
1193 
1194         a1 = Math.PI / 4, a2 = Math.PI / 2 + a1,
1195 
1196         onUp = event => {
1197             domElement.removeEventListener("pointerup", onUp);
1198             domElement.removeEventListener('pointermove', onMove);
1199         },
1200 
1201         onMove = event => {
1202             dPos++; if(dPos < 4) return;
1203             onUp(event);
1204             const a = Math.atan2(event.pageY - py, event.pageX - px);
1205             if((a < a2 && a >= a1) || (a < -a1 && a >= -a2)){ //y轴
1206                 if(this.#maxSize.y > this.#box.h){
1207                     dPos = py + sPos.y;
1208                     document.body.addEventListener("pointermove", onMoveTop);
1209                     document.body.addEventListener("pointerup", onUpTop);
1210                 }
1211             }
1212             else{ //x轴
1213                 if(this.#maxSize.x > this.#box.w){
1214                     dPos = px + sPos.x;
1215                     document.body.addEventListener("pointermove", onMoveLeft);
1216                     document.body.addEventListener("pointerup", onUpLeft);
1217                 }
1218             }
1219         }
1220     
1221         this.#canvasImageEvent.add(eventTarget, "down", event => {
1222             px = event.offsetX;
1223             py = event.offsetY;
1224             dPos = 0;
1225             sPos.set(this.#box.x, this.#box.y);
1226             sTime = Date.now();
1227 
1228             if(inertia === true){
1229                 aniamteRun = inertiaAnimate.running;
1230                 inertiaAnimate.stop();
1231             }
1232 
1233             //防止没触发 move 或 up 事件;
1234             onUpLeft(null);
1235             onUpTop(null);
1236             onUp();
1237             
1238             domElement.addEventListener("pointermove", onMove);
1239             domElement.addEventListener("pointerup", onUp);
1240         });
1241     }
1242 
1243     _bindScroll(box){
1244         var _x = box.x, _y = box.y, _w = box.w, _h = box.h, nm, om;
1245 
1246         const writeBoxX = () => {
1247             if(nm > this.#maxSize.x) this.#maxSize.x = nm;
1248             else if(nm < this.#maxSize.x){
1249                 if(om >= this.#maxSize.x) this._resetMaxSizeX();
1250             }
1251         },
1252 
1253         writeBoxY = () => {
1254             if(nm > this.#maxSize.y) this.#maxSize.y = nm;
1255             else if(nm < this.#maxSize.y){
1256                 if(om >= this.#maxSize.y) this._resetMaxSizeY();
1257             }
1258         };
1259 
1260         Object.defineProperties(box, {
1261 
1262             x: {
1263                 get: () => {return _x;},
1264                 set: v => {
1265                     om = _x+_w;
1266                     _x = v;
1267                     nm = v+_w;
1268                     writeBoxX();
1269                 }
1270             },
1271 
1272             y: {
1273                 get: () => {return _y;},
1274                 set: v => {
1275                     om = _y+_h;
1276                     _y = v;
1277                     nm = v+_h;
1278                     writeBoxY();
1279                 }
1280             },
1281 
1282             w: {
1283                 get: () => {return _w;},
1284                 set: v => {
1285                     om = _w+_x;
1286                     _w = v;
1287                     nm = v+_x;
1288                     writeBoxX();
1289                 }
1290             },
1291 
1292             h: {
1293                 get: () => {return _h;},
1294                 set: v => {
1295                     om = _h+_y;
1296                     _h = v;
1297                     nm = v+_y;
1298                     writeBoxY();
1299                 }
1300             },
1301 
1302         });
1303     }
1304 
1305     _resetMaxSizeX(){
1306         this.#maxSize.x = 0;
1307         for(let k = 0, len = this.list.length, m; k < len; k++){
1308             m = this.list[k].box.mx;
1309             if(m > this.#maxSize.x) this.#maxSize.x = m;
1310         }
1311     }
1312 
1313     _resetMaxSizeY(){
1314         this.#maxSize.y = 0;
1315         for(let k = 0, len = this.list.length, m; k < len; k++){
1316             m = this.list[k].box.my;
1317             if(m > this.#maxSize.y) this.#maxSize.y = m;
1318         }
1319     }
1320 
1321     _cursorX(v = this.#box.x){
1322         return v/this.#maxSize.x*this.#box.w;
1323     }
1324 
1325     _cursorY(v = this.#box.y){
1326         return v/this.#maxSize.y*this.#box.h;
1327     }
1328 
1329 }
1330 
1331 
1332 
1333 
1334 /* CanvasImage 
1335 parameter: 
1336     image (构造器会调用一次 .setImage(image) 来处理 image 参数)
1337 
1338 attribute:
1339     opacity: Float;     //透明度; 值0至1之间; 默认1;
1340     visible: Boolean;    //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件)
1341     box: Box;             //.x.y 图像的位置, .w.h 图像的宽高;
1342     rotate: Roate;        //旋转; 默认 null (注意: 旋转的原点是相对于 box.x.y 的)
1343     暂未实现, 先用box.w.h代替缩放//scale: Box;            //缩放; .x.y中心点, .w.h缩放; 默认 null; (注意: 缩放的原点是相对于 box.x.y 的)
1344     x, y: Number;        //this.box 的 .x.y
1345 
1346     //以下属性不建议直接修改
1347     overlap: Object; //CanvasImageRender.computeOverlaps(ca) 方法更新
1348     index: Integer; //CanvasImageRender.index(ca) 修改
1349 
1350     //只读
1351     width, height, image; isRotate, isScale
1352 
1353 method:
1354     setImage(image): this;         //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image);
1355     loadImage(src, onload): this;    //加载并设置图像 (onload 如果是 CanvasImageRender 则加载完后自动调用一次 redraw 或 render 方法);
1356     loadVideo(src, onload, type = "mp4")
1357     pos(x, y): this;             //设置位置; x 可以是: Number, Object{x,y}
1358 
1359 demo:
1360     //CanvasImageRender
1361     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1362     cir.domElement.style = `
1363         position: absolute;
1364         z-index: 9999;
1365         background: rgb(127,127,127);
1366     `;
1367 
1368     //values
1369     const ciA = cir.add(new CanvasImage()).pos(59, 59).load("view/examples/img/test.png", cir);
1370     ciA.opacity = 0.2; //设置透明度
1371 
1372     const ciB = cir.add(new CanvasImage(ciA)).pos(59, 120);
1373     ciB.rotate = new Rotate().toAngle(45); //旋转45度
1374 
1375     //event
1376     const cie = new CanvasImageEvent(cir); //注意: CanvasImage 的缩放和旋转都会影响到 CanvasImageEvent
1377     cie.add(ciB, "click", event => { //ciB 添加 点击事件
1378         console.log("click ciB: ", event);
1379     });
1380 
1381 
1382     //这个用于搭载H5视频 video
1383     cir.add(new CanvasImage())
1384 
1385     .loadVideo("view/examples/video/test.mp4", ci => {
1386         
1387         //同比例缩放视频
1388         const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100});
1389         ci.box.size(newSize.width, newSize.height).center(cir.box);
1390 
1391         //播放按钮
1392         const cic = cir.add(new CanvasImageCustom())
1393         .size(50, 30).text("PLAY", "#fff")
1394         .rect(4).stroke("blue");
1395         cic.box.center(cir.box);
1396 
1397         //动画循环
1398         const animateLoop = new AnimateLoop(() => cir.redraw());
1399 
1400         //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画)
1401         cir.addEvent(cic, "up", () => {
1402             cic.visible = false;
1403             ci.image.play(); // ci.image 其实是一个 video 元素
1404             animateLoop.play(); //播放动画
1405         });
1406         
1407         //把 canvas 添加至 dom 树
1408         cir.render();
1409 
1410     });
1411 
1412 */
1413 class CanvasImage{
1414 
1415     #box = new Box();
1416     #image = null;
1417     #isLoadImage = false;
1418 
1419     get name(){return this.constructor.name;}
1420     get box(){return this.#box;}
1421     get image(){return this.#image;}
1422     get isScale(){return this.#image.width !== this.box.w || this.#image.height !== this.box.h;}
1423     get isLoadImage(){return this.#isLoadImage;}
1424     get width(){return this.#image !== null ? this.#image.width : 0;}
1425     get height(){return this.#image !== null ? this.#image.height : 0;}
1426     get x(){return this.box.x;}
1427     get y(){return this.box.y;}
1428     
1429     constructor(image){
1430         this.opacity = 1;
1431         this.visible = true;
1432         this.rotate = null;
1433         //this.scale = null;
1434 
1435         //以下属性不建议直接修改
1436         this.overlap = null;
1437         this.index = -1;
1438 
1439         this.setImage(image);
1440     }
1441 
1442     pos(x, y){
1443         if(UTILS.isNumber(x)){
1444             this.box.x = x;
1445             this.box.y = y;
1446         }
1447         else if(UTILS.isObject(x)){
1448             this.box.x = x.x;
1449             this.box.y = x.y;
1450         }
1451         return this;
1452     }
1453 
1454     setImage(image){
1455         if(CanvasImageRender.isCanvasImage(image)){
1456             this.box.size(image.width, image.height);
1457             this.#image = image;
1458         }
1459         else if(CanvasImage.prototype.isPrototypeOf(image)){
1460             if(image.isLoadImage){
1461                 if(Array.isArray(image.loadImage_cis)) image.loadImage_cis.push(this);
1462                 else image.loadImage_cis = [this];
1463             }
1464             else this.setImage(image.image);
1465         }
1466         else{
1467             this.box.size(0, 0);
1468             this.#image = null;
1469         }
1470         return this;
1471     }
1472 
1473     loadImage(src, onload){
1474         this.#isLoadImage = true;
1475         const image = new Image();
1476         image.onload = () => this._loadSuccess(image, onload);
1477         image.src = src;
1478         return this;
1479     }
1480 
1481     loadVideo(src, onload, type = "mp4"){
1482         /*    video 加载事件 的顺序
1483             onloadstart
1484             ondurationchange
1485             onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。
1486             onloadeddata
1487             onprogress
1488             oncanplay
1489             oncanplaythrough
1490 
1491             //控制事件:
1492             onended //播放结束
1493             onpause //暂停播放
1494             onplay //开始播放
1495         */
1496         this.#isLoadImage = true;
1497         const video = document.createElement("video"),
1498         source = document.createElement("source");
1499         video.appendChild(source);
1500         source.type = `video/${type}`;
1501 
1502         video.oncanplay = () => {
1503             //video 的 width, height 属性如果不设的话永远都是0
1504             video.width = video.videoWidth;
1505             video.height = video.videoHeight;
1506             this._loadSuccess(video, onload);
1507         };
1508 
1509         source.src = src;
1510         return this;
1511     }
1512 
1513     _loadSuccess(image, onload){
1514         this.setImage(image);
1515         this.#isLoadImage = false;
1516 
1517         if(Array.isArray(this.loadImage_cis)){
1518             this.loadImage_cis.forEach(ci => ci.setImage(image));
1519             delete this.loadImage_cis;
1520         }
1521 
1522         if(typeof onload === "function") onload(this);
1523         else if(CanvasImageRender.prototype.isPrototypeOf(onload)){
1524             if(onload.domElement.parentElement !== null) onload.redraw();
1525             else onload.render();
1526         }
1527     }
1528 
1529     /* setScale(nx = 0, ny = 0, ratio = 1){
1530         this.scale.w = this.#box.w * ratio;
1531         this.scale.h = this.#box.h * ratio;
1532         this.scale.x = nx - ((nx - this.scale.x) * ratio + this.scale.x) + this.scale.x;
1533         this.scale.y = ny - ((ny - this.scale.y) * ratio + this.scale.y) + this.scale.y;
1534         return this;
1535     }
1536 
1537     setScaleToSameScaleFromBox(newWidth = this.#box.w){
1538         if(this.#image === null || this.scale === null) return this;
1539 
1540         const width = this.#image.width, 
1541         height = this.#image.height,
1542         ratio = width / height,
1543         scale = ratio < 1 ? ratio * newWidth / width : newWidth / width;
1544 
1545         this.scale.w = width * scale;
1546         this.scale.h = height * scale;
1547         
1548         return this;
1549     }
1550 
1551     setScaleToCenterFromBox(){
1552         if(this.scale === null) return this;
1553 
1554         this.scale.x = (this.#box.w - this.scale.w) / 2;
1555         this.scale.y = (this.#box.h - this.scale.h) / 2;
1556 
1557         return this;
1558     } */
1559 
1560 }
1561 
1562 
1563 
1564 
1565 /* CanvasImages
1566 
1567 */
1568 class CanvasImages extends CanvasImage{
1569 
1570     #i = -1;
1571     get cursor(){return this.#i;}
1572     set cursor(i){
1573         super.setImage(this.images[i]);
1574         this.#i = this.image !== null ? i : -1;
1575     }
1576 
1577     constructor(images = []){
1578         super(images[0]);
1579         this.images = images;
1580         if(this.image !== null) this.#i = 0;
1581     }
1582 
1583     setImage(image){
1584         super.setImage(image);
1585     
1586         if(this.image !== null && Array.isArray(this.images)){
1587             const i = this.images.indexOf(this.image);
1588             if(i === -1){
1589                 this.#i = this.images.length;
1590                 this.images.push(this.image);
1591             }
1592             else this.#i = i;
1593         }
1594 
1595         return this;
1596     }
1597 
1598     next(){
1599         const len = this.images.length - 1;
1600         if(len !== -1){
1601             if(this.#i < len) this.#i++;
1602             else this.#i = 0;
1603             super.setImage(this.images[this.#i]);
1604         }
1605     }
1606 
1607     loadImages(srcs, onDone, onUpdate){
1608         onUpdate = typeof onUpdate === "function" ? onUpdate : null;
1609         var i = 0, c = srcs.length, img = null, _i = this.images.length;
1610 
1611         const len = srcs.length, 
1612         func = ()=>{
1613             i++; if(onUpdate !== null) onUpdate(this.images, _i);
1614             if(i === c && typeof onDone === "function"){
1615                 this.cursor = 0;
1616                 onDone(this.images, _i, srcs);
1617             }
1618             else _i++;
1619         }
1620 
1621         for(let k = 0, ty = ""; k < len; k++){
1622             ty = typeof srcs[k];
1623             if(ty === "string" || ty === "object"){
1624                 ty = ty === "string" ? srcs[k] : srcs[k].src;
1625                 if(ty !== "" && typeof ty === "string"){
1626                     img = new Image();
1627                     img.onload = func;
1628                     this.images.push(img);
1629                     img.src = ty;
1630                 }
1631                 else c--;
1632             }
1633         }
1634 
1635         return this;
1636     }
1637 
1638 }
1639 
1640 
1641 
1642 
1643 /* CanvasImageCustom
1644 
1645 demo:
1646     //CanvasImageRender
1647     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1648     cir.domElement.style = `
1649         position: absolute;
1650         z-index: 9999;
1651         background: rgb(127,127,127);
1652     `;
1653 
1654     //values
1655     cir.add(new CanvasImageCustom())
1656     .pos(59, 180)
1657     .load("view/examples/img/test.png", cir);
1658 
1659     cir.add(new CanvasImageCustom())
1660     .pos(59, 180)
1661     .text("value", "red")
1662     .rect().stroke()
1663 
1664 */
1665 class CanvasImageCustom extends CanvasImage{
1666 
1667     constructor(canvas, autoSize = true){
1668         super(canvas);
1669         this.autoSize = autoSize;
1670         this.fontSize = parseFloat(CanvasImageRender.defaultStyles.font);
1671         this.fillStyle = CanvasImageRender.defaultStyles.fillStyle;
1672         this.strokeStyle = CanvasImageRender.defaultStyles.strokeStyle;
1673         this.shadowColor = CanvasImageRender.defaultStyles.shadowColor;
1674     }
1675 
1676     toImage(callback){
1677         var image = new Image();
1678         image.src = this.image.toDataURL("image/png");
1679         image.onload = callback;
1680     }
1681 
1682     shear(canvas, dx = 0, dy = 0){
1683         if(CanvasImageRender.isCanvas(canvas) === false){
1684             canvas = document.createElement("canvas");
1685             canvas.width = this.width;
1686             canvas.height = this.height;
1687         }
1688 
1689         canvas.getContext("2d").drawImage(this.image, dx, dy); 
1690 
1691         return canvas;
1692     }
1693 
1694     setImage(image){
1695         if(CanvasImageRender.isCanvas(image)){ //image 如果是画布
1696             super.setImage(image);
1697             this.context = CanvasImageRender.getContext(image);
1698             this.size(image.width, image.height);
1699         }
1700         else{
1701             if(CanvasImageRender.isCanvasImage(image)){ //image 如果是图像
1702                 this.context = CanvasImageRender.getContext();
1703                 super.setImage(this.context.canvas);
1704                 this.size(image.width, image.height);
1705                 this.context.drawImage(image, 0, 0);
1706             }else{ //image 如果是其它对象
1707                 if(image) super.setImage(image);
1708                 else{
1709                     this.context = CanvasImageRender.getContext();
1710                     super.setImage(this.context.canvas);
1711                     this.size(this.width, this.height);
1712                 }
1713             }
1714         }
1715         
1716         return this;
1717     }
1718 
1719     clear(){
1720         this.context.clearRect(0, 0, this.box.w, this.box.h);
1721         return this;
1722     }
1723 
1724     size(w, h){
1725         this.box.size(w, h);
1726         this.image.width = w;
1727         this.image.height = h;
1728         CanvasImageRender.setDefaultStyles(this.context);
1729         
1730         if(this.context.font !== parseFloat(this.fontSize)) this.context.font = this.fontSize+"px SimSun, Songti SC";//
1731         if(this.context.fillStyle !== this.fillStyle) this.context.fillStyle = this.fillStyle; //
1732         if(this.context.strokeStyle !== this.strokeStyle) this.context.strokeStyle = this.strokeStyle; //
1733         if(this.context.shadowColor !== this.shadowColor) this.context.shadowColor = this.shadowColor; //
1734 
1735         return this;
1736     }
1737 
1738     shadow(shadowColor = "rgba(0,0,0,0)", shadowBlur, shadowOffsetX, shadowOffsetY){
1739         const con = this.context;
1740         if(typeof shadowColor === "string" && this.shadowColor !== shadowColor) this.shadowColor = con.shadowColor = shadowColor;
1741         if(con.shadowBlur !== shadowBlur && typeof shadowBlur === "number") con.shadowBlur = shadowBlur;
1742         if(con.shadowOffsetX !== shadowOffsetX && typeof shadowOffsetX === "number") con.shadowOffsetX = shadowOffsetX;
1743         if(con.shadowOffsetY !== shadowOffsetY && typeof shadowOffsetY === "number") con.shadowOffsetY = shadowOffsetY;
1744         return this;
1745     }
1746 
1747     line(x, y, x1, y1){
1748         this.context.beginPath();
1749         this.context.moveTo(x, y);
1750         this.context.lineTo(x1, y1);
1751         return this;
1752     }
1753 
1754     path(arr, close = false){
1755         const con = this.context;
1756         con.beginPath();
1757         con.moveTo(arr[0], arr[1]);
1758         for(let k = 2, len = arr.length; k < len; k+=2) con.lineTo(arr[k], arr[k+1]);
1759         if(close === true) con.closePath();
1760         return this;
1761     }
1762 
1763     rect(r, box = this.box){
1764         const con = this.context, s = con.lineWidth;
1765 
1766         if(box === this.box){
1767             var x = s / 2,
1768             y = s / 2,
1769             w = box.w - s,
1770             h = box.h - s;
1771         }
1772 
1773         else{
1774             var x = box.x,
1775             y = box.y,
1776             w = box.w,
1777             h = box.h;
1778         }
1779 
1780         if(UTILS.isNumber(r) === false || r <= 0){
1781             con.rect(x, y, w, h);
1782             return this;
1783         }
1784 
1785         const _x = x + r, 
1786         _y = y + r, 
1787         mx = x + w, 
1788         my = y + h, 
1789         _mx = mx - r, 
1790         _my = my - r;
1791         
1792         //
1793         con.moveTo(_x, y);
1794         con.lineTo(_mx, y);
1795         con.arcTo(mx, y, mx, _y, r);
1796 
1797         //
1798         con.lineTo(mx, _y);
1799         con.lineTo(mx, _my);
1800         con.arcTo(mx, my, _x, my, r);
1801 
1802         //
1803         con.lineTo(_x, my);
1804         con.lineTo(_mx, my);
1805         con.arcTo(x, my, x, _my, r);
1806 
1807         //
1808         con.lineTo(x, _y);
1809         con.lineTo(x, _my);
1810         con.arcTo(x, y, _x, y, r);
1811 
1812         return this;
1813     }
1814 
1815     stroke(color, lineWidth){
1816         if(typeof color === "string" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color;
1817         if(UTILS.isNumber(lineWidth) && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
1818         this.context.stroke();
1819         return this;
1820     }
1821 
1822     fill(color){
1823         if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1824         this.context.fill();
1825         return this;
1826     }
1827 
1828     text(value, color, fontSize = this.fontSize, x = -1, y = -1){
1829         if(UTILS.isNumber(fontSize) && fontSize > 1 && this.fontSize !== fontSize){
1830             this.fontSize = fontSize;
1831             this.context.font = fontSize+"px SimSun, Songti SC";
1832         }
1833         
1834         const textWidth = this.context.measureText(value).width;
1835         if(this.autoSize === true && textWidth > this.box.w || this.fontSize > this.box.h){
1836             this.size(textWidth+4, this.fontSize+4);
1837             this.fontSize = fontSize;
1838             this.context.font = fontSize+"px SimSun, Songti SC";
1839         }
1840 
1841         if(x === -1) x = (this.box.w - textWidth) / 2;
1842         if(y === -1) y = (this.box.h - this.fontSize) / 2;
1843 
1844         if(typeof color === "string" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1845         this.context.fillText(value, x, y);
1846 
1847         return this;
1848     }
1849 
1850 }
1851 
1852 
1853 
1854 
1855 /* Carousel 轮播图 (支持pc和mobile端)
1856 
1857 */
1858 class CanvasCarousel extends CanvasImageRender{
1859 
1860     constructor(){
1861         super();
1862         
1863     }
1864 
1865     init(images){
1866         const len = images.length;
1867         for(let k = 0; k < len; k++) this.context.drawImage(images[k], k*this.box.w, 0, this.box.w, this.box.h);
1868         
1869     }
1870 
1871 }
1872 
1873 
1874 
1875 /* CanvasTab 
1876 
1877 */
1878 class CanvasTab extends CanvasImageRender{
1879 
1880     constructor(){
1881         super();
1882         
1883     }
1884 
1885 }
1886 
1887 
1888 export {
1889     CanvasImageRender,
1890     CanvasImage, 
1891     CanvasImages,
1892     CanvasImageCustom,
1893 }
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(null, false);
 15 
 16 function isS(v){
 17     return UTILS.isNumber(v) && v >= 1 && v !== Infinity;
 18 }
 19 
 20 function getW(v){
 21     return emptyCIC.context.measureText(v).width+padding+padding;
 22 }
 23 
 24 function getH(w){
 25     return Math.max(emptyCIC.fontSize+padding+padding, Math.round(0.618*w));
 26 }
 27 
 28 function initCIC(w, h){
 29     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.1);
 30     return emptyCIC;
 31 }
 32 
 33 function but_rect(w, h, v){
 34     if(!isS(w)) w = getW(v);
 35     if(!isS(h)) h = getH(w);
 36 
 37     const bgColor = "#b3b3b3", color = "#ffffff", borderColor = "#999999",
 38     
 39     a = initCIC(w, h)
 40     .fill(bgColor)
 41     .text(v, color)
 42     .stroke(borderColor)
 43     .shear(),
 44 
 45     c = emptyCIC
 46     .fill("rgba(0,0,0,0.75)")
 47     .shear(),
 48 
 49     b = emptyCIC.clear()
 50     .fill(bgColor)
 51     .text(v, borderColor)
 52     .stroke(color)
 53     .shear();
 54     
 55     return [a, b, c];
 56 }
 57 
 58 function but_circle(w, v){
 59     if(!isS(w)) w = getW(v);
 60 
 61     emptyCIC.size(w, w).rect(w / 2);
 62 
 63     const bgColor = "#b3b3b3", color = "#ffffff", borderColor = "#999999",
 64 
 65     a = emptyCIC
 66     .fill(bgColor)
 67     .text(v, color)
 68     .stroke(borderColor)
 69     .shear(),
 70 
 71     c = emptyCIC
 72     .fill("rgba(0,0,0,0.75)")
 73     .shear(),
 74 
 75     b = emptyCIC.clear()
 76     .fill(bgColor)
 77     .text(v, borderColor)
 78     .stroke(color)
 79     .shear();
 80 
 81     return [a, b, c];
 82 }
 83 
 84 function bool_rect(w, h){
 85     if(!isS(w)) w = getW("✔");
 86     if(!isS(h)) h = w;
 87     
 88     const bgColor = "#b3b3b3", color = "#ffffff", borderColor = "#999999",
 89 
 90     //true
 91     a = initCIC(w, h)
 92     .fill(bgColor)
 93     .text("✔", color)
 94     .stroke(borderColor)
 95     .shear(),
 96 
 97     b = emptyCIC
 98     .fill("rgba(0,0,0,0.75)")
 99     .shear(),
100 
101     c = emptyCIC.clear()
102     .fill(bgColor)
103     .text("✔", borderColor)
104     .stroke(color)
105     .shear(),
106     
107     //false
108     d = emptyCIC.clear()
109     .fill(bgColor)
110     .stroke(borderColor)
111     .shear(),
112 
113     e = emptyCIC
114     .fill("rgba(0,0,0,0.75)")
115     .shear(),
116 
117     f = emptyCIC.clear()
118     .fill(bgColor)
119     .stroke(color)
120     .shear();
121 
122     return [a, b, c, d, e, f];
123 }
124 
125 function bool_fillet(w, h){
126     const textW = emptyCIC.context.measureText("开").width;
127     if(!isS(w)) w = textW*2+padding+padding + emptyCIC.fontSize;
128     if(!isS(h)) h = Math.max(emptyCIC.fontSize+padding+padding, getH(w) / 2)
129 
130     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.5);
131 
132     const halfW = w / 2, cx = (halfW-textW)/2,
133     bgColor = "#b3b3b3", color = "#ffffff", borderColor = "#999999",
134 
135     //true
136     a = emptyCIC
137     .fill(bgColor)
138     .text("开", color, 0, cx)
139     .text("关", borderColor, 0, halfW+cx)
140     .stroke(borderColor)
141     .shear(),
142 
143     b = emptyCIC
144     .fill("rgba(0,0,0,0.75)")
145     .shear(),
146 
147     c = emptyCIC.clear()
148     .fill(bgColor)
149     .text("开", color, 0, cx)
150     .text("关", borderColor, 0, halfW+cx)
151     .stroke(color)
152     .shear(),
153     
154     //false
155     d = emptyCIC.clear()
156     .fill(bgColor)
157     .text("开", borderColor, 0, cx)
158     .text("关", color, 0, halfW+cx)
159     .stroke(borderColor)
160     .shear(),
161 
162     e = emptyCIC
163     .fill("rgba(0,0,0,0.75)")
164     .shear(),
165 
166     f = emptyCIC.clear()
167     .fill(bgColor)
168     .text("开", borderColor, 0, cx)
169     .text("关", color, 0, halfW+cx)
170     .stroke(color)
171     .shear();
172 
173     return [a, b, c, d, e, f];
174 }
175 
176 
177 class CanvasUI extends CanvasImages{
178 
179     static fontSize(v){
180         v = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 12;
181         if(v === emptyCIC.fontSize) return v;
182         emptyCIC.fontSize = v;
183         emptyCIC.context.font = v+"px SimSun, Songti SC";
184         return v;
185     }
186 
187     static padding(v){
188         padding = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 4;
189         return padding;
190     }
191 
192     constructor(images){
193         super(images);
194     }
195 
196 }
197 
198 
199 
200 
201 /* CanvasButton 为画布预设按钮 (防止移动端多指混乱)
202 parameter: 
203     value: String; //按钮名; 默认 "Button"
204     width, height: Number; //默认自适应
205     style: String; //可能值: 默认"rect", "circle"
206 
207 attribute:
208     enable: Bool;     //是否启用;
209     value: String;     //只读
210 
211 method:
212     bindEvent(cir: CanvasImageRender, callback: Func, delay: Number): this;
213     //cir: 内部使用cir来绑定或解绑事件和更新自己(cir.redrawTarget(this)); 必须
214     //callback: 默认null
215     //delay: 可以限制每次点击的延迟毫秒时间; 默认0
216 
217 demo:
218     //button
219     const cb = cir.add(new CanvasButton({
220         width: 80,
221         value: "测试 TEST",
222     }));
223 
224     cb.bindEvent(cir, event => console.log(1));
225     cb.box.center(cir.box);
226     console.log(cb);
227 */
228 class CanvasUIButton extends CanvasUI{
229 
230     #value = "";
231     get value(){return this.#value;}
232 
233     #enable = true;
234     get enable(){return this.#enable;}
235     set enable(v){
236         if(v === false){
237             this.cursor = 2;
238             this.#enable = false;
239         }
240         else if(v === true){
241             this.cursor = 0;
242             this.#enable = true;
243         }
244     }
245 
246     constructor(value, width, height, style = "rect"){
247 
248         value = value !== "" && typeof value === "string" ? value : "Button";
249 
250         switch(style){
251             case "circle": 
252             var images = but_circle(width, value);
253             break;
254 
255             case "rect": 
256             default: 
257             var images = but_rect(width, height, value);
258             break;
259         }
260         
261         super(images);
262         this.#value = value;
263         
264     }
265 
266     bindEvent(cir, callback, delay = 0){
267         callback = typeof callback === "function" ? callback : null;
268         
269         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
270 
271         const ondelay = () => {
272             if(this.cursor === 2){
273                 this.cursor = 0;
274                 cir.redrawTarget(this);
275             }
276             isDelay = false;
277         },
278         
279         onup_body = event => {
280             if(event.pointerId !== pointerId || this.enable === false) return;
281             document.body.removeEventListener("pointerup", onup_body);
282             isDown = false;
283             if(this.cursor === 1){
284                 this.cursor = 0;
285                 if(cir.list.includes(this)) cir.redrawTarget(this);
286             }
287         },
288 
289         onup = event => {
290             if(isDown && callback !== null && this.enable && event.pointerId === pointerId){
291                 const dTime = Date.now() - sTime;
292                 if(dTime < delay){
293                     isDelay = true;
294                     setTimeout(ondelay, delay - dTime);
295                     this.cursor = 2;
296                     cir.redrawTarget(this);
297                 }
298 
299                 isDown = false;
300                 callback(event, this);
301             }
302         },
303 
304         ondown = event => {
305             if(isDelay || event.button !== 0 || this.enable === false) return;
306             pointerId = event.pointerId;
307             document.body.addEventListener("pointerup", onup_body);
308             this.cursor = 1;
309             isDown = true;
310             sTime = Date.now();
311             cir.redrawTarget(this);
312         },
313 
314         remove = () => {
315             document.body.removeEventListener("pointerup", onup_body);
316             cir.removeEvent(this, "up", onup);
317             cir.removeEvent(this, "down", ondown);
318             cir.removeEvent(null, "remove", remove);
319         }
320 
321         cir.addEvent(this, "down", ondown);
322         cir.addEvent(this, "up", onup);
323         cir.addEvent(null, "remove", remove);
324     
325         return this;
326     }
327     
328 }
329 
330 
331 /* CanvasUIBool 开关控件
332 parameter:
333     value: Bool; //默认 false
334     width, height: Number; //默认自适应
335     style: 默认 rect 矩形开关, fillet 圆角开关
336 
337 attribute:
338     enable: Bool;     //是否启用;
339     value: String;     //设置控件的值;(注意: 它不会自动更新画布)
340 
341 method:
342     bindEvent(cir, callback, delay = 0): this;
343 
344 */
345 class CanvasUIBool extends CanvasUI{
346 
347     #value = false;
348     get value(){return this.#value;}
349     set value(v){
350         if(this.#enable && typeof v === "boolean"){
351             this.#value = v;
352             this.cursor = v ? 0 : 3;
353         }
354     }
355 
356     #enable = true;
357     get enable(){return this.#enable;}
358     set enable(v){
359         if(v === false){
360             this.cursor = this.#value ? 1 : 4;
361             this.#enable = false;
362         }
363         else if(v === true){
364             this.cursor = this.#value ? 0 : 3;
365             this.#enable = true;
366         }
367     }
368 
369     constructor(value, width, height, style = "rect"){
370         value = typeof value === "boolean" ? value : false;
371 
372         switch(style){
373             case "fillet": 
374             var images = bool_fillet(width, height);
375             break;
376 
377             case "rect": 
378             default: 
379             var images = bool_rect(width, height);
380             break;
381         }
382         
383         super(images);
384         this.value = value;
385         
386     }
387 
388     bindEvent(cir, callback, delay = 0){
389         callback = typeof callback === "function" ? callback : null;
390         
391         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
392 
393         const ondelay = () => {
394             if(this.cursor === 1){
395                 this.cursor = 0;
396                 cir.redrawTarget(this);
397             }
398             else if(this.cursor === 4){
399                 this.cursor = 3;
400                 cir.redrawTarget(this);
401             }
402             isDelay = false;
403         },
404         
405         onup_body = event => {
406             if(event.pointerId !== pointerId || this.enable === false) return;
407             document.body.removeEventListener("pointerup", onup_body);
408             if(this.cursor === 2){
409                 this.cursor = 0;
410                 cir.redrawTarget(this);
411             }
412             else if(this.cursor === 5){
413                 this.cursor = 3;
414                 cir.redrawTarget(this);
415             }
416             isDown = false;
417         },
418 
419         onup = event => {
420             if(isDown && this.enable && event.pointerId === pointerId){
421                 this.value = !this.#value;
422 
423                 if(callback !== null){
424                     const dTime = Date.now() - sTime;
425                     if(dTime < delay){
426                         isDelay = true;
427                         setTimeout(ondelay, delay - dTime);
428                         this.cursor = this.#value ? 1 : 4;
429                     }
430                     callback(event, this);
431                 }
432 
433                 cir.redrawTarget(this);
434                 isDown = false;
435             }
436         },
437 
438         ondown = event => {
439             if(isDelay || event.button !== 0 || this.enable === false) return;
440             pointerId = event.pointerId;
441             document.body.addEventListener("pointerup", onup_body);
442             this.cursor = this.cursor === 0 ? 2 : 5;
443             isDown = true;
444             sTime = Date.now();
445             cir.redrawTarget(this);
446         },
447 
448         remove = () => {
449             document.body.removeEventListener("pointerup", onup_body);
450             cir.removeEvent(this, "up", onup);
451             cir.removeEvent(this, "down", ondown);
452             cir.removeEvent(null, "remove", remove);
453         }
454 
455         cir.addEvent(this, "down", ondown);
456         cir.addEvent(this, "up", onup);
457         cir.addEvent(null, "remove", remove);
458         
459         return this;
460     }
461 
462 }
463 
464 
465 class CanvasUIText extends CanvasUI{
466 
467 }
468 
469 class CanvasUINumber extends CanvasUI{
470 
471 }
472 
473 class CanvasUIColor extends CanvasUI{
474 
475 }
476 
477 
478 
479 export {
480     CanvasUI,
481     CanvasUIButton,
482     CanvasUIBool,
483 }
CanvasUI.js

 

main.js:

 1 "use strict";
 2 
 3 import { Ajax, IndexedDB, EventDispatcher, UTILS } from './lib/Utils.js';
 4 import { 
 5     CanvasImageRender,
 6     CanvasImage, 
 7 } from './lib/CanvasImageRender.js';
 8 
 9 
10 const main = function () {
11     const test_redrawTarget = new CanvasImage();
12 
13     //CanvasImageRender
14     const cir = new CanvasImageRender({
15         width: WORLD.width, 
16         height: WORLD.height,
17 
18         //一下属性 scroll 为 true 才有效
19         scroll: true,             //是否启用滚动轴; 默认 false
20         scrollSize: 10,        //滚动轴的宽或高; 默认 6; (如果想隐藏滚动条最好用 scrollVisible, 而不是把此属性设为0)
21         scrollVisible: "auto",    //滚动轴的显示类型; 可能值 "auto" || "visible" || 默认"";
22 
23         scrollEventType: "touch",    //可能的值: 默认"", "default", "touch";
24         inertia: true,            //是否启用移动端滚动轴的惯性; 默认 false
25         inertiaLife: 0.03,        //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04;
26 
27         //test
28         test_redrawTarget: test_redrawTarget, //用于查看 redrawTarget(ca) 方法的重绘范围
29     });
30 
31     cir.domElement.style = `
32         position: absolute;
33         z-index: 9999;
34         background: rgb(127,127,127);
35     `;
36 
37     var randomCI;
38     const pi2 = Math.PI * 2,
39     ciA = new CanvasImage().loadImage("view/examples/img/test.png", cir);
40 
41     //随机位置创建
42     for(let k = 0, ca; k < 1000; k++){
43         ca = cir.list[k] = new CanvasImage(ciA);
44         ca.pos(Math.floor(UTILS.random(0, 5000)), Math.floor(UTILS.random(0, 2000)));
45         //ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2));
46     }
47 
48     //高, 边距, 控件事件的延迟毫秒
49     const y = cir.box.cy, margin = 10, delay = 500, 
50 
51     //矩形按钮
52     buttonRect = new CanvasUIButton("按钮延迟500ms").pos(margin, y)
53     .bindEvent(cir, event => console.log("buttonRect"), delay),
54 
55     //圆形按钮
56     buttonCircle = new CanvasUIButton("重新前往", 0, 0, "circle").pos(buttonRect.box.mx+margin, y)
57     .bindEvent(cir, event => {
58         if(randomCI){
59             cir.box.pos(randomCI.x, randomCI.y);
60             cir.redraw();
61         }
62         else confirm(`先点"${but_to.value}"按钮`);
63     }, delay),
64 
65     //矩形开关控件
66     boolRect = new CanvasUIBool(true).pos(buttonCircle.box.mx+margin, y)
67     .bindEvent(cir, event => console.log("boolRect6s"), delay),
68 
69     //圆角开关控件
70     boolFillet = new CanvasUIBool(true, 60, 0, "fillet").pos(boolRect.box.mx+margin, y)
71     .bindEvent(cir, event => console.log("boolFillet10s"), 300);
72 
73     //滚动条定位到 randomCI
74     CanvasUI.fontSize(28);
75     const but_to = new CanvasUIButton("GOTO(30秒)").pos(boolFillet.box.mx+margin, y)
76     .bindEvent(cir, event => {
77         randomCI = cir.list[Math.floor(UTILS.random(0, cir.list.length))];
78         test_redrawTarget.box.copy(randomCI.box);
79         cir.box.pos(randomCI.x, randomCI.y);
80         cir.redraw();
81     }, 30000);
82     
83     //初始化渲染列表
84     cir.list.push(buttonRect, buttonCircle, boolRect, boolFillet, but_to);
85     cir.initList();
86 
87     console.log(cir, buttonRect, buttonCircle, boolRect, boolFillet);
88 }();

 

视图:

 

posted @ 2022-09-14 15:38  鸡儿er  阅读(114)  评论(0编辑  收藏  举报