js 原生 canvas 颜色调试器 (CanvasImageRender.js)

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

 

   1 "use strict";
   2 
   3 import {
   4     UTILS, 
   5     Box, 
   6     Rotate,
   7     EventDispatcher,
   8     Point,
   9     AnimateLoop,
  10     TreeStruct,
  11 } from './Utils.js';
  12 
  13 
  14 
  15 
  16 /* CanvasImageRender (渲染 CanvasImage)
  17 
  18 关于canvas的优化:
  19     离屏渲染(预处理图像的 缩放, 旋转, 剪裁, 等)
  20     避免浮点坐标(用Math.floor()函数对所有的坐标点取整, 浏览器为了达到抗锯齿的效果会做额外的运算)
  21     如果可能请关闭透明度 (CanvasImageRender.paramCon.alpha = false)
  22 
  23 关于渲染 CanvasImage
  24     尽量让box.w和box.h与image的宽高保持一致, 否则在渲染时会缩放image
  25     如果你不在使用这些属性请将它们设为默认值: .opacity 默认1, .rotate 默认null
  26     如果多个 CanvasImage 的image是一样的应该: a.loadImage(src); b.setImage(a); c.setImage(a);
  27 
  28 ImageData:
  29     length: w * h * 4
  30     r: 0 - 255
  31     g: 0 - 255
  32     b: 0 - 255
  33     a: 0 - 255 Math.round(255 * a)
  34 
  35     遍历:
  36     const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
  37     for(let k = 0, x, y, r, g, b, a, i; k < len; k++){
  38         x = k % width;
  39         y = Math.floor(k / width);
  40 
  41         i = k*4;
  42         r = data[i]
  43         g = data[i+1]
  44         b = data[i+2]
  45         a = data[i+3]
  46 
  47         console.log(x, y, r, g, b, a, i);
  48     }
  49 
  50 parameter: 
  51     option = {
  52         canvas             //默认 新的canvas
  53         width, height     //默认 1
  54         className, id    //默认 ""
  55     }
  56 
  57 attribute:
  58 
  59 method:
  60     size(w, h: Number): this;                //设置box和canvas的宽高
  61     render(parentElem: HTMLElement): this;    //canvas添加至dom树
  62     initList(i): this;                        //初始化列表
  63     add(ci: CanvasImage): ci;                //添加至列表并初始化ci
  64     remove(ci: CanvasImage): ci;            //从列表中删除ci
  65     redraw(): undefined;                     //重绘在 this.box 内或相交的 CanvasImage
  66 
  67     redrawTarget(ca: CanvasImage|Box): undefined;
  68     //工作原理: 收集与ca重叠的ca; 重绘这些重叠的ca, 前提是ca的box没发生改变; (缩放或旋转ca的box都会发生改变)
  69     例子:
  70         ca.opacity = 0.6;
  71         cir.redrawTarget(ca);
  72 
  73         ca.visible = false;
  74         cir.redrawTarget(ca);
  75 
  76         cir.remove(ca);
  77         cir.redrawTarget(ca);
  78 
  79         cir.add(ca);
  80         cir.redrawTarget(ca);
  81 
  82         ca.setImage(newImage) //假设newImage与ca的box一样大
  83         cir.redrawTarget(ca);
  84 
  85     initEventDispatcher(): this; //初始化自定义事件 (如果你不需要这些事件可以不初始化)
  86     支持的事件名:
  87         beforeDraw
  88         afterDraw
  89         beforeDrawTarget
  90         afterDrawTarget
  91         boxX
  92         boxY
  93         initList
  94         size
  95         add
  96         remove
  97 
  98 demo:
  99     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height})
 100 
 101     .initEventDispatcher(), //初始化 CanvasImageRender 内部的自定义事件
 102 
 103     cie = new CanvasImageEvent(cir), //启用dom事件
 104 
 105     cis = new CanvasImageScroll(cir, cie, {scrollEventType: "touch"}), //启用滚动条
 106     
 107     ci = new CanvasImage().loadImage("view/examples/img/test.png", cir); //图片加载完后更新一次画布
 108 
 109     for(let k = 0, pi2 = Math.PI*2, ca; k < 1000000; k++){
 110         ca = cir.list[k] = new CanvasImage(ci);                     //添加至渲染列表
 111         ca.pos(UTILS.random(0, 1000000), UTILS.random(0, 1000000));    //随机位置
 112         //ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2));         //随机旋转
 113     }
 114     
 115     //为什么要用 initList() ? 因为for循环中是直接把ca加入到了列表, 没对ca做任何初始化操作;
 116     //如果全用.add() 就不需要在调用 initList(); 但 .add() 方法不适合一次性添加多个 CanvasImage;
 117     cir.initList(); 
 118 
 119 */
 120 class CanvasImageRender{
 121 
 122     static emptyBox = new Box();
 123     static emptyArrA = []
 124     static emptyArrB = []
 125     static paramCon = {alpha: true}
 126 
 127     static defaultStyles = {
 128         filter: "none",
 129         globalAlpha: 1,
 130         globalCompositeOperation: "source-over",
 131         imageSmoothingEnabled: true,
 132         miterLimit: 10,
 133         font: "12px SimSun, Songti SC",
 134         textAlign: "left",
 135         textBaseline: "top",
 136         lineCap: "butt",
 137         lineJoin: "miter",
 138         lineDashOffset: 0,
 139         lineWidth: 1,
 140         shadowColor: "rgba(0, 0, 0, 0)",
 141         shadowBlur: 0,
 142         shadowOffsetX: 0,
 143         shadowOffsetY: 0,
 144         fillStyle: "rgba(255,255,255,0.4)",
 145         strokeStyle: "rgba(210,210,210,0.6)",
 146     }
 147 
 148     static setDefaultStyles(context){
 149         let k = "", styles = CanvasImageRender.defaultStyles;
 150         for(k in styles){
 151             if(context[k] !== styles[k]) context[k] = styles[k];
 152         }
 153     }
 154 
 155     static getContext(canvas, className, id){
 156         if(CanvasImageRender.isCanvas(canvas) === false) canvas = document.createElement("canvas");
 157         const context = canvas.getContext("2d", CanvasImageRender.paramCon);
 158 
 159         if(typeof className === "string") canvas.className = className;
 160         if(typeof id === "string") canvas.setAttribute('id', id);
 161         
 162         return context;
 163     }
 164 
 165     static downloadImage(func){
 166         const input = document.createElement("input");
 167         input.type = "file";
 168         input.multiple = "multiple";
 169         input.accept = ".png, .jpg, .jpeg, .bmp, .gif";
 170     
 171         input.onchange = e => {
 172             if(e.target.files.length === 0) return;
 173             const fr = new FileReader();
 174             fr.onloadend = e1 => {
 175                 const img = new Image();
 176                 img.onload = () => func(img);
 177                 img.src = e1.target.result;
 178             }
 179 
 180             fr.readAsDataURL(e.target.files[0]);
 181         }
 182         
 183         input.click();
 184     }
 185 
 186     static isCanvasImage(img){ //OffscreenCanvas:ImageBitmap; CSSImageValue:
 187 
 188         return ImageBitmap["prototype"]["isPrototypeOf"](img) || 
 189         HTMLImageElement["prototype"]["isPrototypeOf"](img) || 
 190         HTMLCanvasElement["prototype"]["isPrototypeOf"](img) || 
 191         CanvasRenderingContext2D["prototype"]["isPrototypeOf"](img) || 
 192         HTMLVideoElement["prototype"]["isPrototypeOf"](img);
 193         
 194     }
 195 
 196     static isCanvas(canvas){
 197         return HTMLCanvasElement["prototype"]["isPrototypeOf"](canvas);
 198     }
 199     
 200     #box = new Box();
 201     get box(){return this.#box;}
 202 
 203     #eventDispatcher = null;
 204     get eventDispatcher(){return this.#eventDispatcher;}
 205 
 206     constructor(option = {}){
 207         this.list = [];
 208         this.context = CanvasImageRender.getContext(option.canvas, option.className, option.id);
 209         this.domElement = this.context.canvas;
 210         this.size(option.width, option.height);
 211     }
 212 
 213     initEventDispatcher(){
 214         this.#eventDispatcher = new EventDispatcher();
 215         const eventParam = {target: this}, 
 216         eventParam1 = {target: this, value: null};
 217         this.#eventDispatcher.
 218         customEvents("beforeDraw", eventParam)
 219         .customEvents("afterDraw", eventParam)
 220         .customEvents("beforeDrawTarget", eventParam)
 221         .customEvents("afterDrawTarget", eventParam)
 222         .customEvents("boxX", eventParam)
 223         .customEvents("boxY", eventParam)
 224         .customEvents("initList", {target: this, index: 0})
 225         .customEvents("size", eventParam)
 226         .customEvents("add", eventParam1)
 227         .customEvents("remove", eventParam1);
 228 
 229         let _x = this.#box.x, _y = this.#box.y;
 230         Object.defineProperties(this.#box, {
 231 
 232             x: {
 233                 get: () => {return _x;},
 234                 set: v => {
 235                     _x = v;
 236                     this.#eventDispatcher.trigger("boxX");
 237                 }
 238             },
 239 
 240             y: {
 241                 get: () => {return _y;},
 242                 set: v => {
 243                     _y = v;
 244                     this.#eventDispatcher.trigger("boxY");
 245                 }
 246             },
 247 
 248         });
 249 
 250         return this;
 251     }
 252 
 253     strokeBox(box){
 254         this.context.strokeRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h);
 255     }
 256 
 257     fillBox(box){
 258         this.context.fillRect(box.x-this.#box.x, box.y-this.#box.y, box.w, box.h);
 259     }
 260 
 261     clearBox(box){
 262         this['context']['clearRect'](box.x-this.#box.x, box.y-this.#box.y, box.w, box.h);
 263     }
 264 
 265     isDraw(ca){
 266         return ca["visible"] === true && ca["image"] !== null && this["box"]["intersectsBox"](ca["box"]);
 267     }
 268 
 269     size(w = 1, h = 1){
 270         this.domElement.width = w;
 271         this.domElement.height = h;
 272         CanvasImageRender.setDefaultStyles(this.context);
 273         this.box.size(w, h);
 274         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("size");
 275         return this;
 276     }
 277 
 278     append(ca){
 279         this.add(ca);
 280         return this;
 281     }
 282 
 283     add(ca){
 284         if(CanvasImage.prototype.isPrototypeOf(ca) && !this.list.includes(ca)){
 285             const len = this.list.length;
 286             
 287             if(this.list[ca.index] === undefined){
 288                 ca.index = len;
 289                 this.list.push(ca);
 290             }
 291 
 292             else{
 293                 const arr = this.list.splice(ca.index);
 294                 this.list.push(ca);
 295                 for(let k = 0, c = arr.length; k < c; k++){
 296                     this.list.push(arr[k]);
 297                     arr[k].index++;
 298                 }
 299             }
 300 
 301             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("add", param => param.value = ca);
 302         }
 303         
 304         return ca;
 305     }
 306 
 307     remove(ca){
 308         var i = ca.index;
 309 
 310         if(this.list[i] !== ca) i = this.list.indexOf(ca);
 311 
 312         if(i !== -1){
 313             this.list.splice(i, 1);
 314             for(let k = i, len = this.list.length; k < len; k++) this.list[k].index -= 1;
 315             if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("remove", param => param.value = ca);
 316         }
 317 
 318         return ca;
 319     }
 320 
 321     render(parentElem = document.body){
 322         this.redraw();
 323         if(this.domElement.parentElement === null) parentElem.appendChild(this.domElement);
 324         return this;
 325     }
 326 
 327     initList(i){
 328         if(i === undefined || i < 0) i = 0;
 329         const len = this.list.length;
 330         for(let k = i; k < len; k++) this.list[k].index = k;
 331         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("initList", param => param.index = i);
 332         return this;
 333     }
 334 
 335     clear(){
 336         this['context']['clearRect'](0, 0, this['box']['w'], this['box']['h']);
 337     }
 338 
 339     draw(){
 340         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDraw");
 341         const len = this["list"]["length"];
 342         for(let k = 0, ca; k < len; k++){
 343             ca = this["list"][k];
 344             if(this["isDraw"](ca) === true) this["_draw"](ca);
 345         }
 346         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDraw");
 347     }
 348 
 349     redrawTarget(box){
 350         box = CanvasImageRender.emptyBox.copy(CanvasImage.prototype.isPrototypeOf(box) ? box.box : box);
 351         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("beforeDrawTarget");
 352         this._computeOverlap(box);
 353         this["context"]["clearRect"](box["x"] - this.#box.x, box["y"] - this.#box.y, box["w"], box["h"]);
 354         this["_drawTarget"]();
 355         if(this.#eventDispatcher !== null) this.#eventDispatcher.trigger("afterDrawTarget");
 356     }
 357 
 358     redraw(){
 359         this.clear();
 360         this.draw();
 361     }
 362 
 363     //限内部使用
 364     _computeOverlap(tar){ //暂不支持带有旋转的 CanvasImage (如遇到将忽略)
 365         //1 如果检索到与box相交的ca时, 合并其box执行以下步骤: 
 366         //2 检索 已检索过的 并且 没有相交的ca
 367         //3 如果存在相交的ca就合并box并继续第 2 步; 如果不存在继续第 1 步
 368         const _list = CanvasImageRender.emptyArrA, list = CanvasImageRender.emptyArrB, len = this.list.length, box = tar;
 369 
 370         for(let k = 0, i = 0, c = 0, _c = 0, a = _list, b = list, loop = false; k < len; k++){
 371             tar = this["list"][k];
 372             
 373             if(this.isDraw(tar) === false){
 374                 if(tar["overlap"] !== false) tar["overlap"] = false;
 375                 continue;
 376             }
 377 
 378             if(box["intersectsBox"](tar["box"]) === true){
 379                 if(tar["overlap"] !== true) tar["overlap"] = true;
 380                 box["expand"](tar["box"]);
 381                 loop = true;
 382 
 383                 while(loop === true){
 384                     b["length"] = 0;
 385                     loop = false;
 386                     c = _c;
 387 
 388                     for(i = 0; i < c; i++){
 389                         tar = a[i];
 390 
 391                         if(box["intersectsBox"](tar["box"]) === true){
 392                             if(tar["overlap"] !== true) tar["overlap"] = true;
 393                             box["expand"](tar["box"]);
 394                             loop = true; _c--;
 395                         }
 396 
 397                         else b.push(tar);
 398                         
 399                     }
 400 
 401                     a = a === _list ? list : _list;
 402                     b = b === _list ? list : _list;
 403 
 404                 }
 405                 
 406             }
 407 
 408             else{
 409                 _c++;
 410                 a["push"](tar);
 411                 if(tar["overlap"] !== false) tar["overlap"] = false;
 412             }
 413 
 414         }
 415         
 416         _list.length = list.length = 0;
 417     }
 418 
 419     _drawImage(ca, ox = 0, oy = 0){
 420         if(ca["opacity"] === 1){
 421             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 422             else this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy, ca.box.w, ca.box.h);
 423         }
 424 
 425         else{
 426             this["context"]["globalAlpha"] = ca["opacity"];
 427             if(ca.isScale === false) this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy);
 428             else this["context"]["drawImage"](ca["image"], ca["box"]["x"]-this.#box.x+ox, ca["box"]["y"]-this.#box.y+oy, ca.box.w, ca.box.h);
 429             this["context"]["globalAlpha"] = 1;
 430         }
 431     }
 432 
 433     _draw(ca){
 434         if(ca.rotate === null) this._drawImage(ca);
 435         else{
 436             const cx = ca.rotate.origin.x + ca.box.x - this.#box.x, 
 437             cy = ca.rotate.origin.y + ca.box.y - this.#box.y;
 438             this.context.translate(cx, cy);
 439             this.context.rotate(ca.rotate.angle);
 440             this._drawImage(ca, -cx, -cy);
 441             this.context.rotate(-ca.rotate.angle);
 442             this.context.translate(-cx, -cy);
 443         }
 444     }
 445     
 446     _drawTarget(){
 447         const len = this["list"]["length"];
 448         for(let k = 0, ci; k < len; k++){
 449             ci = this["list"][k];
 450             if(ci["overlap"] === true) this._draw(ci);
 451         }
 452     }
 453 
 454 }
 455 
 456 
 457 
 458 
 459 /* CanvasImageEvent (CanvasImageRender 的dom事件)
 460 ca 必须的属性: visible, box, rotate, index
 461 ca 必须的方法: 没有;
 462 
 463 parameter:
 464     domElement: CanvasImageRender.domElement; //必须
 465     box: CanvasImageRender.box; //必须
 466     ||或者第一个参数为 CanvasImageRender
 467 
 468     与.initEvent(domElement, box)参数一样
 469 
 470 attribute:
 471     domElement
 472     box: Box;
 473 
 474 method:
 475     add(ca: CanvasImage, eventName: String, callback: Function): ca; //ca添加事件
 476     remove(ca: CanvasImage, eventName: String, callback: Function): ca; //ca删除事件
 477         eventName: 可能的值为 CanvasImageEvent.canvasEventsList 的属性名
 478         callback: 参数 event, ca
 479     
 480     clear(ca: CanvasImage, eventName: String): ca; //ca 必须; eventName 可选, 如果未定义则清空ca的所有事件;
 481     disposeEvent(eventName): this; //.initEvent(domElement, box)调用一次此方法
 482     initEvent(domElement, box): this; //每次更换 domElement, box 后应调用此方法; (CanvasAnimateEvent初始化时自动调用一次)
 483     setScale(x, y: Number): undefiend; //
 484 
 485 eventName:
 486     (如果注册了 "out"|"over" 事件, 在弃用时(CanvasImageRender 不在使用): 
 487     必需要调用.disposeEvent(eventName)方法清理注册的dom事件, 
 488     因为这两个事件用的是 pointermove 而不是 onpointermove);
 489 
 490     down    //鼠标左右键按下
 491     move    //鼠标左右键移动
 492     up        //鼠标左右键抬起
 493 
 494     click    //鼠标左键
 495     wheel    //鼠标滚动轮
 496     out        //移出
 497     over    //移入
 498 
 499 demo:
 500     const car = new CanvasImageRender({width: 100, height: 100}),
 501     cae = new CanvasImageEvent(car),
 502     ca = car.add(new CanvasImage(image));
 503 
 504     //ca添加点击事件
 505     cae.add(ca, 'click', (event, target) => console.log(event, target));
 506 
 507     car.render();
 508 
 509 */
 510 class CanvasImageEvent{
 511 
 512     static bind(obj, is){
 513         /* const _eventList = {}
 514         Object.defineProperty(obj, "_eventList", {
 515             get(){return _eventList;}
 516         }); */
 517         obj._eventList = {};
 518         if(is === true){
 519             let k, evns = CanvasImageEvent.canvasEventsList;
 520             for(k in evns) obj._eventList[k] = [];
 521         }
 522             
 523     }
 524 
 525     static canvasEventsList = {
 526         down: "onpointerdown",
 527         move: "onpointermove",
 528         up: "onpointerup",
 529         click: "onclick",
 530         wheel: "onmousewheel",
 531         out: "pointermove", //移出 
 532         over: "pointermove", //移入
 533     }
 534 
 535     static isEventName(eventName){
 536 
 537         return CanvasImageEvent.canvasEventsList[eventName] !== undefined;
 538 
 539     }
 540     
 541     static emptyBox = new Box();
 542     static emptyRotate = new Rotate();
 543 
 544     constructor(domElement, box){
 545         this._running = "";
 546         this._delList = [];
 547         CanvasImageEvent.bind(this);
 548         this.initEvent(domElement, box);
 549     }
 550 
 551     initEvent(domElement, box){
 552         this.disposeEvent();
 553 
 554         if(CanvasImageRender.prototype.isPrototypeOf(domElement)){
 555             this.domElement = domElement.domElement;
 556             this.box = domElement.box;
 557         }
 558 
 559         else{
 560             this.domElement = domElement;
 561             this.box = box;
 562         }
 563 
 564         if(this._eventList !== undefined){
 565             for(let evn in this._eventList){
 566                 if(this._eventList[evn] !== undefined) this._createEvent(evn);
 567             }
 568 
 569         }
 570         
 571         return this;
 572     }
 573 
 574     add(ca, eventName, callback){
 575         if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName);
 576         if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误");
 577         
 578         this._add(ca, eventName);
 579         this._addCA(ca, eventName, callback);
 580         
 581         return ca;
 582     }
 583     
 584     remove(ca, eventName, callback){
 585         if(CanvasImageEvent.canvasEventsList[eventName] === undefined) return console.warn("CanvasImageEvent: 参数错误 "+ eventName);
 586         if(typeof callback !== "function") return console.warn("CanvasImageEvent: 事件添加失败,参数错误");
 587         if(this._running !== eventName){
 588             this._remove(ca, eventName);
 589             this._removeCA(ca, eventName, callback);
 590         }
 591 
 592         else this._delList.push(ca, eventName, callback);
 593 
 594         return ca;
 595     }
 596 
 597     disposeEvent(eventName){
 598         if(eventName === "over" || eventName === "out"){
 599 
 600             if(typeof this["_"+eventName] === "function"){
 601                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList[eventName], this["_"+eventName]);
 602                 delete this["_"+eventName];
 603             }
 604 
 605         }
 606 
 607         else{
 608 
 609             if(typeof this["_over"] === "function"){
 610                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["over"], this["_over"]);
 611                 delete this["_over"];
 612             }
 613 
 614             if(typeof this["_out"] === "function"){
 615                 this.domElement.removeEventListener(CanvasImageEvent.canvasEventsList["out"], this["_out"]);
 616                 delete this["_out"];
 617             }
 618 
 619         }
 620 
 621         return this;
 622     }
 623     
 624     clear(ca, eventName){
 625         if(eventName === undefined){
 626             var k; for(k in this._eventList){
 627                 this._remove(ca, k);
 628             }
 629             
 630             if(ca._eventList !== undefined) delete ca._eventList; //CanvasImageEvent.bind(ca, true);
 631             
 632         }
 633 
 634         else if(CanvasImageEvent.canvasEventsList[eventName] !== undefined){
 635             this._remove(ca, eventName);
 636 
 637             if(ca._eventList !== undefined) ca._eventList[eventName].length = 0;
 638             
 639         }
 640 
 641         return ca;
 642     }
 643 
 644     _addCA(ca, eventName, callback){
 645         if(ca._eventList === undefined) CanvasImageEvent.bind(ca);
 646         if(ca._eventList[eventName] === undefined) ca._eventList[eventName] = [];
 647         ca._eventList[eventName].push(callback);
 648 
 649     }
 650 
 651     _removeCA(ca, eventName, callback){
 652         if(ca._eventList !== undefined && ca._eventList[eventName] !== undefined){
 653             for(let k = 0, len = ca._eventList[eventName].length; k < len; k++){
 654                 if(ca._eventList[eventName][k] === callback){
 655                     ca._eventList[eventName].splice(k, 1);
 656                     break;
 657                 }
 658             }
 659         }
 660 
 661     }
 662 
 663     _add(ca, eventName){
 664         if(this._eventList[eventName] === undefined){
 665             this._eventList[eventName] = [];
 666             this._createEvent(eventName);
 667         }
 668 
 669         //if(this._eventList[eventName].includes(ca) === false) this._eventList[eventName].push(ca);
 670         this._eventList[eventName].push(ca);
 671     }
 672 
 673     _remove(ca, eventName){
 674         if(this._eventList[eventName] !== undefined){
 675             let key = this._eventList[eventName].indexOf(ca);
 676             if(key !== -1) this._eventList[eventName].splice(key, 1);
 677             if(key === 0){
 678                 if(eventName == "over" || eventName === "out") this.disposeEvent(eventName);
 679                 else this.domElement[CanvasImageEvent.canvasEventsList[eventName]] = null;
 680                 delete this._eventList[eventName];
 681             }
 682 
 683         }
 684 
 685     }
 686 
 687     _createEvent(evn){
 688         var k, len, ca, arr, tar = null, oldTar = null, _run = null, offsetX, offsetY;
 689 
 690         const _box = CanvasImageEvent.emptyBox, _rotate = CanvasImageEvent.emptyRotate,
 691 
 692         run = event => {
 693             len = this["_eventList"][evn].length;
 694             if(len === 0) return;
 695 
 696             offsetX = event.offsetX + this.box.x;
 697             offsetY = event.offsetY + this.box.y;
 698 
 699             tar = null;
 700             for(k = 0; k < len; k++){
 701                 ca = this["_eventList"][evn][k];
 702                 _box.copy(ca.box);
 703 
 704                 //计算旋转后的box
 705                 if(ca.rotate !== null) _box.setFromRotate(_rotate.set(_box.x+ca.rotate.origin.x, _box.y+ca.rotate.origin.y, ca.rotate.angle));
 706                 
 707                 if(ca["visible"] === true && _box.containsPoint(offsetX, offsetY)){
 708                     
 709                     if(tar === null || tar["index"] < ca["index"]) tar = ca;
 710                     
 711                 }
 712 
 713             }
 714             
 715             if(_run !== null) _run();
 716             if(tar !== null){
 717                 this._running = evn;
 718                 arr = tar["_eventList"][evn]; 
 719                 len = arr.length;
 720                 for(k = 0; k < len; k++) arr[k](event, tar);
 721                 
 722                 tar = null;
 723 
 724                 len = this._delList.length;
 725                 for(k = 0; k < len; k += 3){
 726                     this._remove(this._delList[k], this._delList[k+1]);
 727                     this._removeCA(this._delList[k], this._delList[k+1], this._delList[k+2]);
 728                 }
 729                 this._running = "";
 730                 this._delList.length = 0;
 731             }
 732             
 733         }
 734         
 735         if(evn === "over" || evn === "out"){
 736             this.domElement.addEventListener(CanvasImageEvent.canvasEventsList[evn], run);
 737             this["_"+evn] = run;
 738             if(evn === "over"){
 739                 _run = ()=>{
 740                     if(tar !== null){
 741                         if(oldTar !== null){
 742                             if(oldTar !== tar) oldTar = tar;
 743                             else tar = null;
 744                         }
 745                         else oldTar = tar;
 746                     }
 747                     else if(oldTar !== null) oldTar = null;
 748     
 749                 }
 750 
 751             }
 752 
 753             else{
 754                 let _tar = null;
 755                 _run = ()=>{
 756                     if(tar !== null){
 757                         if(oldTar !== null){
 758                             if(oldTar !== tar){
 759                                 _tar = tar;
 760                                 tar = oldTar;
 761                                 oldTar = _tar;
 762                             }
 763                             else tar = null;
 764                         }
 765                         else{
 766                             oldTar = tar;
 767                             tar = null;
 768                         }
 769                     }
 770                     else if(oldTar !== null){
 771                         tar = oldTar;
 772                         oldTar = null;
 773                     }
 774     
 775                 }
 776 
 777             }
 778 
 779             /* _run = ()=>{
 780                 if(tar !== null){
 781                     if(oldTar !== null){
 782 
 783                         if(oldTar !== tar){
 784                             if(evn === "over") oldTar = tar;
 785                             else{
 786                                 let _tar = tar;
 787                                 tar = oldTar;
 788                                 oldTar = _tar;
 789                             }
 790                         }
 791 
 792                         else tar = null;
 793 
 794                     }
 795 
 796                     else{
 797                         oldTar = tar;
 798                         if(evn === "out") tar = null;
 799                         
 800                     }
 801                     
 802                 }
 803 
 804                 else{
 805                     if(oldTar !== null){
 806                         if(evn === "out") tar = oldTar;
 807                         oldTar = null;
 808                     }
 809                     
 810                 }
 811 
 812             } */
 813             
 814         }
 815 
 816         else this.domElement[CanvasImageEvent.canvasEventsList[evn]] = run;
 817 
 818     }
 819 
 820 }
 821 
 822 
 823 
 824 
 825 /* CanvasImageScroll (CanvasImageRender 的滚动条)
 826 
 827 待解决的问题:
 828     1 当某个ci与scroll重叠时, cir.redrawTarget(ci)方法无法检测到scroll(不能重绘滚动条); 把scroll定义为ci可以解决此问题
 829 
 830 parameter:
 831     cir: CanvasImageRender, 
 832     cie: CanvasImageEvent, 
 833     option: Object{
 834         domEventTarget: //dom事件的绑定目标html元素; 默认 cir.domElement
 835         scrollSize        //滚动轴的宽或高; 默认 10; (如果想隐藏滚动条最好用 scrollVisible, 而不是把此属性设为0)
 836         scrollVisible    //滚动轴的显示类型; 可能值: 默认"auto" || "visible" || "";
 837         scrollEventType    //可能的值: 默认"default", "touch" || "";
 838         inertia            //是否启用移动端滚动轴的惯性; 默认 true;
 839         inertiaLife        //移动端滚动轴惯性生命(阻力强度); 0 - 1; 默认 0.04;
 840     }
 841 
 842 */
 843 class CanvasImageScroll{
 844 
 845     static unbindScroll(box){
 846         var _x = box.x, _y = box.y, _w = box.w, _h = box.h;
 847         Object.defineProperties(box, {
 848 
 849             x: {
 850                 get(){return _x;},
 851                 set(v){_x = v;}
 852             },
 853 
 854             y: {
 855                 get(){return _y;},
 856                 set(v){_y = v;}
 857             },
 858 
 859             w: {
 860                 get(){return _w;},
 861                 set(v){_w = v;}
 862             },
 863 
 864             h: {
 865                 get(){return _h;},
 866                 set(v){_h = v;}
 867             },
 868 
 869         });
 870     }
 871 
 872     #cir = null;
 873     #scrollViewBoxX = new Box();
 874     #scrollViewBoxY = new Box();
 875     #maxSize = new Point();
 876     get maxSize(){return this.#maxSize;}
 877     
 878     constructor(cir, cie, option = {}){
 879         this.#cir = cir;
 880         this.scrollSize = UTILS.isNumber(option.scrollSize) ? option.scrollSize : 10;
 881         
 882         const oninitList = event => {
 883             const len = cir.list.length;
 884             for(let k = event ? event.index : 0; k < len; k++) this.bindScroll(cir.list[k].box);
 885             this.resetMaxSizeX();
 886             this.resetMaxSizeY();
 887         }
 888 
 889         switch(option.scrollVisible){
 890             case "visible": 
 891             this.scrollVisible = option.scrollVisible;
 892             break;
 893 
 894             case "": 
 895             this.scrollVisible = "";
 896             break;
 897 
 898             case "auto": 
 899             default: 
 900             this.scrollVisible = "auto";
 901             break;
 902         }
 903 
 904         switch(option.scrollEventType){
 905             case "touch": 
 906             this.createScrollEventMobile(cie, option.domEventTarget || cir.domElement, option.inertia, option.inertiaLife);
 907             break;
 908 
 909             case "": 
 910             break;
 911 
 912             case "default": 
 913             default: 
 914             this.createScrollEventPC(cie);
 915             break;
 916         }
 917         
 918         if(EventDispatcher.prototype.isPrototypeOf(cir.eventDispatcher)){
 919             const box = this.#cir.box;
 920 
 921             cir.eventDispatcher.register("boxX", ()=>{
 922                 if(box.x < 0) box.x = 0;
 923                 else if(box.mx > this.#maxSize.x) box.x = this.#maxSize.x - box.w;
 924             });
 925             cir.eventDispatcher.register("boxY", ()=>{
 926                 if(box.y < 0) box.y = 0;
 927                 else if(box.my > this.#maxSize.y) box.y = this.#maxSize.y - box.h;
 928             });
 929 
 930             cir.eventDispatcher.register("add", event => {
 931                 const ciBox = event.value.box;
 932                 this.bindScroll(ciBox);
 933                 ciBox.w = ciBox.w;
 934                 ciBox.h = ciBox.h;
 935             });
 936             cir.eventDispatcher.register("remove", event => {
 937                 const ciBox = event.value.box;
 938                 if(ciBox.mx >= this.#maxSize.x) this.resetMaxSizeX();
 939                 if(ciBox.my >= this.#maxSize.y) this.resetMaxSizeY();
 940                 CanvasImageScroll.unbindScroll(ciBox);
 941             });
 942             
 943             cir.eventDispatcher.register("initList", oninitList);
 944 
 945             cir.eventDispatcher.register("afterDraw", () => {
 946                 if(this.scrollVisible !== ""){
 947                     this.updateScrollViewBox();
 948                     this.drawScroll();
 949                 }
 950             });
 951 
 952         }
 953 
 954         else console.warn("CanvasImageScroll: 请定义 eventDispatcher");
 955         
 956         if(cir.list.length !== 0) oninitList();
 957     }
 958 
 959     updateScrollViewBox(){
 960         this.#scrollViewBoxX.x = this.cursorX();
 961         this.#scrollViewBoxX.y = this.#cir.box.h - this.scrollSize;
 962         this.#scrollViewBoxX.w = this.#maxSize.x <= this.#cir.box.w ? this.#cir.box.w : this.#cir.box.w / this.#maxSize.x * this.#cir.box.w;
 963         this.#scrollViewBoxX.h = this.scrollSize;
 964 
 965         this.#scrollViewBoxY.x = this.#cir.box.w - this.scrollSize;
 966         this.#scrollViewBoxY.y = this.cursorY();
 967         this.#scrollViewBoxY.w = this.scrollSize;
 968         this.#scrollViewBoxY.h = this.#maxSize.y <= this.#cir.box.h ? this.#cir.box.h : this.#cir.box.h / this.#maxSize.y * this.#cir.box.h;
 969     }
 970 
 971     drawScroll(){
 972         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.x > this.#cir.box.w)){
 973             this.#cir.context.fillRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h);
 974             this.#cir.context.strokeRect(this.#scrollViewBoxX.x, this.#scrollViewBoxX.y, this.#scrollViewBoxX.w, this.#scrollViewBoxX.h);
 975         }
 976 
 977         if(this.scrollVisible === "visible" || (this.scrollVisible === "auto" && this.#maxSize.y > this.#cir.box.h)){
 978             this.#cir.context.fillRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h);
 979             this.#cir.context.strokeRect(this.#scrollViewBoxY.x, this.#scrollViewBoxY.y, this.#scrollViewBoxY.w, this.#scrollViewBoxY.h);
 980         }
 981     }
 982 
 983     createScrollEventPC(cie){
 984         var dPos = -1;
 985 
 986         const _box = this.#cir.box, scope = this,
 987         
 988         setTop = (event, top) => {
 989             _box.y = top / _box.h * this.#maxSize.y;
 990             this.#cir.redraw();
 991         },
 992 
 993         setLeft = (event, left) => {
 994             _box.x = left / _box.w * this.#maxSize.x;
 995             this.#cir.redraw();
 996         },
 997         
 998         onMoveTop = event => {
 999             setTop(event, event.offsetY - dPos);
1000         },
1001 
1002         onMoveLeft = event => {
1003             setLeft(event, event.offsetX - dPos);
1004         },
1005 
1006         onUpTop = event => {
1007             document.body.removeEventListener('pointermove', onMoveTop);
1008             document.body.removeEventListener('pointerup', onUpTop);
1009             if(event !== null) onMoveTop(event);
1010         },
1011 
1012         onUpLeft = event => {
1013             document.body.removeEventListener('pointermove', onMoveLeft);
1014             document.body.removeEventListener('pointerup', onUpLeft);
1015             if(event !== null) onMoveLeft(event);
1016         },
1017 
1018         //伪 CanvasImage, 是 CanvasImageEvent 使用的必须属性
1019         boxX = new Box(),
1020         eventTargetX = {
1021             get index(){return Infinity;},
1022             get visible(){return true;},
1023             get box(){return boxX},
1024             get rotate(){return null},
1025         },
1026         
1027         boxY = new Box(),
1028         eventTargetY = {
1029             get index(){return Infinity;},
1030             get visible(){return true;},
1031             get box(){return boxY},
1032             get rotate(){return null},
1033         },
1034 
1035         eventTargetWheel = {
1036             get index(){return -1;},
1037             get visible(){return true;},
1038             get box(){return _box},
1039             get rotate(){return null},
1040         }
1041 
1042         Object.defineProperties(boxX, {
1043             x: {get: () => {return _box.x;}},
1044             y: {get: () => {return _box.h - scope.scrollSize + _box.y;}},
1045             w: {get: () => {return _box.w;}},
1046             h: {get: () => {return scope.scrollSize;}},
1047         });
1048 
1049         Object.defineProperties(boxY, {
1050             x: {get: () => {return _box.w - scope.scrollSize + _box.x;}},
1051             y: {get: () => {return _box.y;}},
1052             w: {get: () => {return scope.scrollSize;}},
1053             h: {get: () => {return _box.h;}},
1054         });
1055 
1056         //pc event
1057         cie.add(eventTargetX, "down", event => {
1058             dPos = event.offsetX - this.cursorX();
1059             onUpLeft(null);
1060             document.body.addEventListener("pointermove", onMoveLeft);
1061             document.body.addEventListener("pointerup", onUpLeft);
1062         });
1063 
1064         cie.add(eventTargetY, "down", event => {
1065             dPos = event.offsetY - this.cursorY();
1066             onUpTop(null);
1067             document.body.addEventListener("pointermove", onMoveTop);
1068             document.body.addEventListener("pointerup", onUpTop);
1069         });
1070 
1071         cie.add(eventTargetWheel, "wheel", event => {
1072             if(this.#maxSize.y > _box.h){
1073                 dPos = 50 / this.#maxSize.y * _box.h;
1074                 setTop(event, this.cursorY() + (event.wheelDelta === 120 ? -dPos : dPos));
1075             }
1076             else if(this.#maxSize.x > _box.w){
1077                 dPos = 50 / this.#maxSize.x * _box.w;
1078                 setLeft(event, this.cursorX() + (event.wheelDelta === 120 ? -dPos : dPos));
1079             }
1080         });
1081 
1082     }
1083 
1084     createScrollEventMobile(cie, domElement, inertia = true, inertiaLife){
1085         var px, py, dPos = 0, sPos = new Point(), step = 1, aniamteRun = false, stepA = "", stepB = "", sTime = 0;
1086 
1087         const _box = this.#cir.box;
1088 
1089         if(inertia === true){
1090             inertiaLife = 1 - ((UTILS.isNumber(inertiaLife) && inertiaLife >= 0 && inertiaLife <= 1) ? inertiaLife : 0.04);
1091             var inertiaAnimate = new AnimateLoop(null),
1092         
1093             inertiaTop = (event, speed) => {
1094                 if(Math.abs(speed) < 1) return;
1095                 
1096                 stepA = speed < 0 ? "-top" : "top";
1097                 if(aniamteRun && stepA === stepB) step += 0.3;
1098                 else{
1099                     step = 1;
1100                     stepB = stepA;
1101                 }
1102                 
1103                 inertiaAnimate.play(() => {
1104                     speed *= inertiaLife;
1105                     setTop(event, _box.y + step * 20 * speed);
1106                     if(Math.abs(speed) < 0.001 || _box.y <= 0 || _box.my >= this.#maxSize.y) inertiaAnimate.stop();
1107                 });
1108             },
1109 
1110             inertiaLeft = (event, speed) => {
1111                 if(Math.abs(speed) < 1) return;
1112                 stepA = speed < 0 ? "-left" : "left";
1113                 if(aniamteRun && stepA === stepB) step += 0.3;
1114                 else{
1115                     step = 1;
1116                     stepB = stepA;
1117                 }
1118                 inertiaAnimate.play(() => {
1119                     speed *= inertiaLife;
1120                     setLeft(event, _box.x + step * 20 * speed);
1121                     if(Math.abs(speed) < 0.001 || _box.x <= 0 || _box.mx >= this.#maxSize.x) inertiaAnimate.stop();
1122                 });
1123             }
1124         }
1125 
1126         const setTop = (event, top) => {
1127             _box.y = top;
1128             this.#cir.redraw();
1129         },
1130 
1131         setLeft = (event, left) => {
1132             _box.x = left;
1133             this.#cir.redraw();
1134         },
1135         
1136         onMoveTop = event => {
1137             setTop(event, dPos - event.offsetY);
1138         },
1139 
1140         onMoveLeft = event => {
1141             setLeft(event, dPos - event.offsetX);
1142         },
1143 
1144         onUpTop = event => {
1145             document.body.removeEventListener('pointermove', onMoveTop);
1146             document.body.removeEventListener('pointerup', onUpTop);
1147             if(event !== null){
1148                 onMoveTop(event);
1149                 if(inertia === true) inertiaTop(event, (_box.y - sPos.y) / (Date.now() - sTime));
1150             }
1151         },
1152 
1153         onUpLeft = event => {
1154             document.body.removeEventListener('pointermove', onMoveLeft);
1155             document.body.removeEventListener('pointerup', onUpLeft);
1156             if(event !== null){
1157                 onMoveLeft(event);
1158                 if(inertia === true) inertiaLeft(event, (_box.x - sPos.x) / (Date.now() - sTime));
1159             }
1160         },
1161 
1162         eventTarget = {
1163             get index(){return -1;}, //最低优先
1164             get visible(){return true;},
1165             get box(){return _box;},
1166             get rotate(){return null},
1167         },
1168 
1169         a1 = Math.PI / 4, a2 = Math.PI / 2 + a1,
1170 
1171         onUp = event => {
1172             domElement.removeEventListener("pointerup", onUp);
1173             domElement.removeEventListener('pointermove', onMove);
1174         },
1175 
1176         onMove = event => {
1177             dPos++; if(dPos < 4) return;
1178             onUp(event);
1179             const a = Math.atan2(event.pageY - py, event.pageX - px);
1180             if((a < a2 && a >= a1) || (a < -a1 && a >= -a2)){ //y轴
1181                 if(this.#maxSize.y > _box.h){
1182                     dPos = py + sPos.y;
1183                     document.body.addEventListener("pointermove", onMoveTop);
1184                     document.body.addEventListener("pointerup", onUpTop);
1185                 }
1186             }
1187             else{ //x轴
1188                 if(this.#maxSize.x > _box.w){
1189                     dPos = px + sPos.x;
1190                     document.body.addEventListener("pointermove", onMoveLeft);
1191                     document.body.addEventListener("pointerup", onUpLeft);
1192                 }
1193             }
1194         }
1195     
1196         cie.add(eventTarget, "down", event => {
1197             px = event.offsetX;
1198             py = event.offsetY;
1199             dPos = 0;
1200             sPos.set(_box.x, _box.y);
1201             sTime = Date.now();
1202 
1203             if(inertia === true){
1204                 aniamteRun = inertiaAnimate.running;
1205                 inertiaAnimate.stop();
1206             }
1207 
1208             //防止没触发 move 或 up 事件;
1209             onUpLeft(null);
1210             onUpTop(null);
1211             onUp();
1212             
1213             domElement.addEventListener("pointermove", onMove);
1214             domElement.addEventListener("pointerup", onUp);
1215         });
1216     }
1217 
1218     bindScroll(box){
1219         var _x = box.x, _y = box.y, _w = box.w, _h = box.h, nm, om;
1220 
1221         const writeBoxX = () => {
1222             if(nm > this.#maxSize.x) this.#maxSize.x = nm;
1223             else if(nm < this.#maxSize.x){
1224                 if(om >= this.#maxSize.x) this.resetMaxSizeX();
1225             }
1226         },
1227 
1228         writeBoxY = () => {
1229             if(nm > this.#maxSize.y) this.#maxSize.y = nm;
1230             else if(nm < this.#maxSize.y){
1231                 if(om >= this.#maxSize.y) this.resetMaxSizeY();
1232             }
1233         };
1234 
1235         Object.defineProperties(box, {
1236 
1237             x: {
1238                 get: () => {return _x;},
1239                 set: v => {
1240                     om = _x+_w;
1241                     _x = v;
1242                     nm = v+_w;
1243                     writeBoxX();
1244                 }
1245             },
1246 
1247             y: {
1248                 get: () => {return _y;},
1249                 set: v => {
1250                     om = _y+_h;
1251                     _y = v;
1252                     nm = v+_h;
1253                     writeBoxY();
1254                 }
1255             },
1256 
1257             w: {
1258                 get: () => {return _w;},
1259                 set: v => {
1260                     om = _w+_x;
1261                     _w = v;
1262                     nm = v+_x;
1263                     writeBoxX();
1264                 }
1265             },
1266 
1267             h: {
1268                 get: () => {return _h;},
1269                 set: v => {
1270                     om = _h+_y;
1271                     _h = v;
1272                     nm = v+_y;
1273                     writeBoxY();
1274                 }
1275             },
1276 
1277         });
1278     }
1279 
1280     resetMaxSizeX(){
1281         this.#maxSize.x = 0;
1282         for(let k = 0, len = this.#cir.list.length, m; k < len; k++){
1283             m = this.#cir.list[k].box.mx;
1284             if(m > this.#maxSize.x) this.#maxSize.x = m;
1285         }
1286     }
1287 
1288     resetMaxSizeY(){
1289         this.#maxSize.y = 0;
1290         for(let k = 0, len = this.#cir.list.length, m; k < len; k++){
1291             m = this.#cir.list[k].box.my;
1292             if(m > this.#maxSize.y) this.#maxSize.y = m;
1293         }
1294     }
1295 
1296     cursorX(v = this.#cir.box.x){
1297         return v/this.#maxSize.x*this.#cir.box.w;
1298     }
1299 
1300     cursorY(v = this.#cir.box.y){
1301         return v/this.#maxSize.y*this.#cir.box.h;
1302     }
1303 
1304 }
1305 
1306 
1307 
1308 
1309 /* CanvasImage (CanvasImageRender 的渲染目标)
1310 parameter: 
1311     image (构造器会调用一次 .setImage(image) 来处理 image 参数)
1312 
1313 attribute:
1314     opacity: Float;     //透明度; 值0至1之间; 默认1;
1315     visible: Boolean;    //默认true; 完全隐藏(既不绘制视图, 也不触发绑定的事件)
1316     box: Box;             //.x.y 图像的位置, .w.h 图像的宽高;
1317     rotate: Roate;        //旋转; 默认 null (注意: 旋转的原点是相对于 box.x.y 的)
1318     暂未实现, 先用box.w.h代替缩放//scale: Box;            //缩放; .x.y中心点, .w.h缩放; 默认 null; (注意: 缩放的原点是相对于 box.x.y 的)
1319     x, y: Number;        //this.box 的 .x.y
1320 
1321     //以下属性不建议直接修改
1322     overlap: Object; //CanvasImageRender.computeOverlaps(ca) 方法更新
1323     index: Integer; //CanvasImageRender.index(ca) 修改
1324 
1325     //只读
1326     width, height, image; isRotate, isScale
1327 
1328 method:
1329     setImage(image): this;         //设置图像 (image 如果是 CanvasImage 并它正在加载图像时, 则会在它加载完成时自动设置 image);
1330     loadImage(src, onload): this;    //加载并设置图像 (onload 如果是 CanvasImageRender 则加载完后自动调用一次 redraw 或 render 方法);
1331     loadVideo(src, onload, type = "mp4")
1332     pos(x, y): this;             //设置位置; x 可以是: Number, Object{x,y}
1333 
1334 demo:
1335     //CanvasImageRender
1336     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1337     cir.domElement.style = `
1338         position: absolute;
1339         z-index: 9999;
1340         background: rgb(127,127,127);
1341     `;
1342 
1343     //values
1344     const ciA = cir.add(new CanvasImage()).pos(59, 59).load("view/examples/img/test.png", cir);
1345     ciA.opacity = 0.2; //设置透明度
1346 
1347     const ciB = cir.add(new CanvasImage(ciA)).pos(59, 120);
1348     ciB.rotate = new Rotate().toAngle(45); //旋转45度
1349 
1350     //event
1351     const cie = new CanvasImageEvent(cir); //注意: CanvasImage 的缩放和旋转都会影响到 CanvasImageEvent
1352     cie.add(ciB, "click", event => { //ciB 添加 点击事件
1353         console.log("click ciB: ", event);
1354     });
1355 
1356 
1357     //这个用于搭载H5视频 video
1358     cir.add(new CanvasImage())
1359 
1360     .loadVideo("view/examples/video/test.mp4", ci => {
1361         
1362         //同比例缩放视频
1363         const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100});
1364         ci.box.size(newSize.width, newSize.height).center(cir.box);
1365 
1366         //播放按钮
1367         const cic = cir.add(new CanvasImageCustom())
1368         .size(50, 30).text("PLAY", "#fff")
1369         .rect(4).stroke("blue");
1370         cic.box.center(cir.box);
1371 
1372         //动画循环
1373         const animateLoop = new AnimateLoop(() => cir.redraw());
1374 
1375         //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画)
1376         cir.addEvent(cic, "up", () => {
1377             cic.visible = false;
1378             ci.image.play(); // ci.image 其实是一个 video 元素
1379             animateLoop.play(); //播放动画
1380         });
1381         
1382         //把 canvas 添加至 dom 树
1383         cir.render();
1384 
1385     });
1386 
1387 */
1388 class CanvasImage{
1389 
1390     #box = new Box();
1391     #image = null;
1392     #isLoadImage = false;
1393 
1394     get name(){return this.constructor.name;}
1395     get box(){return this.#box;}
1396     get image(){return this.#image;}
1397     get isScale(){return this.#image.width !== this.box.w || this.#image.height !== this.box.h;}
1398     get isLoadImage(){return this.#isLoadImage;}
1399     get width(){return this.#image !== null ? this.#image.width : 0;}
1400     get height(){return this.#image !== null ? this.#image.height : 0;}
1401     get x(){return this.box.x;}
1402     get y(){return this.box.y;}
1403     
1404     constructor(image){
1405         this.opacity = 1;
1406         this.visible = true;
1407         this.rotate = null;
1408         //this.scale = null;
1409 
1410         //以下属性不建议直接修改
1411         this.overlap = false;
1412         this.index = -1;
1413 
1414         this.setImage(image);
1415     }
1416 
1417     pos(x, y){
1418         if(UTILS.isNumber(x)){
1419             this.box.x = x;
1420             this.box.y = y;
1421         }
1422         else if(UTILS.isObject(x)){
1423             this.box.x = x.x;
1424             this.box.y = x.y;
1425         }
1426         return this;
1427     }
1428 
1429     setImage(image){
1430         if(CanvasImageRender.isCanvasImage(image)){
1431             this.box.size(image.width, image.height);
1432             this.#image = image;
1433         }
1434         else if(CanvasImage.prototype.isPrototypeOf(image)){
1435             if(image.isLoadImage){
1436                 if(Array.isArray(image.loadImage_cis)) image.loadImage_cis.push(this);
1437                 else image.loadImage_cis = [this];
1438             }
1439             else this.setImage(image.image);
1440         }
1441         else{
1442             this.box.size(0, 0);
1443             this.#image = null;
1444         }
1445         return this;
1446     }
1447 
1448     loadImage(src, onload){
1449         this.#isLoadImage = true;
1450         const image = new Image();
1451         image.onload = () => this._loadSuccess(image, onload);
1452         image.src = src;
1453         return this;
1454     }
1455 
1456     loadVideo(src, onload, type = "mp4"){
1457         /*    video 加载事件 的顺序
1458             onloadstart
1459             ondurationchange
1460             onloadedmetadata //元数据加载完成包含: 时长,尺寸大小(视频),文本轨道。
1461             onloadeddata
1462             onprogress
1463             oncanplay
1464             oncanplaythrough
1465 
1466             //控制事件:
1467             onended //播放结束
1468             onpause //暂停播放
1469             onplay //开始播放
1470         */
1471         this.#isLoadImage = true;
1472         const video = document.createElement("video"),
1473         source = document.createElement("source");
1474         video.appendChild(source);
1475         source.type = `video/${type}`;
1476 
1477         video.oncanplay = () => {
1478             //video 的 width, height 属性如果不设的话永远都是0
1479             video.width = video.videoWidth;
1480             video.height = video.videoHeight;
1481             this._loadSuccess(video, onload);
1482         };
1483 
1484         source.src = src;
1485         return this;
1486     }
1487 
1488     _loadSuccess(image, onload){
1489         this.setImage(image);
1490         this.#isLoadImage = false;
1491 
1492         if(Array.isArray(this.loadImage_cis)){
1493             this.loadImage_cis.forEach(ci => ci.setImage(image));
1494             delete this.loadImage_cis;
1495         }
1496 
1497         if(typeof onload === "function") onload(this);
1498         else if(CanvasImageRender.prototype.isPrototypeOf(onload)){
1499             if(onload.domElement.parentElement !== null) onload.redraw();
1500             else onload.render();
1501         }
1502     }
1503 
1504     /* setScale(nx = 0, ny = 0, ratio = 1){
1505         this.scale.w = this.#box.w * ratio;
1506         this.scale.h = this.#box.h * ratio;
1507         this.scale.x = nx - ((nx - this.scale.x) * ratio + this.scale.x) + this.scale.x;
1508         this.scale.y = ny - ((ny - this.scale.y) * ratio + this.scale.y) + this.scale.y;
1509         return this;
1510     }
1511 
1512     setScaleToSameScaleFromBox(newWidth = this.#box.w){
1513         if(this.#image === null || this.scale === null) return this;
1514 
1515         const width = this.#image.width, 
1516         height = this.#image.height,
1517         ratio = width / height,
1518         scale = ratio < 1 ? ratio * newWidth / width : newWidth / width;
1519 
1520         this.scale.w = width * scale;
1521         this.scale.h = height * scale;
1522         
1523         return this;
1524     }
1525 
1526     setScaleToCenterFromBox(){
1527         if(this.scale === null) return this;
1528 
1529         this.scale.x = (this.#box.w - this.scale.w) / 2;
1530         this.scale.y = (this.#box.h - this.scale.h) / 2;
1531 
1532         return this;
1533     } */
1534 
1535 }
1536 
1537 
1538 
1539 
1540 /* CanvasImages
1541 
1542 */
1543 class CanvasImages extends CanvasImage{
1544 
1545     #i = -1;
1546     get cursor(){return this.#i;}
1547     set cursor(i){
1548         super.setImage(this.images[i]);
1549         this.#i = this.image !== null ? i : -1;
1550     }
1551 
1552     constructor(images = []){
1553         super(images[0]);
1554         this.images = images;
1555         if(this.image !== null) this.#i = 0;
1556     }
1557 
1558     setImage(image){
1559         super.setImage(image);
1560     
1561         if(this.image !== null && Array.isArray(this.images)){
1562             const i = this.images.indexOf(this.image);
1563             if(i === -1){
1564                 this.#i = this.images.length;
1565                 this.images.push(this.image);
1566             }
1567             else this.#i = i;
1568         }
1569 
1570         return this;
1571     }
1572 
1573     next(){
1574         const len = this.images.length - 1;
1575         if(len !== -1){
1576             if(this.#i < len) this.#i++;
1577             else this.#i = 0;
1578             super.setImage(this.images[this.#i]);
1579         }
1580     }
1581 
1582     loadImages(srcs, onDone, onUpdate){
1583         onUpdate = typeof onUpdate === "function" ? onUpdate : null;
1584         var i = 0, c = srcs.length, img = null, _i = this.images.length;
1585 
1586         const len = srcs.length, 
1587         func = ()=>{
1588             i++; if(onUpdate !== null) onUpdate(this.images, _i);
1589             if(i === c && typeof onDone === "function"){
1590                 this.cursor = 0;
1591                 onDone(this.images, _i, srcs);
1592             }
1593             else _i++;
1594         }
1595 
1596         for(let k = 0, ty = ""; k < len; k++){
1597             ty = typeof srcs[k];
1598             if(ty === "string" || ty === "object"){
1599                 ty = ty === "string" ? srcs[k] : srcs[k].src;
1600                 if(ty !== "" && typeof ty === "string"){
1601                     img = new Image();
1602                     img.onload = func;
1603                     this.images.push(img);
1604                     img.src = ty;
1605                 }
1606                 else c--;
1607             }
1608         }
1609 
1610         return this;
1611     }
1612 
1613 }
1614 
1615 
1616 
1617 
1618 /* CanvasImageCustom
1619 
1620 demo:
1621     //CanvasImageRender
1622     const cir = new CanvasImageRender({width: WORLD.width, height: WORLD.height});
1623     cir.domElement.style = `
1624         position: absolute;
1625         z-index: 9999;
1626         background: rgb(127,127,127);
1627     `;
1628 
1629     //values
1630     cir.add(new CanvasImageCustom())
1631     .pos(59, 180)
1632     .load("view/examples/img/test.png", cir);
1633 
1634     cir.add(new CanvasImageCustom())
1635     .pos(59, 180)
1636     .text("value", "red")
1637     .rect().stroke()
1638 
1639 */
1640 class CanvasImageCustom extends CanvasImage{
1641 
1642     constructor(canvas, autoSize = true){
1643         super(canvas);
1644         this.autoSize = autoSize;
1645         this.fontSize = parseFloat(CanvasImageRender.defaultStyles.font);
1646         this.fillStyle = CanvasImageRender.defaultStyles.fillStyle;
1647         this.strokeStyle = CanvasImageRender.defaultStyles.strokeStyle;
1648         this.shadowColor = CanvasImageRender.defaultStyles.shadowColor;
1649     }
1650 
1651     toImage(callback){
1652         var image = new Image();
1653         image.src = this.image.toDataURL("image/png");
1654         image.onload = callback;
1655     }
1656 
1657     shear(canvas, dx = 0, dy = 0){
1658         if(CanvasImageRender.isCanvas(canvas) === false){
1659             canvas = document.createElement("canvas");
1660             canvas.width = this.width;
1661             canvas.height = this.height;
1662         }
1663 
1664         canvas.getContext("2d").drawImage(this.image, dx, dy); 
1665 
1666         return canvas;
1667     }
1668 
1669     setImage(image){
1670         if(CanvasImageRender.isCanvas(image)){ //image 如果是画布
1671             super.setImage(image);
1672             this.context = CanvasImageRender.getContext(image);
1673             this.size(image.width, image.height);
1674         }
1675         else{
1676             if(CanvasImageRender.isCanvasImage(image)){ //image 如果是图像
1677                 this.context = CanvasImageRender.getContext();
1678                 super.setImage(this.context.canvas);
1679                 this.size(image.width, image.height);
1680                 this.context.drawImage(image, 0, 0);
1681             }else{ //image 如果是其它对象
1682                 if(image) super.setImage(image);
1683                 else{
1684                     this.context = CanvasImageRender.getContext();
1685                     super.setImage(this.context.canvas);
1686                     this.size(this.width, this.height);
1687                 }
1688             }
1689         }
1690         
1691         return this;
1692     }
1693 
1694     clear(){
1695         this.context.clearRect(0, 0, this.box.w, this.box.h);
1696         return this;
1697     }
1698 
1699     size(w, h){
1700         this.box.size(w, h);
1701         this.image.width = w;
1702         this.image.height = h;
1703         CanvasImageRender.setDefaultStyles(this.context);
1704         
1705         if(this.context.font !== parseFloat(this.fontSize)) this.context.font = this.fontSize+"px SimSun, Songti SC";//
1706         if(this.context.fillStyle !== this.fillStyle) this.context.fillStyle = this.fillStyle; //
1707         if(this.context.strokeStyle !== this.strokeStyle) this.context.strokeStyle = this.strokeStyle; //
1708         if(this.context.shadowColor !== this.shadowColor) this.context.shadowColor = this.shadowColor; //
1709 
1710         return this;
1711     }
1712 
1713     shadow(shadowColor = "rgba(0,0,0,0)", shadowBlur, shadowOffsetX, shadowOffsetY){
1714         const con = this.context;
1715         if(typeof shadowColor === "string" && this.shadowColor !== shadowColor) this.shadowColor = con.shadowColor = shadowColor;
1716         if(con.shadowBlur !== shadowBlur && typeof shadowBlur === "number") con.shadowBlur = shadowBlur;
1717         if(con.shadowOffsetX !== shadowOffsetX && typeof shadowOffsetX === "number") con.shadowOffsetX = shadowOffsetX;
1718         if(con.shadowOffsetY !== shadowOffsetY && typeof shadowOffsetY === "number") con.shadowOffsetY = shadowOffsetY;
1719         return this;
1720     }
1721 
1722     line(x, y, x1, y1){
1723         this.context.beginPath();
1724         this.context.moveTo(x, y);
1725         this.context.lineTo(x1, y1);
1726         return this;
1727     }
1728 
1729     path(arr, close = false){
1730         const con = this.context;
1731         con.beginPath();
1732         con.moveTo(arr[0], arr[1]);
1733         for(let k = 2, len = arr.length; k < len; k+=2) con.lineTo(arr[k], arr[k+1]);
1734         if(close === true) con.closePath();
1735         return this;
1736     }
1737 
1738     rect(r, box){
1739         const con = this.context, s = con.lineWidth;
1740 
1741         if(Box.prototype.isPrototypeOf(box)){
1742             var x = box.x,
1743             y = box.y,
1744             w = box.w,
1745             h = box.h;
1746             
1747         }else{
1748             var x = s / 2,
1749             y = s / 2,
1750             w = this.box.w - s,
1751             h = this.box.h - s;
1752         }
1753 
1754         if(UTILS.isNumber(r) === false || r <= 0){
1755             con.rect(x, y, w, h);
1756             return this;
1757         }
1758 
1759         const _x = x + r, 
1760         _y = y + r, 
1761         mx = x + w, 
1762         my = y + h, 
1763         _mx = mx - r, 
1764         _my = my - r;
1765         
1766         con.beginPath();
1767 
1768         //
1769         con.moveTo(_x, y);
1770         con.lineTo(_mx, y);
1771         con.arcTo(mx, y, mx, _y, r);
1772 
1773         //
1774         con.lineTo(mx, _y);
1775         con.lineTo(mx, _my);
1776         con.arcTo(mx, my, _x, my, r);
1777 
1778         //
1779         con.lineTo(_x, my);
1780         con.lineTo(_mx, my);
1781         con.arcTo(x, my, x, _my, r);
1782 
1783         //
1784         con.lineTo(x, _y);
1785         con.lineTo(x, _my);
1786         con.arcTo(x, y, _x, y, r);
1787 
1788         return this;
1789     }
1790 
1791     stroke(color = this.fillStyle, lineWidth){
1792         if(color !== "" && this.strokeStyle !== color) this.strokeStyle = this.context.strokeStyle = color;
1793         if(UTILS.isNumber(lineWidth) && this.context.lineWidth !== lineWidth) this.context.lineWidth = lineWidth;
1794         this.context.stroke();
1795         return this;
1796     }
1797 
1798     fill(color = this.fillStyle){
1799         if(color !== "" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1800         this.context.fill();
1801         return this;
1802     }
1803 
1804     text(value, color = this.fillStyle, fontSize = this.fontSize, x = -1, y = -1){
1805         if(UTILS.isNumber(fontSize) && fontSize > 1 && this.fontSize !== fontSize){
1806             this.fontSize = fontSize;
1807             this.context.font = fontSize+"px SimSun, Songti SC";
1808         }
1809         
1810         const textWidth = this.context.measureText(value).width;
1811         if(this.autoSize === true && textWidth > this.box.w || this.fontSize > this.box.h){
1812             this.size(textWidth+4, this.fontSize+4);
1813             this.fontSize = fontSize;
1814             this.context.font = fontSize+"px SimSun, Songti SC";
1815         }
1816 
1817         if(x === -1) x = (this.box.w - textWidth) / 2;
1818         if(y === -1) y = (this.box.h - this.fontSize) / 2;
1819 
1820         if(color !== "" && this.fillStyle !== color) this.fillStyle = this.context.fillStyle = color;
1821         this.context.fillText(value, x, y);
1822 
1823         return this;
1824     }
1825 
1826     linearGradient(sx, sy, dx, dy){
1827         return this.context.createLinearGradient(sx || 0, sy || 0, dx || this.box.w, dy || 0);
1828     }
1829 
1830     gradientColors(gradient, colors, close = false){
1831         if(UTILS.emptyArray(colors)) return gradient;
1832         const len = colors.length;
1833         if(close === false){
1834             for(let k = 0, _len = len - 1; k < len; k++) gradient.addColorStop(k / _len, colors[k]);
1835         }else{
1836             for(let k = 0; k < len; k++) gradient.addColorStop(k / len, colors[k]);
1837             gradient.addColorStop(1, colors[0]);
1838         }
1839         return gradient;
1840     }
1841 
1842     drawTransparentBG(r, box, size = 5){
1843         if(Box.prototype.isPrototypeOf(box)){
1844             var x = box.x,
1845             y = box.y,
1846             w = box.w,
1847             h = box.h;
1848         }else{
1849             var x = 0,
1850             y = 0,
1851             w = this.box.w,
1852             h = this.box.h;
1853         }
1854 
1855         const con = this.context, 
1856         c1 = "rgb(255,255,255)", c2 = "rgb(127,127,127)", 
1857         lenX = Math.ceil(w/size), lenY = Math.ceil(h/size);
1858         
1859         con.save();
1860         this.rect(r, box);
1861         con.clip();
1862 
1863         for(let ix = 0, iy = 0; ix < lenX; ix++){
1864 
1865             for(iy = 0; iy < lenY; iy++){
1866 
1867                 con.fillStyle = (ix + iy) % 2 === 0 ? c1 : c2;
1868                 con.fillRect(ix * size + x, iy * size + y, size, size);
1869                 
1870             }
1871 
1872         }
1873 
1874         con.restore();
1875         return this;
1876     }
1877 
1878 }
1879 
1880 
1881 
1882 export {
1883     CanvasImageEvent,
1884     CanvasImageScroll,
1885     CanvasImageRender,
1886     CanvasImage, 
1887     CanvasImages,
1888     CanvasImageCustom,
1889 }
CanvasImageRender.js

 

   1 "use strict";
   2 
   3 import {
   4     UTILS, 
   5     TreeStruct,
   6     RGBColor,
   7     Box,
   8 } from './Utils.js';
   9 
  10 import {
  11     CanvasImageRender,
  12     CanvasImage,
  13     CanvasImages,
  14     CanvasImageCustom,
  15 } from './CanvasImageRender.js';
  16 
  17 var disableColor = "rgba(0,0,0,0.75)";
  18 
  19 const emptyCIC = new CanvasImageCustom(null, false), 
  20 bgColor = {down: "#b3b3b3", up: "#b3b3b3"}, //如果为null不创建背景
  21 textColor = {down: "#999999", up: "#ffffff"},
  22 borderColor = {down: "#ffffff", up: "#999999"},
  23 padding = {x: 4, y: 4};
  24 
  25 function isS(v){
  26     return UTILS.isNumber(v) && v >= 1 && v !== Infinity;
  27 }
  28 
  29 function getW(v){
  30     return emptyCIC.context.measureText(v).width+padding.x+padding.x;
  31 }
  32 
  33 function getH(w){
  34     return Math.max(emptyCIC.fontSize+padding.y+padding.y, Math.round(0.618*w));
  35 }
  36 
  37 function initCIC(w, h){
  38     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.1);
  39     return emptyCIC;
  40 }
  41 
  42 function getItem(v, t){
  43     return emptyCIC.fill(bgColor[t])
  44     .text(v, textColor[t])
  45     .stroke(borderColor[t])
  46     .shear();
  47 }
  48 
  49 function but_rect(w, h, v){
  50     if(!isS(w)) w = getW(v);
  51     if(!isS(h)) h = getH(w);
  52     
  53     //up
  54     initCIC(w, h);
  55     const a = getItem(v, "up"),
  56 
  57     //disable
  58     c = emptyCIC.fill(disableColor).shear();
  59 
  60     //down
  61     emptyCIC.clear();
  62     const b = getItem(v, "down");
  63     
  64     return [a, b, c];
  65 }
  66 
  67 function but_circle(w, v){
  68     if(!isS(w)) w = getW(v);
  69 
  70     //up
  71     emptyCIC.size(w, w).rect(w / 2);
  72     const a = getItem(v, "up"),
  73 
  74     //disable
  75     c = emptyCIC.fill(disableColor).shear();
  76 
  77     //down
  78     emptyCIC.clear();
  79     const b = getItem(v, "down");
  80 
  81     return [a, b, c];
  82 }
  83 
  84 function bool_rect(w, h){
  85     if(!isS(w)) w = getW("✔");
  86     if(!isS(h)) h = w;
  87     
  88     //true
  89     initCIC(w, h);
  90     const a = getItem("✔", "up"),
  91 
  92     b = emptyCIC.fill(disableColor).shear();
  93 
  94     emptyCIC.clear();
  95     const c = getItem("✔", "down");
  96     
  97     //false
  98     emptyCIC.clear()
  99     const d = getItem("", "up"),
 100 
 101     e = emptyCIC.fill(disableColor).shear();
 102 
 103     emptyCIC.clear();
 104     const f = getItem("", "down");
 105 
 106     return [a, b, c, d, e, f];
 107 }
 108 
 109 function bool_fillet(w, h){
 110     const textW = emptyCIC.context.measureText("开").width;
 111     if(!isS(w)) w = textW*2+padding.x+padding.x + emptyCIC.fontSize;
 112     if(!isS(h)) h = Math.max(emptyCIC.fontSize+padding.y+padding.y, getH(w) / 2)
 113     
 114     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.5);
 115 
 116     const halfW = w / 2, cx = (halfW-textW)/2,
 117 
 118     //true
 119     a = emptyCIC
 120     .fill(bgColor.up)
 121     .text("开", textColor.up, 0, cx)
 122     .text("关", textColor.disable, 0, halfW+cx)
 123     .stroke(borderColor.up)
 124     .shear(),
 125 
 126     b = emptyCIC.fill(disableColor).shear(),
 127 
 128     c = emptyCIC.clear()
 129     .fill(bgColor.down)
 130     .text("开", textColor.down, 0, cx)
 131     .text("关", textColor.down, 0, halfW+cx)
 132     .stroke(borderColor.down)
 133     .shear(),
 134     
 135     //false
 136     d = emptyCIC.clear()
 137     .fill(bgColor.up)
 138     .text("开", textColor.disable, 0, cx)
 139     .text("关", textColor.up, 0, halfW+cx)
 140     .stroke(borderColor.up)
 141     .shear(),
 142     e = emptyCIC.fill(disableColor).shear(),
 143     f = c;
 144 
 145     return [a, b, c, d, e, f];
 146 }
 147 
 148 
 149 class CanvasUI extends CanvasImages{
 150 
 151     static fontSize(v){
 152         v = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 12;
 153         if(v === emptyCIC.fontSize) return v;
 154         emptyCIC.fontSize = v;
 155         emptyCIC.context.font = v+"px SimSun, Songti SC";
 156         return v;
 157     }
 158 
 159     constructor(images){
 160         super(images);
 161     }
 162 
 163 }
 164 
 165 
 166 
 167 
 168 /* CanvasButton 为画布预设按钮 (防止移动端多指混乱)
 169 parameter: 
 170     value: String; //按钮名; 默认 "Button"
 171     width, height: Number; //默认自适应
 172     style: String; //可能值: 默认"rect", "circle"
 173 
 174 attribute:
 175     enable: Bool;     //是否启用;
 176     value: String;     //只读
 177 
 178 method:
 179     bindEvent(cir: CanvasImageRender, callback: Func, delay: Number): this;
 180     //cir: 内部使用cir来绑定或解绑事件和更新自己(cir.redrawTarget(this)); 必须
 181     //callback: 默认null
 182     //delay: 可以限制每次点击的延迟毫秒时间; 默认0
 183 
 184 demo:
 185     //button
 186     const cb = cir.add(new CanvasButton({
 187         width: 80,
 188         value: "测试 TEST",
 189     }));
 190 
 191     cb.bindEvent(cir, event => console.log(1));
 192     cb.box.center(cir.box);
 193     console.log(cb);
 194 */
 195 class CanvasUIButton extends CanvasUI{
 196 
 197     #value = "";
 198     get value(){return this.#value;}
 199 
 200     #enable = true;
 201     get enable(){return this.#enable;}
 202     set enable(v){
 203         if(v === false){
 204             this.cursor = 2;
 205             this.#enable = false;
 206         }
 207         else if(v === true){
 208             this.cursor = 0;
 209             this.#enable = true;
 210         }
 211     }
 212 
 213     constructor(value, width, height, style = "rect"){
 214         
 215         value = value !== "" && typeof value === "string" ? value : "Button";
 216 
 217         switch(style){
 218             case "circle": 
 219             var images = but_circle(width, value);
 220             break;
 221 
 222             case "rect": 
 223             default: 
 224             var images = but_rect(width, height, value);
 225             break;
 226         }
 227         
 228         super(images);
 229         this.#value = value;
 230         
 231     }
 232 
 233     bindEvent(cir, cie, callback, delay = 0){
 234         if(cir.eventDispatcher === null) return this;
 235         callback = typeof callback === "function" ? callback : null;
 236         
 237         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
 238 
 239         const ondelay = () => {
 240             if(this.cursor === 2){
 241                 this.cursor = 0;
 242                 cir.redrawTarget(this);
 243             }
 244             isDelay = false;
 245         },
 246         
 247         onup_body = event => {
 248             if(event.pointerId !== pointerId || this.enable === false) return;
 249             document.body.removeEventListener("pointerup", onup_body);
 250             isDown = false;
 251             if(this.cursor === 1){
 252                 this.cursor = 0;
 253                 if(cir.list.includes(this)) cir.redrawTarget(this);
 254             }
 255         },
 256 
 257         onup = event => {
 258             if(isDown && callback !== null && this.enable && event.pointerId === pointerId){
 259                 const dTime = Date.now() - sTime;
 260                 if(dTime < delay){
 261                     isDelay = true;
 262                     setTimeout(ondelay, delay - dTime);
 263                     this.cursor = 2;
 264                     cir.redrawTarget(this);
 265                 }
 266 
 267                 isDown = false;
 268                 callback(event, this);
 269             }
 270         },
 271 
 272         ondown = event => {
 273             if(isDelay || event.button !== 0 || this.enable === false) return;
 274             pointerId = event.pointerId;
 275             document.body.addEventListener("pointerup", onup_body);
 276             this.cursor = 1;
 277             isDown = true;
 278             sTime = Date.now();
 279             cir.redrawTarget(this);
 280         },
 281 
 282         remove = () => {
 283             document.body.removeEventListener("pointerup", onup_body);
 284             cir.remove(this, "up", onup);
 285             cir.remove(this, "down", ondown);
 286             cir.eventDispatcher.deleteEvent("remove", remove);
 287         }
 288 
 289         cie.add(this, "down", ondown);
 290         cie.add(this, "up", onup);
 291         cir.eventDispatcher.register("remove", remove);
 292     
 293         return this;
 294     }
 295     
 296 }
 297 
 298 
 299 /* CanvasUIBool 开关控件
 300 parameter:
 301     value: Bool; //默认 false
 302     width, height: Number; //默认自适应
 303     style: 默认 rect 矩形开关, fillet 圆角开关
 304 
 305 attribute:
 306     enable: Bool;     //是否启用;
 307     value: String;     //设置控件的值;(注意: 它不会自动更新画布)
 308 
 309 method:
 310     bindEvent(cir, callback, delay = 0): this;
 311 
 312 */
 313 class CanvasUIBool extends CanvasUI{
 314 
 315     #value = false;
 316     get value(){return this.#value;}
 317     set value(v){
 318         if(this.#enable && typeof v === "boolean"){
 319             this.#value = v;
 320             this.cursor = v ? 0 : 3;
 321         }
 322     }
 323 
 324     #enable = true;
 325     get enable(){return this.#enable;}
 326     set enable(v){
 327         if(v === false){
 328             this.cursor = this.#value ? 1 : 4;
 329             this.#enable = false;
 330         }
 331         else if(v === true){
 332             this.cursor = this.#value ? 0 : 3;
 333             this.#enable = true;
 334         }
 335     }
 336 
 337     constructor(value, width, height, style = "rect"){
 338         value = typeof value === "boolean" ? value : false;
 339 
 340         switch(style){
 341             case "fillet": 
 342             var images = bool_fillet(width, height);
 343             break;
 344 
 345             case "rect": 
 346             default: 
 347             var images = bool_rect(width, height);
 348             break;
 349         }
 350         
 351         super(images);
 352         this.value = value;
 353         
 354     }
 355 
 356     bindEvent(cir, cie, callback, delay = 0){
 357         if(cir.eventDispatcher === null) return this;
 358         callback = typeof callback === "function" ? callback : null;
 359         
 360         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
 361 
 362         const ondelay = () => {
 363             if(this.cursor === 1){
 364                 this.cursor = 0;
 365                 cir.redrawTarget(this);
 366             }
 367             else if(this.cursor === 4){
 368                 this.cursor = 3;
 369                 cir.redrawTarget(this);
 370             }
 371             isDelay = false;
 372         },
 373         
 374         onup_body = event => {
 375             if(event.pointerId !== pointerId || this.enable === false) return;
 376             document.body.removeEventListener("pointerup", onup_body);
 377             if(this.cursor === 2){
 378                 this.cursor = 0;
 379                 cir.redrawTarget(this);
 380             }
 381             else if(this.cursor === 5){
 382                 this.cursor = 3;
 383                 cir.redrawTarget(this);
 384             }
 385             isDown = false;
 386         },
 387 
 388         onup = event => {
 389             if(isDown && this.enable && event.pointerId === pointerId){
 390                 this.value = !this.#value;
 391 
 392                 if(callback !== null){
 393                     const dTime = Date.now() - sTime;
 394                     if(dTime < delay){
 395                         isDelay = true;
 396                         setTimeout(ondelay, delay - dTime);
 397                         this.cursor = this.#value ? 1 : 4;
 398                     }
 399                     callback(event, this);
 400                 }
 401 
 402                 cir.redrawTarget(this);
 403                 isDown = false;
 404             }
 405         },
 406 
 407         ondown = event => {
 408             if(isDelay || event.button !== 0 || this.enable === false) return;
 409             pointerId = event.pointerId;
 410             document.body.addEventListener("pointerup", onup_body);
 411             this.cursor = this.cursor === 0 ? 2 : 5;
 412             isDown = true;
 413             sTime = Date.now();
 414             cir.redrawTarget(this);
 415         },
 416 
 417         remove = () => {
 418             document.body.removeEventListener("pointerup", onup_body);
 419             cie.remove(this, "up", onup);
 420             cie.remove(this, "down", ondown);
 421             cir.eventDispatcher.deleteEvent("remove", remove);
 422         }
 423 
 424         cie.add(this, "down", ondown);
 425         cie.add(this, "up", onup);
 426         cir.eventDispatcher.register("remove", remove);
 427         
 428         return this;
 429     }
 430 
 431 }
 432 
 433 
 434 class CanvasUIText extends CanvasUI{
 435 
 436     constructor(value){
 437         super();
 438 
 439     }
 440 
 441 }
 442 
 443 class CanvasUINumber extends CanvasUI{
 444 
 445     constructor(value, min, max, style){
 446         super();
 447 
 448     }
 449 
 450 }
 451 
 452 class CanvasUIColor extends CanvasUI{
 453 
 454 }
 455 
 456 
 457 /* ColorTestViewer 颜色调试器
 458 parameter:
 459     w, 默认 250 
 460     h, 默认 w*0.618 
 461     r, 默认 0
 462 
 463 attribute:
 464     hsv: Object{h,s,v};
 465     alpha: Number;
 466     只读: value: RGBColor, width, height, x, y: Number;
 467 
 468 method:
 469     visible(v: Bool): undefined;//显示或隐藏所以的ci
 470     pos(x, y): undefined;         //如果你不希望排版错乱的话用此方法设置它们的位置位置
 471     toArray(arr): undefined;     //把创建的 CanvasImage push到arr数组 (arr数组一般为某个 CanvasImageRender.list)
 472     update(): undefined;         //当颜色发送改变时更新: ctv.set("red").update()
 473     set(r, g, b, a): this;         //第一个参数r可以时字符串样式的颜色(rgb|rgba|十进制|英文)
 474 
 475     bindEvent(cir: CanvasImageRender, cie: CanvasImageEvent, onchange: Func): this;    //
 476 
 477 demo:
 478     const ctv = new ColorTestViewer(),
 479     cir = new CanvasImageRender({width: ctv.box.w - 5, height: ctv.box.h - 5}),
 480     cie = new CanvasImageEvent(cir); //启用dom事件
 481     
 482     ctv.set("blue").update();
 483     ctv.toArray(cir.list);
 484     ctv.bindEvent(cir, cie);
 485     cir.render();
 486 */
 487 class ColorTestViewer{
 488 
 489     static emptyColor = new RGBColor();
 490     static bgColor = "rgb(127,127,127)";
 491     static textColor = "#fff";
 492 
 493     #value = new RGBColor();
 494     get value(){
 495         return this.#value;
 496     }
 497 
 498     #box = null;
 499     get box(){
 500         return this.#box;
 501     }
 502 
 503     #alpha = 1;
 504     get alpha(){
 505         return this.#alpha;
 506     }
 507 
 508     set alpha(v){
 509         this.#alpha = UTILS.floatKeep(v);
 510     }
 511 
 512     constructor(w, h, r = 4){
 513         if(!isS(w)) w = 250;
 514         if(!isS(h)) h = getH(w);
 515 
 516         this.hsv = {h:0, s:100, v:100};
 517 
 518         const colors = [], lw = w * 0.3, rw = w - lw, sh = 10,
 519         cursorImage = emptyCIC.size(10, 10).rect(5).fill("#fff").shear();
 520 
 521 
 522         //h
 523         this.ciH = new CanvasImageCustom().size(w, h).rect(r);
 524 
 525         colors.length = 0;
 526         for(let h = 0, c = ColorTestViewer.emptyColor; h < 6; h++){
 527             c.setFormHSV(h/6*360, 100, 100);
 528             colors[h] = `rgb(${c.r},${c.g},${c.b})`;
 529         }
 530         emptyCIC.size(rw-10, sh).rect(2).fill(emptyCIC.gradientColors(emptyCIC.linearGradient(0, sh, rw-10, sh), colors, true));
 531         this.ciH_scroll = new CanvasImage(emptyCIC.shear());
 532 
 533         this.cursorH = new CanvasImage(cursorImage);
 534 
 535 
 536         //sv 饱和度&明度
 537         emptyCIC.size(w, h).rect(r);
 538         colors.length = 0;
 539         for(let s = 0; s < 100; s++) colors[s] = `rgba(255,255,255,${1 - s / 99})`;
 540         emptyCIC.fill(emptyCIC.gradientColors(emptyCIC.linearGradient(0, h, w, h), colors));
 541         
 542         colors.length = 0;
 543         for(let v = 0; v < 100; v++) colors[v] = `rgba(0,0,0,${1 - v / 99})`;
 544         emptyCIC.fill(emptyCIC.gradientColors(emptyCIC.linearGradient(w, h, w, 0), colors));
 545 
 546         this.ciSV = new CanvasImage(emptyCIC.shear());
 547 
 548         this.cursorSV = new CanvasImage(emptyCIC.size(10, 10).rect(5).fill("rgba(255,255,255,0.4)").stroke("rgba(0,0,0,0.6)").shear());
 549 
 550 
 551         //a
 552         this.ciA = new CanvasImage(emptyCIC.size(rw-10, sh).drawTransparentBG(2, null, sh/2).shear());
 553 
 554         colors.length = 0;
 555         for(let a = 0; a < 10; a++) colors[a] = `rgba(0,0,0,${a / 9})`;
 556         emptyCIC.size(this.ciA.box.w, sh).rect(2).fill(emptyCIC.gradientColors(emptyCIC.linearGradient(0, sh, this.ciA.box.w, sh), colors));
 557         this.ciA_scroll = new CanvasImage(emptyCIC.shear());
 558 
 559         this.cursorA = new CanvasImage(cursorImage);
 560 
 561         //bottom bg
 562         this.bottomBG = new CanvasImage(emptyCIC.size(w, lw).rect(r).fill(ColorTestViewer.bgColor).shear());
 563 
 564         //result
 565         this.resultBG = new CanvasImage(emptyCIC.size(lw-10, lw-10).drawTransparentBG(2, null, 5).shear());
 566         this.resultColor = new CanvasImageCustom().size(this.resultBG.box.w, this.resultBG.box.h).rect(2);
 567         this.resultText = new CanvasImageCustom().size(this.ciA.box.w, this.resultColor.box.h - this.ciH_scroll.box.h - this.ciA_scroll.box.h - 15);
 568         
 569         const _box = new Box(), scope = this, _s = this.cursorSV.box.w / 2;
 570         this.#box = _box;
 571         Object.defineProperties(_box, {
 572             x: {get: () => {return scope.ciH.box.x - _s;}},
 573             y: {get: () => {return scope.ciH.box.y - _s;}},
 574             w: {get: () => {return scope.ciH.box.w + _s;}},
 575             h: {get: () => {return scope.ciH.box.h + scope.bottomBG.box.h + _s;}},
 576         });
 577 
 578         this.updateCIH();
 579         this.updateResult();
 580         this.pos(0, 0);
 581     }
 582 
 583     bindEvent(cir, cie, onchange = null){
 584         var sx = 0, sy = 0, v;
 585         const cursorSV_half = this.cursorSV.box.w / 2,
 586 
 587 
 588         //SV
 589         setSV = (x, y) => {
 590             if(x < this.#box.x) x = this.#box.x;
 591             else{
 592                 v = this.ciSV.box.mx - cursorSV_half;
 593                 if(x > v) x = v;
 594             }
 595 
 596             if(y < this.#box.y) y = this.#box.y;
 597             else{
 598                 v = this.ciSV.box.my - cursorSV_half;
 599                 if(y > v) y = v;
 600             }
 601 
 602             this.cursorSV.box.pos(x, y);
 603             
 604             x += cursorSV_half;
 605             y += cursorSV_half;
 606             this.hsv.s = (x - this.ciSV.box.x) / this.ciSV.box.w * 100;
 607             this.hsv.v = (1 - (y - this.ciSV.box.y) / this.ciSV.box.h) * 100;
 608             this.updateResult();
 609 
 610             if(onchange !== null) onchange(this);
 611             cir.redrawTarget(this.#box);
 612         },
 613 
 614         onmoveSV = event => {
 615             setSV(event.offsetX - sx, event.offsetY - sy);
 616         },
 617 
 618         onupSV = event => {
 619             document.body.removeEventListener('pointermove', onmoveSV);
 620             document.body.removeEventListener('pointerup', onupSV);
 621             setSV(event.offsetX - sx, event.offsetY - sy);
 622         },
 623 
 624         ondownSV = event => {
 625             sx = event.offsetX - this.cursorSV.box.x;
 626             sy = event.offsetY - this.cursorSV.box.y;
 627             onupSV(event);
 628             document.body.addEventListener("pointermove", onmoveSV);
 629             document.body.addEventListener("pointerup", onupSV);
 630         },
 631 
 632 
 633         //H
 634         setH = x => {
 635             v = this.ciH_scroll.box.x - cursorSV_half;
 636             if(x < v) x = v;
 637             else{
 638                 v = this.ciH_scroll.box.mx - cursorSV_half;
 639                 if(x > v) x = v;
 640             }
 641 
 642             this.cursorH.box.x = x;
 643             
 644             x += cursorSV_half;
 645             this.hsv.h = (x - this.ciH_scroll.box.x) / this.ciH_scroll.box.w * 360;
 646             this.updateCIH();
 647             this.updateResult();
 648 
 649             if(onchange !== null) onchange(this);
 650             cir.redrawTarget(this.#box);
 651         },
 652 
 653         onmoveH = event => {
 654             setH(event.offsetX - sx);
 655         },
 656 
 657         onupH = event => {
 658             document.body.removeEventListener('pointermove', onmoveH);
 659             document.body.removeEventListener('pointerup', onupH);
 660             setH(event.offsetX - sx);
 661         },
 662 
 663         ondownH = event => {
 664             sx = event.offsetX - this.cursorH.box.x;
 665             sy = event.offsetY - this.cursorH.box.y;
 666             onupH(event);
 667             document.body.addEventListener("pointermove", onmoveH);
 668             document.body.addEventListener("pointerup", onupH);
 669         },
 670 
 671 
 672         //A
 673         setA = x => {
 674             v = this.ciA_scroll.box.x - cursorSV_half;
 675             if(x < v) x = v;
 676             else{
 677                 v = this.ciA_scroll.box.mx - cursorSV_half;
 678                 if(x > v) x = v;
 679             }
 680 
 681             this.cursorA.box.x = x;
 682             
 683             x += cursorSV_half;
 684             this.alpha = (x - this.ciA_scroll.box.x) / this.ciA_scroll.box.w * 1;
 685             this.updateResult();
 686 
 687             if(onchange !== null) onchange(this);
 688             cir.redrawTarget(this.#box);
 689         },
 690 
 691         onmoveA = event => {
 692             setA(event.offsetX - sx);
 693         },
 694 
 695         onupA = event => {
 696             document.body.removeEventListener('pointermove', onmoveA);
 697             document.body.removeEventListener('pointerup', onupA);
 698             setA(event.offsetX - sx);
 699         },
 700 
 701         ondownA = event => {
 702             sx = event.offsetX - this.cursorA.box.x;
 703             sy = event.offsetY - this.cursorA.box.y;
 704             onupA(event);
 705             document.body.addEventListener("pointermove", onmoveA);
 706             document.body.addEventListener("pointerup", onupA);
 707         }
 708 
 709 
 710         cie.add(this.cursorSV, "down", ondownSV);
 711         cie.add(this.ciSV, "down", ondownSV);
 712 
 713         cie.add(this.cursorH, "down", ondownH);
 714         cie.add(this.ciH_scroll, "down", ondownH);
 715 
 716         cie.add(this.cursorA, "down", ondownA);
 717         cie.add(this.ciA_scroll, "down", ondownA);
 718 
 719         return this;
 720     }
 721 
 722     pos(x, y){
 723         this.ciH.box.pos(x, y);
 724         this.ciSV.box.pos(x, y);
 725         this.updateCursorSV();
 726 
 727         this.bottomBG.pos(this.ciH.x, this.ciH.box.my);
 728         this.resultBG.pos(this.bottomBG.x+5, this.bottomBG.y+5);
 729         this.resultColor.pos(this.resultBG.x, this.resultBG.y);
 730 
 731         this.ciH_scroll.pos(this.resultBG.box.mx + 5, this.resultBG.y + 5);
 732         this.updateCursorH();
 733 
 734         this.ciA.pos(this.ciH_scroll.x, this.ciH_scroll.box.my + 5);
 735         this.ciA_scroll.pos(this.ciA.x, this.ciA.y);
 736         this.updateCursorA();
 737 
 738         this.resultText.pos(this.ciA_scroll.x, this.ciA_scroll.box.my + 5);
 739     }
 740 
 741     visible(v){
 742         this.ciH.visible = this.ciSV.visible = this.cursorSV.visible = 
 743         this.bottomBG.visible = this.resultBG.visible = this.resultColor.visible = 
 744         this.resultText.visible = this.ciH_scroll.visible = this.cursorH.visible = 
 745         this.ciA.visible = this.ciA_scroll.visible = this.cursorA.visible = v;
 746     }
 747 
 748     toArray(arr){
 749         arr.push(this.ciH, this.ciSV, this.cursorSV, this.bottomBG, this.resultBG, this.resultColor, this.resultText, this.ciH_scroll, this.cursorH, this.ciA, this.ciA_scroll, this.cursorA);
 750     }
 751 
 752     set(r, g, b, a){
 753         if(typeof r !== "string"){
 754             ColorTestViewer.emptyColor.set(r, g, b).getHSV(this.hsv);
 755             this.alpha = a || 1;
 756         }
 757         else{
 758             this.alpha = ColorTestViewer.emptyColor.setFormString(r);
 759             ColorTestViewer.emptyColor.getHSV(this.hsv);
 760         }
 761         return this;
 762     }
 763 
 764     update(){
 765         this.updateCIH();
 766         this.updateResult();
 767 
 768         this.updateCursorSV();
 769         this.updateCursorH();
 770         this.updateCursorA();
 771     }
 772 
 773     updateCursorSV(){
 774         this.cursorSV.box.x = this.hsv.s / 100 * this.ciSV.box.w + this.ciSV.box.x - this.cursorSV.box.w / 2;
 775         this.cursorSV.box.y = (1 - this.hsv.v / 100) * this.ciSV.box.h + this.ciSV.box.y - this.cursorSV.box.h / 2;
 776     }
 777 
 778     updateCursorH(){
 779         this.cursorH.box.x = this.hsv.h / 360 * this.ciH_scroll.box.w + this.ciH_scroll.box.x - this.cursorH.box.w / 2;
 780         this.cursorH.box.y = this.ciH_scroll.box.y;
 781     }
 782 
 783     updateCursorA(){
 784         this.cursorA.box.x = this.alpha * this.ciA_scroll.box.w + this.ciA_scroll.box.x - this.cursorA.box.w / 2;
 785         this.cursorA.box.y = this.ciA_scroll.box.y;
 786     }
 787 
 788     updateCIH(){
 789         const c = ColorTestViewer.emptyColor.setFormHSV(this.hsv.h, 100, 100);
 790         this.ciH.fill(`rgb(${c.r},${c.g},${c.b})`);
 791     }
 792 
 793     updateResult(){
 794         this.#value.setFormHSV(this.hsv.h, this.hsv.s, this.hsv.v);
 795         this.resultColor.clear().fill(this.#value.getRGBA(this.alpha));
 796         this.resultText.clear().text(this.resultColor.fillStyle, ColorTestViewer.textColor);
 797     }
 798 
 799 }
 800 
 801 
 802 /* IconMenu 图标菜单
 803 parameter:
 804     data: Array[Object{
 805         icon: Image,    //默认 null
 806         text: String,    //默认 "空"
 807         func: Func,     //默认 null
 808     }]
 809 
 810 demo:
 811     new CanvasImage().loadImage("./test.png", ci => {
 812         const iconMenu = new IconMenu([
 813             {
 814                 icon: ci.image,
 815                 text: "StringStringStringStringString",
 816             },
 817             {
 818                 icon: ci.image,
 819                 text: "String",
 820                 func: (cisA, cisB, data) => {
 821                     //cisA: 0:"✘", 1:"✔", 2:"●" 3:">"; .visible 默认 false;
 822                     //cisB: 0: 禁用, 1: 选取; .visible 默认 false;
 823                     cisA.next();
 824                     cisA.visible = true;
 825                 },
 826             },
 827         ]);
 828 
 829         iconMenu.domElement.style = `
 830             background: #666;
 831             position: absolute;
 832             left: 100px;
 833             top: 100px;
 834             border-radius: 4px;
 835         `;
 836 
 837         iconMenu.render().bindEvent();
 838     });
 839 */
 840 class IconMenu{
 841 
 842     static iconSize = 16;
 843     static selectColor = "rgba(85,146,209,0.4)";
 844     static disableColor = disableColor;
 845     static getImages(){
 846         const s = IconMenu.iconSize;
 847         emptyCIC.size(s, s);
 848 
 849         //0:"✘", 1:"✔", 2:"●" 3:">", 
 850         return [
 851             emptyCIC.clear().text("✘", textColor.up).shear(),
 852             emptyCIC.clear().text("✔", textColor.up).shear(),
 853             emptyCIC.clear().text("●", textColor.up).shear(),
 854             emptyCIC.path([1, 1, 1, s-1, s-1, s/2], true).clear().fill(textColor.up).shear(),
 855         ]
 856     }
 857 
 858     static getMaxWidth(data){
 859         const s = IconMenu.iconSize * 2 + padding.x * 2;
 860         var mx = 0, maxX = 0;
 861         data.forEach(o => {
 862             mx = getW(o.text) + s;
 863             if(maxX < mx) maxX = mx;
 864         });
 865         return maxX;
 866     }
 867 
 868     #box = null;
 869     #iconSize = 0;
 870     #paddingX = 0;
 871     #paddingY = 0;
 872     #len = 0;
 873 
 874     constructor(data){
 875         if(UTILS.emptyArray(data)) return;
 876         this.list = [];
 877 
 878         const mw = IconMenu.getMaxWidth(data), 
 879         iconSize = IconMenu.iconSize, 
 880         paddingX = padding.x, 
 881         paddingY = padding.y;
 882         
 883         const icons = IconMenu.getImages();
 884 
 885         initCIC(mw - paddingX * 2, iconSize);
 886 
 887         const mask = [
 888             emptyCIC.clear().fill(IconMenu.disableColor).shear(),
 889             emptyCIC.fill(IconMenu.selectColor).shear(),
 890         ];
 891 
 892         emptyCIC.size(mw, iconSize);
 893         data.forEach((o, i) => {
 894             emptyCIC.clear();
 895             if(CanvasImageRender.isCanvasImage(o.icon)) emptyCIC.context.drawImage(o.icon, 0, 0, iconSize, iconSize);
 896             if(typeof o.text === "string" && o.text !== "") emptyCIC.text(o.text, textColor.up, 0, iconSize+paddingX);
 897 
 898             const a = new CanvasImage(emptyCIC.shear()),
 899             b = new CanvasImages(icons),
 900             c = new CanvasImages(mask);
 901             
 902             b.visible = c.visible = false;
 903             c.cursor = 1;
 904             this.list.push(a, b, c);
 905         });
 906 
 907         this.#iconSize = iconSize;
 908         this.#paddingX = paddingX;
 909         this.#paddingY = paddingY;
 910         this.#len = this.list.length / 3;
 911         this.data = data;
 912         this.bg = new CanvasImageCustom().size(mw, this.#len * emptyCIC.box.h + paddingY * this.#len + paddingY).rect(4).fill("rgb(127,127,127)");
 913         this.#box = this.bg.box;
 914     }
 915 
 916     pos(x, y){
 917         for(let k = 0, len = this.list.length, i, _y; k < len; k += 3){
 918             i = k / 3;
 919             _y = this.#iconSize * i + this.#paddingY * i + this.#paddingY+y;
 920             this.list[k].pos(this.#paddingX+x, _y);
 921             this.list[k+1].pos(this.#box.w - this.#iconSize - this.#paddingX+x, _y),
 922             this.list[k+2].pos(this.#paddingX+x, _y);
 923         }
 924 
 925         this.#box.pos(x, y);
 926     }
 927 
 928     toArray(arr){
 929         arr.push(this.bg);
 930         this.list.forEach(ci => arr.push(ci));
 931     }
 932 
 933     bindEvent(cir, cie){
 934         var pointerId = -1, i = -1, isDown = false, cis = null;
 935 
 936         const _box = this.#box,
 937         
 938         eventTarget = {
 939             get index(){return Infinity;},
 940             get visible(){return true;},
 941             get box(){return _box;},
 942             get rotate(){return null},
 943         },
 944 
 945         onup_body = () => {
 946             document.body.removeEventListener("pointerup", onup_body);
 947 
 948             if(cis && cis.visible === true && cis.cursor !== 0){
 949                 cis.visible = false;
 950                 cir.redrawTarget(cis);
 951                 cis = null;
 952             }
 953         },
 954 
 955         onup = event => {
 956             if(isDown && event.pointerId === pointerId && i === Math.floor(event.offsetY/_box.h*this.#len) && this.list[i*3+2].cursor !== 0){
 957                 const item = this.data[i];
 958                 if(UTILS.isObject(item) && typeof item.func === "function") item.func(this.list[i*3+1], cis, item);
 959             }
 960 
 961             isDown = false;
 962         },
 963 
 964         ondown = event => {
 965             onup_body();
 966             if(event.button !== 0) return;
 967             i = Math.floor((event.offsetY + cir.box.y - _box.y)/_box.h*this.#len);
 968             cis = this.list[i*3+2];
 969             if(cis && cis.visible === false && cis.cursor !== 0){
 970                 cis.cursor = 1;
 971                 cis.visible = true;
 972                 cir.redrawTarget(cis);
 973                 pointerId = event.pointerId;
 974                 isDown = true;
 975                 document.body.addEventListener("pointerup", onup_body);
 976             }
 977         }
 978 
 979         cie.add(eventTarget, "down", ondown);
 980         cie.add(eventTarget, "up", onup);
 981         return this;
 982     }
 983 
 984 }
 985 
 986 
 987 /* TreeIconMenu 树结构图标菜单
 988 
 989 */
 990 class TreeIconMenu extends TreeStruct{
 991 
 992     constructor(data){
 993         
 994     }
 995 
 996 }
 997 
 998 
 999 
1000 
1001 /* Carousel 轮播图 (支持pc和mobile端)
1002 
1003 */
1004 class CanvasCarousel extends CanvasImageRender{
1005 
1006     constructor(){
1007         super();
1008         
1009     }
1010 
1011     init(images){
1012         const len = images.length;
1013         for(let k = 0; k < len; k++) this.context.drawImage(images[k], k*this.box.w, 0, this.box.w, this.box.h);
1014         
1015     }
1016 
1017 }
1018 
1019 
1020 
1021 
1022 /* CanvasTab 
1023 
1024 */
1025 class CanvasTab{
1026 
1027     constructor(){
1028         
1029     }
1030 
1031 }
1032 
1033 
1034 export {
1035     CanvasUI,
1036     CanvasUIButton,
1037     CanvasUIBool,
1038     IconMenu,
1039     TreeIconMenu,
1040     ColorTestViewer,
1041 }
CanvasUI.js

 

API: 

 1 /* ColorTestViewer 颜色调试器
 2 parameter:
 3     w, 默认 250 
 4     h, 默认 w*0.618 
 5     r, 默认 0
 6 
 7 attribute:
 8     hsv: Object{h,s,v};
 9     alpha: Number;
10     只读: value: RGBColor, width, height, x, y: Number;
11 
12 method:
13     visible(v: Bool): undefined;//显示或隐藏所以的ci
14     pos(x, y): undefined;         //如果你不希望排版错乱的话用此方法设置它们的位置位置
15     toArray(arr): undefined;     //把创建的 CanvasImage push到arr数组 (arr数组一般为某个 CanvasImageRender.list)
16     update(): undefined;         //当颜色发送改变时更新: ctv.set("red").update()
17     set(r, g, b, a): this;         //第一个参数r可以时字符串样式的颜色(rgb|rgba|十进制|英文)
18 
19     bindEvent(cir: CanvasImageRender, cie: CanvasImageEvent, onchange: Func): this;    //
20 
21 demo:
22     const ctv = new ColorTestViewer(),
23     cir = new CanvasImageRender({width: ctv.box.w - 5, height: ctv.box.h - 5}),
24     cie = new CanvasImageEvent(cir); //启用dom事件
25     
26     ctv.set("blue").update();
27     ctv.toArray(cir.list);
28     ctv.bindEvent(cir, cie);
29     cir.render();
30 */

 

 

main.js:

 1 "use strict";
 2 import { UTILS } from './Utils.js';
 3 import { CanvasImage, CanvasImageRender, CanvasImageEvent, CanvasImageScroll } from './CanvasImageRender.js';
 4 import { IconMenu, ColorTestViewer } from './CanvasUI.js';
 5 
 6 document.body.style = `
 7     width: ${innerWidth}px;
 8     height: ${innerHeight}px;
 9     overflow: hidden;
10     background: #000;
11 `;
12 
13 //CanvasImageRender
14 const cir = new CanvasImageRender({width: innerWidth, height: innerHeight})
15 .initEventDispatcher(), //初始化自定义事件
16 cie = new CanvasImageEvent(cir), //启用dom事件
17 cis = new CanvasImageScroll(cir, cie, {scrollEventType: "touch"}), //启用滚动条
18 
19 //随机创建 CanvasImage
20 ci = new CanvasImage().loadImage("lan.png", cir); //图片加载完后更新一次画布
21 for(let k = 0, pi2 = Math.PI*2, ca; k < 1000; k++){
22     ca = cir.list[k] = new CanvasImage(ci);                     //添加至渲染列表
23     ca.pos(UTILS.random(0, 5000), UTILS.random(0, 5000));    //随机位置
24     //ca.rotate = new Rotate(0, 0, UTILS.random(0, pi2));         //随机旋转
25 }
26 
27 //颜色调试器
28 const ctv = new ColorTestViewer();
29 ctv.pos(200, 100);
30 ctv.toArray(cir.list);
31 ctv.bindEvent(cir, cie);
32 ctv.set("blue").update();
33 
34 //带图标的菜单
35 new CanvasImage().loadImage("hong.png", ci => {
36     const im = new IconMenu([
37         {
38             icon: ci.image,
39             text: "string",
40         },
41         {
42             icon: ci.image,
43             text: "stringstringstringstringstring",
44         },
45         {
46             icon: ci.image,
47             text: "string",
48         },
49     ]);
50 
51     im.pos(ctv.box.x, ctv.box.my+10);
52     im.toArray(cir.list);
53     im.bindEvent(cir, cie);
54     cir.redraw();
55 });
56 
57 //为什么要用 initList() ? 因为for循环中是直接把ca加入到了列表, 没对ca做任何初始化操作;
58 //如果全用.add() 就不需要在调用 initList(); 但 .add() 方法不适合一次性添加多个 CanvasImage;
59 cir.initList(); 

 

如果一个canvas只装一个颜色调试器(让canvas的宽高等于调试器的宽高):

"use strict";

import { CanvasImageRender, CanvasImageEvent } from './CanvasImageRender.js';
import { ColorTestViewer } from './CanvasUI.js';

document.body.style = `
    width: ${innerWidth}px;
    height: ${innerHeight}px;
    overflow: hidden;
    background: #000;
`;

const ctv = new ColorTestViewer(),
cir = new CanvasImageRender({width: ctv.box.w - 5, height: ctv.box.h - 5}),
cie = new CanvasImageEvent(cir); //启用dom事件

cir.domElement.style = `
    position: absolute;
    left: 200px;
    top: 100px;
`;

ctv.toArray(cir.list);
ctv.bindEvent(cir, cie, ()=>console.log(1));
cir.render();

 

结果图:

 

 

posted @ 2022-09-20 15:56  鸡儿er  阅读(29)  评论(0编辑  收藏  举报