用原生js canvas 播放视频示例

前言:

  为什么要用canvas渲染视频?

  个人理解,不喜勿喷!

  用来测试的视频是我在某个视频网站按F12偷来的(它们并不想用户下载视频),

  所以用canvas渲染视频增加视频盗取的难度吧!

  H5的video控件太丑,不可自定义, 所以也可以用canvas创建自定义的视频控件

  ... 

创建 canvas:

 1    //CanvasImageRender
 2     const cir = new CanvasImageRender({
 3         width: window.innerWidth,   //canvas 的宽高
 4         height: window.innerHeight,
 5     }); console.log(cir);
 6 
 7     //.domElement 是一个 canvas
 8     cir.domElement.style = `
 9         position: absolute;
10         z-index: 9999;
11         background: rgb(127,127,127);
12     `;

 

主要代码:

    //这用于搭载H5视频 video
    cir.add(new CanvasImage())

    .loadVideo("view/examples/video/test.mp4", ci => {
        
        //同比例缩放视频
        const newSize = UTILS.setSizeToSameScale(ci.image, {width: 100, height: 100});
        ci.box.size(newSize.width, newSize.height).center(cir.box);

        //播放按钮
        const cic = cir.add(new CanvasImageCustom())
        .size(50, 30).text("PLAY", "#fff")
        .rect(4).stroke("blue");
        cic.box.center(cir.box);

        //动画循环
        const animateLoop = new AnimateLoop(() => cir.redraw());

        //谷歌浏览器必须要用户与文档交互一次才能播放视频 (点击后播放动画)
        cir.addEvent(cic, "up", () => {
            cic.visible = false;
            ci.image.play(); // ci.image 其实是一个 video 元素
            animateLoop.play(); //播放动画
        });
        
        //把 canvas 添加至 dom 树
        cir.render();

    });
    

 

结果图:

 

 

 

 

 

依赖的内裤源码:

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

 

posted @ 2022-09-09 21:16  鸡儿er  阅读(1366)  评论(0编辑  收藏  举报