可视化调试某个js对象的属性UI插件 class HTUI

 

依赖的类:

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

 

   1 "use strict";
   2 import {
   3     UTILS, 
   4     Box, 
   5     EventDispatcher,
   6     Point,
   7     AnimateLoop,
   8     TreeStruct,
   9     Timer,
  10     TweenCache,
  11     RGBColor,
  12     Line,
  13     Polygon,
  14     Circle,
  15     RoundedRectangle,
  16     Meter,
  17     SecurityDoor,
  18 } from './Utils.js';
  19 
  20 
  21 /* Touch 事件
  22 touchstart
  23 当用户在触摸平面上放置了一个触点时触发。事件的目标 element 将是触点位置上的那个目标 element
  24 
  25 touchend
  26 当一个触点被用户从触摸平面上移除(即用户的一个手指或手写笔离开触摸平面)时触发。当触点移出触摸平面的边界时也将触发。例如用户将手指划出屏幕边缘。
  27 事件的目标 element 与触发 touchstart 事件的目标 element 相同,即使 touchend 事件触发时,触点已经移出了该 element 。
  28 已经被从触摸平面上移除的触点,可以在 changedTouches 属性定义的 TouchList 中找到。
  29 
  30 touchmove
  31 当用户在触摸平面上移动触点时触发。事件的目标 element 和触发 touchstart 事件的目标 element 相同,即使当 touchmove 事件触发时,触点已经移出了该 element 。
  32 当触点的半径、旋转角度以及压力大小发生变化时,也将触发此事件。
  33 注意: 不同浏览器上 touchmove 事件的触发频率并不相同。这个触发频率还和硬件设备的性能有关。因此决不能让程序的运作依赖于某个特定的触发频率。
  34 
  35 touchcancel
  36 当触点由于某些原因被中断时触发。有几种可能的原因如下(具体的原因根据不同的设备和浏览器有所不同):
  37 由于某个事件出现而取消了触摸:例如触摸过程被弹窗打断。
  38 触点离开了文档窗口,而进入了浏览器的界面元素、插件或者其他外部内容区域。
  39 当用户产生的触点个数超过了设备支持的个数,从而导致 TouchList 中最早的 Touch 对象被取消。
  40 
  41 
  42 TouchEvent.changedTouches
  43 这个 TouchList 对象列出了和这个触摸事件对应的 Touch 对象。
  44 对于 touchstart 事件,这个 TouchList 对象列出在此次事件中新增加的触点。
  45 对于 touchmove 事件,列出和上一次事件相比较,发生了变化的触点。
  46 对于 touchend 事件,changedTouches 是已经从触摸面的离开的触点的集合(也就是说,手指已经离开了屏幕/触摸面)。
  47 
  48 TouchEvent.targetTouches
  49 targetTouches 是一个只读的 TouchList 列表,包含仍与触摸面接触的所有触摸点的 Touch 对象。touchstart (en-US)事件触发在哪个element内,它就是当前目标元素。
  50 
  51 TouchEvent.touches
  52 一个 TouchList,其会列出所有当前在与触摸表面接触的 Touch 对象,不管触摸点是否已经改变或其目标元素是在处于 touchstart 阶段。
  53 
  54 1 event.changedTouches 上一次的触点列表 
  55 1 event.targetTouches 某个元素的当前触点列表 
  56 1 event.touches 屏幕上所有的当前触点列表 
  57 5 touches: Array[Object{
  58     clientX, clientY
  59     pageX, pageY
  60     screenX, screenY
  61     radiusX, radiusY 
  62     force
  63     identifier
  64     rotationAngle
  65     target
  66 }]
  67 
  68 */
  69 
  70 
  71 function _roundRect(con, x, y, w, h, r){ //con: context || Path2D
  72     const _x = x + r, 
  73     _y = y + r, 
  74     mx = x + w, 
  75     my = y + h, 
  76     _mx = mx - r, 
  77     _my = my - r;
  78 
  79     //
  80     con.moveTo(_x, y);
  81     con.lineTo(_mx, y);
  82     con.arcTo(mx, y, mx, _y, r);
  83 
  84     //
  85     con.lineTo(mx, _y);
  86     con.lineTo(mx, _my);
  87     con.arcTo(mx, my, _x, my, r);
  88 
  89     //
  90     con.lineTo(_x, my);
  91     con.lineTo(_mx, my);
  92     con.arcTo(x, my, x, _my, r);
  93 
  94     //
  95     con.lineTo(x, _y);
  96     con.lineTo(x, _my);
  97     con.arcTo(x, y, _x, y, r);
  98 }
  99 
 100 
 101 const PI2 = Math.PI*2;
 102 
 103 const ElementUtils = {
 104 
 105     getRect(elem){
 106         return elem.getBoundingClientRect();
 107     },
 108 
 109     downloadFile(blob, fileName){
 110         const link = document.createElement("a");
 111         link.href = URL.createObjectURL(blob);
 112         link.download = fileName;
 113         link.click();
 114     },
 115 
 116     loadFileJSON(onload){
 117         const input = document.createElement("input");
 118         input.type = "file";
 119         input.accept = ".json";
 120         
 121         input.onchange = a => {
 122             if(a.target.files.length === 0) return;
 123             const fr = new FileReader();
 124             fr.onloadend = b => onload(b.target.result);
 125             fr.readAsText(a.target.files[0]); //fr.readAsDataURL(a.target.files[0]);
 126         }
 127         
 128         input.click();
 129     },
 130 
 131     loadFileImages(onload){
 132         const input = document.createElement("input");
 133         input.type = "file";
 134         input.multiple = "multiple";
 135         input.accept = ".png, .PNG, .jpg, .JPG, .jpeg, .JPEG, .bmp, .BMP, .gif, .GIF";
 136         
 137         input.onchange = e => {
 138             const files = e.target.files, len = files.length;
 139             if(len === 0) return;
 140 
 141             var i = 0;
 142             const fr = new FileReader(), result = [];
 143             fr.onerror = () => {
 144                 i++; if(i === len && typeof onload === "function") onload(result);
 145             }
 146             fr.onloadend = ef => {
 147                 if(typeof ef.target.result === "string" && ef.target.result.length > 0) result.push(ef.target.result);
 148                 i++; 
 149                 if(i === len && typeof onload === "function") onload(result);
 150                 else fr.readAsDataURL(files[i]);
 151             }
 152 
 153             fr.readAsDataURL(files[0]);
 154         }
 155         
 156         input.click();
 157     },
 158 
 159     createCanvas(w = 1, h = 1, className = ""){
 160         const canvas = document.createElement("canvas");
 161         canvas.width = w;
 162         canvas.height = h;
 163         canvas.className = className;
 164         return canvas;
 165     },
 166 
 167     createContext(w = 1, h = 1, alpha = true){
 168         const canvas = document.createElement("canvas"),
 169         context = canvas.getContext("2d", {alpha: alpha});
 170         canvas.width = w;
 171         canvas.height = h;
 172         return context;
 173     },
 174 
 175     //加载图片并把图片缩放至 w, h 大小(如果与w,h大小一样则不缩放直接返回image而不是canvas);
 176     //urls: Array[string]; 此参数为引用(只对urls读操作)
 177     //constrainScale: 在缩放图像时是否约束其比例; 默认 false
 178     createCanvasFromURL(w, h, urls, constrainScale, onload, onchange){
 179         var i = 0; 
 180         const len = urls.length, images = [],
 181         done = () => {
 182             i++; if(len !== i){
 183                 if(typeof onchange === "function") onchange(i, len);
 184                 return;
 185             }
 186             
 187             for(let k = 0; k < len; k++){
 188                 const image = images[k];
 189                 if(image.width !== w || image.height !== h){
 190                     const context = this.createContext(w, h, true);
 191                     if(constrainScale === true){
 192                         const scale = UTILS.getSameScale(image, {width: w, height: h}),
 193                         nw = scale * image.width,
 194                         nh = scale * image.height;
 195                         context.drawImage(image, (w - nw) / 2, (h - nh) / 2, nw, nh);
 196                     } else {
 197                         context.drawImage(image, 0, 0, w, h);
 198                     }
 199                     images[k] = context.canvas;
 200                 }
 201             }
 202 
 203             if(typeof onload === "function") onload(images);
 204         }
 205 
 206         for(let k = 0; k < len; k++){
 207             images[k] = new Image();
 208             images[k].onload = done;
 209             images[k].src = urls[k];
 210         }
 211     },
 212 
 213     //创建画布, 并且在此画布上绘制两种颜色交叉的底板色
 214     createCanvasTCC(width, height, size, round = 0, c1 = "rgb(255,255,255)", c2 = "rgb(127,127,127)"){
 215         const lenX = Math.ceil(width/size), lenY = Math.ceil(height/size),
 216         con = this.createContext(width, height, true), 
 217         l_2 = con.lineWidth / 2, p$1 = new Path2D(), p$2 = new Path2D();
 218         
 219         if(round < 1){
 220             con.rect(l_2, l_2, width - con.lineWidth, height - con.lineWidth);
 221         } else {
 222             _roundRect(con, l_2, l_2, width - con.lineWidth, height - con.lineWidth, round);
 223             con.clip();
 224         }
 225 
 226         for(let ix = 0, iy = 0; ix < lenX; ix++){
 227     
 228             for(iy = 0; iy < lenY; iy++){
 229                 
 230                 ((ix + iy) % 2 === 0 ? p$1 : p$2).rect(ix * size, iy * size, size, size);
 231                 
 232             }
 233     
 234         }
 235 
 236         con.fillStyle = c1;
 237         con.fill(p$1);
 238 
 239         con.fillStyle = c2;
 240         con.fill(p$2);
 241 
 242         return con.canvas;
 243     },
 244 
 245     createElem(tagName, className = "", textContent = ""){
 246         const elem = document.createElement(tagName);
 247         elem.className = className;
 248         elem.textContent = textContent;
 249         return elem;
 250     },
 251 
 252     createInput(type, className = ""){
 253         const input = document.createElement("input");
 254         input.type = type;
 255         input.className = className;
 256         return input;
 257     },
 258 
 259     appendChilds(parentElem, ...nodes){
 260         const msgContainer = document.createDocumentFragment();
 261         for(let k = 0, len = nodes.length; k < len; k++) msgContainer.appendChild(nodes[k]);
 262         parentElem.appendChild(msgContainer);
 263     },
 264 
 265     removeChild(elem){
 266         if(elem.parentElement) elem.parentElement.removeChild(elem);
 267     },
 268 
 269     rotate(elem, a, o = "center"){
 270         elem.style.transformOrigin = o;
 271         elem.style.transform = `rotate(${a}deg)`;
 272     },
 273 
 274     bindButton(elem, callback, ondown = null){
 275         var timeout = 0;
 276 
 277         const param = {offsetX: 0, offsetY: 0},
 278 
 279         onUp = event => {
 280             elem.removeEventListener('pointerup', onUp);
 281             if(Date.now() - timeout < 300) callback(event, param);
 282         },
 283 
 284         onDown = event => {
 285             timeout = Date.now();
 286             param.offsetX = event.offsetX;
 287             param.offsetY = event.offsetY;
 288             if(ondown !== null) ondown(event, param);
 289             elem.removeEventListener('pointerup', onUp);
 290             elem.addEventListener('pointerup', onUp);
 291         }
 292 
 293         elem.addEventListener('pointerdown', onDown);
 294 
 295         return function (){
 296             elem.removeEventListener('pointerup', onUp);
 297             elem.removeEventListener('pointerdown', onDown);
 298         }
 299     },
 300 
 301     bindMobileButton(elem, callback, ondown = null){
 302         var timeout = 0, rect;
 303 
 304         const param = {offsetX: 0, offsetY: 0},
 305         
 306         onUp = event => {
 307             event.preventDefault();
 308             if(Date.now() - timeout < 300) callback(event, param);
 309         },
 310 
 311         onDown = event => {
 312             event.preventDefault();
 313             timeout = Date.now();
 314             rect = elem.getBoundingClientRect();
 315             param.offsetX = event.targetTouches[0].pageX - rect.x;
 316             param.offsetY = event.targetTouches[0].pageY - rect.y;
 317             if(ondown !== null) ondown(event, param);
 318         }
 319 
 320         elem.addEventListener('touchend', onUp);
 321         elem.addEventListener('touchstart', onDown);
 322 
 323         return function (){
 324             elem.removeEventListener('touchend', onUp);
 325             elem.removeEventListener('touchstart', onDown);
 326         }
 327     },
 328 
 329 }
 330 
 331 
 332 function gradientColor(gradient, colors, close = false){
 333     if(UTILS.emptyArray(colors)) return;
 334     const len = colors.length;
 335     if(close === false){
 336         for(let k = 0, _len = len - 1; k < len; k++) gradient.addColorStop(k / _len, colors[k]);
 337     }else{
 338         for(let k = 0; k < len; k++) gradient.addColorStop(k / len, colors[k]);
 339         gradient.addColorStop(1, colors[0]);
 340     }
 341     return gradient;
 342 }
 343 
 344 function gradientColorSymme(gradient, colors){
 345     if(Array.isArray(colors) === true){
 346 
 347         const len = Math.round(colors.length/2), count = len * 2;
 348         
 349         for(let k = 0; k < len; k++){
 350             gradient.addColorStop(k / count, colors[k]);
 351         }
 352 
 353         for(let k = len, i = len; k >= 0; k--, i++){
 354             gradient.addColorStop(i / count, colors[k]);
 355         }
 356         
 357     }
 358     return gradient;
 359 }
 360 
 361 //解决像素过高导致模糊问题
 362 function setCS(c, w, h){ 
 363     if(devicePixelRatio <= 1){
 364         c.width = Math.round(w);
 365         c.height = Math.round(h);
 366     } else {
 367         c.style.width = w + 'px';
 368         c.style.height = h + 'px';
 369         c.width = Math.round(w * devicePixelRatio);
 370         c.height = Math.round(h * devicePixelRatio);
 371     }
 372 }
 373 function getCX(x){
 374     return devicePixelRatio <= 1 ? x : x * devicePixelRatio;
 375 }
 376 
 377 
 378 /* CanvasElementEvent domElement 绑定 移动 或 桌面 down, move, up 事件 (使两端的事件触发逻辑和参数保持一致)
 379 parameter: null
 380 attributes: null
 381 method: 
 382     initEvent(domElement, list, box): function;
 383     initEventMobile(domElement, list, box): function;
 384         domElement: HTMLCanvasElement
 385         list: Array[CanvasEventTarget]
 386         box: Box
 387         function: 删除绑定的事件
 388 */
 389 class CanvasElementEvent{
 390 
 391     initEvent(domElement, list, box, cis = null){
 392         const onups = [];
 393 
 394         const ondown = event => {
 395             let i, ci, len = list.length;
 396             for(i = 0; i < onups.length; i++) onups[i]();
 397             onups.length = 0;
 398 
 399             const sTime = Date.now();
 400             //为什么不用event.offsetX, 而是 rect, 用rect是为了鼠标即使移出了目标dom的范围其参数值也是有效的
 401             const targets = [], rect = domElement.getBoundingClientRect(),
 402             _offsetX = event.pageX - rect.x,
 403             _offsetY = event.pageY - rect.y,
 404             offsetX = _offsetX + box.x, 
 405             offsetY = _offsetY + box.y;
 406             
 407             for(i = 0; i < len; i++){
 408                 ci = list[i];
 409                 if(ci.visible === false) continue;
 410                 if(ci.position === ""){
 411                     if(ci.box.containsPoint(offsetX, offsetY) === false) continue;
 412                 } else if(ci.position === "fixed"){
 413                     if(ci.box.containsPoint(_offsetX, _offsetY) === false) continue;
 414                 }
 415                 
 416                 if(targets.length === 0) targets[0] = ci;
 417                 else{
 418                     if(ci.index === targets[0].index) targets.push(ci);
 419                     else if(ci.index > targets[0].index){
 420                         targets.length = 0;
 421                         targets[0] = ci;
 422                     }
 423                 }
 424             }
 425             
 426             len = targets.length;
 427 
 428             if(len !== 0){
 429                 if(cis !== null) cis.disable(this);
 430                 const info = {targets: targets, target: targets[len-1], offsetX: offsetX, offsetY: offsetY, delta: 0, moveStep: 0},
 431                 
 432                 onmove = e => {
 433                     info.moveStep++;
 434                     info.offsetX = e.pageX - rect.x + box.x, 
 435                     info.offsetY = e.pageY - rect.y + box.y;
 436                     info.delta = Date.now() - sTime;
 437                     for(i = 0; i < len; i++){
 438                         if(targets[i].hasEventListener("move") === true) targets[i].trigger("move", info, e);
 439                     }
 440                 },
 441         
 442                 onup = e => {
 443                     domElement.releasePointerCapture(e.pointerId);
 444                     domElement.removeEventListener("pointerup", onup);
 445                     domElement.removeEventListener("pointermove", onmove);
 446                     info.delta = Date.now() - sTime;
 447                     for(i = 0; i < len; i++){
 448                         if(targets[i].hasEventListener("up") === true) targets[i].trigger("up", info, e);
 449                     }
 450 
 451                     //click
 452                     if(info.delta < 300 && info.moveStep === 0){
 453                         for(i = 0; i < len; i++){
 454                             if(targets[i].hasEventListener("click") === true) targets[i].trigger("click", info, e);
 455                         }
 456                     }
 457 
 458                     if(cis !== null) cis.enable(this);
 459                 }
 460 
 461                 onups.push(() => {
 462                     domElement.removeEventListener("pointerup", onup);
 463                     domElement.removeEventListener("pointermove", onmove);
 464                 });
 465 
 466                 domElement.setPointerCapture(event.pointerId);
 467                 domElement.addEventListener("pointerup", onup);
 468                 domElement.addEventListener("pointermove", onmove);
 469                 for(i = 0; i < len; i++){
 470                     if(targets[i].hasEventListener("down") === true) targets[i].trigger("down", info, event);
 471                 }
 472 
 473             }
 474             
 475         }
 476 
 477         domElement.addEventListener("pointerdown", ondown);
 478         return function (){
 479             for(let i = 0; i < onups.length; i++) onups[i]();
 480             onups.length = 0;
 481             domElement.removeEventListener("pointerdown", ondown);
 482             if(cis !== null) cis.enable(this);
 483         }
 484     }
 485 
 486     initEventMobile(domElement, list, box, cis = null){
 487 
 488         const ondown = event => {
 489             const sTime = Date.now();
 490             event.preventDefault();
 491             let i, ci, len = list.length;
 492 
 493             const targets = [], rect = domElement.getBoundingClientRect(),
 494             _offsetX = event.targetTouches[0].pageX - rect.x,
 495             _offsetY = event.targetTouches[0].pageY - rect.y,
 496             offsetX = _offsetX + box.x, 
 497             offsetY = _offsetY + box.y;
 498 
 499             for(i = 0; i < len; i++){
 500                 ci = list[i];
 501                 if(ci.visible === false) continue;
 502                 if(ci.position === ""){
 503                     if(ci.box.containsPoint(offsetX, offsetY) === false) continue;
 504                 } else if(ci.position === "fixed"){
 505                     if(ci.box.containsPoint(_offsetX, _offsetY) === false) continue;
 506                 }
 507                 if(targets.length === 0) targets[0] = ci;
 508                 else{
 509                     if(ci.index === targets[0].index) targets.push(ci);
 510                     else if(ci.index > targets[0].index){
 511                         targets.length = 0;
 512                         targets[0] = ci;
 513                     }
 514                 }
 515             }
 516             
 517             len = targets.length;
 518 
 519             if(len !== 0){
 520                 if(cis !== null) cis.disable(this);
 521                 const info = {targets: targets, target: targets[len-1], offsetX: offsetX, offsetY: offsetY, delta: 0, moveStep: 0},
 522                 
 523                 onmove = e => {
 524                     info.moveStep++;
 525                     info.offsetX = e.targetTouches[0].pageX - rect.x + box.x;
 526                     info.offsetY = e.targetTouches[0].pageY - rect.y + box.y;
 527                     info.delta = Date.now() - sTime;
 528                     for(i = 0; i < len; i++){
 529                         if(targets[i].hasEventListener("move") === true) targets[i].trigger("move", info, e);
 530                     }
 531                 },
 532         
 533                 onup = e => {
 534                     domElement.removeEventListener("touchmove", onmove);
 535                     domElement.removeEventListener("touchend", onup);
 536                     domElement.removeEventListener("touchcancel", onup);
 537                     info.delta = Date.now() - sTime;
 538                     for(i = 0; i < len; i++){
 539                         if(targets[i].hasEventListener("up") === true) targets[i].trigger("up", info, e);
 540                     }
 541 
 542                     //click
 543                     if(info.delta < 300 && info.moveStep === 0){
 544                         for(i = 0; i < len; i++){
 545                             if(targets[i].hasEventListener("click") === true) targets[i].trigger("click", info, e);
 546                         }
 547                     }
 548 
 549                     if(cis !== null) cis.enable(this);
 550                 }
 551 
 552                 domElement.addEventListener("touchcancel", onup);
 553                 domElement.addEventListener("touchend", onup);
 554                 domElement.addEventListener("touchmove", onmove);
 555                 for(i = 0; i < len; i++){
 556                     if(targets[i].hasEventListener("down") === true) targets[i].trigger("down", info, event);
 557                 }
 558             }
 559             
 560         }
 561 
 562         domElement.addEventListener("touchstart", ondown);
 563         return function (){
 564             domElement.removeEventListener("touchstart", ondown);
 565             if(cis !== null) cis.enable(this);
 566         }
 567 
 568     }
 569 
 570 }
 571 
 572 
 573 /* CanvasPath2D CanvasImage.path2D
 574 注意: 线模糊问题任然存在(只有.rect().progress()做了模糊优化)
 575 parameter: 
 576     drawType = "stroke", drawStyle = null, order = "after"
 577 
 578 attributes:
 579     drawType: String;    //填充路径或描绘路径 可能值: 默认 stroke | fill
 580     drawStyle: Object;    //canvas.context的属性
 581     order: Bool;     //是否在 CanvasImage 之前绘制 "before"||"after"默认
 582     value: any;        
 583 
 584 method:
 585     reset(): this;                                //设为零值(清空不在绘制)
 586     line(line: Line): this;                     //线段
 587     rect(rect: Box||RoundedRectangle): this;    //矩形
 588     circle(circle: Circle): this;                //圆
 589     path(polygon: Polygon): this;                //线
 590     progress(meter: Meter): this;                //进度条(为 CanvasImage 创建默认的进度条, 修改 meter.value 更新进度条视图)
 591 
 592 demo:
 593     const test = new CanvasImageCustom().size(100, 100).pos(100, 100).rect().fill("#664466");
 594     test.path2D = new CanvasPath2D("stroke", {strokeStyle: "blue", lineWidth: 4});
 595 
 596     const path2D = new Path2D();
 597     path2D.roundRect(12, 12, 40, 40, 10); //圆角矩形
 598     test.path2D.path(path2D);
 599 
 600     //test.path2D.line(new Line(4, 4, 4, 150000));
 601 
 602 */
 603 class CanvasPath2D{
 604 
 605     #pathType = "";
 606     get isDraw(){
 607         return this.value && this.#pathType !== "";
 608     }
 609 
 610     constructor(drawType = "stroke", drawStyle = null, order = "after"){
 611         this.order = order;
 612         this.drawType = drawType;
 613         this.drawStyle = drawStyle;
 614         this.value = undefined;
 615     }
 616 
 617     reset(){
 618         this.#pathType = "";
 619         this.value = undefined;
 620         return this;
 621     }
 622 
 623     line(line = new Line()){
 624         this.#pathType = "line";
 625         this.value = line;
 626         return this;
 627     }
 628 
 629     rect(rect = new RoundedRectangle()){
 630         this.#pathType = "rect";
 631         this.value = rect;
 632         return this;
 633     }
 634 
 635     circle(circle = new Circle()){
 636         this.#pathType = "circle";
 637         this.value = circle;
 638         return this;
 639     }
 640 
 641     path(polygon = new Polygon()){
 642         this.#pathType = "path";
 643         this.value = polygon;
 644         return this;
 645     }
 646 
 647     progress(meter = new Meter()){
 648         this.#pathType = "progress";
 649         this.value = meter;
 650         return this;
 651     }
 652 
 653     _drawPath(con){
 654         if(this.drawStyle === null) con[this.drawType]();
 655         else{
 656             con.save();
 657             for(let n in this.drawStyle){
 658                 if(this.drawStyle[n] !== con[n]) con[n] = this.drawStyle[n];
 659             }
 660             con[this.drawType]();
 661             con.restore();
 662         }
 663     }
 664 
 665     _draw(con, x, y, w, h){
 666         var lw;
 667         con.save();
 668         con.beginPath();
 669         con.rect(x, y, w, h);
 670         con.clip();
 671         con.translate(x, y);
 672         const val = this.value;
 673         switch(this.#pathType){
 674             case "line": 
 675             con.beginPath();
 676             con.moveTo(val.x, val.y);
 677             con.lineTo(val.x1, val.y1);
 678             this._drawPath(con);
 679             break;
 680 
 681             case "rect": 
 682             con.beginPath();
 683             lw = this.drawStyle.lineWidth || con.lineWidth;
 684             if(lw % 2 === 0){
 685                 const lw_2 = lw / 2;
 686                 x = Math.floor(val.x+lw_2);
 687                 y = Math.floor(val.y+lw_2);
 688             } else {
 689                 const lw_2 = lw / 2;
 690                 x = Math.floor(val.x+lw_2)+0.5;
 691                 y = Math.floor(val.y+lw_2)+0.5;
 692             }
 693             if(val.r === undefined || val.r < 1) con.rect(x, y, Math.floor(val.w-lw), Math.floor(val.h-lw));
 694             else _roundRect(con, x, y, Math.floor(val.w-lw), Math.floor(val.h-lw), val.r);
 695             this._drawPath(con);
 696             break;
 697 
 698             case "circle": 
 699             con.beginPath();
 700             con.arc(val.x, val.y, val.r, 0, PI2);
 701             this._drawPath(con);
 702             break;
 703 
 704             case "path": 
 705             con.beginPath();
 706             con.moveTo(val.points[0], val.points[1]);
 707             for(let k = 2, len = val.points.length; k < len; k += 2) con.lineTo(val.points[k], val.points[k+1]);
 708             this._drawPath(con);
 709             break;
 710 
 711             case "progress": 
 712             con.beginPath();
 713             lw = this.drawStyle.lineWidth || con.lineWidth;
 714             if(w >= h){
 715                 x = lw % 2 === 0 ? Math.round(h-lw) : Math.floor(h-lw)+0.5;
 716                 con.moveTo(0, x);
 717                 con.lineTo(w * val.ratio, x);
 718             } else {
 719                 x = lw % 2 === 0 ? Math.round(w-lw) : Math.floor(w-lw)+0.5;
 720                 con.moveTo(x, 0);
 721                 con.lineTo(x, h * val.ratio);
 722             }
 723             this._drawPath(con);
 724             break;
 725         }
 726         con.restore();
 727     }
 728 
 729 }
 730 
 731 
 732 /* CanvasImageDraw 渲染器
 733 parameter: 
 734     option = {
 735         drawType           //默认 3 
 736         alpha             //默认 true
 737         className        //默认 ""
 738         width, height: Number || objcet: HTMLCanvasElement, CanvasImageCustom
 739     }
 740 
 741 attribute:
 742     domElement: HTMLCanvasElement;
 743     context: CanvasRenderingContext2D;
 744     list: Array[CanvasImage]
 745     drawType: Number; //怎么去绘制列表, 默认 3
 746         0: 纯净模式(遍历列表: context.drawImage(CI.image, CI.x, CI.y))
 747         1: 应用CIR内置属性
 748         2: 应用CI内置属性
 749         3: 1 + 2
 750     
 751 method:
 752     append(...ci: CanvasImage): this;        //追加多个ci到列表
 753     size(w, h: Number): this;                //设置box和canvas的宽高
 754     render(parentElem: HTMLElement): this;    //重绘所有的 CanvasImage 并把 canvas 添加至dom树
 755     redraw(): undefined;                     //重绘所有的 CanvasImage
 756     redrawCI(ci: CanvasImage): undefined;    //重绘一个 CanvasImage
 757     exit(): undefined;                        //不在使用此类应调用此方法清除相关缓存并从dom树删除画布;
 758     sortCIIndex(): undefined;                //所有ci按其 index 属性值从小到大排序一次(视图的层级, 可以绑定 "beforeDraw" 事件, 达到自动更新)
 759 
 760     sortCIPosEquals(list, option): this; //平铺排序(假设list里的所有CanvasImage的大小都一样)
 761         list: Array[CanvasImage]; //默认 this.list
 762         option: Object{
 763             disX, disY: Number,        //CanvasImage 之间的间距,默认 0
 764             sx, sy: Number,            //开始位置, 默认 0
 765             size: Object{w,h},        //如果定义就计算并在其上设置所占的宽高 (x,y 为option.sx.sy)
 766             lenX: Number,            //x轴最多排多少个 默认 1
 767         }
 768         //根据参数算出每个item的宽
 769         const width = 600, lenX = 5, disX = 4, sx = 2, sy = 2,
 770         itemSize = (width - sx * 2 - disX * (lenX - 1)) / lenX;
 771 
 772     sortCIPos(list, option): this; //平铺排序 (此排序相对于 .sortCIPosEquals() 较慢)
 773         list: Array[CanvasImage]; //默认 this.list
 774         option: Object{
 775             disX, disY: Number,        //CanvasImage 之间的间距,默认 0
 776             sx, sy: Number,            //开始位置, 默认 0
 777             size: Object{w,h},        //如果定义就计算并在其上设置所占的宽高 (x,y 为option.sx.sy)
 778             width: Number            //默认 this.box.w
 779             lineHeight: String,        //可能值: top, middle, bottom; 默认 top
 780         }
 781 
 782     initEventDispatcher(): this; //初始化自定义事件 (如果不需要这些事件可以不用初始化)
 783         支持的事件名:
 784         beforeDraw: eventParam{target: CanvasImageDraw}
 785         afterDraw: eventParam{target: CanvasImageDraw}
 786         boxX: eventParam{target: CanvasImageDraw}
 787         boxY: eventParam{target: CanvasImageDraw}
 788         size: eventParam{target: CanvasImageDraw}
 789         exit: eventParam{target: CanvasImageDraw}
 790         append: eventParam{target: Array[CanvasImage]}
 791         add: eventParam{target: CanvasImage}
 792         remove: eventParam{target: CanvasImage}
 793     
 794     createCircleDiffusion(t, mr, fillStyle): Function; //点击画布时播放向外扩散的圆
 795         t: Number;             //扩散至最大半径的时间, 默认200
 796         mr: Number;         //扩散圆的最大半径, 默认25
 797         fillStyle: String;    //填充扩散圆的主要颜色; 默认"rgba(0,244,255,0.5)"
 798         Function            //返回值, 用于退出此程序的函数
 799 
 800 demo:
 801     //用 new Image() 加载20万个图片直接崩了 (用canvas就稍微有点卡)
 802     //如果用 CanvasImageText 去绘制形状或文字一样会崩
 803 
 804     const cir = new CanvasImageDraw({width: 600, height: 300}),
 805     cis = new CanvasImageScroll(cir, {scrollSize: 4});
 806     cir.domElement.style = `
 807         background: rgb(127,127,127);
 808     `;
 809     
 810     const img = new CanvasImage().loadImage(`${RootDir}examples/img/Stuffs/1.png`, () => {
 811         const option = {
 812             disX: 10, disY: 10,        //CanvasImage 之间的间距,默认 0
 813             sx: 0, sy: 0,            //开始位置, 默认 0
 814             size: {},            //如果定义就在其上设置结束时的矩形 
 815             lineHeight: "",        //可能值: top, middle, bottom; 默认 top
 816             lenX: Math.floor(cir.box.w / 50),
 817         }
 818 
 819         //测试排序
 820         for(let i = 0; i < 20; i++){
 821             console.time("test");
 822             //cir.sortCIPos(null, option);
 823             cir.sortCIPosEquals(null, option);
 824             console.timeEnd("test");
 825         }
 826 
 827         cir.render();
 828         console.log(cir, cis, option.size);
 829     });
 830 
 831     for(let i = 0; i < 200000; i++){
 832         const ci = new CanvasImage(img);
 833         cis.bindScroll(ci); //cis 能够监听此ci
 834         cis.changeCIAdd(ci); //cis 初始化此ci
 835         cir.list[i] = ci; //cir 能够绘制此ci
 836     }
 837 */
 838 class CanvasImageDraw{
 839     
 840     static arrSort = function (a, b){return a["index"] - b["index"];}
 841     static paramCon = {alpha: true}
 842     
 843     static defaultStyles = {
 844         imageSmoothingEnabled: false, //是否启用平滑处理
 845         imageSmoothingQuality: "low", //平滑处理的质量
 846         globalCompositeOperation: "source-over", //混合模式
 847         filter: "none", //过滤器
 848         globalAlpha: 1, //透明度
 849         
 850         font: "10px sans-serif", //"10px sans-serif" ("bold 48px serif");
 851         textAlign: "left", //start
 852         textBaseline: "top", //alphabetic
 853         direction: "inherit", //"ltr"左往右 || "rtl"右往左 || "inherit"继承父elem 默认 字体方向
 854         wordSpacing: "0px",  //字体之间的间距(空格的距离)
 855         letterSpacing: "0px", //字体之间的间距
 856         textRendering: "auto", //如何绘制字体: auto 默认 || optimizeSpeed 速度 || optimizeLegibility 质量 || geometricPrecision 几何精度(最接近字体大小)
 857         fontKerning: "auto", //字体紧排
 858         fontStretch: "normal", //字体拉伸
 859         fontVariantCaps: "normal",
 860 
 861         lineCap: "butt", //butt (默认), round, square 线末端
 862         lineJoin: "miter", //round, bevel, miter(默认) 线转角
 863         lineDashOffset: 0,
 864         lineWidth: 1,
 865         miterLimit: 10,
 866 
 867         shadowColor: "rgba(0, 0, 0, 0)",
 868         shadowBlur: 0,
 869         shadowOffsetX: 0,
 870         shadowOffsetY: 0,
 871 
 872         fillStyle: "#000000",
 873         strokeStyle: "#000000",
 874     }
 875 
 876     static setDefaultStyles(context){
 877         const styles = CanvasImageDraw.defaultStyles;
 878         for(let k in styles){
 879             if(context[k] !== styles[k]) context[k] = styles[k];
 880         }
 881     }
 882 
 883     static getContext(canvas, className, alpha = true){
 884         if(CanvasImageDraw.isCanvas(canvas) === false) canvas = document.createElement("canvas");
 885         CanvasImageDraw.paramCon.alpha = alpha;
 886         const context = canvas.getContext("2d", CanvasImageDraw.paramCon);
 887 
 888         if(typeof className === "string") canvas.className = className;
 889         //if(typeof id === "string") canvas.setAttribute('id', id);
 890         
 891         return context;
 892     }
 893 
 894     static isCanvasImage(img){ //OffscreenCanvas: ImageBitmap;
 895 
 896         return ImageBitmap["prototype"]["isPrototypeOf"](img) || 
 897         HTMLImageElement["prototype"]["isPrototypeOf"](img) || 
 898         HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 
 899         CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 
 900         HTMLVideoElement["prototype"]["isPrototypeOf"](img);
 901         
 902     }
 903 
 904     static isCanvas(canvas){
 905         return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas);
 906     }
 907 
 908     #eventDispatcher = null;
 909     get eventDispatcher(){return this.#eventDispatcher;}
 910     
 911     #pointEV = new Point();
 912     #boxV = new Box();
 913     #box = null;
 914     get box(){return this.#box;}
 915     
 916     constructor(option = {}){
 917         this.list = [];
 918         this.drawType = option.drawType === undefined ? 3 : option.drawType;
 919         
 920         if(UTILS.isObject(option.object) === false){
 921             
 922             this.#box = new Box();
 923             this.context = CanvasImageDraw.getContext(null, option.className, option.alpha);
 924             this.domElement = this.context.canvas;
 925             this.size(option.width, option.height);
 926 
 927         } else {
 928 
 929             if(CanvasImageDraw.isCanvas(option.object) === true){
 930                 this.#box = new Box();
 931                 this.context = CanvasImageDraw.getContext(option.object, option.className, option.alpha);
 932                 this.domElement = this.context.canvas;
 933                 this.size(option.object.width, option.object.height);
 934             }
 935     
 936             else if(CanvasImageCustom.prototype.isPrototypeOf(option.object) === true){
 937                 this.#box = option.object.box;
 938                 this.context = option.object.context;
 939                 this.domElement = option.object.image;
 940             }
 941 
 942             else{
 943                 this.#box = new Box();
 944                 this.context = CanvasImageDraw.getContext(null, option.className, option.alpha);
 945                 this.domElement = this.context.canvas;
 946                 this.size(option.width, option.height);
 947             }
 948 
 949         }
 950 
 951     }
 952 
 953     createCircleDiffusion(t = 200, mr = 25, fillStyle = "rgba(0,244,255,0.5)"){
 954         var x = 0, y = 0, st = 0, r = 0;
 955         const PI2 = Math.PI*2, context = this.context, oldFillStyle = context.fillStyle, 
 956         colors = [
 957             "rgba(0,0,0,0)", 
 958             fillStyle,
 959             "rgba(0,0,0,0)",
 960         ],
 961         
 962         animateLoop = new AnimateLoop(() => {
 963             if(r <= mr){
 964                 this.redraw();
 965                 r = (Date.now() - st) / t * mr;
 966                 context.beginPath();
 967                 context.arc(x, y, r, 0, PI2);
 968                 const radialGradient = context.createRadialGradient(x, y, 0, x, y, r);
 969                 gradientColorSymme(radialGradient, colors);
 970                 context.fillStyle = radialGradient;
 971                 context.fill();
 972             }
 973             else onstop();
 974         }),
 975 
 976         onstop = () => {
 977             context.fillStyle = oldFillStyle;
 978             animateLoop.stop();
 979             this.redraw();
 980         },
 981 
 982         ondown = event => {
 983             x = event.offsetX;
 984             y = event.offsetY;
 985             st = Date.now();
 986             r = 0;
 987             animateLoop.play();
 988         }
 989         
 990         this.domElement.addEventListener("pointerdown", ondown);
 991         return function (){
 992             onstop();
 993             this.domElement.removeEventListener("pointerdown", ondown);
 994         }
 995     }
 996 
 997     initEventDispatcher(){
 998         this.#eventDispatcher = new EventDispatcher();
 999         const paramA = {target: this}, paramB = {target: null}
1000 
1001         this.#eventDispatcher
1002         .customEvents("beforeDraw", paramA)
1003         .customEvents("afterDraw", paramA)
1004         .customEvents("boxX", paramA)
1005         .customEvents("boxY", paramA)
1006         .customEvents("size", paramA)
1007         .customEvents("exit", paramA)
1008         .customEvents("append", paramB)
1009         .customEvents("add", paramB)
1010         .customEvents("remove", paramB);
1011 
1012         let _x = this.#box.x, _y = this.#box.y;
1013         Object.defineProperties(this.#box, {
1014 
1015             x: {
1016                 get: () => {return _x;},
1017                 set: v => {
1018                     if(v !== _x && isNaN(v) === false){
1019                         _x = v;
1020                         this.#eventDispatcher.trigger("boxX");
1021                     }
1022                 }
1023             },
1024 
1025             y: {
1026                 get: () => {return _y;},
1027                 set: v => {
1028                     if(v !== _y && isNaN(v) === false){
1029                         _y = v;
1030                         this.#eventDispatcher.trigger("boxY");
1031                     }
1032                 }
1033             },
1034 
1035         });
1036 
1037         return this;
1038     }
1039 
1040     sortCIIndex(){
1041         this.list.sort(CanvasImageDraw.arrSort);
1042     }
1043 
1044     sortCIPosEquals(list, option = {}){
1045         if(Array.isArray(list) === false) list = this.list;
1046         if(list.length === 0) return this;
1047 
1048         const lenX = option.lenX || 1, 
1049         disX = option.disX || 0,
1050         disY = option.disY || 0,
1051         x = option.sx || 0, y = option.sy || 0, 
1052         w = list[0].w, h = list[0].h;
1053 
1054         for(let i = 0, ix, iy; i < list.length; i++){
1055             ix = i % lenX;
1056             iy = Math.floor(i / lenX);
1057             list[i].box.pos(ix * w + ix * disX + x, iy * h + iy * disY + y);
1058         }
1059 
1060         if(option.size !== undefined){
1061             option.size.w = w * lenX + disX * lenX - disX;
1062             const lenY = Math.ceil(list.length / lenX);
1063             option.size.h = h * lenY + disY * lenY - disY;
1064         }
1065         
1066         return this;
1067     }
1068 
1069     sortCIPos(list, option = {}){
1070         if(Array.isArray(list) === false) list = this.list;
1071         const len = list.length;
1072         if(len === 0) return this;
1073 
1074         const mw = option.width || this.box.w,
1075         indexs = option.lineHeight === "middle" || option.lineHeight === "bottom" ? [] : null, //[sIndex, length, mHeight]
1076         sx = option.sx || 0,
1077         sy = option.sy || 0,
1078         disX = option.disX || 0,
1079         disY = option.disY || 0,
1080         rect = option.size || null;
1081         if(rect !== null){
1082             rect.w = list[0].box.mx;
1083             rect.h = list[0].box.my;
1084         }
1085 
1086         var y = sy, x = sx, w = 0, h = list[0].h;
1087         for(let i = 0; i < len; i++){
1088             if(indexs !== null && indexs.length % 3 === 0) indexs.push(i);
1089             w = list[i].w;
1090             if(x + w + disX > mw){
1091                 if(indexs !== null) indexs.push(i, h, i);
1092                 x = w + sx + disX;
1093                 y += h + disY;
1094                 h = list[i].h;
1095                 list[i].box.pos(sx, y);
1096             } else {
1097                 list[i].box.pos(x, y);
1098                 x += w + disX;
1099                 h = Math.max(list[i].h, h);
1100                 if(rect !== null){
1101                     rect.w = Math.max(list[i].box.mx, rect.w);
1102                     rect.h = Math.max(list[i].box.my, rect.h);
1103                 }
1104             }
1105         }
1106 
1107         if(indexs !== null){
1108             if(indexs.length % 3 === 1) indexs.push(len, h);
1109             switch(option.lineHeight){
1110                 case "middle": 
1111                 for(let i = 0; i < indexs.length; i += 3){
1112                     for(let k = indexs[i]; k < indexs[i + 1]; k++) list[k].box.y += (indexs[i + 2] - list[k].h) / 2;
1113                 }
1114                 break;
1115                 case "bottom": 
1116                 for(let i = 0; i < indexs.length; i += 3){
1117                     for(let k = indexs[i]; k < indexs[i + 1]; k++) list[k].box.y += indexs[i + 2] - list[k].h;
1118                 }
1119                 break;
1120             }
1121         }
1122         
1123         if(rect !== null){
1124             rect.w -= sx;
1125             rect.h -= sx;
1126         }
1127 
1128         return this;
1129     }
1130 
1131     size(w, h){
1132         switch(typeof w) {
1133             case "number": 
1134             this.box.size(w, h);
1135             break;
1136             case "object": 
1137             this.box.size(w.width||w.w||this.box.w, w.height||w.h||this.box.h);
1138             break;
1139         }
1140 
1141         this.domElement.width = this.box.w;
1142         this.domElement.height = this.box.h;
1143         this.context.imageSmoothingEnabled = false;
1144 
1145         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("size");
1146 
1147         return this;
1148     }
1149 
1150     render(parentElem = document.body){
1151         this.redraw();
1152         if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement);
1153         return this;
1154     }
1155 
1156     exit(){
1157         if(this.domElement.parentElement) this.domElement.parentElement.removeChild(this.domElement);
1158         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("exit");
1159         if(this.#eventDispatcher !== null) this.#eventDispatcher.clearEvents();
1160         this.list.length = 0;
1161     }
1162 
1163     append(...cis){
1164         const len = this.list.length;
1165         for(let i = 0; i < cis.length; i++) this.list[i + len] = cis[i];
1166         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("append", param => param.target = cis);
1167         return this;
1168     }
1169 
1170     add(ci = new CanvasImage()){
1171         if(this.list.includes(ci) === false){
1172             this.list.push(ci);
1173             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("add", param => param.target = ci);
1174         }
1175         return ci;
1176     }
1177 
1178     remove(ci){
1179         const i = this.list.indexOf(ci);
1180         if(i !== -1){
1181             this.list.splice(i, 1);
1182             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("remove", param => param.target = ci);
1183         }
1184         return ci;
1185     }
1186 
1187     isDraw(ca){
1188         switch(ca.position){
1189             case "fixed": 
1190             if(this.#boxV.equals(this.box) === false) this.#boxV.set(0, 0, this.box.w, this.box.h);
1191             return ca["visible"] === true && ca["image"] && this.#boxV["intersectsBox"](ca["box"]);
1192 
1193             default: 
1194             return ca["visible"] === true && ca["image"] && this["box"]["intersectsBox"](ca["box"]);
1195         }
1196     }
1197 
1198     redraw(){
1199         this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']);
1200         switch(this.drawType){
1201             case 0: 
1202             for(let k = 0, ci; k < this.list.length; k++){
1203                 ci = this.list[k];
1204                 if(ci["image"] && this["box"]["intersectsBox"](ci["box"])) this.context.drawImage(ci.image, ci.x, ci.y);
1205             }
1206             return;
1207 
1208             case 1: 
1209             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw");
1210             for(let k = 0, ci; k < this.list.length; k++){
1211                 ci = this.list[k];
1212                 if(ci["image"] !== null && this["box"]["intersectsBox"](ci["box"])) this.context.drawImage(ci.image, ci.x, ci.y);
1213             }
1214             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw");
1215             return;
1216 
1217             case 2: 
1218             for(let k = 0, ci; k < this.list.length; k++){
1219                 ci = this.list[k];
1220                 if(this.isDraw(ci) === true) this._draw(ci);
1221             }
1222             return;
1223 
1224             case 3: 
1225             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw");
1226             for(let k = 0, ci; k < this.list.length; k++){
1227                 ci = this.list[k];
1228                 if(this.isDraw(ci) === true) this._draw(ci);
1229             }
1230             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw");
1231             return;
1232         }
1233     }
1234 
1235     redrawCI(ci){
1236         if(this.isDraw(ci) === true){
1237             if(ci.position === "fixed"){
1238                 this.context.clearRect(ci.x, ci.y, ci.w, ci.h);
1239             } else {
1240                 this.context.clearRect(ci.x - this.box.x, ci.y - this.box.y, ci.w, ci.h);
1241             }
1242             
1243             this._draw(ci);
1244         }
1245     }
1246 
1247     redrawTarget(box){
1248         if(CanvasImage["prototype"]["isPrototypeOf"](box) === true) box = box.box;
1249 
1250         const _list = [], list = [], len = this.list.length;
1251 
1252         for(let k = 0, tar, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){
1253             tar = this["list"][k];
1254             
1255             if(this.isDraw(tar) === false) continue;
1256 
1257             if(box["intersectsBox"](tar["box"]) === true){
1258                 tar["__overlap"] = true;
1259                 box["expand"](tar["box"]);
1260                 loop = true;
1261 
1262                 while(loop === true){
1263                     b["length"] = 0;
1264                     loop = false;
1265                     c = _c;
1266 
1267                     for(i = 0; i < c; i++){
1268                         tar = a[i];
1269 
1270                         if(box["intersectsBox"](tar["box"]) === true){
1271                             tar["__overlap"] = true;
1272                             box["expand"](tar["box"]);
1273                             loop = true; _c--;
1274                         }
1275 
1276                         else b.push(tar);
1277                         
1278                     }
1279 
1280                     a = a === _list ? list : _list;
1281                     b = b === _list ? list : _list;
1282 
1283                 }
1284                 
1285             }
1286 
1287             else{
1288                 _c++;
1289                 a["push"](tar);
1290                 tar["__overlap"] = false;
1291             }
1292         }
1293 
1294         this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']);
1295         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw");
1296         for(let k = 0, ci; k < this.list.length; k++){
1297             ci = this.list[k];
1298             if(ci["__overlap"] === true) this._draw(ci);
1299         }
1300         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw");
1301     }
1302 
1303     _draw(ci){
1304         const con = this.context;
1305         
1306         switch(ci.position){
1307             case "fixed": 
1308             this.#pointEV.set(0, 0);
1309             break;
1310             case "": 
1311             default: 
1312             this.#pointEV.set(this.#box.x, this.#box.y);
1313             break;
1314         }
1315 
1316         if(ci.hasEventListener("beforeDraw") === true) ci.trigger("beforeDraw", con, this.#pointEV);
1317         
1318         const x = ci.x - this.#pointEV.x, y = ci.y - this.#pointEV.y;
1319 
1320         if(ci.opacity !== con.globalAlpha) con.globalAlpha = ci.opacity;
1321 
1322         if(ci.shadow === null){
1323             if(con.shadowColor !== "rgba(0, 0, 0, 0)") con.shadowColor = "rgba(0, 0, 0, 0)";
1324             //if(con.shadowBlur !== 0) con.shadowBlur = 0;
1325         } else {
1326             if(ci.shadow.blur !== con.shadowBlur) con.shadowBlur = ci.shadow.blur;
1327             if(ci.shadow.color !== con.shadowColor) con.shadowColor = ci.shadow.color;
1328             if(ci.shadow.offsetX !== con.shadowOffsetX) con.shadowOffsetX = ci.shadow.offsetX;
1329             if(ci.shadow.offsetY !== con.shadowOffsetY) con.shadowOffsetY = ci.shadow.offsetY;
1330         }
1331         
1332         if(ci.isDrawPath2D === true && ci.path2D.order === "before") ci.path2D._draw(con, x, y, ci.w, ci.h);
1333 
1334         if(ci.w === ci.width && ci.h === ci.height) con.drawImage(ci.image, x, y);
1335         else con.drawImage(ci.image, x, y, ci.w, ci.h);
1336 
1337         if(ci.isDrawPath2D === true && ci.path2D.order === "after") ci.path2D._draw(con, x, y, ci.w, ci.h);
1338 
1339         if(ci.hasEventListener("afterDraw") === true) ci.trigger("afterDraw", con, this.#pointEV);
1340     }
1341 
1342 }
1343 
1344 
1345 /* CanvasEvent 支持 移动 和 桌面 端
1346 parameter: 
1347     cid: CanvasImageDraw;    //必须
1348     cis: CanvasImageScroll; //可选(如果定义,那么在down到up期间cis为禁用状态)
1349 
1350 method: 
1351     unbindEvent(): undefined;    //如果不在使用调用
1352 */
1353 class CanvasEvent extends CanvasElementEvent{
1354 
1355     constructor(cid, cis){
1356         super();
1357         if(UTILS.isMobile){
1358             this.__exitEventFunc = this.initEventMobile(cid.domElement, cid.list, cid.box, cis);
1359         }else{
1360             this.__exitEventFunc = this.initEvent(cid.domElement, cid.list, cid.box, cis);
1361         }
1362     }
1363 
1364     unbindEvent(){
1365         if(typeof this.__exitEventFunc === "function"){
1366             this.__exitEventFunc();
1367             this.__exitEventFunc = undefined;
1368         }
1369     }
1370 
1371 }
1372 
1373 
1374 /* CanvasImageScroll 画布滚动条(CanvasImageDraw.box.xy的控制器)
1375 parameter:
1376     cid: CanvasImageDraw, 
1377     option: Object{
1378         scrollVisible: string,    //滚动条的显示; 可能值: 默认"visible" || "" || "auto"; //注意: auto 只支持移动端环境, 由事件驱动视图的显示或隐藏
1379         scrollSize: number,        //滚动条的宽或高; 默认10; (如果想隐藏滚动条最好 scrollVisible: "", 而不是把此属性设为0)
1380 
1381         scrollEventType: string,    //可能的值: "default", "touch" || ""; 默认 根据自己所处的环境自动选择创建
1382         inertia: bool,                //是否启用移动端滚动轴的惯性; 默认 true; (当前环境处于移动端才有效)
1383         inertiaLife: number,        //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.06; (inertia 为true才有效)
1384         isPage: bool,                //是否添加分页效果,拖拽查看每一页,每一页的大小为cid.box.wh; 默认 false;  (inertia 为true才有效)
1385 
1386         domElement: HTMLElement,    //dom事件绑定的目标; 默认 cid.domElement
1387     }
1388 
1389 attribute:
1390     scrollXView: CanvasImageCustom; //scroll的背景是.image, 游标是.path2D (参见 CanvasImage)
1391     scrollYView: CanvasImageCustom;
1392     cursorView: CanvasPath2D;        //
1393     maxSize: Point;        //只读, 最大边界
1394 
1395 method: 
1396     enable(sign: any): undefined;    //启用(只对DOM事件有效)
1397     disable(sign: any): undefined;    //禁用
1398     resetMaxSizeX(): undefined;        //重新计算最大边界x
1399     resetMaxSizeX(): undefined;     //重新计算最大边界y
1400     unbindEvent(): undefined;         //如果不在使用此类调用
1401 
1402     bindScrolls(list: Array[CanvasImage]): undefined;    //监听数组里所有的ci, 并更新x或y轴最大边界; list: 默认 cid.list
1403 
1404     //其它无用的方法(内部自动调用)
1405     changeCursorX(): undefined;        //此方法保证游标不会超出可视范围
1406     changeCursorY(): undefined;
1407     changeCIAdd(ci): undefined;        //根据ci更新边界
1408     changeCIDel(ci): undefined;
1409     bindScroll(ci): undefined;        //监听ci
1410     unbindScroll(ci): undefined;    //ci解除监听
1411     drawScrollX(): undefined;        //绘制滚动条视图
1412     drawScrollY(): undefined;
1413     createScrollEventPC(): undefined;//滚动条创建dom事件
1414     createScrollEventMobile(): undefined;
1415 
1416 demo:
1417     修改滚动条样式:
1418     cis.scrollXView.size(cis.scrollXView, true).rect(5, 1).fill("red").stroke("blue"); //修改X轴的背景框样式 (参考 CanvasImageCustom 类)
1419 
1420     cis.scrollYView.size(cis.scrollYView, true).rect(5, 1).fill("red").stroke("blue"); //修改Y轴的背景框样式 (参考 CanvasImageCustom 类)
1421 
1422     cis.cursorView = new CanvasPath2D("fill", {fillStyle: "yellow"}) //修改的游标样式 (参考 CanvasPath2D 类)
1423     .rect(new RoundedRectangle(0,0,0,0,5));    //游标的xywh每次绘制前更新,所以不用填xywh
1424 
1425     
1426     option.isPage 滚动条变成翻页效果例子(注意桌面端无效):
1427     const urls = ["1.jpg", "2.jpg", "3.jpg"],
1428     cid = new CanvasImageDraw({width: 300, height: 300, alpha: false}), 
1429     cis = new CanvasImageScroll(cid, {scrollVisible: "auto", scrollSize: 4, isPage: true});
1430 
1431     ElementUtils.createCanvasFromURL(cid.box.w, cid.box.h, urls, true, imgs => {
1432 
1433         //创建 CanvasImage 并将其位置从左往右排序
1434         for(let i = 0; i < imgs.length; i++) cid.list[i] = new CanvasImage(imgs[i]).pos(i * imgs[i].width, 0);
1435 
1436         //cis 能够监听 CanvasImage
1437         cis.bindScrolls();
1438 
1439         //绘制一次画布并把画布加入到DOM树
1440         cid.render();
1441 
1442     });
1443 */
1444 class CanvasImageScroll{
1445 
1446     #cid = null;
1447     #maxSize = new Point();
1448     #securityDoor = new SecurityDoor();
1449     #cursorView = null;
1450     #scrollVisible = "";
1451     #eventType = "";
1452     get maxSize(){return this.#maxSize;}
1453     get cursorX(){return this.#cid.box.x/this.#maxSize.x*this.#cid.box.w;}
1454     get cursorY(){return this.#cid.box.y/this.#maxSize.y*this.#cid.box.h;}
1455     get cursorW(){return this.#maxSize.x <= this.#cid.box.w ? this.#cid.box.w : this.#cid.box.w / this.#maxSize.x * this.#cid.box.w;}
1456     get cursorH(){return this.#maxSize.y <= this.#cid.box.h ? this.#cid.box.h : this.#cid.box.h / this.#maxSize.y * this.#cid.box.h;}
1457     get cursorView(){return this.#cursorView;}
1458     set cursorView(v){this.#cursorView = this.scrollXView.path2D = this.scrollYView.path2D = v||null;}
1459     
1460     constructor(cid, option = {}){
1461         if(!cid.eventDispatcher) cid.initEventDispatcher();
1462         this.#cid = cid;
1463 
1464         switch(option.scrollVisible){
1465             case "auto": 
1466             case "": 
1467             this.#scrollVisible = option.scrollVisible;
1468             break;
1469 
1470             case "visible": 
1471             default: 
1472             this.#scrollVisible = "visible";
1473             break;
1474         }
1475 
1476         switch(option.scrollEventType){
1477             case "touch": 
1478             this.#eventType = "touch";
1479             this.__unbindEvent = this.createScrollEventMobile(option.domElement || cid.domElement, option.inertia, option.inertiaLife, option.isPage);
1480             break;
1481 
1482             case "default": 
1483             this.#eventType = "default";
1484             if(this.#scrollVisible !== "") this.__unbindEvent = this.createScrollEventPC(option.domElement || cid.domElement);
1485             break;
1486 
1487             default: 
1488             if(UTILS.isMobile){
1489                 this.#eventType = "touch";
1490                 this.__unbindEvent = this.createScrollEventMobile(option.domElement || cid.domElement, option.inertia, option.inertiaLife, option.isPage);
1491             } else {
1492                 this.#eventType = "default";
1493                 this.__unbindEvent = this.createScrollEventPC(option.domElement || cid.domElement);
1494             }
1495             break;
1496         }
1497         
1498         const cirED = cid.eventDispatcher;
1499         cirED.register("boxX", () => this.changeCursorX());
1500         cirED.register("boxY", () => this.changeCursorY());
1501         cirED.register("append", event => this.bindScrolls(event.target));
1502         
1503         cirED.register("add", event => {
1504             this.bindScroll(event.target);
1505             this.changeCIAdd(event.target);
1506         });
1507 
1508         cirED.register("remove", event => {
1509             this.unbindScroll(event.target);
1510             this.changeCIDel(event.target);
1511         });
1512 
1513         if(this.#scrollVisible !== ""){
1514             const scrollSize = option.scrollSize || 10;
1515             this.scrollXView = new CanvasImageCustom().pos(0, cid.box.h - scrollSize);
1516             this.scrollYView = new CanvasImageCustom().pos(cid.box.w - scrollSize, 0);
1517 
1518             if(this.#scrollVisible === "visible" || (this.#eventType === "default" && this.#scrollVisible === "auto")){
1519                 this.scrollXView.size(cid.box.w-scrollSize, scrollSize).rect(scrollSize/2, 1).fill("#eeeeee").stroke("#aaaaaa");
1520                 this.scrollYView.size(scrollSize, cid.box.h-scrollSize).rect(scrollSize/2, 1).fill("#eeeeee").stroke("#aaaaaa");
1521             } else {
1522                 this.scrollXView.size(cid.box.w, scrollSize).rect(scrollSize/2, 1).fill("#eeeeee").stroke("#aaaaaa");
1523                 this.scrollYView.size(scrollSize, cid.box.h).rect(scrollSize/2, 1).fill("#eeeeee").stroke("#aaaaaa");
1524             }
1525 
1526             cirED.register("afterDraw", () => {
1527                 if(this.#scrollVisible === "visible"){
1528                     this.drawScrollX();
1529                     this.drawScrollY();
1530                 } else if(this.#eventType === "default" && this.#scrollVisible === "auto"){
1531                     if(this.#maxSize.x > cid.box.w) this.drawScrollX();
1532                     if(this.#maxSize.y > cid.box.h) this.drawScrollY();
1533                 }
1534             });
1535             
1536             cirED.register("size", () => {
1537                 this.scrollXView.pos(0, cid.box.h - scrollSize);
1538                 this.scrollYView.pos(cid.box.w - scrollSize, 0);
1539                 this.changeCursorX();
1540                 this.changeCursorY();
1541             });
1542 
1543             this.scrollXView.index = this.scrollYView.index = Infinity;
1544             this.scrollXView.position = this.scrollYView.position = "fixed";
1545             //this.scrollXView.shadow = this.scrollYView.shadow = {blur: 4, color: "#666666", offsetX: 0, offsetY: 0};
1546             this.cursorView = new CanvasPath2D("fill", {fillStyle: "#666666"}).rect(new RoundedRectangle(0,0,0,0,scrollSize/2));
1547         }
1548 
1549         if(cid.list.length !== 0) this.bindScrolls();
1550     }
1551 
1552     enable(sign){
1553         this.#securityDoor.remove(sign);
1554     }
1555 
1556     disable(sign){
1557         this.#securityDoor.add(sign);
1558     }
1559 
1560     unbindEvent(){
1561         if(typeof this.__unbindEvent === "function"){
1562             this.__unbindEvent();
1563             delete this.__unbindEvent;
1564         }
1565     }
1566 
1567     resetMaxSizeX(){
1568         var v = this.#cid.box.w;
1569         for(let k = 0, len = this.#cid.list.length, ci, m; k < len; k++){
1570             ci = this.#cid.list[k];
1571             if(ci.visible === true){
1572                 m = ci.box.mx;
1573                 if(m > v) v = m;
1574             }
1575         }
1576         if(v !== this.#maxSize.x){
1577             this.#maxSize.x = v;
1578             this.changeCursorX();
1579         }
1580     }
1581 
1582     resetMaxSizeY(){
1583         var v = this.#cid.box.h;
1584         const list = this.#cid.list, len = list.length;
1585         for(let k = 0, len = this.#cid.list.length, ci, m; k < len; k++){
1586             ci = this.#cid.list[k];
1587             if(ci.visible === true){
1588                 m = ci.box.my;
1589                 if(m > v) v = m;
1590             }
1591         }
1592         if(v !== this.#maxSize.y){
1593             this.#maxSize.y = v;
1594             this.changeCursorY();
1595         }
1596     }
1597 
1598     bindScrolls(list = this.#cid.list){
1599         var x = this.#cid.box.w, y = this.#cid.box.h;
1600         for(let k = 0, len = list.length, ci, m; k < len; k++){
1601             ci = list[k];
1602             this.bindScroll(ci);
1603             if(ci.visible === true){
1604                 m = ci.box.mx;
1605                 if(m > x) x = m;
1606                 m = ci.box.my;
1607                 if(m > y) y = m;
1608             }
1609         }
1610         if(x > this.#maxSize.x){
1611             this.#maxSize.x = x;
1612             this.changeCursorX();
1613         }
1614         if(y > this.#maxSize.y){
1615             this.#maxSize.y = y;
1616             this.changeCursorY();
1617         }
1618     }
1619 
1620     //以下方法内部自动调用
1621     changeCursorX(){
1622         if(this.#cid.box.x < 0 || this.#cid.box.w >= this.#maxSize.x) this.#cid.box.x = 0;
1623         else if(this.#cid.box.mx > this.#maxSize.x) this.#cid.box.x = this.#maxSize.x - this.#cid.box.w;
1624     }
1625 
1626     changeCursorY(){
1627         if(this.#cid.box.y < 0 || this.#cid.box.h >= this.#maxSize.y) this.#cid.box.y = 0;
1628         else if(this.#cid.box.my > this.#maxSize.y) this.#cid.box.y = this.#maxSize.y - this.#cid.box.h;
1629     }
1630 
1631     changeCIAdd(ci){
1632         if(ci.visible === false) return;
1633         var v = ci.box.mx;
1634         if(v > this.#maxSize.x){
1635             this.#maxSize.x = v;
1636             this.changeCursorX();
1637         }
1638         v = ci.box.my;
1639         if(v > this.#maxSize.y){
1640             this.#maxSize.y = v;
1641             this.changeCursorY();
1642         }
1643     }
1644 
1645     changeCIDel(ci){
1646         if(ci.visible === false) return;
1647         if(ci.box.mx >= this.#maxSize.x){
1648             this.resetMaxSizeX();
1649             this.changeCursorX();
1650         }
1651         if(ci.box.my >= this.#maxSize.y){
1652             this.resetMaxSizeY();
1653             this.changeCursorY();
1654         }
1655     }
1656 
1657     unbindScroll(ci){
1658         var x = ci.box.x;
1659         delete ci.box.x;
1660         ci.box.x = x;
1661 
1662         x = ci.box.y;
1663         delete ci.box.y;
1664         ci.box.y = x;
1665 
1666         x = ci.box.w;
1667         delete ci.box.w;
1668         ci.box.w = x;
1669 
1670         x = ci.box.h;
1671         delete ci.box.h;
1672         ci.box.h = x;
1673 
1674         x = ci.visible;
1675         delete ci.visible;
1676         ci.visible = x;
1677     }
1678     
1679     bindScroll(ci){
1680         var _visible = typeof ci.visible === "boolean" ? ci.visible : true, 
1681         _x = ci.box.x, _y = ci.box.y, _w = ci.box.w, _h = ci.box.h, nm, om;
1682 
1683         const writeBoxX = () => {
1684             if(nm > this.#maxSize.x){
1685                 this.#maxSize.x = nm;
1686                 this.changeCursorX();
1687             } else if(nm < this.#maxSize.x){
1688                 if(om >= this.#maxSize.x) this.resetMaxSizeX();
1689             }
1690         },
1691 
1692         writeBoxY = () => {
1693             if(nm > this.#maxSize.y){
1694                 this.#maxSize.y = nm;
1695                 this.changeCursorY();
1696             } else if(nm < this.#maxSize.y){
1697                 if(om >= this.#maxSize.y) this.resetMaxSizeY();
1698             }
1699         };
1700 
1701         Object.defineProperties(ci.box, {
1702 
1703             x: {
1704                 get: () => {return _x;},
1705                 set: v => {
1706                     if(_visible){
1707                         om = _x+_w;
1708                         _x = v;
1709                         nm = v+_w;
1710                         writeBoxX();
1711                     }else{
1712                         _x = v;
1713                     }
1714                 }
1715             },
1716 
1717             y: {
1718                 get: () => {return _y;},
1719                 set: v => {
1720                     if(_visible){
1721                         om = _y+_h;
1722                         _y = v;
1723                         nm = v+_h;
1724                         writeBoxY();
1725                     }else{
1726                         _y = v;
1727                     }
1728                 }
1729             },
1730 
1731             w: {
1732                 get: () => {return _w;},
1733                 set: v => {
1734                     if(_visible){
1735                         om = _w+_x;
1736                         _w = v;
1737                         nm = v+_x;
1738                         writeBoxX();
1739                     }else{
1740                         _w = v;
1741                     }
1742                 }
1743             },
1744 
1745             h: {
1746                 get: () => {return _h;},
1747                 set: v => {
1748                     if(_visible){
1749                         om = _h+_y;
1750                         _h = v;
1751                         nm = v+_y;
1752                         writeBoxY();
1753                     }else{
1754                         _h = v;
1755                     }
1756                 }
1757             },
1758 
1759         });
1760 
1761         Object.defineProperties(ci, {
1762 
1763             visible: {
1764                 get: () => {return _visible;},
1765                 set: v => {
1766                     if(v === true){
1767                         _visible = true;
1768                         this.changeCIAdd(ci);
1769                     }
1770                     else if(v === false){
1771                         this.changeCIDel(ci);
1772                         _visible = false;
1773                     }
1774                 }
1775             },
1776 
1777         });
1778     }
1779 
1780     drawScrollX(){
1781         if(this.#cursorView === null) return;
1782         const num = this.#cid.box.w - this.scrollXView.w, cursorW = this.cursorW, cursorX = this.cursorX;
1783         this.#cursorView.value.size(cursorW < num ? num : cursorW - num, this.scrollXView.h)
1784         .pos(cursorX+this.#cursorView.value.w > this.scrollXView.w ? this.scrollXView.w-this.#cursorView.value.w : cursorX, 0);
1785         this.#cid._draw(this.scrollXView);
1786     }
1787 
1788     drawScrollY(){
1789         if(this.#cursorView === null) return;
1790         const num = this.#cid.box.h - this.scrollYView.h, cursorH = this.cursorH, cursorY = this.cursorY;
1791         this.#cursorView.value.size(this.scrollYView.w, cursorH < num ? num : cursorH - num);
1792         this.#cursorView.value.pos(0, cursorY+this.#cursorView.value.h > this.scrollYView.h ? this.scrollYView.h-this.#cursorView.value.h : cursorY);
1793         this.#cid._draw(this.scrollYView);
1794     }
1795 
1796     createScrollEventPC(domElement){
1797         var dPos = -1, rect;
1798 
1799         const box = this.#cid.box, 
1800         _redraw = () => {
1801             this.#cid.redraw();
1802             al_draw.stop();
1803         },
1804         redraw = () => this.#cid.redraw(),
1805         al_draw = new Timer(redraw, 1000/30, 1, false),
1806         
1807         setTop = top => {
1808             if(this.#securityDoor.empty === false) return;
1809             box.y = top / box.h * this.#maxSize.y;
1810             if(al_draw.running === false) redraw();
1811             al_draw.start();
1812         },
1813 
1814         setLeft = left => {
1815             if(this.#securityDoor.empty === false) return;
1816             box.x = left / box.w * this.#maxSize.x;
1817             if(al_draw.running === false) redraw();
1818             al_draw.start();
1819         },
1820         
1821         onMoveTop = event => {
1822             setTop(event.clientY - rect.y - dPos);
1823         },
1824 
1825         onMoveLeft = event => {
1826             setLeft(event.clientX - rect.x - dPos);
1827         },
1828 
1829         onUpTop = event => {
1830             domElement.releasePointerCapture(event.pointerId);
1831             domElement.removeEventListener('pointermove', onMoveTop);
1832             domElement.removeEventListener('pointerup', onUpTop);
1833             _redraw();
1834         },
1835 
1836         onUpLeft = event => {
1837             domElement.releasePointerCapture(event.pointerId);
1838             domElement.removeEventListener('pointermove', onMoveLeft);
1839             domElement.removeEventListener('pointerup', onUpLeft);
1840             _redraw();
1841         },
1842 
1843         ondown = event => {
1844             if(!this.scrollXView) return;
1845             onUpTop(event);
1846             onUpLeft(event);
1847             rect = domElement.getBoundingClientRect();
1848             domElement.setPointerCapture(event.pointerId);
1849             if(this.scrollXView.box.containsPoint(event.offsetX, event.offsetY)){
1850                 domElement.addEventListener("pointermove", onMoveLeft);
1851                 domElement.addEventListener("pointerup", onUpLeft);
1852                 dPos = event.offsetX - this.cursorX;
1853             } else if(this.scrollYView.box.containsPoint(event.offsetX, event.offsetY)){
1854                 domElement.addEventListener("pointermove", onMoveTop);
1855                 domElement.addEventListener("pointerup", onUpTop);
1856                 dPos = event.offsetY - this.cursorY;
1857             }
1858         },
1859 
1860         onwheel = event => {
1861             if(this.#maxSize.y > box.h){
1862                 dPos = 50 / this.#maxSize.y * box.h;
1863                 setTop(this.cursorY + (event.wheelDelta === 120 ? -dPos : dPos));
1864             } else if(this.#maxSize.x > box.w){
1865                 dPos = 50 / this.#maxSize.x * box.w;
1866                 setLeft(this.cursorX + (event.wheelDelta === 120 ? -dPos : dPos));
1867             }
1868         }
1869 
1870         domElement.addEventListener("pointerdown", ondown);
1871         domElement.addEventListener("mousewheel", onwheel);
1872 
1873         return function (){
1874             al_draw.stop();
1875             domElement.removeEventListener("pointerdown", ondown);
1876             domElement.removeEventListener("mousewheel", onwheel);
1877             domElement.removeEventListener('pointermove', onMoveTop);
1878             domElement.removeEventListener('pointerup', onUpTop);
1879             domElement.removeEventListener('pointermove', onMoveLeft);
1880             domElement.removeEventListener('pointerup', onUpLeft);
1881         }
1882     }
1883 
1884     createScrollEventMobile(domElement, inertia = true, inertiaLife = 0.06, isPage = false){
1885         var sTime = 0, dis = "", sx = 0, sy = 0, isRun = this.#securityDoor.empty; 
1886         const box = this.#cid.box,
1887         _redraw = () => {
1888             this.#cid.redraw();
1889             al_draw.stop();
1890         },
1891         redraw = () => {
1892             this.#cid.redraw();
1893             if(this.#scrollVisible === "auto"){
1894                 switch(dis){
1895                     case "x": 
1896                     if(this.#maxSize.x > box.w) this.drawScrollX();
1897                     break;
1898                     case "y": 
1899                     if(this.#maxSize.y > box.h) this.drawScrollY();
1900                     break;
1901                 }
1902             }
1903         },
1904         al_draw = new Timer(redraw, 1000/30, 1, false);
1905 
1906         if(inertia === true){
1907             let tweenCache;
1908             if(isPage === true) tweenCache = new TweenCache({x:0}, {x:0}, 300);
1909             const _inertiaLife = inertiaLife;
1910             inertiaLife = 1 - inertiaLife;
1911             var inertiaAnimate = new AnimateLoop(null, null, 1000/30),
1912             step = 1, aniamteRun = false, stepA = "", stepB = "", _sx = 0, _sy = 0,
1913             
1914             pageY = (sy, st) => {
1915                 sy = Math.floor(sy / box.h)*box.h;
1916                 const _v = box.y - sy;
1917                 if(_v > 0 && sy + box.h <= this.#maxSize.y){ //
1918                     if(_v > box.h * 0.4 || Date.now() - st < 300){ //翻页
1919                         tweenCache.end.x = sy + box.h;
1920                     } else { //不翻页
1921                         tweenCache.end.x = sy;
1922                     }
1923                 } else if(_v < 0 && sy - box.h >= 0) { //
1924                     if(Math.abs(_v) > box.h * 0.4 || Date.now() - st < 300){ //翻页
1925                         tweenCache.end.x = sy - box.h;
1926                     } else { //不翻页
1927                         tweenCache.end.x = sy;
1928                     }
1929                 }
1930                 
1931                 tweenCache.origin.x = box.y;
1932                 tweenCache.start();
1933                 inertiaAnimate.play(() => {
1934                     tweenCache.update();
1935                     box.y = tweenCache.origin.x;
1936                     this.#cid.redraw();
1937                     if(tweenCache.origin.x !== tweenCache.end.x && this.#scrollVisible === "auto") this.drawScrollY();
1938                 });
1939             },
1940 
1941             inertiaY = speed => {
1942                 if(Math.abs(speed) < 0.7) return _redraw();
1943                 stepA = speed < 0 ? "-top" : "top";
1944                 if(aniamteRun && stepA === stepB) step += 0.3;
1945                 else{
1946                     step = 1;
1947                     stepB = stepA;
1948                 }
1949                 inertiaAnimate.play(() => {
1950                     speed *= inertiaLife;
1951                     box.y += step * 20 * speed;
1952                     this.#cid.redraw();
1953                     if(Math.abs(speed) < _inertiaLife || box.y <= 0 || box.my >= this.#maxSize.y) inertiaAnimate.stop();
1954                     else {
1955                         if(this.#scrollVisible === "auto"){
1956                             if(this.#maxSize.y > box.h) this.drawScrollY();
1957                         }
1958                     }
1959                 });
1960             },
1961 
1962             pageX = (sx, st) => {
1963                 sx = Math.floor(sx / box.w)*box.w;
1964                 const _v = box.x - sx;
1965                 if(_v > 0 && sx + box.w <= this.#maxSize.x){ //
1966                     if(_v > box.w * 0.4 || Date.now() - st < 300){ //翻页
1967                         tweenCache.end.x = sx + box.w;
1968                     } else { //不翻页
1969                         tweenCache.end.x = sx;
1970                     }
1971                 } else if(_v < 0 && sx - box.w >= 0) { //
1972                     if(Math.abs(_v) > box.w * 0.4 || Date.now() - st < 300){ //翻页
1973                         tweenCache.end.x = sx - box.w;
1974                     } else { //不翻页
1975                         tweenCache.end.x = sx;
1976                     }
1977                 }
1978                 
1979                 tweenCache.origin.x = box.x;
1980                 tweenCache.start();
1981                 inertiaAnimate.play(() => {
1982                     tweenCache.update();
1983                     box.x = tweenCache.origin.x;
1984                     this.#cid.redraw();
1985                     if(tweenCache.origin.x !== tweenCache.end.x && this.#scrollVisible === "auto") this.drawScrollX();
1986                 });
1987             },
1988 
1989             inertiaX = speed => {
1990                 if(Math.abs(speed) < 0.7)  return _redraw();
1991                 stepA = speed < 0 ? "-left" : "left";
1992                 if(aniamteRun && stepA === stepB) step += 0.3;
1993                 else{
1994                     step = 1;
1995                     stepB = stepA;
1996                 }
1997                 inertiaAnimate.play(() => {
1998                     speed *= inertiaLife;
1999                     box.x += step * 20 * speed;
2000                     this.#cid.redraw();
2001                     if(Math.abs(speed) < _inertiaLife || box.x <= 0 || box.mx >= this.#maxSize.x) inertiaAnimate.stop();
2002                     else {
2003                         if(this.#scrollVisible === "auto"){
2004                             if(this.#maxSize.x > box.w) this.drawScrollX();
2005                         }
2006                     }
2007                 });
2008             }
2009         }
2010         
2011         const update = event => {
2012             if(dis === "x") box.x = sx - event.targetTouches[0].pageX;
2013             else if(dis === "y") box.y = sy - event.targetTouches[0].pageY;
2014             if(al_draw.running === false) redraw();
2015             al_draw.start();
2016         },
2017         
2018         onup = event => {
2019             event.preventDefault();
2020             if(isRun === false) return;
2021             if(inertia === true){
2022                 if(dis === "x"){
2023                     if(isPage === false) inertiaX((box.x - _sx) / (Date.now() - sTime));
2024                     else pageX(_sx, sTime);
2025                 }
2026                 else if(dis === "y"){
2027                     if(isPage === false) inertiaY((box.y - _sy) / (Date.now() - sTime));
2028                     else pageY(_sy, sTime);
2029                 }
2030                 else _redraw();
2031             }
2032             else _redraw();
2033         },
2034 
2035         onmove = event => {
2036             isRun = this.#securityDoor.empty;
2037             if(isRun === false) return;
2038             if(dis !== "") return update(event);
2039             if(Date.now() - sTime < 60) return;
2040             
2041             if(Math.abs(event.targetTouches[0].pageX - sx) > Math.abs(event.targetTouches[0].pageY - sy) && this.#maxSize.x > box.w){
2042                 sx = event.targetTouches[0].pageX + box.x;
2043                 dis = "x";
2044             } else if(this.#maxSize.y > box.h){
2045                 sy = event.targetTouches[0].pageY + box.y;
2046                 dis = "y";
2047             }
2048             
2049             if(inertia === true){
2050                 sTime = Date.now();
2051                 _sx = box.x;
2052                 _sy = box.y;
2053             }
2054         },
2055         
2056         ondown = event => {
2057             event.preventDefault();
2058             if(this.#maxSize.x <= box.w && this.#maxSize.y <= box.h) return;
2059             sx = event.targetTouches[0].pageX;
2060             sy = event.targetTouches[0].pageY;
2061             sTime = Date.now();
2062             dis = "";
2063             if(inertia === true){
2064                 aniamteRun = inertiaAnimate.running;
2065                 inertiaAnimate.stop();
2066             }
2067         }
2068 
2069         domElement.addEventListener("touchend", onup);
2070         domElement.addEventListener("touchmove", onmove);
2071         domElement.addEventListener("touchstart", ondown);
2072 
2073         return function (){
2074             al_draw.stop();
2075             if(inertia === true) inertiaAnimate.stop();
2076             domElement.removeEventListener("touchend", onup);
2077             domElement.removeEventListener("touchmove", onmove);
2078             domElement.removeEventListener("touchstart", ondown);
2079         }
2080     }
2081 
2082 }
2083 
2084 
2085 /* CanvasEventTarget 
2086 parameter: 
2087     box: Box;            //默认创建一个新的Box
2088 
2089 attribute: 
2090     box: Box;             //.x.y 相对于画布的位置, .w.h 宽高;
2091     visible: Boolean;    //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件, scroll也忽略此ci)
2092     index: Integer;     //层级(不必唯一) -Infinity 最底层; Infinity 最上层
2093     position: String;    //定位 可能值: 默认"", "fixed" (如果为 fixed 其绘制的位置将无视滚动条, 如果存在的话)
2094 
2095 method: 
2096     addEventListener(eventName, callback): undefined;        //添加事件
2097     removeEventListener(eventName, callback): undefined;    //删除事件
2098     clearEventListener(eventName): undefined;                //删除 eventName 栏的所有回调, 如果eventName未定义则清空所有回调
2099     hasEventListener(eventName): bool;                        //查询是否注册了 eventName 事件
2100     trigger(eventName, info, event): undefined;             //触发事件 (注意: .hasEventListener(eventName) 必须为true才能调用此方法)
2101 
2102 eventNames: 
2103     click(info, event)    //点击(按下的时间和移动次数来模拟点击事件, 先触发up, 然后在是click)
2104     down(info, event)    //按下
2105     move(info, event)    //移动(按下触发才有效)
2106     up(info, event)        //抬起(按下触发才有效)
2107         info: Object{  //此参数的属性兼容于两端
2108             targets: Array[CanvasEventTarget],     //层级相同的CanvasEventTarget
2109             target: CanvasEventTarget,             //最顶层的CanvasEventTarget(相较于视图)
2110             offsetX: number,                     //画布左上角的距离
2111             offsetY: number,  
2112             delta: number,                      //延迟毫秒
2113             moveStep: number,                     //移动的次数
2114         }
2115         event: PointerEvent||TouchEvent;
2116 
2117     beforeDraw(context, point)     //绘制之前
2118     afterDraw(context, point)     //绘制之后
2119         context: CanvasRender2D; //当前绘制画布的上下文
2120         point: Point;             //CanvasImageDraw.box.xy(滚动条游标的坐标) (如果 .position 为 "fixed", 此对象的值总是 0)
2121 
2122 demo: 
2123     //创建一个点击区域(cid: CanvasImageDraw):
2124     const eventTarget = new CanvasEventTarget(new Box(10, 10, 100, 100));
2125     eventTarget.addEventListener("click", event => console.log(event));
2126     cid.add(eventTarget); //cid 必须绑定到 CanvasEvent 的实例才有效
2127 */
2128 class CanvasEventTarget{
2129 
2130     #eventObject = {
2131         click: null,
2132         down: null, //Array[function]
2133         move: null,
2134         up: null,
2135         beforeDraw: null,
2136         afterDraw: null,
2137     }
2138 
2139     constructor(box = new Box()){
2140         this.box = box;
2141         this.visible = true;
2142         this.index = 0;
2143         this.position = "";
2144     }
2145 
2146     trigger(eventName, info = null, event = null){
2147         const arr = this.#eventObject[eventName];
2148         for(let i = 0, len = arr.length; i < len; i++) arr[i](info, event);
2149     }
2150 
2151     addEventListener(eventName, callback){
2152         if(this.#eventObject[eventName] === undefined || typeof callback !== "function") return;
2153         if(this.#eventObject[eventName] === null) this.#eventObject[eventName] = [callback];
2154         else{
2155             const i = this.#eventObject[eventName].indexOf(callback);
2156             if(i === -1) this.#eventObject[eventName].push(callback);
2157             else this.#eventObject[eventName][i] = callback;
2158         }
2159     }
2160 
2161     removeEventListener(eventName, callback){
2162         if(Array.isArray(this.#eventObject[eventName]) === false) return;
2163         const i = this.#eventObject[eventName].indexOf(callback);
2164         if(i !== -1) this.#eventObject[eventName].splice(i, 1);
2165         if(this.#eventObject[eventName].length === 0) this.#eventObject[eventName] = null;
2166     }
2167 
2168     clearEventListener(eventName){
2169         if(this.#eventObject[eventName] !== undefined) this.#eventObject[eventName] = null;
2170         else{
2171             for(let n in this.#eventObject) this.#eventObject[n] = null;
2172         }
2173     }
2174 
2175     hasEventListener(eventName){
2176         return this.#eventObject[eventName] !== null;
2177     }
2178     
2179 }
2180 
2181 
2182 /* CanvasImage 
2183 parameter: 
2184     image (构造器会调用一次 .setImage(image) 来处理 image 参数)
2185 
2186 attribute:
2187     image: CanvasImage;        //目标图像, 默认为null;
2188     opacity: number;        //透明度; 值0至1之间; 默认1; (如果短暂的隐藏显示效果建议把此值设为0,而不是.visible,如果有滚动条时.visible会成为一个状态属性)
2189     path2D: CanvasPath2D;    //此属性一般用于动态绘制某个形状
2190     shadow: Object;            //阴影, 默认 null; shadow{blur, color, offsetX, offsetY}
2191     loadingImage: bool;     //只读, ci是否正在加载图片或视频
2192     x,y,w,h: number;        //只读, 返回 this.box.xywh 属性值
2193     width, height: number;    //只读, 返回 image 的宽高; 如果未定义就返回 0
2194     
2195 method:
2196     pos(x, y): this;             //设置位置; x 可以是: Number, Object{x,y}
2197     setImage(image): this;         //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image);
2198 
2199     loadImage(src, onload): this;    //加载并设置图像 (onload 如果是 CanvasImageDraw 则加载完后自动调用一次 redraw 或 render 方法);
2200     loadVideo(src, onload, type = "mp4") //与 .loadImage() 类似
2201 
2202     setScaleX(s, x): undefined;    //缩放x(注意: 是设置ci的box属性实现的缩放)
2203     setScaleY(s, y): undefined;    //缩放y
2204         s为必须, s小于1为缩小, 大于1放大;
2205         x默认为0, x是box的局部位置, 如果是居中缩放x应为: this.w/2, 
2206 
2207     createRotate(angle, cx , cy: Number): Object; //旋转(注意: 用 beforeDraw, afterDraw 事件设置ci新的绘制位置实现的旋转)
2208         angle: 旋转弧度, 默认0
2209         cx, cy: 旋转中心点(局部), 默认 this.w/2, this.h/2
2210         Object: {
2211             set(angle, cx , cy: Number): undefined; //
2212             offset(cx , cy: Number): undefined;     //更新旋转中心点(局部)
2213             angle(angle: Number): undefined;         //更新旋转弧度
2214 
2215             bind(): undefined;         //初始化时自动调用一次此方法
2216             unbind(): undefined;     //解除绑定(如果ci不在使用则不需要调用此方法)
2217         }
2218 
2219     setPath2DToCircleDiffusion(x,y,option): function; //圆形扩散特效(.path2D 实现的)
2220         x, y: Number;             //扩散原点 (相对于画布原点,event.offsetX,event.offsetY)
2221         option: Object{
2222             cir: CanvasImageDraw;     //必须;
2223             animateLoop: AnimateLoop;    //默认一个新的 AnimateLoop
2224             t: Number;                     //持续毫秒时间, 默认 200
2225             mr: Number;                 //扩散的最大半径, 默认 覆盖整个box: this.box.distanceFromPoint(x, y, true)
2226             path2D: CanvasPath2D;         //圆的样式, 默认: new CanvasPath2D("fill", {fillStyle: "rgba(255,255,255,0.2)"})
2227         }
2228 
2229 demo:
2230     //CanvasImageDraw
2231     const cid = new CanvasImageDraw({width: WORLD.width, height: WORLD.height});
2232 
2233     //图片
2234     const ciA = cid.add(new CanvasImage()).pos(59, 59).load("view/img/test.png", cid);
2235     
2236     //视频
2237     cid.add(new CanvasImage())
2238     .loadVideo("view/examples/video/test.mp4", ci => {
2239         
2240         //同比例缩放视频
2241         const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100});
2242         ci.box.size(newSize.width, newSize.height).center(cid.box);
2243 
2244         //播放按钮
2245         const cic = cid.add(new CanvasImageCustom())
2246         .size(50, 30).text("PLAY", "#fff")
2247         .rect(4).stroke("blue");
2248         cic.box.center(cid.box);
2249 
2250         //动画循环
2251         const animateLoop = new AnimateLoop(() => cid.redraw());
2252 
2253         //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画)
2254         cid.addEvent(cic, "up", () => {
2255             cic.visible = false;
2256             ci.image.play(); // ci.image 其实是一个 video 元素
2257             animateLoop.play(); //播放动画
2258         });
2259         
2260         //把 canvas 添加至 dom 树
2261         cid.render();
2262 
2263     });
2264 
2265     //鼠标位置缩放图片例子: (target: CanvasImage)
2266     cie.add(target, "wheel", event => {
2267 
2268         const scale = target.scaleX + event.wheelDelta * 0.001,
2269 
2270         //offsetX,offsetY 是鼠标到 element 的距离, 现在把它们转为 target 的局部距离
2271         localPositionX = event.offsetX - target.x,
2272         localPositionY = event.offsetY - target.y;
2273 
2274         //x,y缩放至 scale
2275         target.setScaleX(scale, localPositionX);
2276         target.setScaleY(scale, localPositionY);
2277 
2278         //重绘画布
2279         cid.redraw();
2280 
2281     });
2282 */
2283 class CanvasImage extends CanvasEventTarget{
2284 
2285     #loadingImage = false;
2286     get loadingImage(){return this.#loadingImage;}
2287     get x(){return this.box.x;}
2288     get y(){return this.box.y;}
2289     get w(){return this.box.w;}
2290     get h(){return this.box.h;}
2291     get width(){return this.image === null ? 0 : this.image.width;}
2292     get height(){return this.image === null ? 0 : this.image.height;}
2293     get isDrawPath2D(){return this.path2D !== null && this.path2D.isDraw;}
2294     get className(){return this.constructor.name;}
2295     
2296     constructor(image){
2297         super();
2298         this.image = null;
2299         this.opacity = 1;
2300         this.path2D = null;
2301         this.shadow = null;//Object{blur, color, offsetX, offsetY}
2302         
2303         this.setImage(image);
2304     }
2305 
2306     pos(x, y){
2307         switch(typeof x) {
2308             case "number": 
2309             this.box.x = x;
2310             this.box.y = y;
2311             break;
2312 
2313             case "object": 
2314             this.box.x = UTILS.isNumber(x.x) ? x.x : this.box.x;
2315             this.box.y = UTILS.isNumber(x.y) ? x.y : this.box.y;
2316             break;
2317         }
2318 
2319         return this;
2320     }
2321 
2322     setImage(image){
2323         //如果是image
2324         if(CanvasImageDraw.isCanvasImage(image)){
2325             this.box.size(image.width, image.height);
2326             this.image = image;
2327         }
2328         //如果是 CanvasImage
2329         else if(CanvasImage.prototype.isPrototypeOf(image)){
2330             if(image.loadingImage){
2331                 if(Array.isArray(image.__setImageList)) image.__setImageList.push(this);
2332                 else image.__setImageList = [this];
2333             }
2334             else this.setImage(image.image);
2335         }
2336         //忽略此次操作
2337         else{
2338             this.box.size(0, 0);
2339             this.image = null;
2340         }
2341         return this;
2342     }
2343 
2344     setScaleX(s, x = 0){
2345         const oldVal = this.box.w;
2346         this.box.w = this.width * s;
2347         this.box.x += x - this.box.w / oldVal * x;
2348     }
2349 
2350     setScaleY(s, y = 0){
2351         const oldVal = this.box.h;
2352         this.box.h = this.height * s;
2353         this.box.y += y - this.box.h / oldVal * y;
2354     }
2355 
2356     loadImage(src, onload){
2357         /*
2358             // 加载成功
2359             image.onload = () => {...}
2360 
2361             // 加载错误
2362             image.onerror = () => {...}
2363 
2364             // 取消加载
2365             image.onabort = () => {...}
2366         */
2367         this.#loadingImage = true;
2368         const image = new Image();
2369         image.onload = image.onerror = image.onabort = () => this._loadSuccess(image, onload);
2370         image.src = src;
2371         return this;
2372     }
2373 
2374     loadVideo(src, onload, type = "mp4"){
2375         /*    video 加载事件 的顺序
2376             onloadstart
2377             ondurationchange
2378             onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。
2379             onloadeddata
2380             onprogress
2381             oncanplay
2382             oncanplaythrough
2383 
2384             //控制事件:
2385             onended //播放结束
2386             onpause //暂停播放
2387             onplay //开始播放
2388         */
2389         this.#loadingImage = true;
2390         const video = document.createElement("video"),
2391         source = document.createElement("source");
2392         video.appendChild(source);
2393         source.type = `video/${type}`;
2394 
2395         video.oncanplay = () => {
2396             //video 的 width, height 属性如果不设的话永远都是0
2397             video.width = video.videoWidth;
2398             video.height = video.videoHeight;
2399             this._loadSuccess(video, onload);
2400         };
2401 
2402         source.src = src;
2403         return this;
2404     }
2405     
2406     createRotate(angle = 0, cx = this.w/2, cy = this.h/2){
2407         var nx, ny;
2408 
2409         const beforeDraw = (con, point) => {
2410             nx = cx + this.x - point.x;
2411             ny = cy + this.y - point.y;
2412 
2413             con.translate(nx, ny);
2414             con.rotate(angle);
2415 
2416             //ci 减去translate的nx,ny 和 scroll的point
2417             point.set(nx + point.x, ny + point.y);
2418 
2419             //在此之后 cid 会这样操作:
2420             //x = ci.x - point.x
2421             //y = ci.y - point.y
2422             //con.drawImage(ci.image, x, y);
2423             //触发 afterDraw 事件
2424         },
2425 
2426         afterDraw = (con) => {
2427             con.rotate(-angle);
2428             con.translate(-nx, -ny);
2429         }
2430 
2431         this.addEventListener("beforeDraw", beforeDraw);
2432         this.addEventListener("afterDraw", afterDraw);
2433 
2434         return {
2435             set(x, y, a){
2436                 cx = x;
2437                 cy = y;
2438                 angle = a;
2439             },
2440 
2441             offset(x, y){
2442                 cx = x;
2443                 cy = y;
2444             },
2445 
2446             angle(v){
2447                 angle = v;
2448             },
2449 
2450             bind: () => {
2451                 this.addEventListener("beforeDraw", beforeDraw);
2452                 this.addEventListener("afterDraw", afterDraw);
2453             },
2454 
2455             unbind: () => {
2456                 this.removeEventListener("beforeDraw", beforeDraw);
2457                 this.removeEventListener("afterDraw", afterDraw);
2458             }
2459         }
2460     }
2461 
2462     setPath2DToCircleDiffusion(x, y, option = {}){
2463         const oldPath2D = this.path2D, circle = new Circle(x-this.x, y-this.y, 0);
2464         this.path2D = option.path2D || new CanvasPath2D("fill", {fillStyle: "rgba(255,255,255,0.2)"});
2465         this.path2D.circle(circle);
2466         
2467         const t = option.t || 200, 
2468         mr = option.mr || this.box.distanceFromPoint(x, y, true),
2469         cid = option.cir, 
2470         animateLoop = option.animateLoop || new AnimateLoop(null, null, 1000 / 30);
2471         
2472         var st = Date.now();
2473 
2474         animateLoop.play(() => {
2475             if(circle.r <= mr) circle.r = (Date.now() - st) / t * mr; 
2476             else {
2477                 animateLoop.stop();
2478                 this.path2D = oldPath2D;
2479             }
2480             
2481             cid.redraw();
2482         });
2483 
2484         return () => {
2485             animateLoop.stop();
2486             this.path2D = oldPath2D;
2487         }
2488     }
2489 
2490     _loadSuccess(image, onload){
2491         this.setImage(image);
2492         
2493         this.#loadingImage = false;
2494         if(Array.isArray(this.__setImageList)){
2495             this.__setImageList.forEach(ci => ci.setImage(image));
2496             delete this.__setImageList;
2497         }
2498 
2499         if(typeof onload === "function") onload(this);
2500         else if(CanvasImageDraw.prototype.isPrototypeOf(onload)){
2501             if(onload.domElement.parentElement !== null) onload.redraw();
2502             else onload.render();
2503         }
2504     }
2505 
2506 }
2507 
2508 
2509 /* CanvasImages
2510 parameter: 
2511     images: Array[image]
2512 
2513 attribute:
2514     images: Array[image]    
2515     cursor: Number;
2516     
2517 method:
2518     next(): undefined;
2519     loadImages(urls, onDone, onUpdate): CanvasImages;
2520         urls: Array[String||Object{url||src:String}];
2521         onDone, onUpdate: Function;
2522 
2523 demo:
2524     //加载图片, 显示加载进度例子:
2525     const cid = new CanvasImageDraw({width: innerWidth, height: innerHeight, alpha: true});
2526     
2527     const canvasPath2D = new CanvasPath2D("stroke", {strokeStyle: "#00ff00"}),
2528     meter = new Meter();
2529     canvasPath2D.progress(meter);
2530 
2531     const ci = new CanvasImages([ElementUtils.createCanvas(100, 100)]);
2532     ci.path2D = canvasPath2D;
2533     ci.loadImages(
2534         [
2535             "./img/0.jpg",
2536             "./img/1.jpg",
2537             "./img/2.jpg",
2538             "./img/3.jpeg",
2539             "./img/4.jpeg",
2540         ], 
2541         cid, 
2542         (i, c) => {
2543             console.log(i, c);
2544             meter.setFromRatio(i / c);
2545             ci.next();
2546             cid.redraw();
2547         }
2548     );
2549     
2550     cid.list[0] = ci.pos(10, 10);
2551     cid.render();
2552 */
2553 class CanvasImages extends CanvasImage{
2554 
2555     #i = -1;
2556     get cursor(){return this.#i;}
2557     set cursor(i){this.set(i);}
2558 
2559     constructor(images = []){
2560         super(images[0]);
2561         this.images = images;
2562         if(this.image) this.#i = 0;
2563     }
2564 
2565     set(i){
2566         super.setImage(this.images[i]);
2567         this.#i = this.image ? i : -1;
2568     }
2569 
2570     next(){
2571         const len = this.images.length - 1;
2572         if(len !== -1){
2573             this.#i = this.#i < len ? this.#i+1 : 0;
2574             this.image = this.images[this.#i]; //super.setImage(this.images[this.#i]);
2575         }
2576     }
2577 
2578     setImage(image){
2579         super.setImage(image);
2580     
2581         if(this.image && Array.isArray(this.images)){
2582             const i = this.images.indexOf(this.image);
2583             if(i === -1){
2584                 this.#i = this.images.length;
2585                 this.images.push(this.image);
2586             }
2587             else this.#i = i;
2588         }
2589 
2590         return this;
2591     }
2592 
2593     loadImages(srcs, onDone, onUpdate){
2594         onUpdate = typeof onUpdate === "function" ? onUpdate : null;
2595 
2596         var i = 0, img = null, ty = "";
2597         const len = srcs.length, 
2598 
2599         func = () => {
2600             i++; if(onUpdate !== null) onUpdate(i, len);
2601             if(i === len){
2602                 if(typeof onDone === "function") onDone(this.images, srcs);
2603                 else if(CanvasImageDraw.prototype.isPrototypeOf(onDone)){
2604                     if(onDone.domElement.parentElement !== null) onDone.redraw();
2605                     else onDone.render();
2606                 }
2607             }
2608         }
2609 
2610         for(let k = 0; k < len; k++){
2611             ty = typeof srcs[k];
2612             if(ty === "string" || ty === "object"){
2613                 ty = ty === "string" ? srcs[k] : srcs[k].src || srcs[k].url;
2614                 if(ty !== "" && typeof ty === "string"){
2615                     img = new Image();
2616                     img.onload = img.onerror = img.onabort = func;
2617                     this.images.push(img);
2618                     img.src = ty;
2619                 }
2620                 else func();
2621             }
2622         }
2623 
2624         return this;
2625     }
2626 
2627 }
2628 
2629 
2630 /* CanvasImageCustom
2631 注意: 线模糊问题任然存在(只有.rect()做了模糊优化)
2632 parameter:
2633     canvas || image || undefined
2634     value: Path2D || undefined //如果未定义,初始化时创建一个Path2D,如果是其它值则不创建(例如 null,new Path2D())
2635 
2636 attribute:
2637     value: Path2D; //更换新的 value, 类似 context.beginPath();
2638 
2639 method:
2640     //以下是操作 画布 的方法
2641     cloneCanvas(canvas, dx = 0, dy = 0)
2642     clear(): this;
2643     size(w, h: Number): this;
2644     stroke(color: strokeColor, lineWidth: Number): this;
2645     fill(color: fillColor): this;
2646     
2647     //以下是操作 Path2D 的方法
2648     line(x, y, x1, y1: Number): this;
2649     path(arr: Array[x,y], close: Bool): this;
2650     rect(round, lineWidth: Number): this;
2651     strokeRect(color: strokeColor, round, lineWidth: Number): this;
2652     fillRect(color: fillColor, round, lineWidth: Number): this;
2653 */
2654 class CanvasImageCustom extends CanvasImage{
2655 
2656     constructor(canvas, value = new Path2D()){
2657         super(canvas);
2658         if(!this.image) this.setImage(ElementUtils.createCanvas());
2659         this.context = this.image.getContext("2d");
2660         this.value = value;
2661     }
2662 
2663     cloneCanvas(canvas, dx = 0, dy = 0){
2664         if(CanvasImageDraw.isCanvas(canvas) === false){
2665             canvas = document.createElement("canvas");
2666             canvas.width = this.width;
2667             canvas.height = this.height;
2668         }
2669     
2670         canvas.getContext("2d").drawImage(this.image, dx, dy);
2671     
2672         return canvas;
2673     }
2674 
2675     clear(){
2676         this.context.clearRect(0, 0, this.width, this.height); //this.context.clearRect(0, 0, this.box.w, this.box.h);
2677         return this;
2678     }
2679 
2680     size(w, h, np = false){
2681         switch(typeof w) {
2682             case "number": 
2683             this.box.size(w, h);
2684             break;
2685             case "object": 
2686             this.box.size(w.width||w.w||this.box.w, w.height||w.h||this.box.h);
2687             np = h;
2688             break;
2689         }
2690         
2691         if(np === true) this.value = new Path2D();
2692 
2693         this.image.width = this.box.w;
2694         this.image.height = this.box.h;
2695 
2696         return this;
2697     }
2698 
2699     stroke(color){
2700         if(color && this.context.strokeStyle !== color) this.context.strokeStyle = color;
2701         this.context.stroke(this.value);
2702         return this;
2703     }
2704 
2705     fill(color){
2706         if(color && this.context.fillStyle !== color) this.context.fillStyle = color;
2707         this.context.fill(this.value);
2708         return this;
2709     }
2710 
2711     line(x, y, x1, y1, lineWidth){
2712         if(lineWidth !== undefined && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
2713         this.value.moveTo(x, y);
2714         this.value.lineTo(x1, y1);
2715         return this;
2716     }
2717 
2718     path(arr, close = false, lineWidth){
2719         if(lineWidth !== undefined && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
2720         this.value.moveTo(arr[0], arr[1]);
2721         for(let k = 2; k < arr.length; k += 2) this.value.lineTo(arr[k], arr[k+1]);
2722         if(close === true) this.value.closePath();
2723         return this;
2724     }
2725 
2726     rect(round, lineWidth){
2727         if(lineWidth !== undefined && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
2728 
2729         const lw = this.context.lineWidth, 
2730         w = Math.floor(this.box.w-lw), 
2731         h = Math.floor(this.box.h-lw),
2732         x = lw % 2 === 0 ? Math.floor(lw/2) : Math.floor(lw/2)+0.5;
2733 
2734         if(typeof round !== "number" || round < 1){
2735             this.value.rect(x, x, w, h);
2736             return this;
2737         }
2738 
2739         //.roundRect() 兼容处理
2740         if(typeof this.value.roundRect === "function"){
2741             this.value.roundRect(x, x, w, h, round);
2742         } else {
2743             _roundRect(this.value, x, x, w, h, round);
2744         }
2745         
2746         return this;
2747     }
2748 
2749     strokeRect(color, round, lineWidth){
2750         this.rect(round, lineWidth);
2751         this.stroke(color);
2752         return this;
2753     }
2754 
2755     fillRect(color, round, lineWidth){
2756         this.rect(round, lineWidth);
2757         this.fill(color);
2758         return this;
2759     }
2760 
2761 }
2762 
2763 
2764 /* CanvasImageText
2765 method:
2766     setFont(font: string||number): this; 
2767     getTextWidth(text): Number;
2768 
2769     fillText(text, color, x, y): this; //填充文字, x,y 默认为居中
2770 
2771     fillTextWrap(value, option): this; //填充文字,可以自动换行
2772         value: string || Array[...string]
2773         option: Object{
2774             color,         //context.fillStyle, 默认 this.fillStyle
2775             padding,    //内边距, 可能的值: Number || Object{top,right,bottom,left:Number}, 默认 0
2776             distance,    //文字之间的间距, 可能的值: Number || Object{x,y:Number}, 默认 0
2777             width,         //如果定义则内部调用.size(w, h)方法
2778             height,        //必须定义width才有效, 默认 text排序后占的高
2779             ePos: Object{x, y}, //如果定义就在其上设置结束时的位置
2780         }
2781 
2782 
2783     const ciB = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px serif").fillText("TEST test 撒旦给个", "#000000");
2784     const ciC = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px sans-serif").fillText("TEST test 撒旦给个", "#000000");
2785     const ciD = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px cursive").fillText("TEST test 撒旦给个", "#000000");
2786     const ciE = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px fantasy").fillText("TEST test 撒旦给个", "#000000");
2787     const ciF = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px monospace").fillText("TEST test 撒旦给个", "#000000");
2788     const ciG = new CanvasImageText(ElementUtils.createCanvas(200, 20)).setFont("bold 18px 微软雅黑").fillText("TEST test 撒旦给个", "#000000");
2789     cid.list.push(ciB.pos(0, 100), ciC.pos(0, 120), ciD.pos(0, 140), ciE.pos(0, 160), ciF.pos(0, 180), ciG.pos(0, 200), new CanvasImage(ExitImage20).pos(100, 220));
2790 */
2791 class CanvasImageText extends CanvasImageCustom{
2792 
2793     get fontSize(){
2794         const strs = this.context.font.split(" ");
2795         if(UTILS.emptyArray(strs) === false){
2796             let s = 0;
2797             for(let i = 0; i < strs.length; i++){
2798                 s = parseFloat(strs[i]);
2799                 if(UTILS.isNumber(s)) return s;
2800             }
2801         }
2802         return 10;
2803     }
2804 
2805     constructor(canvas, value = null){
2806         super(canvas, value);
2807         this.context.textAlign = "left";
2808         this.context.textBaseline = "top";
2809         this.context.font = "10px serif, monospace, SimSun";
2810     }
2811 
2812     size(w, h, np){
2813         super.size(w, h, np);
2814         this.context.textAlign = "left";
2815         this.context.textBaseline = "top";
2816         return this;
2817     }
2818 
2819     setFont(font){
2820         switch(typeof font){
2821             case "string": 
2822             if(font !== this.context.font) this.context.font = font;
2823             break;
2824             case "number": 
2825             font = font+"px serif, SimSun, monospace";
2826             if(font !== this.context.font) this.context.font = font;
2827             break;
2828         }
2829 
2830         return this;
2831     }
2832 
2833     getTextWidth(text){
2834         return this.context.measureText(text).width;
2835     }
2836 
2837     fillText(value, color, x, y){
2838         if(!value) return this;
2839         if(color && this.context.fillStyle !== color) this.context.fillStyle = color;
2840         if(x === undefined){
2841             const w = this.context.measureText(value).width;
2842             x = w < this.box.w ? (this.box.w - w) / 2 : 0;
2843         }
2844         if(y === undefined){
2845             y = (this.box.h - this.fontSize) / 2;
2846         }
2847         this.context.fillText(value, x, y);
2848         return this;
2849     }
2850 
2851     fillTextWrap(value, option = {}){
2852         if(value.length === 0) return this;
2853         if(option.color && this.context.fillStyle !== option.color) this.context.fillStyle = option.color;
2854 
2855         const con = this.context, pos = [], 
2856         mw = option.width || this.box.w, fontSize = this.fontSize,
2857 
2858         padIsObj = UTILS.isObject(option.padding),
2859         padT = padIsObj ? padIsObj.top : option.padding || 0,
2860         padR = padIsObj ? padIsObj.right : option.padding || 0,
2861         padB = padIsObj ? padIsObj.bottom : option.padding || 0,
2862         padL = padIsObj ? padIsObj.left : option.padding || 0,
2863 
2864         disIsObj = UTILS.isObject(option.distance),
2865         disX = disIsObj ? disIsObj.x : option.distance || 0,
2866         disY = disIsObj ? disIsObj.y : option.distance || 0;
2867 
2868         var y = padT, x = padL;
2869         for(let i = 0, w; i < value.length; i++){
2870             w = con.measureText(value[i]).width;
2871             if(x + w + disX + padR > mw){
2872                 x = w + padL + disX;
2873                 y += fontSize + disY;
2874                 pos.push(padL, y);
2875             } else {
2876                 pos.push(x, y);
2877                 x += w + disX;
2878             }
2879         }
2880         
2881         if(option.width !== undefined) this.size(option.width, option.height || (y + fontSize + padB));
2882         for(let i = 0; i < pos.length; i += 2) con.fillText(value[i / 2], pos[i], pos[i+1]);
2883         
2884         if(option.ePos !== undefined){
2885             option.ePos.x = x;
2886             option.ePos.y = y;
2887         }
2888 
2889         return this;
2890     }
2891 
2892 }
2893 
2894 
2895 /* CarouselFigure 轮播图 (走马灯)
2896 parameter: 
2897 attribute:
2898 method:
2899     init(option: Object{
2900         animateStep: number,    //动画每秒刷新多少次; 默认 30
2901         timerSpeed: number,        //计时器的速度; 默认 1000
2902         speed: number,            //每次花费的时间(0 -> this.box.w); 默认 timerSpeed/2 (不应该大于 timerSpeed);
2903     }): this;
2904     play(): this; //开始自动播放
2905     stop(): this; //停止自动播放
2906 
2907 demo: 
2908     const cf = new CarouselFigure({width: 100, height: 100}),
2909     urls = ["1.jpg", "2.jpg"];
2910     ElementUtils.createCanvasFromURL(cf.box.w, cf.box.h, urls, true, canvass => {
2911         for(let i = 0; i < canvass.length; i++) cf.list[i] = new CanvasImage(canvass[i]);
2912         cf.init().start().render();
2913     });
2914 */
2915 class CarouselFigure extends CanvasImageDraw{
2916 
2917     #timer = null;
2918     #animateLoop = null;
2919 
2920     constructor(option){
2921         super(option);
2922     }
2923 
2924     init(option = {}){
2925         if(this.list.length <= 0){
2926             console.warn("CarouselFigure: 轮播图初始化失败");
2927             return this;
2928         }
2929 
2930         var sKey = -1, eKey = 0;
2931         const width = this.box.w, timerSpeed = option.timerSpeed || 1000, s = 4, d = 10, len = this.list.length, mKey = len - 1, 
2932         defDotImg = new CanvasImageCustom().size(s, s).fillRect("rgba(255,255,255,0.2)", s/2).stroke("#666666").image,
2933         selDotImg = new CanvasImageCustom().size(s, s).fillRect("#0000ff", s/2).stroke("#666666").image,
2934         sTween = new TweenCache({x: 0}, {x: 0}, option.speed || timerSpeed / 2),
2935         eTween = new TweenCache({x: 0}, {x: 0}, sTween.time, () => animateLoop.stop()),
2936         animateLoop = new AnimateLoop(() => {
2937             sTween.update();
2938             eTween.update();
2939             this.list[sKey].box.x = sTween.origin.x;
2940             this.list[eKey].box.x = eTween.origin.x;
2941             this.redraw();
2942         }, null, option.animateStep === undefined ? 1000 / 30 : 1000 / option.animateStep),
2943         onup = () => this.#timer.start(),
2944         ondown = () => this.#timer.stop();
2945 
2946         for(let i = 0; i < len; i++){
2947             this.list[i].pos(width * i, 0);
2948             this.list[len + i] = new CanvasImage(defDotImg).pos(i * s + i * d + (width - (len * s + len * d)) / 2, this.box.h - s - 10);
2949             this.list[i].addEventListener("up", onup);
2950             this.list[i].addEventListener("down", ondown);
2951         }
2952 
2953         this.#timer = new Timer(() => {
2954             for(let i = 0; i < len; i++){
2955                 this.list[len + eKey].image = defDotImg;
2956                 this.list[i].box.x = width;
2957             }
2958 
2959             sKey = sKey === mKey ? 0 : sKey + 1;
2960             eKey = eKey === mKey ? 0 : eKey + 1;
2961             
2962             this.list[sKey].box.x = sTween.origin.x = 0;
2963             sTween.end.x = width;
2964             sTween.start();
2965             
2966             this.list[eKey].box.x = eTween.origin.x = -width;
2967             eTween.end.x = 0;
2968             eTween.start();
2969 
2970             this.list[len + eKey].image = selDotImg;
2971             animateLoop.play();
2972         }, timerSpeed, Infinity, false);
2973         this.#animateLoop = animateLoop;
2974         
2975         return this;
2976     }
2977 
2978     play(){
2979         if(this.#timer !== null) this.#timer.start();
2980         return this;
2981     }
2982 
2983     stop(){
2984         if(this.#timer !== null) this.#timer.stop();
2985         return this;
2986     }
2987 
2988     exit(){
2989         if(this.#timer !== null){
2990             this.#timer.stop();
2991             this.#timer = null;
2992         }
2993         if(this.#animateLoop !== null){
2994             this.#animateLoop.stop();
2995             this.#animateLoop = null;
2996         }
2997         super.exit();
2998     }
2999 
3000 }
3001 
3002 
3003 
3004 
3005 const emptyColor = new RGBColor(),
3006 emptyCIC = new CanvasImageCustom(null, null),
3007 emptyCIT = new CanvasImageText();
3008 
3009 const CPath2DMeter = new CanvasPath2D("stroke", {strokeStyle: "#84c3f9"});
3010 CPath2DMeter.progress(new Meter());
3011 
3012 const ExitImage20 = new CanvasImageText(ElementUtils.createCanvas(20, 20), new Path2D())
3013 .rect(2, 1).fill("#eeeeee")
3014 .setFont("bold 14px sans-serif").fillText("✘", "#000000")
3015 .stroke("#666666").image;
3016 
3017 
3018 /* CanvasProgressBar 进度条
3019 parameter: 
3020     option: Object{
3021         min: number,
3022         max: number,
3023 
3024         width: number,
3025         height: number,
3026         cursorSize: number,
3027         position: string,
3028 
3029         bgColor: string,
3030         valueColor: string,
3031         cursorColor: string,
3032         borderColor: string,
3033     }
3034 
3035 attribute:
3036     meter: Meter;
3037     
3038 method:
3039     setValue(v: number): undefined;     //v 为 min 与 max 之间
3040     pos(x, y: number): undefined;         //初始化或设置位置
3041     addToList(arr: Array): undefined;    //
3042     removeToList(arr: Array): undefined;//
3043     bindEvent(cid: CanvasImageDraw, onchange: function): undefined;
3044 
3045 demo:
3046     const cid = new CanvasImageDraw();
3047     const progress = new CanvasProgressBar({
3048         min: -100,
3049         max: 100,
3050         width: 120,
3051         height: 10,
3052         cursorSize: 20,
3053     });
3054 
3055     progress.pos(10, 10);
3056     progress.addToList(cid.list);
3057     progress.bindEvent(cid, v => console.log(v));
3058 */
3059 class CanvasProgressBar{
3060 
3061     #background = null;
3062     #rBox = null;
3063     #cursor = null;
3064     
3065     constructor(option = {}){
3066         this.meter = new Meter(option.min, option.max);
3067 
3068         const width = option.width || 100, height = option.height || 10;
3069 
3070         //init cursor
3071         if(width > height){
3072             var round = height / 2;
3073             const cursorSize = option.cursorSize || height;
3074             emptyCIC.size(cursorSize, cursorSize, true).rect(cursorSize/2, 1);
3075         } else {
3076             var round = width / 2;
3077             const cursorSize = option.cursorSize || width;
3078             emptyCIC.size(cursorSize, cursorSize, true).rect(cursorSize/2, 1);
3079         }
3080         this.#cursor = new CanvasImage(emptyCIC.fill(option.cursorColor || "#ffffff").cloneCanvas());
3081         
3082         //init background
3083         this.#background = new CanvasImage(emptyCIC.size(width, height, true).fillRect(option.bgColor || "#000000", round, 1).stroke(option.borderColor||"rgba(0,0,0,0)").cloneCanvas());
3084         this.#rBox = new RoundedRectangle(0, 0, 0, 0, round);
3085         this.#background.path2D = new CanvasPath2D("fill", {fillStyle: option.valueColor || "rgba(255,255,255,0.2)"});
3086         this.#background.path2D.rect(this.#rBox);
3087         this.#background.position = this.#cursor.position = option.position || "";
3088     }
3089 
3090     _updateCX(){
3091         const nx = this.meter.ratio * this.#background.w, hs = this.#cursor.w / 2;
3092         this.#rBox.w = nx < this.#background.h ? this.#background.h : nx;
3093         this.#cursor.box.x = nx - hs < 0 ? 
3094         this.#background.x : nx + hs >= this.#background.w ? 
3095         this.#background.box.mx - this.#cursor.w : nx + this.#background.x - hs;
3096     }
3097 
3098     _updateCY(){
3099         const nx = this.meter.ratio * this.#background.h, hs = this.#cursor.h / 2;
3100         this.#rBox.h = nx < this.#background.w ? this.#background.w : nx;
3101         this.#cursor.box.y = nx - hs < 0 ? 
3102         this.#background.y : nx + hs >= this.#background.h ? 
3103         this.#background.box.my - this.#cursor.h : nx + this.#background.y - hs;
3104     }
3105 
3106     setValue(v){
3107         this.meter.value = v;
3108         if(this.#background.w > this.#background.h) this._updateCX();
3109         else this._updateCY();
3110     }
3111 
3112     pos(x, y){
3113         if(this.#background.w > this.#background.h){
3114             this.#cursor.pos(this.#cursor.x - this.#background.x + x, y - (this.#cursor.h - this.#background.h) / 2);
3115             this.#rBox.size(this.#cursor.x - x + this.#cursor.w, this.#background.h);
3116         } else {
3117             this.#cursor.pos(x - (this.#cursor.w - this.#background.w) / 2, this.#cursor.y - this.#background.y + y);
3118             this.#rBox.size(this.#background.w, this.#cursor.y - y + this.#cursor.h);
3119         }
3120 
3121         this.#rBox.pos(0, 0);
3122         this.#background.pos(x, y);
3123     }
3124 
3125     addToList(arr){
3126         arr.push(this.#background, this.#cursor);
3127     }
3128 
3129     removeToList(arr){
3130         const i = arr.indexOf(this.#background);
3131         if(i !== -1) arr.splice(i, 2);
3132     }
3133 
3134     addToCID(cid){
3135         cid.append(this.#background, this.#cursor);
3136     }
3137     
3138     bindEvent(cid, cis, onchange = null){
3139         var ov;
3140         if(cis){
3141             this.#cursor.addEventListener("down", () => cis.disable(this));
3142             this.#cursor.addEventListener("up", () => cis.enable(this));
3143         }
3144 
3145         if(this.#background.w > this.#background.h){
3146             this.#cursor.addEventListener("move", event => {
3147                 ov = this.meter.value;
3148                 this.meter.setFromRatio((event.offsetX - this.#background.x - cid.box.x) / this.#background.w);
3149                 if(ov !== this.meter.value){
3150                     ov = this.meter.value;
3151                     if(onchange !== null) onchange(this);
3152                     this._updateCX();
3153                     cid.redraw();
3154                 }
3155             });
3156         } else {
3157             this.#cursor.addEventListener("move", event => {
3158                 ov = this.meter.value;
3159                 this.meter.setFromRatio((event.offsetY - this.#background.y - cid.box.y) / this.#background.h);
3160                 if(ov !== this.meter.value){
3161                     ov = this.meter.value;
3162                     if(onchange !== null) onchange(this);
3163                     this._updateCY();
3164                     cid.redraw();
3165                 }
3166             });
3167         }
3168     }
3169 
3170 }
3171 
3172 
3173 /* CanvasColorTestViewer 颜色调试器
3174 parameter:
3175     option = {
3176         width,             //默认 250 
3177         height:         //默认 w*0.618
3178         bgColor,        //默认 #ffffff
3179         borderRadius,    //默认 4
3180         textColor        //默认 #002e23
3181     }
3182 
3183 attribute:
3184     hsv: Object{h,s,v};
3185     alpha: Number;
3186     只读: value: RGBColor, box: Box;
3187 
3188 method:
3189     visible(v: Bool): undefined;//显示或隐藏所以的ci
3190     pos(x, y): undefined;         //如果你不希望排版错乱的话用此方法设置它们的位置位置
3191     update(): undefined;         //当颜色发送改变时更新: ctv.set("red").update()
3192     set(r, g, b, a): this;         //第一个参数r可以时字符串样式的颜色(rgb|rgba|十进制|英文)
3193 
3194     addToList(arr): undefined;         //把所有的 CanvasImage 追加到arr数组
3195     removeToList(arr): undefined;     //所有的 CanvasImage 从arr数组删除
3196 
3197     bindEvent(cir: CanvasImageRender, onchange: Func): this; //cir必须已开启dom事件,否则无效
3198 
3199 demo:
3200     const ctv = new CanvasColorTestViewer(),
3201     cir = new CanvasImageRender({width: ctv.box.w - 5, height: ctv.box.h - 5}),
3202     cie = new CanvasEvent(cir); //启用dom事件
3203     
3204     ctv.set("blue").update();
3205     ctv.addToList(cir.list);
3206     ctv.bindEvent(cir, null, v => console.log(v));
3207     cir.render();
3208 
3209     // 将图片变灰,灰度算法公式: 0.299*r + 0.587*g + 0.114*b 
3210 */
3211 class CanvasColorTestViewer{
3212 
3213     #value = new RGBColor();
3214     get value(){return this.#value;}
3215 
3216     #box = null;
3217     get box(){return this.#box;}
3218 
3219     #alpha = 1;
3220     get alpha(){return this.#alpha;}
3221     set alpha(v){this.#alpha = UTILS.floatKeep(v);}
3222 
3223     constructor(option = {}){
3224         this.textColor = option.textColor || "#002e23";
3225         this.hsv = {h:0, s:100, v:100};
3226 
3227         const w = option.width || 250, h = option.height||Math.round(0.618*w), r = option.borderRadius || 4, 
3228         colors = [], lw = w * 0.3, rw = w - lw, sh = 10;
3229         const cursorImage = emptyCIC.size(10, 10, true).fillRect("#ffffff", 5).cloneCanvas();
3230 
3231 
3232         //h
3233         this.ciH = new CanvasImageCustom().size(w, h).rect(r);
3234 
3235         colors.length = 0;
3236         for(let h = 0, c = emptyColor; h < 6; h++){
3237             c.setFormHSV(h/6*360, 100, 100);
3238             colors[h] = `rgb(${c.r},${c.g},${c.b})`;
3239         }
3240 
3241         emptyCIC.size(rw-10, sh, true).rect(2);
3242         const linearGradientH = emptyCIC.context.createLinearGradient(0, sh, rw-10, sh);
3243         emptyCIC.fill(gradientColor(linearGradientH, colors, true));
3244         this.ciH_scroll = new CanvasImage(emptyCIC.cloneCanvas());
3245         this.cursorH = new CanvasImage(cursorImage);
3246 
3247         
3248         //sv 饱和度&明度
3249         emptyCIC.size(w, h, true).rect(r);
3250         colors.length = 0;
3251         for(let s = 0; s < 100; s++) colors[s] = `rgba(255,255,255,${1 - s / 99})`;
3252         const linearGradientS = emptyCIC.context.createLinearGradient(0, h, w, h);
3253         emptyCIC.fill(gradientColor(linearGradientS, colors));
3254         
3255         colors.length = 0;
3256         for(let v = 0; v < 100; v++) colors[v] = `rgba(0,0,0,${1 - v / 99})`;
3257         const linearGradientV = emptyCIC.context.createLinearGradient(w, h, w, 0);
3258         emptyCIC.fill(gradientColor(linearGradientV, colors));
3259 
3260         this.ciSV = new CanvasImage(emptyCIC.cloneCanvas());
3261         this.cursorSV = new CanvasImage(emptyCIC.size(10, 10, true).rect(5).fill("rgba(255,255,255,0.4)").stroke("rgba(0,0,0,0.6)").cloneCanvas());
3262         
3263 
3264         //a
3265         this.ciA = new CanvasImage(ElementUtils.createCanvasTCC(rw-10, sh, sh/2, 2));
3266 
3267         colors.length = 0;
3268         for(let a = 0; a < 10; a++) colors[a] = `rgba(0,0,0,${a / 9})`;
3269         const linearGradientA = emptyCIC.context.createLinearGradient(0, sh, this.ciA.box.w, sh);
3270         this.ciA_scroll = new CanvasImage(emptyCIC.size(this.ciA.box.w, sh, true).rect(2).fill(gradientColor(linearGradientA, colors)).cloneCanvas());
3271         this.cursorA = new CanvasImage(cursorImage);
3272         
3273 
3274         //bottom bg
3275         this.bottomBG = new CanvasImage(emptyCIC.size(w, lw, true).rect(r, null, 1).fill(option.bgColor||"#ffffff").cloneCanvas());
3276 
3277 
3278         //result
3279         this.resultBG = new CanvasImage(ElementUtils.createCanvasTCC(lw-10, lw-10, 5, 2));
3280         this.resultColor = new CanvasImageCustom().size(this.resultBG).rect(2, 1);
3281         this.resultText = new CanvasImageText().size(this.ciA.box.w, this.resultColor.box.h - this.ciH_scroll.box.h - this.ciA_scroll.box.h - 15);
3282         
3283 
3284         //box
3285         const _box = new Box(), scope = this;
3286         Object.defineProperties(_box, {
3287             x: {get: () => {return scope.ciH.box.x;}},
3288             y: {get: () => {return scope.ciH.box.y;}},
3289             w: {get: () => {return scope.ciH.box.w;}},
3290             h: {get: () => {return scope.ciH.box.h + scope.bottomBG.box.h;}},
3291         });
3292 
3293         this.#box = _box;
3294         this.updateCIH();
3295         this.updateResult();
3296         this.pos(0, 0);
3297     }
3298 
3299     pos(x, y){
3300         this.ciH.box.pos(x, y);
3301         this.ciSV.box.pos(x, y);
3302         this.updateCursorSV();
3303 
3304         this.bottomBG.pos(this.ciH.x, this.ciH.box.my);
3305         this.resultBG.pos(this.bottomBG.x+5, this.bottomBG.y+5);
3306         this.resultColor.pos(this.resultBG.x, this.resultBG.y);
3307 
3308         this.ciH_scroll.pos(this.resultBG.box.mx + 5, this.resultBG.y + 5);
3309         this.updateCursorH();
3310 
3311         this.ciA.pos(this.ciH_scroll.x, this.ciH_scroll.box.my + 5);
3312         this.ciA_scroll.pos(this.ciA.x, this.ciA.y);
3313         this.updateCursorA();
3314 
3315         this.resultText.pos(this.ciA_scroll.x, this.ciA_scroll.box.my + 5);
3316     }
3317     
3318     addToList(arr){
3319         arr.push(
3320             this.ciH, 
3321             this.ciSV, 
3322             this.bottomBG, 
3323             this.resultBG, 
3324             this.resultColor, 
3325             this.resultText, 
3326             this.ciH_scroll, 
3327             this.ciA, 
3328             this.ciA_scroll, 
3329             this.cursorSV, 
3330             this.cursorH, 
3331             this.cursorA
3332         );
3333     }
3334 
3335     removeToList(arr){
3336         const i = arr.indexOf(this.ciH);
3337         if(i !== -1) arr.splice(i, 12);
3338     }
3339 
3340     addToCID(cid){
3341         cid.append(
3342             this.ciH, 
3343             this.ciSV, 
3344             this.bottomBG, 
3345             this.resultBG, 
3346             this.resultColor, 
3347             this.resultText, 
3348             this.ciH_scroll, 
3349             this.ciA, 
3350             this.ciA_scroll, 
3351             this.cursorSV, 
3352             this.cursorH, 
3353             this.cursorA
3354         );
3355     }
3356 
3357     bindEvent(cid, cis, onchange = null){
3358         var sx = 0, sy = 0, v;
3359         const cursorSV_half = this.cursorSV.box.w / 2,
3360         cis_d = cis ? () => cis.disable(this) : null, 
3361         cis_e = cis ? () => cis.enable(this) : null;
3362 
3363 
3364         //SV
3365         const setSV = (x, y) => {
3366             var m = this.#box.x-cursorSV_half;
3367             if(x < m) x = m;
3368             else{
3369                 v = this.ciSV.box.mx - cursorSV_half;
3370                 if(x > v) x = v;
3371             }
3372 
3373             m = this.#box.y-cursorSV_half;
3374             if(y < m) y = m;
3375             else{
3376                 v = this.ciSV.box.my - cursorSV_half;
3377                 if(y > v) y = v;
3378             }
3379 
3380             this.cursorSV.box.pos(x, y);
3381             
3382             x += cursorSV_half;
3383             y += cursorSV_half;
3384             this.hsv.s = (x - this.ciSV.box.x) / this.ciSV.box.w * 100;
3385             this.hsv.v = (1 - (y - this.ciSV.box.y) / this.ciSV.box.h) * 100;
3386             this.updateResult();
3387 
3388             if(onchange !== null) onchange(this.#value.getRGBA(this.#alpha));
3389             cid.redraw();
3390         },
3391 
3392         onmoveSV = event => {
3393             if(event.target === this.cursorSV || event.target === this.ciSV) setSV(event.offsetX - sx, event.offsetY - sy);
3394         },
3395 
3396         ondownSV = event => {
3397             sx = event.offsetX - this.cursorSV.box.x;
3398             sy = event.offsetY - this.cursorSV.box.y;
3399             onmoveSV(event);
3400         }
3401 
3402         this.cursorSV.addEventListener("down", ondownSV);
3403         this.ciSV.addEventListener("down", ondownSV);
3404         this.cursorSV.addEventListener("move", onmoveSV);
3405         this.ciSV.addEventListener("move", onmoveSV);
3406         if(cis){
3407             this.cursorSV.addEventListener("down", cis_d);
3408             this.cursorSV.addEventListener("up", cis_e);
3409             this.ciSV.addEventListener("down", cis_d);
3410             this.ciSV.addEventListener("up", cis_e);
3411         }
3412         
3413         
3414         //H
3415         const setH = x => {
3416             v = this.ciH_scroll.box.x - cursorSV_half;
3417             if(x < v) x = v;
3418             else{
3419                 v = this.ciH_scroll.box.mx - cursorSV_half;
3420                 if(x > v) x = v;
3421             }
3422 
3423             this.cursorH.box.x = x;
3424             
3425             x += cursorSV_half;
3426             this.hsv.h = (x - this.ciH_scroll.box.x) / this.ciH_scroll.box.w * 360;
3427             this.updateCIH();
3428             this.updateResult();
3429 
3430             if(onchange !== null) onchange(this.#value.getRGBA(this.#alpha));
3431             cid.redraw();
3432         },
3433 
3434         onmoveH = event => {
3435             if(event.target === this.cursorH || event.target === this.ciH_scroll) setH(event.offsetX - sx);
3436         },
3437 
3438         ondownH = event => {
3439             sx = event.offsetX - this.cursorH.box.x;
3440             sy = event.offsetY - this.cursorH.box.y;
3441             onmoveH(event);
3442         }
3443 
3444         this.cursorH.addEventListener("down", ondownH);
3445         this.ciH_scroll.addEventListener("down", ondownH);
3446         this.cursorH.addEventListener("move", onmoveH);
3447         this.ciH_scroll.addEventListener("move", onmoveH);
3448         if(cis){
3449             this.cursorH.addEventListener("down", cis_d);
3450             this.cursorH.addEventListener("up", cis_e);
3451             this.ciH_scroll.addEventListener("down", cis_d);
3452             this.ciH_scroll.addEventListener("up", cis_e);
3453         }
3454 
3455 
3456         //A
3457         const setA = x => {
3458             v = this.ciA_scroll.box.x - cursorSV_half;
3459             if(x < v) x = v;
3460             else{
3461                 v = this.ciA_scroll.box.mx - cursorSV_half;
3462                 if(x > v) x = v;
3463             }
3464 
3465             this.cursorA.box.x = x;
3466             
3467             x += cursorSV_half;
3468             this.alpha = (x - this.ciA_scroll.box.x) / this.ciA_scroll.box.w * 1;
3469             this.updateResult();
3470 
3471             if(onchange !== null) onchange(this.#value.getRGBA(this.#alpha));
3472             cid.redraw();
3473         },
3474 
3475         onmoveA = event => {
3476             if(event.target === this.cursorA || event.target === this.ciA_scroll) setA(event.offsetX - sx);
3477         },
3478 
3479         ondownA = event => {
3480             sx = event.offsetX - this.cursorA.box.x;
3481             sy = event.offsetY - this.cursorA.box.y;
3482             onmoveA(event);
3483         }
3484 
3485         this.cursorA.addEventListener("down", ondownA);
3486         this.ciA_scroll.addEventListener("down", ondownA);
3487         this.cursorA.addEventListener("move", onmoveA);
3488         this.ciA_scroll.addEventListener("move", onmoveA);
3489         if(cis){
3490             this.cursorA.addEventListener("down", cis_d);
3491             this.cursorA.addEventListener("up", cis_e);
3492             this.ciA_scroll.addEventListener("down", cis_d);
3493             this.ciA_scroll.addEventListener("up", cis_e);
3494         }
3495 
3496         return this;
3497     }
3498 
3499     set(r, g, b, a){
3500         if(typeof r !== "string"){
3501             emptyColor.set(r, g, b).getHSV(this.hsv);
3502             this.alpha = a || 1;
3503         }
3504         else{
3505             this.alpha = emptyColor.setFormString(r);
3506             emptyColor.getHSV(this.hsv);
3507         }
3508         return this;
3509     }
3510 
3511     update(){
3512         this.updateCIH();
3513         this.updateResult();
3514 
3515         this.updateCursorSV();
3516         this.updateCursorH();
3517         this.updateCursorA();
3518     }
3519 
3520     updateCursorSV(){
3521         this.cursorSV.box.x = this.hsv.s / 100 * this.ciSV.box.w + this.ciSV.box.x - this.cursorSV.box.w / 2;
3522         this.cursorSV.box.y = (1 - this.hsv.v / 100) * this.ciSV.box.h + this.ciSV.box.y - this.cursorSV.box.h / 2;
3523     }
3524 
3525     updateCursorH(){
3526         this.cursorH.box.x = this.hsv.h / 360 * this.ciH_scroll.box.w + this.ciH_scroll.box.x - this.cursorH.box.w / 2;
3527         this.cursorH.box.y = this.ciH_scroll.box.y;
3528     }
3529 
3530     updateCursorA(){
3531         this.cursorA.box.x = this.alpha * this.ciA_scroll.box.w + this.ciA_scroll.box.x - this.cursorA.box.w / 2;
3532         this.cursorA.box.y = this.ciA_scroll.box.y;
3533     }
3534 
3535     updateCIH(){
3536         const c = emptyColor.setFormHSV(this.hsv.h, 100, 100);
3537         this.ciH.fill(`rgb(${c.r},${c.g},${c.b})`);
3538     }
3539 
3540     updateResult(){
3541         this.#value.setFormHSV(this.hsv.h, this.hsv.s, this.hsv.v);
3542         const str = this.#value.getRGBA(this.#alpha);
3543         this.resultColor.clear().fill(str);
3544         this.resultText.clear().fillText(str, this.textColor);
3545     }
3546 
3547 }
3548 
3549 
3550 /* CanvasIconMenu 图标菜单
3551 
3552 备注: 
3553     每个 CanvasIconMenu 会创建3个CanvasImage对象
3554     
3555 parameter:
3556     icon: Image, //左边不可以更换的图标
3557     text,
3558     option: Object{
3559         padding: number,            //border 以内的间隔
3560         margin: number,             //border 以外的间隔
3561         iconSize: number,                //
3562         backgroundColor: string,    //border 以内的背景颜色(最底层的)
3563         
3564         //text
3565         textWidth: number,     //如果为0或未定义则根据文字内容自适应宽
3566         textSize: number,     //最好不要大于 iconSize
3567         textColor: string,    //
3568 
3569         //边框
3570         borderSize: number,     //默认 0
3571         borderColor: string,     //默认 #ffffff
3572         borderRadius: number,    //默认 0
3573     }
3574 
3575 attributes:
3576     background: CanvasImage;    //它同时实现了 backgroundColor, icon, text
3577     icons: CanvasImages;    //右边可更换的图标 宽高为 option.iconSize, option.iconSize
3578     masks: CanvasImages;     //遮罩层(最顶层) 宽高为 this.contentWidth, this.contentHeight;
3579 
3580 method:
3581     pos(x, y)
3582     addToList(arr)
3583     removeToList(arr)
3584 
3585 demo:
3586     const iconMenu = new CanvasIconMenu(null, "test");
3587     iconMenu.pos(10, 10);
3588     iconMenu.addToList(cir.list);
3589 */
3590 class CanvasIconMenu{
3591 
3592     static option = {
3593         padding: 2,
3594         margin: 0, 
3595         iconSize: 20,
3596         backgroundColor: "#000000",
3597         textWidth: 0,
3598         textSize: 12,
3599         textColor: "#ffffff",
3600         borderSize: 0,
3601         borderColor: "#ffffff",
3602         borderRadius: 0,
3603     }
3604 
3605     #borderRadius = 0;
3606     #iconsOffsetX = 0;
3607     #iconsOffsetY = 0;
3608 
3609     #background = null;
3610     #icons = null;
3611     #masks = null;
3612     get background(){return this.#background;}
3613     get icons(){return this.#icons;}
3614     get masks(){return this.#masks;}
3615     get box(){return this.#background.box}
3616 
3617     #masksOffset = 0;
3618     #contentHeight = 0;
3619     get masksOffset(){return this.#masksOffset;}
3620     get contentHeight(){return this.#contentHeight;}
3621 
3622     #contentWidth = 0;
3623     get contentWidth(){return this.#contentWidth;}
3624 
3625     constructor(icon, text, option = CanvasIconMenu.option){
3626         if(typeof text !== "string" || text === "") text = "empty";
3627 
3628         const padding = UTILS.isNumber(option.padding) ? option.padding : 0,
3629         margin = UTILS.isNumber(option.margin) ? option.margin : 0,
3630         iconSize = UTILS.isNumber(option.iconSize) ? option.iconSize : 12,
3631         backgroundColor = option.backgroundColor || "#000000",
3632         
3633         textSize = option.textSize || 12,
3634         textColor = option.textColor || "#ffffff",
3635 
3636         borderSize = UTILS.isNumber(option.borderSize) ? option.borderSize : 0,
3637         borderColor = option.borderColor || textColor,
3638         borderRadius = option.borderRadius || 0;
3639         
3640         emptyCIT.setFont(textSize);
3641         const textWidth = option.textWidth || emptyCIT.getTextWidth(text), 
3642         contentWidth = padding * 4 + borderSize * 2 + iconSize * 2 + textWidth, 
3643         contentHeight = padding * 2 + borderSize * 2 + iconSize,
3644         contentOffset = borderSize + padding;
3645         
3646         //background
3647         emptyCIT.size(contentWidth, contentHeight, true).rect(borderRadius, borderSize).fill(backgroundColor);
3648         if(CanvasImageDraw.isCanvasImage(icon)){
3649             const size = UTILS.setSizeToSameScale(icon, {width: iconSize, height: iconSize});
3650             emptyCIT.context.drawImage(icon, contentOffset, contentOffset, size.width, size.height);
3651         }
3652 
3653         //text
3654         emptyCIT.fillText(text, textColor, contentOffset + iconSize + padding);
3655         if(borderSize > 0) emptyCIT.stroke(borderColor, borderSize);
3656         const backgroundImage = emptyCIT.cloneCanvas();
3657         
3658         //icons
3659         this.#icons = new CanvasImages();
3660         this.#icons.box.size(iconSize, iconSize);
3661 
3662         //masks
3663         this.#masks = new CanvasImages();
3664         this.#masks.box.size(contentWidth, contentHeight);
3665         
3666         //margin
3667         if(UTILS.isNumber(margin) && margin > 0){
3668             const margin2 = margin * 2;
3669             emptyCIT.size(contentWidth + margin2, contentHeight + margin2);
3670             emptyCIT.context.drawImage(backgroundImage, margin, margin);
3671             this.#background = new CanvasImage(emptyCIT.cloneCanvas());
3672             this.#masksOffset = margin;
3673         }else{
3674             this.#background = new CanvasImage(backgroundImage);
3675             this.#masksOffset = 0;
3676         }
3677 
3678         this.#iconsOffsetX = emptyCIT.box.w - this.#masksOffset - contentOffset - iconSize;
3679         this.#iconsOffsetY = this.#masksOffset + contentOffset;
3680         this.#borderRadius = borderRadius;
3681         this.#contentHeight = contentHeight;
3682         this.#contentWidth = contentWidth;
3683         //this.#contentOffset = contentOffset;
3684     }
3685 
3686     pos(x, y){
3687         this.#background.pos(x, y);
3688         this.#icons.pos(x + this.#iconsOffsetX, y + this.#iconsOffsetY);
3689         this.#masks.pos(x + this.#masksOffset, y + this.#masksOffset);
3690     }
3691 
3692     addToList(arr){
3693         arr.push(this.#background, this.#icons, this.#masks);
3694     }
3695 
3696     removeToList(arr){
3697         const i = arr.indexOf(this.#background);
3698         if(i !== -1) arr.splice(i, 3);
3699     }
3700 
3701     visible(v){
3702         this.#background.visible = 
3703         this.#icons.visible = 
3704         this.#masks.visible = v;
3705     }
3706 
3707     disableStyle(){
3708         if(this.#masks.cursor !== 0) this.#masks.cursor = 0;
3709     }
3710 
3711     selectStyle(){
3712         if(this.#masks.cursor !== 1) this.#masks.cursor = 1;
3713     }
3714 
3715     defaultStyle(){
3716         if(this.#masks.cursor !== 2) this.#masks.cursor = 2;
3717     }
3718 
3719     createMasksImages(colorA = "rgba(0,0,0,0.75)", colorB = "rgba(0,173,230,0.5)"){
3720         emptyCIC.size(this.contentWidth, this.contentHeight, true).rect(this.#borderRadius, 1);
3721         const a = emptyCIC.cloneCanvas(),
3722         b = emptyCIC.fill(colorA).cloneCanvas(),
3723         c = emptyCIC.clear().fill(colorB).cloneCanvas();
3724         this.#masks.images.push(b,c,a);
3725         this.#masks.cursor = 2;
3726     }
3727 
3728     createIconsTriangle(fillStyle = CanvasIconMenu.option.textColor, padding = CanvasIconMenu.option.padding){
3729         const size = this.#icons.box.w, x = size - padding;
3730         emptyCIC.size(size, size, true).path([padding,padding, x,size/2, padding,x], true).fill(fillStyle);
3731         this.#icons.images.push(emptyCIC.cloneCanvas());
3732     }
3733 
3734     createIconsImages(texts, textSize = CanvasIconMenu.option.textSize, fillStyle = CanvasIconMenu.option.textColor){
3735         emptyCIT.size(this.#icons.box.w, this.#icons.box.h).setFont(textSize);
3736         if(Array.isArray(texts)){
3737             texts.forEach(str => {
3738                 this.#icons.images.push(emptyCIT.clear().fillText(str, fillStyle).cloneCanvas());
3739             });
3740         } else if(typeof texts === "string"){
3741             this.#icons.images.push(emptyCIT.clear().fillText(texts, fillStyle).cloneCanvas());
3742         }
3743         if(this.#icons.cursor === -1) this.#icons.cursor = 0;
3744     }
3745 
3746 }
3747 
3748 
3749 /* CanvasTreeList extends TreeStruct 树结构展示对象
3750 parameter:
3751     object: Object,
3752     info: Object{
3753         name_attributesName: String, //object[name_attributesName]
3754         iamges_attributesName: String,
3755         iamges: Object{
3756             object[iamges_attributesName]: Image
3757         },
3758     },
3759     option: Object, //参见 CanvasIconMenu 的 option
3760 
3761 attribute:
3762     objectView: CanvasIconMenu;        //object 展示视图(文字)
3763     childIcon: CanvasImages;        //子视图隐藏开关(三角形图标)
3764     childView: CanvasImageCustom;    //子视图 (线)
3765 
3766     //此属性内部自动更新, 表示自己子视图的高 
3767     //为什么不直接用 childView.box.h 代替? 
3768     //因为如果cir存在scroll则box的属性将会变成一个"状态机", 
3769     //所以就是为了不让它高频率的更新box的属性
3770     childViewHeight: Number;
3771     
3772     //只读: 
3773     object: Object;            //
3774     visible: Bool;            //自己是否已经显示
3775     root: CanvasTreeList;    //向上遍历查找自己的root
3776     rect: Box;                //new一个新的box并根据自己和所有子更新此box的值
3777     box: Box;                //返回 CanvasIconMenu.box
3778 
3779 method:
3780     bindEvent(cir: CanvasImageDraw, root: CanvasTreeList): this; //绑定默认事件
3781     unbindEvent(): undefined;                 //解绑默认事件 (如果此类不在使用的话可以不用解除事件)
3782 
3783     addToList(arr: Array);                    //添加到数组, arr一般是 CanvasImageDraw.list 渲染队列
3784     removeToList(arr: Array);                //从数组删除
3785     
3786     visibleChild(root): undefined;             //显示 (如果自己为隐藏状态则会想查找已显示的ctl,并从ctl展开至自己为止)
3787     hiddenChild(root): undefined;            //隐藏
3788 
3789     getByObject(object): CanvasTreeList;    //查找 如果不存在返回 undefined;
3790     setName(name: String, arr: Array);        //销毁已有的.objectView, 重新创建.objectView
3791     pos(x, y: Number): this;                //此方法是专门为 root 设的; 如果你让不是root的root调用root的方法的话 也行
3792 
3793     //删除自己和所有的下级 例子:
3794     removeChild(cir){
3795         const i = cir.list.length;
3796         this.traverse(v => {
3797             v.unbindEvent(); //删除事件
3798             v.removeToList(cir.list); //删除视图
3799         });
3800         
3801         //删除结构
3802         const parent = this.parent;
3803         parent.removeChild(this);
3804         
3805         //结尾
3806         parent.visibleChild(); //如果已知 root 就戴上, 否则它会自己寻找root
3807         cir.redraw();
3808     }
3809 
3810 demo:
3811     const info = {
3812         name_attributesName: "CTL_name",
3813         iamges_attributesName: "className",
3814         images: {
3815             CanvasImage: emptyCIC.size(20, 20).rect().fill("blue").shear(),
3816             CanvasImages: emptyCIC.size(20, 20).rect().stroke("blue").shear(),
3817         },
3818     }
3819 
3820     const cir = new CanvasImageDraw({width: innerWidth, height: innerHeight});
3821     const cie = new CanvasImageEvent(cir);
3822 
3823     const onclick = function (event) {
3824         ctl_root.traverse(ctl => console.log(ctl.visible, ctl.childViewHeight))
3825     }
3826 
3827     const ctl_root = new CanvasTreeList(new CanvasImage(), info)
3828     .addToList(cir.list)
3829     .bindEvent(cir, cie, onclick);
3830 
3831     ctl_root.appendChild(new CanvasTreeList(new CanvasImages(), info))
3832     .addToList(cir.list)
3833     .bindEvent(cir, cie, onclick);
3834 
3835     ctl_root.pos(10, 10).visibleChild();
3836     cir.render();
3837 */
3838 class CanvasTreeList extends TreeStruct{
3839 
3840     static info = null;
3841 
3842     static childLineStyle = {
3843         strokeStyle: "#ffffff",
3844         lineWidth: 2,
3845     }
3846 
3847     #info = null;
3848     #option = null;
3849     #object = null;
3850     get object(){
3851         return this.#object;
3852     }
3853 
3854     #visible = false;
3855     get visible(){
3856         return this.#visible;
3857     }
3858 
3859     get root(){
3860         var par = this;
3861         while(true){
3862             if(par.parent === null) break;
3863             par = par.parent;
3864         }
3865         return par;
3866     }
3867 
3868     get rect(){
3869         var maxX = this.objectView.background.box.mx, _x;
3870         this.traverse(ctl => {
3871             if(ctl.visible === true){
3872                 _x = ctl.objectView.background.box.mx;
3873                 maxX = Math.max(_x, maxX);
3874             }
3875         });
3876         
3877         const masksOffset = this.objectView.masksOffset, x = this.childIcon.box.x - masksOffset;
3878 
3879         return new Box(x, this.childIcon.box.y - masksOffset, maxX - x, this.childViewHeight + this.objectView.background.box.h);
3880     }
3881 
3882     get box(){
3883         return this.objectView.box;
3884     }
3885 
3886     constructor(object, info = CanvasTreeList.info, option = Object.assign({}, CanvasIconMenu.option)){
3887         if(UTILS.isObject(object) === false) return console.warn("[CanvasTreeList] 参数错误: ", object);
3888         super();
3889         this.#object = object;
3890         this.#info = info;
3891         this.#option = option;
3892     
3893         //this.objectView
3894         this.setName();
3895 
3896         //this.childIcon
3897         const size = this.objectView.contentHeight, x = (size - option.textSize) / 2, s = x + option.textSize;
3898         this.childIcon = new CanvasImages([
3899             emptyCIC.size(size, size).rect(option.borderRadius, option.borderSize).fill(option.backgroundColor)
3900             .path([x, x, x, s, s, size/2], true).fill(option.textColor).cloneCanvas(),
3901             emptyCIC.size(size, size).rect(option.borderRadius, option.borderSize).fill(option.backgroundColor)
3902             .path([x, x, s, x, size/2, s], true).fill(option.textColor).cloneCanvas(),
3903         ]);
3904         
3905         //this.childView
3906         this.childView = new CanvasImageCustom().size(1,1);
3907         this.childView.box.size(CanvasTreeList.childLineStyle.lineWidth || 1, 0);
3908         this.childView.path2D = new CanvasPath2D("stroke", CanvasTreeList.childLineStyle, "before");
3909         this.path2DLine = new Line();
3910         this.childView.path2D.line(this.path2DLine);
3911         this.childViewHeight = 0;
3912         
3913         //visible
3914         this.childIcon.cursor = 0;
3915         this.childIcon.visible = 
3916         this.objectView.background.visible = 
3917         this.objectView.icons.visible = 
3918         this.objectView.masks.visible = 
3919         this.childView.visible = false;
3920     }
3921 
3922     getByObject(object){
3923         if(UTILS.isObject(object) === false) return;
3924         var result;
3925         this.traverse(ctl => {
3926             if(result !== undefined) return "continue";
3927             if(ctl.object === object){
3928                 result = ctl;
3929                 return "continue";
3930             }
3931         });
3932         return result;
3933     }
3934 
3935     setName(name, arr, cis){
3936         var objectView;
3937 
3938         if(UTILS.isObject(this.#info)){
3939             const objectType = this.#object[this.#info.iamges_attributesName];
3940 
3941             if(typeof name === "string" && name !== ""){
3942                 this.#object[this.#info.name_attributesName] = name;
3943             }else if(typeof this.#object[this.#info.name_attributesName] === "string" && this.#object[this.#info.name_attributesName] !== ""){
3944                 name = this.#object[this.#info.name_attributesName];
3945             }else{
3946                 name = objectType;
3947             }
3948 
3949             objectView = new CanvasIconMenu(this.#info.images[objectType], name, this.#option);
3950         }
3951 
3952         else objectView = new CanvasIconMenu(null, "", this.#option);
3953 
3954         //补遮罩层样式
3955         objectView.createMasksImages();
3956 
3957         //const scope = this;
3958         //Object.defineProperty(objectView.background, "scope", {get (){return scope;}});
3959 
3960         //更改渲染队列
3961         if(CanvasIconMenu.prototype.isPrototypeOf(this.objectView)){
3962             if(Array.isArray(arr)){
3963                 const i = arr.indexOf(this.objectView.background);
3964                 if(i !== -1){
3965                     //scroll
3966                     if(CanvasImageScroll.prototype.isPrototypeOf(cis)){
3967                         cis.bindScroll(objectView.background);
3968                         cis.bindScroll(objectView.icons);
3969                         cis.bindScroll(objectView.masks);
3970                         cis.changeCIAdd(objectView.background);
3971                         cis.changeCIAdd(objectView.icons);
3972                         cis.changeCIAdd(objectView.masks);
3973                     }
3974 
3975                     arr[i] = objectView.background;
3976                     arr[i+1] = objectView.icons;
3977                     arr[i+2] = objectView.masks;
3978                     
3979                     objectView.background.visible = this.objectView.background.visible;
3980                     objectView.icons.visible = this.objectView.icons.visible;
3981                     objectView.masks.visible = this.objectView.masks.visible;
3982 
3983                     objectView.icons.cursor =  this.objectView.icons.cursor;
3984                     objectView.masks.cursor =  this.objectView.masks.cursor;
3985 
3986                     objectView.pos(this.objectView.box.x, this.objectView.box.y);
3987 
3988                     this.objectView = objectView;
3989 
3990                 }else console.warn("[CanvasTreeList] setName: 修改失败, 无法找到目标");
3991             }else console.warn("[CanvasTreeList] setName: 第二个参数为Array");
3992         }
3993 
3994         else this.objectView = objectView;
3995     }
3996 
3997     pos(x, y){
3998         if(this.#visible === false){
3999             this.childIcon.cursor = 1;
4000             this.childIcon.visible = 
4001             this.childView.visible = true;
4002 
4003             this.#visible = 
4004             this.objectView.background.visible = 
4005             this.objectView.icons.visible = 
4006             this.objectView.masks.visible = true;
4007         }
4008     
4009         const masksOffset = this.objectView.masksOffset;
4010         this.childIcon.pos(masksOffset+x, masksOffset+y); 
4011         this.objectView.pos(this.childIcon.box.mx, this.childIcon.box.y - masksOffset);
4012         this.childView.box.pos(this.childIcon.box.x + this.childIcon.box.w / 2, this.objectView.background.box.my);
4013         return this;
4014     }
4015 
4016     addToList(arr){
4017         this.traverse(ctl => {
4018             arr.push(ctl.childIcon, ctl.objectView.background, ctl.objectView.icons, ctl.objectView.masks, ctl.childView);
4019         });
4020         
4021         return this;
4022     }
4023 
4024     removeToList(arr){
4025         this.traverse(ctl => {
4026             const i = arr.indexOf(ctl.childIcon);
4027             if(i !== -1) arr.splice(i, 5);
4028         });
4029     }
4030 
4031     visibleChild(root = this.root){
4032         this.childIcon.cursor = 1;
4033         if(root !== this){
4034             this.childIcon.visible = 
4035             this.childView.visible = this.children.length === 0 ? false : true;
4036         }
4037         
4038         if(this.#visible){
4039             this._visibleChild();
4040             root.childViewHeight = 0;
4041             root._updateSizeChild();
4042             root._updatePositionChild();
4043             root.childView.box.h = root.path2DLine.y1 = root.childViewHeight;
4044         }else{
4045             if(root === this) return console.warn("[CanvasTreeList] visibleChild: 展开失败, 请使用 root.pos() 方法初始化root");
4046             let _ctl = root;
4047             this.traverseUp(ctl => {
4048                 if(ctl.visible){
4049                     _ctl = ctl;
4050                     return "break";
4051                 }
4052                 if(ctl.childIcon.cursor !== 1) ctl.childIcon.cursor = 1;
4053             });
4054             _ctl.visibleChild(root);
4055         }
4056     }
4057 
4058     hiddenChild(root = this.root){
4059         if(this.#visible){
4060             this.childIcon.cursor = 0;
4061             this._hiddenChild();
4062             root.childViewHeight = 0;
4063             root._updateSizeChild();
4064             root._updatePositionChild();
4065             root.childView.box.h = root.path2DLine.y1 = root.childViewHeight;
4066         }
4067     }
4068 
4069     //修改: 包括自己所有的子
4070     bindEvent(cir, root){
4071         if(CanvasTreeList.prototype.isPrototypeOf(root) === false || root.parent !== null) console.warn("CanvasTreeList: 请显示声明 root");
4072         
4073         const onup = info => {
4074             if(info.delta < 600){
4075                 if(this.childIcon.cursor === 0) this.visibleChild(root);
4076                 else if(this.childIcon.cursor === 1) this.hiddenChild(root);
4077                 cir.redraw();
4078             }
4079         }
4080 
4081         this.childIcon.addEventListener("up", onup);
4082     }
4083 
4084     //以下属性限内部使用
4085     _visible(){
4086         if(this.#visible === false){
4087             this.childIcon.visible = 
4088             this.childView.visible = this.children.length === 0 ? false : true;
4089 
4090             this.#visible = 
4091             this.objectView.background.visible = 
4092             this.objectView.icons.visible = 
4093             this.objectView.masks.visible = true;
4094         }
4095     }
4096 
4097     _visibleChild(){
4098         for(let k = 0, len = this.children.length, child; k < len; k++){
4099             child = this.children[k];
4100             child._visible();
4101             if(child.childIcon.cursor === 1) child._visibleChild();
4102         }
4103     }
4104 
4105     _hidden(){
4106         if(this.#visible === true){
4107             this.#visible = 
4108             this.childIcon.visible = 
4109             this.objectView.background.visible = 
4110             this.objectView.icons.visible = 
4111             this.objectView.masks.visible = 
4112             this.childView.visible = false;
4113         }
4114     }
4115 
4116     _hiddenChild(){
4117         for(let k = 0, len = this.children.length, child; k < len; k++){
4118             child = this.children[k];
4119             child._hidden();
4120             child._hiddenChild();
4121         }
4122     }
4123 
4124     _updateSize(){
4125         const itemHeight = this.objectView.background.box.h;
4126         var par = this.parent;
4127         while(par !== null){
4128             par.childViewHeight += itemHeight;
4129             par.path2DLine.y1 = par.childViewHeight;
4130             par = par.parent;
4131         }
4132     }
4133 
4134     _updateSizeChild(){
4135         for(let k = 0, len = this.children.length, child; k < len; k++){
4136             child = this.children[k];
4137             child.path2DLine.y1 = child.childViewHeight = 0;
4138             if(child.visible){
4139                 child._updateSize();
4140                 child._updateSizeChild();
4141             }
4142         }
4143     }
4144 
4145     _updatePosition(outCTL){
4146         const masksOffset = this.parent.objectView.masksOffset,  
4147         mx_parent = this.parent.childIcon.box.mx,
4148         my_outCTL = outCTL !== undefined ? outCTL.childView.box.y+outCTL.childViewHeight : this.parent.objectView.background.box.my;
4149         
4150         //childIcon
4151         this.childIcon.pos(
4152             mx_parent + masksOffset, 
4153             my_outCTL + masksOffset
4154         );
4155         
4156         //objectView
4157         this.objectView.pos(
4158             this.childIcon.visible === true ? this.childIcon.box.mx : mx_parent, 
4159             my_outCTL
4160         );
4161         
4162         //childView
4163         this.childView.box.pos(mx_parent + this.childIcon.box.w / 2, this.objectView.background.box.my);
4164         this.childView.box.h = this.childViewHeight;
4165     }
4166 
4167     _updatePositionChild(){
4168         for(let k = 0, len = this.children.length, outChild, child; k < len; k++){
4169             child = this.children[k];
4170             if(child.visible){
4171                 child._updatePosition(outChild);
4172                 child._updatePositionChild();
4173                 outChild = child;
4174             }
4175         }
4176     }
4177 
4178 }
4179 
4180 
4181 /* HTUI 调试某对象的属性
4182 static: 
4183     getData(object: Object): Array; //根据 object 生成 HTUI 所需的 data;
4184 
4185 parameter: 
4186     object: Object, //任意一个对象
4187 
4188     data: Array[{
4189         //必须:
4190         valueUrl: string, //相对于.target 对象的路径; (内部使用示例: eval("object"+valueUrl))
4191         
4192         //可选:
4193         twoWayBind: boolean;     //是否双向绑定; 默认是单向(控件 -> object)
4194         type: String;             //指定控件类型, 如果可以最好定义此属性, 如果未定义 HTUI 通过object的属性值来自动选择控件类型; 值参见下面 '支持的类型' 栏
4195         title: String;             //标题
4196         onchange: Function,     //控件改变时触发
4197         // 暂不支持: explain: String;         //详细说明
4198 
4199         mini: boolean; //控件 mini 型; 默认true; (text, select 控件才有效)
4200         range: object{min, max, step: Number}, //(number 控件才有效)
4201         select: Array[name, value || Object{name:String, value:Any}], //(select 控件才有效)
4202         butVal: string; //按钮的value, (button 控件才有效)
4203     }],
4204 
4205     title: string, //标题 默认为 ""
4206 
4207 attributes: 
4208     target: Object;
4209     data: Array;
4210     title: string;
4211     domElement: HTMLDivElement;
4212     setTypeAuto: boolean; //如果遇到未定义type的data是否自动设置type; 默认false (data如果定义了type, visible时较快)
4213 
4214 method: 
4215     render(parentElem): this;     //
4216     visible(): this;    //显示 (根据data创建控件并添加至 DocumentFragment, 然后把 DocumentFragment 丢进 domElement)
4217     hidden(): this;        //隐藏
4218 
4219 支持的类型: 
4220     text    (单行文本控件) 
4221     textarea(多行文本控件) 
4222     select    (单选控件)    
4223 
4224     color    (颜色控件)    
4225     number    (数值控件)    
4226     bool    (bool控件)    
4227     button    (按钮控件)    
4228     image    (上传图片控件)
4229 
4230     //特殊
4231     line    (分界线, .type = 'line'; .value = valueUrl|Array[valueUrl])
4232 
4233 demo: 
4234     const object = {
4235         name: "test",
4236         nameB: "abbbb",
4237         color: "#ff0000",
4238         bool: true,
4239         select: {name: "#ff0000"},
4240         selectA: 0,
4241         numA: 0,
4242     }
4243 
4244     const data = HTUI.getData(object);
4245     const htui = new HTUI(object, data, "test 测试 TEST").visible().render();
4246     console.log(data);
4247 */
4248 class HTUI {
4249 
4250     static CCTV = {
4251         cid: null,
4252         cie: null,
4253         value: null,
4254         elH: null,
4255         visible(v, x, y, w, h, f){
4256             this.hidden();
4257             
4258             const elH = ElementUtils.createElem("div");
4259             elH.onclick = () => this.hidden();
4260             elH.style = `
4261                 position: absolute;
4262                 width: ${innerWidth}px;
4263                 height: ${innerHeight}px;
4264             `;
4265             document.body.appendChild(elH);
4266             
4267             const cctv = new CanvasColorTestViewer({
4268                 width: w,             //默认 250 
4269                 height: h,         //默认 w*0.618
4270             }),
4271             cid = new CanvasImageDraw({width: cctv.box.w, height: cctv.box.h}),
4272             cie = new CanvasEvent(cid);
4273             
4274             
4275             cctv.set(v).update();
4276             cctv.addToList(cid.list);
4277             cctv.bindEvent(cid, null, f);
4278             cid.domElement.style = `
4279                 position: absolute;
4280                 top: ${y}px;
4281                 left: ${x}px;
4282                 -webkit-box-shadow:#666 1px 2px 4px 1px; -moz-box-shadow:#666 1px 2px 4px 1px; box-shadow:#666 1px 2px 4px 1px;
4283             `;
4284             cid.render();
4285             this.cid = cid;
4286             this.cie = cie;
4287             this.value = cctv;
4288             this.elH = elH;
4289         },
4290         hidden(){
4291             if(this.cid === null) return;
4292             this.cie.unbindEvent();
4293             this.cid.exit();
4294             ElementUtils.removeChild(this.elH);
4295             this.elH = this.cid = this.cie = this.value = null;
4296         }
4297     }
4298 
4299     static getValueByUrl(object, valueUrl){
4300         try{
4301             return eval("object" + valueUrl);
4302         }
4303         catch(e){
4304             console.error(e);
4305         }
4306     }
4307 
4308     static readValueUrl(object, valueUrl){
4309         var urls = [], str = "";
4310 
4311         for(let k = 0, v, len = valueUrl.length; k < len; k++){
4312             v = valueUrl[k];
4313             if(v === " ") continue;
4314             
4315             if(v === '.' || v === '[' || v === ']'){
4316                 if(str !== ""){
4317                     urls.push(str);
4318                     str = "";
4319                 }
4320                 
4321             }
4322 
4323             else str += v;
4324 
4325         }
4326 
4327         if(str !== "") urls.push(str);
4328         
4329         if(urls.length > 1){
4330             let _len = urls.length - 1;
4331             for(let k = 0; k < _len; k++) object = object[urls[k]];
4332             str = urls[_len];
4333         }
4334 
4335         else str = urls[0];
4336         
4337         urls = undefined;
4338         
4339         return {
4340             object: object,
4341             name: str,
4342         }
4343     }
4344 
4345     static isValueUrl(object, valueUrl){
4346         const v = HTUI.getValueByUrl(object, valueUrl);
4347 
4348         if(v === undefined) return false;
4349         
4350         switch(typeof v){
4351             case "object":
4352             if(v === null) return false;
4353             else if(v.isColor === true || v.isRGBColor === true || CanvasImageDraw.isCanvasImage(v) === true) return true;
4354             return false;
4355 
4356             case "string": 
4357             case "number": 
4358             case "boolean": 
4359             case "function": 
4360             return true;
4361 
4362             default: return false;
4363         }
4364     }
4365 
4366     static getData(object){
4367         const data = [];
4368         for(let n in object){
4369             const url = "."+n;
4370             if(HTUI.isValueUrl(object, url) === false) continue;
4371             data.push({
4372                 valueUrl: url,
4373                 title: n,
4374             });
4375         }
4376         return data;
4377     }
4378 
4379     #isLine = false;
4380     #itemH = 20;
4381     #df = document.createDocumentFragment();
4382     #clear = document.createElement("div");
4383     
4384     constructor(object = {}, data = [], title = ""){
4385         this.target = object;
4386         this.data = data;
4387         this.title =  title;
4388         this.domElement = document.createElement("div");
4389         this.setTypeAuto = false;
4390         this.#clear.style.clear = "both";
4391         this.domElement.style = `
4392             min-width: 120px;
4393             max-width: 600px;
4394             width: 250px;
4395             border: 1px solid #000000;
4396             background: #ffffff;
4397             position: absolute;
4398             font-size: 12px;
4399             color: #000000;
4400             overflow: hidden auto;
4401         `;
4402     }
4403 
4404     render(parentElem = document.body){
4405         parentElem.appendChild(this.domElement);
4406         this.domElement.style.maxHeight = (innerHeight - this.domElement.getBoundingClientRect().y)+"px";
4407         return this;
4408     }
4409 
4410     visible(){
4411         this.createTitleG(true);
4412         for(let k = 0; k < this.data.length; k++){
4413             this.handler(this.data[k]);
4414             this.#isLine = this.data[k].type === 'line';
4415         }
4416         this.#df.appendChild(this.#clear);
4417         this.domElement.innerHTML = "";
4418         this.domElement.appendChild(this.#df);
4419         return this;
4420     }
4421 
4422     hidden(){
4423         this.createTitleG(false);
4424         this.#df.appendChild(this.#clear);
4425         this.domElement.innerHTML = "";
4426         this.domElement.appendChild(this.#df);
4427         return this;
4428     }
4429 
4430     //以下方法限内部调用
4431     handler(data){
4432         if(data.type === 'line'){
4433             if(this.#isLine === true) return;
4434             return this.createLine(data);
4435         }
4436 
4437         const v = HTUI.getValueByUrl(this.target, data.valueUrl);
4438 
4439         if(v === undefined) return;
4440         
4441         switch(data.type){
4442             case "text": 
4443             this.createText(data, v);
4444             return true;
4445 
4446             case "textarea": 
4447             this.createTextarea(data, v);
4448             return true;
4449 
4450             case "bool": 
4451             this.createCheckbox(data, v);
4452             return true;
4453 
4454             case "color": 
4455             this.createColor(data, v);
4456             return true;
4457 
4458             case "number": 
4459             this.createNumber(data, v);
4460             return true;
4461 
4462             case "button": 
4463             this.createFunc(data);
4464             return true;
4465 
4466             case "image":
4467             this.createFileImage(data, v);
4468             return true;
4469 
4470             case "select":
4471             this.createSelect(data, v);
4472             return true;
4473         }
4474 
4475         if(Array.isArray(data.select) === true){
4476             if(this.setTypeAuto === true) data.type = "select";
4477             this.createSelect(data, v);
4478             return true;
4479         }
4480         
4481         switch(typeof v){
4482             case "object": 
4483             if(v === null) return;
4484             else if(v.isColor === true || v.isRGBColor === true){
4485                 if(this.setTypeAuto === true) data.type = "color";
4486                 this.createColor(data, v);
4487             }
4488             else if(CanvasImageDraw.isCanvasImage(v) === true){
4489                 if(this.setTypeAuto === true) data.type = "image";
4490                 this.createFileImage(data, v);
4491             }
4492             else return;
4493             break;
4494 
4495             case "string": 
4496             const _v = emptyColor.stringToColor(v);
4497             if(_v !== ""){
4498                 if(this.setTypeAuto === true) data.type = "color";
4499                 this.createColor(data, _v);
4500             } else {
4501                 if(this.setTypeAuto === true) data.type = "text";
4502                 this.createText(data, v);
4503             }
4504             break;
4505 
4506             case "number": 
4507             if(this.setTypeAuto === true) data.type = "number";
4508             this.createNumber(data, v);
4509             break;
4510 
4511             case "boolean": 
4512             if(this.setTypeAuto === true) data.type = "bool";
4513             this.createCheckbox(data, v);
4514             break;
4515             
4516             case "function": 
4517             if(this.setTypeAuto === true) data.type = "button";
4518             this.createFunc(data);
4519             break;
4520 
4521             default: return;
4522         }
4523 
4524         return true;
4525     }
4526 
4527     createTitleG(v){
4528         const el = document.createElement("div"),
4529         elA = ElementUtils.createElem("h5", "", v ? "▼" : "▶"),
4530         elT = ElementUtils.createElem("h5", "", this.title),
4531         elM = ElementUtils.createElem("h5", "", ":::");
4532 
4533         var sx, sy, rect;
4534         const onup = event => {
4535             elM.releasePointerCapture(event.pointerId);
4536             elM.onpointermove = elM.onpointerup = null;
4537         },
4538         onmove = event => {
4539             const y = event.pageY - sy;
4540             this.domElement.style.left = (event.pageX - sx)+"px";
4541             this.domElement.style.top = y+"px";
4542             this.domElement.style.maxHeight = (innerHeight - y)+"px";
4543         }
4544 
4545         elM.onpointerdown = event => {
4546             rect = this.domElement.getBoundingClientRect();
4547             sx = event.pageX - rect.x;
4548             sy = event.pageY - rect.y;
4549 
4550             elM.setPointerCapture(event.pointerId);
4551             elM.onpointermove = onmove;
4552             elM.onpointerup = onup;
4553         }
4554 
4555         elA.onclick = elT.onclick = () => {
4556             if(v) this.hidden();
4557             else this.visible();
4558         }
4559 
4560         el.style = `
4561             width: 100%;
4562             height: ${this.#itemH}px;
4563             float: left;
4564             overflow: hidden;
4565             line-height: ${this.#itemH}px;
4566             cursor: default;
4567             text-align: center;
4568             background: #ffffff;
4569             -moz-user-select: none; -o-user-select:none; -khtml-user-select:none;-webkit-user-select:none;-ms-user-select:none; user-select:none;
4570         `;
4571         elA.style = elT.style = elM.style = `
4572             height: ${this.#itemH}px;
4573             position: absolute;
4574         `;
4575 
4576         this.domElement.onscroll = () => {
4577             if(this.domElement.scrollTop > this.#itemH){
4578                 el.style.width = this.domElement.offsetWidth+"px";
4579                 el.style.position = "fixed";
4580                 el.style.float = "unset";
4581             } else {
4582                 el.style.width = "100%";
4583                 el.style.position = "unset";
4584                 el.style.float = "left";
4585             }
4586         }
4587 
4588         elA.style.width = elM.style.width = this.#itemH+"px";
4589         elA.style.left = elM.style.right = "2px";
4590         elT.style.width = "100%";
4591 
4592         elA.title = v ? "点击关闭" : "点击打开";
4593         elT.title = this.title;
4594         elM.title = "按下拖动";
4595         
4596         el.appendChild(elT);
4597         el.appendChild(elA);
4598         el.appendChild(elM);
4599         this.#df.appendChild(el);
4600     }
4601 
4602     createTitle(p, v, ptb = "0"){
4603         const el = ElementUtils.createElem("p", "", v);
4604         const elB = ElementUtils.createElem("p", "", ":");
4605         el.title = elB.title = v;
4606         el.style = `
4607             width: 20%;
4608             height: ${this.#itemH}px;
4609             float: left;
4610             line-height: ${this.#itemH}px;
4611             text-indent: 2px;
4612             padding-top: ${ptb};
4613             padding-bottom: ${ptb};
4614             cursor: default;
4615             overflow: hidden;
4616         `;
4617         elB.style = `
4618             width: 4%;
4619             height: ${this.#itemH}px;
4620             float: left;
4621             text-align: center;
4622             line-height: ${this.#itemH}px;
4623             padding-top: ${ptb};
4624             padding-bottom: ${ptb};
4625             cursor: default;
4626         `;
4627         p.appendChild(el);
4628         p.appendChild(elB);
4629     }
4630 
4631     createLine(d){
4632         var is;
4633 
4634         if(typeof d.value === "string") is = HTUI.getValueByUrl(this.target, d.value) !== undefined;
4635 
4636         else if(Array.isArray(d.value) === true){
4637             for(let k = 0, len = d.value.length; k < len; k++){
4638                 if(HTUI.getValueByUrl(this.target, d.value[k]) !== undefined){
4639                     is = true;
4640                     break;
4641                 }
4642             }
4643         }
4644 
4645         if(is === true){
4646             const p = ElementUtils.createElem("div");
4647             p.style = `
4648                 width: 100%;
4649                 float: left;
4650                 padding-top: 5px;
4651                 padding-bottom: 5px;
4652                 height: ${this.#itemH}px;
4653             `;
4654 
4655             const el = ElementUtils.createElem("div");
4656             el.style = `
4657                 width: 98%;
4658                 height: 1px;
4659                 float: left;
4660                 margin-left: 1%;
4661                 border-top: 1px dashed #000000;
4662                 margin-top: ${this.#itemH/2}px;
4663             `;
4664 
4665             p.appendChild(el);
4666             this.#df.appendChild(p);
4667         }
4668         
4669         return is;
4670     }
4671 
4672     createText(d, v){
4673         const p = ElementUtils.createElem("div");
4674         p.style = `
4675             float: left;
4676             padding-top: 5px;
4677             padding-bottom: 5px;
4678         `;
4679 
4680         const el = ElementUtils.createInput("text");
4681         el.style = `
4682             float: left;
4683             text-indent: 2px;
4684         `;
4685         
4686         el.onchange = event => {
4687             v = event.target.value;
4688             eval("this.target"+d.valueUrl+"=v");
4689             if(typeof d.onchange === "function") d.onchange(v);
4690         };
4691 
4692         if(d.twoWayBind === true){
4693             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4694             Object.defineProperty(read.object, read.name, {
4695                 get (){return v;},
4696                 set (a){v = el.value = a;},
4697             });
4698         }
4699 
4700         if(d.mini !== false){
4701             p.style.width = "26%";
4702             p.style.height = this.#itemH+"px";
4703             el.style.width = "90%";
4704             el.style.height = "100%";
4705             this.createTitle(this.#df, d.title, "5px");
4706         } else {
4707             p.style.width = "100%";
4708             el.style.width = "72%";
4709             el.style.height = this.#itemH+"px";
4710             this.createTitle(p, d.title);
4711         }
4712 
4713         el.value = v;
4714         p.appendChild(el);
4715         this.#df.appendChild(p);
4716     }
4717 
4718     createTextarea(d, v){
4719         const p = ElementUtils.createElem("div");
4720         p.style = `
4721             width: 100%;
4722             float: left;
4723             padding-top: 5px;
4724             padding-bottom: 5px;
4725         `;
4726 
4727         this.createTitle(p, d.title);
4728 
4729         const el = ElementUtils.createElem("textarea");
4730         el.style = `
4731             max-width: 72%;
4732             width: 72%;
4733             height: ${this.#itemH*3}px;
4734             min-height: ${this.#itemH*3}px;
4735             float: left;
4736             padding: 1px;
4737         `;
4738         
4739         el.onchange = event => {
4740             v = event.target.value;
4741             eval("this.target"+d.valueUrl+"=v");
4742             if(typeof d.onchange === "function") d.onchange(v);
4743         };
4744 
4745         if(d.twoWayBind === true){
4746             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4747             Object.defineProperty(read.object, read.name, {
4748                 get (){return v;},
4749                 set (a){v = el.value = a;},
4750             });
4751         }
4752 
4753         el.value = v;
4754         p.appendChild(el);
4755         this.#df.appendChild(p);
4756     }
4757 
4758     createSelect(d, v){
4759         const p = ElementUtils.createElem("div");
4760         p.style = `
4761             float: left;
4762             padding-top: 5px;
4763             padding-bottom: 5px;
4764             font-size: 12px;
4765         `;
4766         
4767         const el = ElementUtils.createElem("select");
4768         el.style = `
4769             float: left;
4770         `;
4771         
4772         const values = []
4773         switch(typeof d.select[0]){
4774             case "string": 
4775             for(let i = 0; i < d.select.length; i += 2){
4776                 const o = ElementUtils.createElem("option");
4777                 o.value = values[i/2] = d.select[i+1];
4778                 o.textContent = d.select[i];
4779                 el.appendChild(o);
4780             }
4781             break;
4782 
4783             case "object": 
4784             for(let i = 0; i < d.select.length; i++){
4785                 const o = ElementUtils.createElem("option");
4786                 o.value = values[i] = d.select[i].value;
4787                 o.textContent = d.select[i].name;
4788                 el.appendChild(o);
4789             }
4790             break;
4791         }
4792 
4793         el.onchange = () => {
4794             v = values[el.options.selectedIndex];
4795             eval("this.target"+d.valueUrl+"=v");
4796             if(typeof d.onchange === "function") d.onchange(v);
4797         };
4798 
4799         if(d.twoWayBind === true){
4800             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4801             Object.defineProperty(read.object, read.name, {
4802                 get (){return v;},
4803                 set (a){
4804                     v = a;
4805                     el.options.selectedIndex = values.indexOf(a);
4806                 },
4807             });
4808         }
4809 
4810         if(d.mini !== false){
4811             p.style.width = "26%";
4812             p.style.height = this.#itemH+"px";
4813             el.style.width = "90%";
4814             el.style.height = "100%";
4815             this.createTitle(this.#df, d.title, "5px");
4816         } else {
4817             p.style.width = "100%";
4818             el.style.width = "72%";
4819             el.style.height = this.#itemH+"px";
4820             this.createTitle(p, d.title);
4821         }
4822 
4823         el.options.selectedIndex = values.indexOf(v);
4824         p.appendChild(el);
4825         this.#df.appendChild(p);
4826     }
4827 
4828     createNumber(d, v){
4829         const p = ElementUtils.createElem("div");
4830         p.style = `
4831             width: 26%;
4832             float: left;
4833             padding-top: 5px;
4834             padding-bottom: 5px;
4835             height: ${this.#itemH}px;
4836         `;
4837 
4838         this.createTitle(this.#df, d.title, "5px");
4839 
4840         const el = ElementUtils.createInput("number");
4841         el.style = `
4842             width: 90%;
4843             height: 100%;
4844         `;
4845 
4846         if(d.range){
4847             el.min = d.range.min;
4848             el.max = d.range.max;
4849             el.step = d.range.step;
4850         }
4851         
4852         el.onchange = () => {
4853             if(v !== el.valueAsNumber){
4854                 v = el.valueAsNumber;
4855                 eval("this.target"+d.valueUrl+"=v");
4856                 if(typeof d.onchange === "function") d.onchange(v);
4857             }
4858         };
4859         const timer = new Timer(el.onchange, 1000/30, Infinity, false),
4860         onup = event => {
4861             el.releasePointerCapture(event.pointerId);
4862             timer.stop();
4863             el.onchange();
4864         }
4865         el.onpointerdown = event => {
4866             el.setPointerCapture(event.pointerId);
4867             el.onpointerup = onup;
4868             el.onchange();
4869             timer.restart();
4870         }
4871         if(d.twoWayBind === true){
4872             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4873             Object.defineProperty(read.object, read.name, {
4874                 get (){return v;},
4875                 set (a){
4876                     v = a;
4877                     el.value = String(a);
4878                 },
4879             });
4880         }
4881 
4882         el.value = String(v);
4883         p.appendChild(el);
4884         this.#df.appendChild(p);
4885     }
4886 
4887     createCheckbox(d, v){
4888         const p = ElementUtils.createElem("div");
4889         p.style = `
4890             width: 26%;
4891             float: left;
4892             padding-top: 5px;
4893             padding-bottom: 5px;
4894             height: ${this.#itemH}px;
4895         `;
4896 
4897         this.createTitle(this.#df, d.title, "5px");
4898 
4899         const el = ElementUtils.createInput("checkbox");
4900         const s = this.#itemH * 0.8;
4901         el.style = `
4902             width: ${s}px;
4903             height: ${s}px;
4904             margin-top: ${(this.#itemH - s) / 2}px;
4905         `;
4906         
4907         el.onchange = event => {
4908             v = event.target.checked;
4909             eval("this.target"+d.valueUrl+"=v");
4910             if(typeof d.onchange === "function") d.onchange(v);
4911         };
4912 
4913         if(d.twoWayBind === true){
4914             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4915             Object.defineProperty(read.object, read.name, {
4916                 get (){return v;},
4917                 set (a){el.checked = v = a;},
4918             });
4919         }
4920         
4921         el.checked = v;
4922         p.appendChild(el);
4923         this.#df.appendChild(p);
4924     }
4925 
4926     createFunc(d){
4927         const p = ElementUtils.createElem("div");
4928         p.style = `
4929             width: 26%;
4930             float: left;
4931             padding-top: 5px;
4932             padding-bottom: 5px;
4933             height: ${this.#itemH}px;
4934         `;
4935 
4936         this.createTitle(this.#df, d.title, "5px");
4937 
4938         const el = ElementUtils.createInput("button");
4939         el.value = d.butVal||"Button";
4940         el.style = `
4941             width: 90%;
4942             height: 100%;
4943         `;
4944         
4945         el.onclick = () => {
4946             if(typeof d.onchange === "function") d.onchange();
4947             else eval('this.target'+d.valueUrl+'()');
4948         }
4949 
4950         p.appendChild(el);
4951         this.#df.appendChild(p);
4952     }
4953 
4954     createColor(d, v){
4955         const p = ElementUtils.createElem("div");
4956         p.style = `
4957             width: 26%;
4958             float: left;
4959             padding-top: 5px;
4960             padding-bottom: 5px;
4961             height: ${this.#itemH}px;
4962         `;
4963 
4964         this.createTitle(this.#df, d.title, "5px");
4965 
4966         const el = ElementUtils.createElem("div");
4967         const s = this.#itemH * 0.8;
4968         el.style = `
4969             width: 90%;
4970             height: ${s}px;
4971             margin-top: ${(this.#itemH - s) / 2}px;
4972             border: 1px solid #000000;
4973             border-radius: 2px;
4974         `;
4975         
4976         const iso = typeof v !== "string",
4977         setColor = color => {
4978             _c = color;
4979             el.style.background = color;
4980             if(iso) v.set(color);
4981             else eval("this.target"+d.valueUrl+"=color");
4982         };
4983 
4984         el.onclick = () => {
4985             const rect = this.domElement.getBoundingClientRect();
4986             HTUI.CCTV.visible(_c, rect.x, rect.y+p.offsetTop+this.#itemH+5, rect.width, 0, color => {
4987                 if(typeof d.onchange === "function") d.onchange(color);
4988                 setColor(color);
4989             });
4990         }
4991 
4992         if(d.twoWayBind === true){
4993             const read = HTUI.readValueUrl(this.target, d.valueUrl);
4994             Object.defineProperty(read.object, read.name, {
4995                 get (){return iso ? v : _c;},
4996                 set (a){
4997                     _c = iso ? a.getStyle() : emptyColor.stringToColor(a)||"#ffffff";
4998                     el.style.background = _c;
4999                 },
5000             });
5001         }
5002 
5003         var _c = iso ? v.getStyle() : v;
5004         el.style.background = _c;
5005         p.appendChild(el);
5006         this.#df.appendChild(p);
5007     }
5008 
5009     createFileImage(d, v){
5010         const p = ElementUtils.createElem("div");
5011         p.style = `
5012             width: 26%;
5013             float: left;
5014             padding-top: 5px;
5015             padding-bottom: 5px;
5016             height: ${this.#itemH}px;
5017         `;
5018 
5019         this.createTitle(this.#df, d.title, "5px");
5020         
5021         const bg = ElementUtils.createCanvasTCC(this.#itemH, this.#itemH, Math.ceil(this.#itemH/5), 2);
5022         const con = ElementUtils.createContext(bg.width, bg.height, true);
5023         con.canvas.style = `
5024             float: left;
5025             border-radius: 2px;
5026         `;
5027 
5028         const setVal = image => {
5029             v = CanvasImageDraw.isCanvasImage(image) ? image : null;
5030             con.clearRect(0,0,bg.width,bg.height);
5031             con.drawImage(bg, 0, 0);
5032             if(v !== null){
5033                 const scale = UTILS.getSameScale(v, {width: bg.width, height: bg.height}),
5034                 nw = scale * v.width, nh = scale * v.height;
5035                 con.drawImage(v, (bg.width - nw) / 2, (bg.height - nh) / 2, nw, nh);
5036             }
5037         }
5038 
5039         con.canvas.onpointerdown = () => {
5040             setVal();
5041             eval("this.target"+d.valueUrl+"=null");
5042             if(typeof d.onchange === "function") d.onchange(null);
5043         }
5044 
5045         con.canvas.onclick = () => {
5046             ElementUtils.loadFileImages(urls => {
5047                 const image = new Image();
5048                 image.onload = () => {
5049                     setVal(image);
5050                     eval("this.target"+d.valueUrl+"=image");
5051                     if(typeof d.onchange === "function") d.onchange(image);
5052                 }
5053                 image.src = urls[0];
5054             });
5055         }
5056 
5057         if(d.twoWayBind === true){
5058             const read = HTUI.readValueUrl(this.target, d.valueUrl);
5059             Object.defineProperty(read.object, read.name, {
5060                 get (){return v;},
5061                 set (a){setVal(a);},
5062             });
5063         }
5064 
5065         setVal(v);
5066         p.appendChild(con.canvas);
5067         this.#df.appendChild(p);
5068     }
5069 
5070 }
5071 
5072 
5073 export {
5074     ElementUtils,
5075     CPath2DMeter,
5076     ExitImage20,
5077     CanvasPath2D,
5078     CanvasImageDraw,
5079     CanvasEvent,
5080     CanvasImageScroll,
5081     CanvasEventTarget,
5082     CanvasImage, 
5083     CanvasImages,
5084     CanvasImageCustom,
5085     CanvasImageText,
5086     CanvasProgressBar,
5087     CanvasColorTestViewer,
5088     CarouselFigure,
5089     CanvasIconMenu,
5090     CanvasTreeList,
5091     HTUI,
5092 }
ElementUtils.js

 

 

HTUI 使用示例:

 1 import { HTUI } from "./ElementUtils.js";
 2 
 3   const object = {
 4         text: "text",
 5         textarea: " a: 阿萨的解决\n b: i是独立精神的\n c: SOS大家",
 6         color: "#00ff00",
 7         bool: true,
 8         select: {v:"#00ff00"},
 9         num: 0,
10         selectM: 1,
11     },
12 
13     data = [
14         //文本控件 object.text
15         {
16             valueUrl: ".text",
17             title: "text",
18             mini: false,
19         },
20 
21         //多行文本控件 object.textarea
22         {
23             valueUrl: ".textarea",
24             title: "textarea",
25             type: "textarea",
26         },
27 
28         //颜色和bool控件
29         {
30             valueUrl: ".color",
31             title: "color",
32             twoWayBind: true, //此控件为双向绑定
33         },
34         {
35             valueUrl: ".bool",
36             title: "bool",
37         },
38 
39         //select单选控件(值可以是任意类型)
40         {
41             valueUrl: ".select",
42             title: "select",
43             mini: false,
44             select: ["红色", {v: "#ff0000"}, "绿色", object.select, "蓝色", {v: "#0000ff"}],
45             onchange: v => object.color = v.v, //Object{v}
46         },
47 
48         //数值控件和迷你型单选控件
49         {
50             valueUrl: ".num",
51             title: "num",
52         },
53         {
54             valueUrl: ".selectM",
55             title: "selectM",
56             twoWayBind: true,
57             select: ["零", 0, "一", 1, "二", 2],
58             onchange: v => console.log(v), //v: number
59         },
60     ];
61 
62     const htui = new HTUI(object, data).visible().render();

 

结果图:

 

 

 

 

 

posted @ 2023-03-05 20:24  鸡儿er  阅读(158)  评论(0编辑  收藏  举报