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