js画布封装之测试旋转

 

依赖:

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

 

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

 

开始:

 1 <!DOCTYPE html>
 2 <html lang = "zh-cn">
 3     
 4     <head>
 5         <title>testCanvas</title>
 6         <meta charset = "utf-8" />
 7         <style>
 8             *{
 9                 margin:0;
10                 padding: 0;
11             }
12             body{
13                 overflow: hidden;
14             }
15         </style>
16     </head>
17 
18     <body>
19 
20         <script src = "./view/js/main.js" type = "module"></script>
21 
22     </body>
23 
24 </html>
index.html

 

main.js

 1 import { CanvasImage, CanvasImageDraw, CanvasImageScroll } from "./ElementUtils.js";
 2 import { UTILS } from "./Utils.js";
 3 
 4 
 5 const PI2 = Math.PI * 2;
 6 
 7 /* 测试 canvas 旋转
 8 
 9 */
10 function main(){
11 
12     //CanvasImage 的渲染器
13     const cid = new CanvasImageDraw({width: innerWidth, height: innerHeight}),
14 
15     //渲染器的滚动条(此类暂时还很简陋,支持移动和桌面端)
16     cis = new CanvasImageScroll(cid, {scrollSize: 8}),
17 
18     //加载图片
19     image = new CanvasImage().loadImage("./view/img/31.jpg", () => {
20 
21         //随机旋转所有的 CanvasImage
22         //因为创建 CanvasImage 时其图片没有加载完
23         //无法获取其宽高, 而旋转的中心点(局部位置)需要知道宽高才行
24         //所以要等这张图片加载完后在创建旋转
25         //所以在这里创建旋转
26         for(let i = 0; i < cid.list.length; i++) cid.list[i].createRotate(UTILS.random(0, PI2));
27 
28         //sortCIPosEquals: ci的位置排序
29         cid.sortCIPosEquals(null, {
30             disX: 10, //ci之间的间距
31             disY: 10, 
32             lenX: 5, //x轴最多5个数
33         });
34 
35         //render 把画布添加至dom树
36         cid.render(document.body);
37 
38     });
39 
40     //批量创建 CanvasImage
41     for(let i = 0; i < 1000; i++){
42         //内部用.setImage(image) 方法处理参数, 
43         //此方法有个特性就是可以监听其它CanvasImage加载完图片时自动设置自己的图片, 前提时参数必须时 CanvasImage
44         //所以 image 变量只是当作一个加载图片的工具而已,没其它用处
45         //如果传的是其它类实例也有其它效果
46         const ci = new CanvasImage(image);
47 
48         //cis.bindScroll(ci: CanvasImage) 方法可以使滚动条(cis)能够监听ci的位置和ci.visible属性的变化来更新滚动条
49         //cis.changeCIAdd(ci: CanvasImage) 方法根据ci主动更新一次滚动条
50         cis.bindScroll(ci);
51         cis.changeCIAdd(ci);
52 
53         //加入到渲染器(cid)队列
54         cid.list[i] = ci;
55     }
56 
57     console.log(cid, cis);
58 }
59 
60 main();

 

 

posted @ 2023-02-03 17:42  鸡儿er  阅读(9)  评论(0编辑  收藏  举报