最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

可视化调试某个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 /* Point1084 parameter: 1085     x = 0, y = 0;1086 1087 attribute1088     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 /* Line1299 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, 否则返回交点Point1307     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合并到this1427     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*21734 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合并到this1743     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 = 02254 2255 attribute: 2256     min, max, value: number;2257     ratio: number; //只读; 返回value与min至max之间的比率;2258 2259 method: 2260     setFromRatio(r: number): undefined;    //通过比率设置value2261     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 /* RGBColor2300 parameter: 2301     r, g, b2302 2303 method:2304     set(r, g, b: Number): this;            //rgb: 0 - 255; 第一个参数可以为 css color2305     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: 默认是一个新的Object2315     getRGBA(alpha: Number): String;             //alpha: 0 - 1; 默认 12316     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.number2536     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 || 162606         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,lenY2620     angle: Number;         //8四方向 或 16八方向 默认 162621     timeout: Number;     //超时毫秒 默认 5002622     mapRange: Box;        //地图box2623     //此属性已废弃(run方法不在检测相邻的高) maxHeight: Number;     //相邻可走的最大高 默认 62624 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, iy2785         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 是否能到 b2879         //获取 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 不可走, 既返回 false2889                 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 通过此方法统一更新 TweenValue3007 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 可选 默认 undefined3168     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.#eventDispatcher1002         .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.domElement1387     }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;        //重新计算最大边界x1399     resetMaxSizeX(): undefined;     //重新计算最大边界y1400     unbindEvent(): undefined;         //如果不在使用此类调用1401 1402     bindScrolls(list: Array[CanvasImage]): undefined;    //监听数组里所有的ci, 并更新x或y轴最大边界; list: 默认 cid.list1403 1404     //其它无用的方法(内部自动调用)1405     changeCursorX(): undefined;        //此方法保证游标不会超出可视范围1406     changeCursorY(): undefined;1407     changeCIAdd(ci): undefined;        //根据ci更新边界1408     changeCIDel(ci): undefined;1409     bindScroll(ci): undefined;        //监听ci1410     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每次绘制前更新,所以不用填xywh1424 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 能够监听 CanvasImage1437         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;            //默认创建一个新的Box2088 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],     //层级相同的CanvasEventTarget2109             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 的宽高; 如果未定义就返回 02194     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;    //缩放y2204         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: 旋转弧度, 默认02209         cx, cy: 旋转中心点(局部), 默认 this.w/2, this.h/22210         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;    //默认一个新的 AnimateLoop2224             t: Number;                     //持续毫秒时间, 默认 2002225             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     //CanvasImageDraw2231     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缩放至 scale2275         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         //如果是image2324         if(CanvasImageDraw.isCanvasImage(image)){2325             this.box.size(image.width, image.height);2326             this.image = image;2327         }2328         //如果是 CanvasImage2329         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             onloadstart2377             ondurationchange2378             onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。2379             onloadeddata2380             onprogress2381             oncanplay2382             oncanplaythrough2383 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 属性如果不设的话永远都是02397             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的point2417             point.set(nx + point.x, ny + point.y);2418 2419             //在此之后 cid 会这样操作:2420             //x = ci.x - point.x2421             //y = ci.y - point.y2422             //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 /* CanvasImages2510 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 /* CanvasImageCustom2631 注意: 线模糊问题任然存在(只有.rect()做了模糊优化)2632 parameter:2633     canvas || image || undefined2634     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 /* CanvasImageText2765 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.fillStyle2775             padding,    //内边距, 可能的值: Number || Object{top,right,bottom,left:Number}, 默认 02776             distance,    //文字之间的间距, 可能的值: Number || Object{x,y:Number}, 默认 02777             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,    //动画每秒刷新多少次; 默认 302901         timerSpeed: number,        //计时器的速度; 默认 10002902         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 cursor3071         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 background3083         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.6183178         bgColor,        //默认 #ffffff3179         borderRadius,    //默认 43180         textColor        //默认 #002e233181     }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;//显示或隐藏所以的ci3190     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         //h3233         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         //a3265         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 bg3275         this.bottomBG = new CanvasImage(emptyCIC.size(w, lw, true).rect(r, null, 1).fill(option.bgColor||"#ffffff").cloneCanvas());3276 3277 3278         //result3279         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         //box3285         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.cursorA3332         );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.cursorA3354         );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         //SV3365         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         //H3415         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         //A3457         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         //text3565         textWidth: number,     //如果为0或未定义则根据文字内容自适应宽3566         textSize: number,     //最好不要大于 iconSize3567         textColor: string,    //3568 3569         //边框3570         borderSize: number,     //默认 03571         borderColor: string,     //默认 #ffffff3572         borderRadius: number,    //默认 03573     }3574 3575 attributes:3576     background: CanvasImage;    //它同时实现了 backgroundColor, icon, text3577     icons: CanvasImages;    //右边可更换的图标 宽高为 option.iconSize, option.iconSize3578     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         //background3647         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         //text3654         emptyCIT.fillText(text, textColor, contentOffset + iconSize + padding);3655         if(borderSize > 0) emptyCIT.stroke(borderColor, borderSize);3656         const backgroundImage = emptyCIT.cloneCanvas();3657         3658         //icons3659         this.#icons = new CanvasImages();3660         this.#icons.box.size(iconSize, iconSize);3661 3662         //masks3663         this.#masks = new CanvasImages();3664         this.#masks.box.size(contentWidth, contentHeight);3665         3666         //margin3667         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]: Image3757         },3758     },3759     option: Object, //参见 CanvasIconMenu 的 option3760 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;    //向上遍历查找自己的root3776     rect: Box;                //new一个新的box并根据自己和所有子更新此box的值3777     box: Box;                //返回 CanvasIconMenu.box3778 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, 重新创建.objectView3791     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 就戴上, 否则它会自己寻找root3807         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.objectView3894         this.setName();3895 3896         //this.childIcon3897         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.childView3906         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         //visible3914         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                     //scroll3966                     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         //childIcon4151         this.childIcon.pos(4152             mx_parent + masksOffset, 4153             my_outCTL + masksOffset4154         );4155         4156         //objectView4157         this.objectView.pos(4158             this.childIcon.visible === true ? this.childIcon.box.mx : mx_parent, 4159             my_outCTL4160         );4161         4162         //childView4163         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.6184270             }),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.text15         {16             valueUrl: ".text",17             title: "text",18             mini: false,19         },20 21         //多行文本控件 object.textarea22         {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: number59         },60     ];61 62     const htui = new HTUI(object, data).visible().render();

结果图:

关键词: 一个新的 如果未定义 是否相交