js 以图片上的某个位置来缩放这个图片

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

 

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

 

main.js:

 1 import { 
 2     CanvasImageEvent, 
 3     CanvasImageRender, 
 4     CanvasImage 
 5 } from './lib/CanvasImageRender.js';
 6 
 7 const cir = new CanvasImageRender({width: innerWidth, height: innerHeight}),
 8 cie = new CanvasImageEvent(cir),
 9 
10 target = cir.add(new CanvasImage()).loadImage("./examples/img/test.jpg", cir);
11 
12 cie.add(target, "wheel", event => {
13 
14     const scale = target.scaleX + event.wheelDelta * 0.001,
15 
16     //offsetX,offsetY 是鼠标到 element 的距离, 现在把它们转为 target 的距离
17     localPositionX = event.offsetX - target.x,
18     localPositionY = event.offsetY - target.y;
19 
20     //xy以相等的比例缩放
21     target.setScaleX(scale, localPositionX);
22     target.setScaleY(scale, localPositionY);
23 
24     cir.redraw();
25 
26 });
27 
28 console.log(cir, cie, target);

 

图:

 

posted @ 2022-10-04 01:12  鸡儿er  阅读(37)  评论(0编辑  收藏  举报