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

 

  1 "use strict";
  2 
  3 import {
  4     UTILS, 
  5     TreeStruct,
  6 } from './Utils.js';
  7 
  8 import {
  9     CanvasImageEvent,
 10     CanvasImageRender,
 11     CanvasImage,
 12     CanvasImages,
 13     CanvasImageCustom,
 14 } from './CanvasImageRender.js';
 15 
 16 var disableColor = "rgba(0,0,0,0.75)";
 17 
 18 const emptyCIC = new CanvasImageCustom(null, false), 
 19 bgColor = {down: "#b3b3b3", up: "#b3b3b3"}, //如果为null不创建背景
 20 textColor = {down: "#999999", up: "#ffffff"},
 21 borderColor = {down: "#ffffff", up: "#999999"},
 22 padding = {x: 4, y: 4};
 23 
 24 function isS(v){
 25     return UTILS.isNumber(v) && v >= 1 && v !== Infinity;
 26 }
 27 
 28 function getW(v){
 29     return emptyCIC.context.measureText(v).width+padding.x+padding.x;
 30 }
 31 
 32 function getH(w){
 33     return Math.max(emptyCIC.fontSize+padding.y+padding.y, Math.round(0.618*w));
 34 }
 35 
 36 function initCIC(w, h){
 37     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.1);
 38     return emptyCIC;
 39 }
 40 
 41 function getItem(v, t){
 42     return emptyCIC.fill(bgColor[t])
 43     .text(v, textColor[t])
 44     .stroke(borderColor[t])
 45     .shear();
 46 }
 47 
 48 function but_rect(w, h, v){
 49     if(!isS(w)) w = getW(v);
 50     if(!isS(h)) h = getH(w);
 51     
 52     //up
 53     initCIC(w, h);
 54     const a = getItem(v, "up"),
 55 
 56     //disable
 57     c = emptyCIC.fill(disableColor).shear();
 58 
 59     //down
 60     emptyCIC.clear();
 61     const b = getItem(v, "down");
 62     
 63     return [a, b, c];
 64 }
 65 
 66 function but_circle(w, v){
 67     if(!isS(w)) w = getW(v);
 68 
 69     //up
 70     emptyCIC.size(w, w).rect(w / 2);
 71     const a = getItem(v, "up"),
 72 
 73     //disable
 74     c = emptyCIC.fill(disableColor).shear();
 75 
 76     //down
 77     emptyCIC.clear();
 78     const b = getItem(v, "down");
 79 
 80     return [a, b, c];
 81 }
 82 
 83 function bool_rect(w, h){
 84     if(!isS(w)) w = getW("✔");
 85     if(!isS(h)) h = w;
 86     
 87     //true
 88     initCIC(w, h);
 89     const a = getItem("✔", "up"),
 90 
 91     b = emptyCIC.fill(disableColor).shear();
 92 
 93     emptyCIC.clear();
 94     const c = getItem("✔", "down");
 95     
 96     //false
 97     emptyCIC.clear()
 98     const d = getItem("", "up"),
 99 
100     e = emptyCIC.fill(disableColor).shear();
101 
102     emptyCIC.clear();
103     const f = getItem("", "down");
104 
105     return [a, b, c, d, e, f];
106 }
107 
108 function bool_fillet(w, h){
109     const textW = emptyCIC.context.measureText("开").width;
110     if(!isS(w)) w = textW*2+padding.x+padding.x + emptyCIC.fontSize;
111     if(!isS(h)) h = Math.max(emptyCIC.fontSize+padding.y+padding.y, getH(w) / 2)
112     
113     emptyCIC.size(w, h).rect(Math.min(w, h) * 0.5);
114 
115     const halfW = w / 2, cx = (halfW-textW)/2,
116 
117     //true
118     a = emptyCIC
119     .fill(bgColor.up)
120     .text("开", textColor.up, 0, cx)
121     .text("关", textColor.disable, 0, halfW+cx)
122     .stroke(borderColor.up)
123     .shear(),
124 
125     b = emptyCIC.fill(disableColor).shear(),
126 
127     c = emptyCIC.clear()
128     .fill(bgColor.down)
129     .text("开", textColor.down, 0, cx)
130     .text("关", textColor.down, 0, halfW+cx)
131     .stroke(borderColor.down)
132     .shear(),
133     
134     //false
135     d = emptyCIC.clear()
136     .fill(bgColor.up)
137     .text("开", textColor.disable, 0, cx)
138     .text("关", textColor.up, 0, halfW+cx)
139     .stroke(borderColor.up)
140     .shear(),
141     e = emptyCIC.fill(disableColor).shear(),
142     f = c;
143 
144     return [a, b, c, d, e, f];
145 }
146 
147 
148 class CanvasUI extends CanvasImages{
149 
150     static fontSize(v){
151         v = UTILS.isNumber(v) && v > 0 && v !== Infinity ? v : 12;
152         if(v === emptyCIC.fontSize) return v;
153         emptyCIC.fontSize = v;
154         emptyCIC.context.font = v+"px SimSun, Songti SC";
155         return v;
156     }
157 
158     constructor(images){
159         super(images);
160     }
161 
162 }
163 
164 
165 
166 
167 /* CanvasButton 为画布预设按钮 (防止移动端多指混乱)
168 parameter: 
169     value: String; //按钮名; 默认 "Button"
170     width, height: Number; //默认自适应
171     style: String; //可能值: 默认"rect", "circle"
172 
173 attribute:
174     enable: Bool;     //是否启用;
175     value: String;     //只读
176 
177 method:
178     bindEvent(cir: CanvasImageRender, callback: Func, delay: Number): this;
179     //cir: 内部使用cir来绑定或解绑事件和更新自己(cir.redrawTarget(this)); 必须
180     //callback: 默认null
181     //delay: 可以限制每次点击的延迟毫秒时间; 默认0
182 
183 demo:
184     //button
185     const cb = cir.add(new CanvasButton({
186         width: 80,
187         value: "测试 TEST",
188     }));
189 
190     cb.bindEvent(cir, event => console.log(1));
191     cb.box.center(cir.box);
192     console.log(cb);
193 */
194 class CanvasUIButton extends CanvasUI{
195 
196     #value = "";
197     get value(){return this.#value;}
198 
199     #enable = true;
200     get enable(){return this.#enable;}
201     set enable(v){
202         if(v === false){
203             this.cursor = 2;
204             this.#enable = false;
205         }
206         else if(v === true){
207             this.cursor = 0;
208             this.#enable = true;
209         }
210     }
211 
212     constructor(value, width, height, style = "rect"){
213         
214         value = value !== "" && typeof value === "string" ? value : "Button";
215 
216         switch(style){
217             case "circle": 
218             var images = but_circle(width, value);
219             break;
220 
221             case "rect": 
222             default: 
223             var images = but_rect(width, height, value);
224             break;
225         }
226         
227         super(images);
228         this.#value = value;
229         
230     }
231 
232     bindEvent(cir, cie, callback, delay = 0){
233         if(cir.eventDispatcher === null) return this;
234         callback = typeof callback === "function" ? callback : null;
235         
236         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
237 
238         const ondelay = () => {
239             if(this.cursor === 2){
240                 this.cursor = 0;
241                 cir.redrawTarget(this);
242             }
243             isDelay = false;
244         },
245         
246         onup_body = event => {
247             if(event.pointerId !== pointerId || this.enable === false) return;
248             document.body.removeEventListener("pointerup", onup_body);
249             isDown = false;
250             if(this.cursor === 1){
251                 this.cursor = 0;
252                 if(cir.list.includes(this)) cir.redrawTarget(this);
253             }
254         },
255 
256         onup = event => {
257             if(isDown && callback !== null && this.enable && event.pointerId === pointerId){
258                 const dTime = Date.now() - sTime;
259                 if(dTime < delay){
260                     isDelay = true;
261                     setTimeout(ondelay, delay - dTime);
262                     this.cursor = 2;
263                     cir.redrawTarget(this);
264                 }
265 
266                 isDown = false;
267                 callback(event, this);
268             }
269         },
270 
271         ondown = event => {
272             if(isDelay || event.button !== 0 || this.enable === false) return;
273             pointerId = event.pointerId;
274             document.body.addEventListener("pointerup", onup_body);
275             this.cursor = 1;
276             isDown = true;
277             sTime = Date.now();
278             cir.redrawTarget(this);
279         },
280 
281         remove = () => {
282             document.body.removeEventListener("pointerup", onup_body);
283             cir.remove(this, "up", onup);
284             cir.remove(this, "down", ondown);
285             cir.eventDispatcher.deleteEvent("remove", remove);
286         }
287 
288         cie.add(this, "down", ondown);
289         cie.add(this, "up", onup);
290         cir.eventDispatcher.register("remove", remove);
291     
292         return this;
293     }
294     
295 }
296 
297 
298 /* CanvasUIBool 开关控件
299 parameter:
300     value: Bool; //默认 false
301     width, height: Number; //默认自适应
302     style: 默认 rect 矩形开关, fillet 圆角开关
303 
304 attribute:
305     enable: Bool;     //是否启用;
306     value: String;     //设置控件的值;(注意: 它不会自动更新画布)
307 
308 method:
309     bindEvent(cir, callback, delay = 0): this;
310 
311 */
312 class CanvasUIBool extends CanvasUI{
313 
314     #value = false;
315     get value(){return this.#value;}
316     set value(v){
317         if(this.#enable && typeof v === "boolean"){
318             this.#value = v;
319             this.cursor = v ? 0 : 3;
320         }
321     }
322 
323     #enable = true;
324     get enable(){return this.#enable;}
325     set enable(v){
326         if(v === false){
327             this.cursor = this.#value ? 1 : 4;
328             this.#enable = false;
329         }
330         else if(v === true){
331             this.cursor = this.#value ? 0 : 3;
332             this.#enable = true;
333         }
334     }
335 
336     constructor(value, width, height, style = "rect"){
337         value = typeof value === "boolean" ? value : false;
338 
339         switch(style){
340             case "fillet": 
341             var images = bool_fillet(width, height);
342             break;
343 
344             case "rect": 
345             default: 
346             var images = bool_rect(width, height);
347             break;
348         }
349         
350         super(images);
351         this.value = value;
352         
353     }
354 
355     bindEvent(cir, cie, callback, delay = 0){
356         if(cir.eventDispatcher === null) return this;
357         callback = typeof callback === "function" ? callback : null;
358         
359         var pointerId = -1, isDown = false, isDelay = false, sTime = 0;
360 
361         const ondelay = () => {
362             if(this.cursor === 1){
363                 this.cursor = 0;
364                 cir.redrawTarget(this);
365             }
366             else if(this.cursor === 4){
367                 this.cursor = 3;
368                 cir.redrawTarget(this);
369             }
370             isDelay = false;
371         },
372         
373         onup_body = event => {
374             if(event.pointerId !== pointerId || this.enable === false) return;
375             document.body.removeEventListener("pointerup", onup_body);
376             if(this.cursor === 2){
377                 this.cursor = 0;
378                 cir.redrawTarget(this);
379             }
380             else if(this.cursor === 5){
381                 this.cursor = 3;
382                 cir.redrawTarget(this);
383             }
384             isDown = false;
385         },
386 
387         onup = event => {
388             if(isDown && this.enable && event.pointerId === pointerId){
389                 this.value = !this.#value;
390 
391                 if(callback !== null){
392                     const dTime = Date.now() - sTime;
393                     if(dTime < delay){
394                         isDelay = true;
395                         setTimeout(ondelay, delay - dTime);
396                         this.cursor = this.#value ? 1 : 4;
397                     }
398                     callback(event, this);
399                 }
400 
401                 cir.redrawTarget(this);
402                 isDown = false;
403             }
404         },
405 
406         ondown = event => {
407             if(isDelay || event.button !== 0 || this.enable === false) return;
408             pointerId = event.pointerId;
409             document.body.addEventListener("pointerup", onup_body);
410             this.cursor = this.cursor === 0 ? 2 : 5;
411             isDown = true;
412             sTime = Date.now();
413             cir.redrawTarget(this);
414         },
415 
416         remove = () => {
417             document.body.removeEventListener("pointerup", onup_body);
418             cie.remove(this, "up", onup);
419             cie.remove(this, "down", ondown);
420             cir.eventDispatcher.deleteEvent("remove", remove);
421         }
422 
423         cie.add(this, "down", ondown);
424         cie.add(this, "up", onup);
425         cir.eventDispatcher.register("remove", remove);
426         
427         return this;
428     }
429 
430 }
431 
432 
433 class CanvasUIText extends CanvasUI{
434 
435     constructor(value){
436         super();
437 
438     }
439 
440 }
441 
442 class CanvasUINumber extends CanvasUI{
443 
444     constructor(value, min, max, style){
445         super();
446 
447     }
448 
449 }
450 
451 class CanvasUIColor extends CanvasUI{
452 
453 }
454 
455 
456 
457 
458 /* IconMenu 图标菜单
459 parameter:
460     data: Array[Object{
461         icon: Image,    //默认 null
462         text: String,    //默认 "空"
463         func: Func,     //默认 null
464     }]
465 
466 demo:
467     new CanvasImage().loadImage("./test.png", ci => {
468         const iconMenu = new IconMenu([
469             {
470                 icon: ci.image,
471                 text: "StringStringStringStringString",
472             },
473             {
474                 icon: ci.image,
475                 text: "String",
476                 func: (cisA, cisB, data) => {
477                     //cisA: 0:"✘", 1:"✔", 2:"●" 3:">"; .visible 默认 false;
478                     //cisB: 0: 禁用, 1: 选取; .visible 默认 false;
479                     cisA.next();
480                     cisA.visible = true;
481                 },
482             },
483         ]);
484 
485         iconMenu.domElement.style = `
486             background: #666;
487             position: absolute;
488             left: 100px;
489             top: 100px;
490             border-radius: 4px;
491         `;
492 
493         iconMenu.render().bindEvent();
494     });
495 */
496 class IconMenu extends CanvasImageRender{
497 
498     static iconSize = 16;
499     static selectColor = "rgba(85,146,209,0.4)";
500     static disableColor = disableColor;
501     static getImages(){
502         const s = IconMenu.iconSize;
503         emptyCIC.size(s, s);
504 
505         //0:"✘", 1:"✔", 2:"●" 3:">", 
506         return [
507             emptyCIC.clear().text("✘", textColor.up).shear(),
508             emptyCIC.clear().text("✔", textColor.up).shear(),
509             emptyCIC.clear().text("●", textColor.up).shear(),
510             emptyCIC.path([1, 1, 1, s-1, s-1, s/2], true).clear().fill(textColor.up).shear(),
511         ]
512     }
513 
514     static getMaxWidth(data){
515         const s = IconMenu.iconSize * 2 + padding.x * 2;
516         var mx = 0, maxX = 0;
517         data.forEach(o => {
518             mx = getW(o.text) + s;
519             if(maxX < mx) maxX = mx;
520         });
521         return maxX;
522     }
523 
524     constructor(data){
525         super();
526         if(UTILS.emptyArray(data)) return;
527 
528         const mw = IconMenu.getMaxWidth(data), 
529         iconSize = IconMenu.iconSize, 
530         paddingX = padding.x, 
531         paddingY = padding.y;
532         var itemLen = 0;
533         
534         const icons = IconMenu.getImages();
535 
536         initCIC(mw - paddingX * 2, iconSize);
537         const mask = [
538             emptyCIC.clear().fill(IconMenu.disableColor).shear(),
539             emptyCIC.fill(IconMenu.selectColor).shear(),
540         ];
541         emptyCIC.size(mw, iconSize);
542         data.forEach((o, i) => {
543             emptyCIC.clear();
544             if(CanvasImageRender.isCanvasImage(o.icon)) emptyCIC.context.drawImage(o.icon, 0, 0, iconSize, iconSize);
545             if(typeof o.text === "string" && o.text !== "") emptyCIC.text(o.text, textColor.up, 0, iconSize+paddingX);
546             const a = new CanvasImage(emptyCIC.shear()).pos(paddingX, emptyCIC.box.h * i + paddingY * i + paddingY),
547             b = new CanvasImages(icons).pos(mw - iconSize - paddingX, a.y),
548             c = new CanvasImages(mask).pos(paddingX, a.y);
549             b.visible = false;
550             c.cursor = 1;
551             c.visible = false;
552             this.list.push(a, b, c);
553             itemLen += 1;
554         });
555 
556         this.initList().size(mw, itemLen * emptyCIC.box.h + paddingY * itemLen + paddingY);
557         this.data = data;
558         this.eanvasImageEvent = null;
559         this.itemLen = itemLen;
560     }
561 
562     bindEvent(){
563         if(this.eanvasImageEvent === null) this.eanvasImageEvent = new CanvasImageEvent(this);
564 
565         var pointerId = -1, i = -1, isDown = false, cis = null;
566 
567         const _box = this.box,
568         
569         eventTarget = {
570             get index(){return Infinity;},
571             get visible(){return true;},
572             get box(){return _box;},
573             get rotate(){return null},
574         },
575 
576         onup_body = () => {
577             document.body.removeEventListener("pointerup", onup_body);
578 
579             if(cis && cis.visible === true && cis.cursor !== 0){
580                 cis.visible = false;
581                 this.redrawTarget(cis);
582                 cis = null;
583             }
584         },
585 
586         onup = event => {
587             if(isDown && event.pointerId === pointerId && i === Math.floor(event.offsetY/_box.h*this.itemLen) && this.list[i*3+2].cursor !== 0){
588                 const item = this.data[i];
589                 if(UTILS.isObject(item) && typeof item.func === "function") item.func(this.list[i*3+1], cis, item);
590             }
591 
592             isDown = false;
593         },
594 
595         ondown = event => {
596             onup_body();
597             if(event.button !== 0) return;
598             i = Math.floor(event.offsetY/_box.h*this.itemLen);
599             cis = this.list[i*3+2];
600             if(cis && cis.visible === false && cis.cursor !== 0){
601                 cis.cursor = 1;
602                 cis.visible = true;
603                 this.redrawTarget(cis);
604                 pointerId = event.pointerId;
605                 isDown = true;
606                 document.body.addEventListener("pointerup", onup_body);
607             }
608         }
609 
610         this.eanvasImageEvent.add(eventTarget, "down", ondown);
611         this.eanvasImageEvent.add(eventTarget, "up", onup);
612         return this;
613     }
614 
615 }
616 
617 
618 /* TreeIconMenu 树结构图标菜单
619 
620 */
621 class TreeIconMenu extends TreeStruct{
622 
623     constructor(data){
624         
625     }
626 
627 }
628 
629 
630 
631 
632 /* Carousel 轮播图 (支持pc和mobile端)
633 
634 */
635 class CanvasCarousel extends CanvasImageRender{
636 
637     constructor(){
638         super();
639         
640     }
641 
642     init(images){
643         const len = images.length;
644         for(let k = 0; k < len; k++) this.context.drawImage(images[k], k*this.box.w, 0, this.box.w, this.box.h);
645         
646     }
647 
648 }
649 
650 
651 
652 
653 /* CanvasTab 
654 
655 */
656 class CanvasTab extends CanvasImageRender{
657 
658     constructor(){
659         super();
660         
661     }
662 
663 }
664 
665 
666 export {
667     CanvasUI,
668     CanvasUIButton,
669     CanvasUIBool,
670     IconMenu,
671     TreeIconMenu,
672 }
CanvasUI.js

 

 

 

使用例子:

"use strict";

import { CanvasImage } from './CanvasImageRender.js';
import { IconMenu } from './CanvasUI.js';

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

new CanvasImage().loadImage("./life.png", ci => {
    const iconMenu = new IconMenu([
        {
            icon: ci.image,
            text: "String",
        },
        {
            icon: ci.image,
            text: "StringStringStringStringString",
        },
        {
            icon: ci.image,
            text: "String2",
        },
        {
            icon: ci.image,
            text: "String3",
            func: (cisA, cisB, data) => {
                console.log(cisA, cisB, data);
                cisB.cursor = 0;
                iconMenu.redrawTarget(cisB);
            }
        },
        {
            icon: ci.image,
            text: "String4",
            func: (cisA, cisB, data) => {
                console.log(cisA, cisB, data);
                cisA.visible = true;
                cisA.next();
            }
        },
    ]);

    iconMenu.domElement.style = `
        background: #666;
        position: absolute;
        left: 100px;
        top: 100px;
        border-radius: 4px;
    `;

    iconMenu.render().bindEvent();
})

 

 

结果图:

 

posted @ 2022-09-18 08:00  鸡儿er  阅读(33)  评论(0编辑  收藏  举报