js以树结构展示js对象

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

 

 1 import { 
 2     CanvasColorTestViewer, 
 3     CanvasIconMenu, 
 4     CanvasImage, 
 5     CanvasImages, 
 6     CanvasImageEvent, 
 7     CanvasImageRender, 
 8     CanvasTreeList, 
 9     CanvasImageCustom 
10 } from './lib/CanvasImageRender.js';
11 
12 
13 const image_CanvasImage = CanvasIconMenu.emptyCIC.size(20, 20).rect().fill("blue").shear(),
14 image_CanvasImages = CanvasIconMenu.emptyCIC.size(20, 20).rect().stroke("blue").shear();
15 
16 const info = {
17     name_attributesName: "test_name",
18     iamges_attributesName: "className",
19     images: {
20         CanvasImage: image_CanvasImage,
21         CanvasImages: image_CanvasImages,
22     },
23 }
24 
25 const onclick = function (event) {
26     ctl_root.traverse(ctl => console.log(ctl.visible, ctl.childViewHeight))
27 }
28 
29 
30 const cir = new CanvasImageRender({width: innerWidth, height: innerHeight}); console.log(cir);
31 const cie = new CanvasImageEvent(cir); console.log(cie);
32 
33 const ctl_root = new CanvasTreeList(new CanvasImage(), info)
34 .addToList(cir.list)
35 .bindEvent(cir, cie, onclick);
36 
37 const ctl_1 = ctl_root.appendChild(new CanvasTreeList(new CanvasImage(), info))
38 .addToList(cir.list)
39 .bindEvent(cir, cie, onclick);
40 
41 const ctl_2 = ctl_1.appendChild(new CanvasTreeList(new CanvasImage(), info))
42 .addToList(cir.list)
43 .bindEvent(cir, cie, onclick);
44 
45 const ctl_3 = ctl_2.appendChild(new CanvasTreeList(new CanvasImage(), info))
46 .addToList(cir.list)
47 .bindEvent(cir, cie, onclick);
48 
49 const ctl_5 = ctl_3.appendChild(new CanvasTreeList(new CanvasImages(), info))
50 .addToList(cir.list)
51 .bindEvent(cir, cie, onclick);
52 
53 ctl_3.appendChild(new CanvasTreeList(new CanvasImages(), info))
54 .addToList(cir.list)
55 .bindEvent(cir, cie, onclick);
56 
57 ctl_root.appendChild(new CanvasTreeList(new CanvasImages(), info))
58 .addToList(cir.list)
59 .bindEvent(cir, cie, onclick);
60 
61 
62 //初始化 ctl_root
63 ctl_root.pos(100, 100).visibleChild();
64 
65 
66 //显示root的rect属性
67 const rect = ctl_root.rect;
68 const cic = cir.add(new CanvasImageCustom()).size(rect.w, rect.h).pos(rect.x, rect.y).rect().fill("rgba(255,255,255,0.2)");
69 
70 
71 //颜色调试器也放里面
72 const ctv = new CanvasColorTestViewer();
73 ctv.set("blue").update();
74 ctv.addToList(cir.list);
75 ctv.bindEvent(cir, cie);
76 ctv.pos(350, 100);
77 console.log(ctv);
78 
79 
80 //初始化cir
81 cir.initList();
82 cir.render();
83 
84 
85 //test
86 setTimeout(() => {
87     //更改 ctl_5 的名字并展开至 ctl_5
88     ctl_5.setName("我更改了名字!", cir.list);
89     ctl_5.visibleChild();
90 
91     //显示root的rect属性
92     let rect = ctl_root.rect;
93     cic.size(rect.w, rect.h).pos(rect.x, rect.y).rect().fill("rgba(255,255,255,0.2)");
94 
95     cir.redraw();
96 }, 3000);
97 
98     

 

结果图:

 

posted @ 2022-10-02 17:33  鸡儿er  阅读(71)  评论(0编辑  收藏  举报