tegaki-test.js
1 /*! tegaki.js, MIT License */'use strict';var TegakiStrings = { 2 // Messages 3 badDimensions: 'Invalid dimensions.', 4 promptWidth: 'Canvas width in pixels', 5 promptHeight: 'Canvas height in pixels', 6 confirmDelLayers: 'Delete selected layers?', 7 confirmMergeLayers: 'Merge selected layers?', 8 tooManyLayers: 'Layer limit reached.', 9 errorLoadImage: 'Could not load the image.', 10 noActiveLayer: 'No active layer.', 11 hiddenActiveLayer: 'The active layer is not visible.', 12 confirmCancel: 'Are you sure? Your work will be lost.', 13 confirmChangeCanvas: 'Are you sure? Changing the canvas will clear all layers and history and disable replay recording.', 14 15 // Controls 16 color: 'Color', 17 size: 'Size', 18 alpha: 'Opacity', 19 flow: 'Flow', 20 zoom: 'Zoom', 21 layers: 'Layers', 22 switchPalette: 'Switch color palette', 23 paletteSlotReplace: 'Right click to replace with the current color', 24 25 // Layers 26 layer: 'Layer', 27 addLayer: 'Add layer', 28 delLayers: 'Delete layers', 29 mergeLayers: 'Merge layers', 30 moveLayerUp: 'Move up', 31 moveLayerDown: 'Move down', 32 toggleVisibility: 'Toggle visibility', 33 34 // Menu bar 35 newCanvas: 'New', 36 open: 'Open', 37 save: 'Save', 38 saveAs: 'Save As', 39 export: 'Export', 40 undo: 'Undo', 41 redo: 'Redo', 42 close: 'Close', 43 finish: 'Finish', 44 45 // Tool modes 46 tip: 'Tip', 47 pressure: 'Pressure', 48 preserveAlpha: 'Preserve Alpha', 49 50 // Tools 51 pen: 'Pen', 52 pencil: 'Pencil', 53 airbrush: 'Airbrush', 54 pipette: 'Pipette', 55 blur: 'Blur', 56 eraser: 'Eraser', 57 bucket: 'Bucket', 58 tone: 'Tone', 59 60 // Replay 61 gapless: 'Gapless', 62 play: 'Play', 63 pause: 'Pause', 64 rewind: 'Rewind', 65 slower: 'Slower', 66 faster: 'Faster', 67 recordingEnabled: 'Recording replay', 68 errorLoadReplay: 'Could not load the replay: ', 69 loadingReplay: 'Loading replay…', 70 }; 71 class TegakiTool { 72 constructor() { 73 this.id = 0; 74 75 this.name = null; 76 77 this.keybind = null; 78 79 this.useFlow = false; 80 81 this.useSizeDynamics = false; 82 this.useAlphaDynamics = false; 83 this.useFlowDynamics = false; 84 85 this.usePreserveAlpha = false; 86 87 this.step = 0.0; 88 89 this.size = 1; 90 this.alpha = 1.0; 91 this.flow = 1.0; 92 93 this.useSize = true; 94 this.useAlpha = true; 95 this.useFlow = true; 96 97 this.noCursor = false; 98 99 this.color = '#000000'; 100 this.rgb = [0, 0, 0]; 101 102 this.brushSize = 0; 103 this.brushAlpha = 0.0; 104 this.brushFlow = 0.0; 105 this.stepSize = 0.0; 106 this.center = 0.0; 107 108 this.sizeDynamicsEnabled = false; 109 this.alphaDynamicsEnabled = false; 110 this.flowDynamicsEnabled = false; 111 this.preserveAlphaEnabled = false; 112 113 this.tip = -1; 114 this.tipList = null; 115 116 this.stepAcc = 0; 117 118 this.shapeCache = null; 119 120 this.kernel = null; 121 } 122 123 brushFn(x, y, offsetX, offsetY) {} 124 125 start(posX, posY) {} 126 127 commit() {} 128 129 draw(posX, posY) {} 130 131 usesDynamics() { 132 return this.useSizeDynamics || this.useAlphaDynamics || this.useFlowDynamics; 133 } 134 135 enabledDynamics() { 136 return this.sizeDynamicsEnabled || this.alphaDynamicsEnabled || this.flowDynamicsEnabled; 137 } 138 139 setSize(size) { 140 this.size = size; 141 } 142 143 setAlpha(alpha) { 144 this.alpha = alpha; 145 this.brushAlpha = alpha; 146 } 147 148 setFlow(flow) { 149 this.flow = flow; 150 this.brushFlow = this.easeFlow(flow); 151 } 152 153 easeFlow(flow) { 154 return flow; 155 } 156 157 setColor(hex) { 158 this.rgb = $T.hexToRgb(hex); 159 } 160 161 setSizeDynamics(flag) { 162 if (!this.useSizeDynamics) { 163 return; 164 } 165 166 if (!flag) { 167 this.setSize(this.size); 168 } 169 170 this.sizeDynamicsEnabled = flag; 171 } 172 173 setAlphaDynamics(flag) { 174 if (!this.useAlphaDynamics) { 175 return; 176 } 177 178 if (!flag) { 179 this.setAlpha(this.alpha); 180 } 181 182 this.alphaDynamicsEnabled = flag; 183 } 184 185 setFlowDynamics(flag) { 186 if (!this.useFlowDynamics) { 187 return; 188 } 189 190 if (!flag) { 191 this.setFlow(this.flow); 192 } 193 194 this.flowDynamicsEnabled = flag; 195 } 196 197 setPreserveAlpha(flag) { 198 this.preserveAlphaEnabled = flag; 199 } 200 201 set() { 202 this.setAlpha(this.alpha); 203 this.setFlow(this.flow); 204 this.setSize(this.size); 205 this.setColor(Tegaki.toolColor); 206 207 Tegaki.onToolChanged(this); 208 } 209 } 210 class TegakiBrush extends TegakiTool { 211 constructor() { 212 super(); 213 } 214 215 generateShape(size) {} 216 217 brushFn(x, y, offsetX, offsetY) { 218 var aData, gData, bData, aWidth, canvasWidth, canvasHeight, 219 kernel, xx, yy, ix, iy, 220 pa, ka, a, sa, 221 kr, kg, kb, 222 r, g, b, 223 pr, pg, pb, 224 px, ba, 225 brushSize, brushAlpha, brushFlow, preserveAlpha; 226 227 preserveAlpha = this.preserveAlphaEnabled; 228 229 kernel = this.kernel; 230 231 brushAlpha = this.brushAlpha; 232 brushFlow = this.brushFlow; 233 brushSize = this.brushSize; 234 235 aData = Tegaki.activeLayer.imageData.data; 236 gData = Tegaki.ghostBuffer.data; 237 bData = Tegaki.blendBuffer.data; 238 239 canvasWidth = Tegaki.baseWidth; 240 canvasHeight = Tegaki.baseHeight; 241 242 aWidth = canvasWidth; 243 244 kr = this.rgb[0]; 245 kg = this.rgb[1]; 246 kb = this.rgb[2]; 247 248 for (yy = 0; yy < brushSize; ++yy) { 249 iy = y + yy + offsetY; 250 251 if (iy < 0 || iy >= canvasHeight) { 252 continue; 253 } 254 255 for (xx = 0; xx < brushSize; ++xx) { 256 ix = x + xx + offsetX; 257 258 if (ix < 0 || ix >= canvasWidth) { 259 continue; 260 } 261 262 ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; 263 264 if (ka <= 0.0) { 265 continue; 266 } 267 268 px = (iy * canvasWidth + ix) * 4; 269 270 sa = bData[px + 3] / 255; 271 sa = sa + ka * brushFlow * (brushAlpha - sa); 272 273 ba = Math.ceil(sa * 255); 274 275 if (ba > bData[px + 3]) { 276 if (bData[px] === 0) { 277 gData[px] = aData[px]; 278 gData[px + 1] = aData[px + 1]; 279 gData[px + 2] = aData[px + 2]; 280 gData[px + 3] = aData[px + 3]; 281 } 282 283 bData[px] = 1; 284 bData[px + 3] = ba; 285 286 pr = gData[px]; 287 pg = gData[px + 1]; 288 pb = gData[px + 2]; 289 pa = gData[px + 3] / 255; 290 291 a = pa + sa - pa * sa; 292 293 r = ((kr * sa) + (pr * pa) * (1 - sa)) / a; 294 g = ((kg * sa) + (pg * pa) * (1 - sa)) / a; 295 b = ((kb * sa) + (pb * pa) * (1 - sa)) / a; 296 297 aData[px] = (kr > pr) ? Math.ceil(r) : Math.floor(r); 298 aData[px + 1] = (kg > pg) ? Math.ceil(g) : Math.floor(g); 299 aData[px + 2] = (kb > pb) ? Math.ceil(b) : Math.floor(b); 300 301 if (!preserveAlpha) { 302 aData[px + 3] = Math.ceil(a * 255); 303 } 304 } 305 } 306 } 307 } 308 309 generateShapeCache(force) { 310 var i, shape; 311 312 if (!this.shapeCache) { 313 this.shapeCache = new Array(Tegaki.maxSize); 314 } 315 316 if (this.shapeCache[0] && !force) { 317 return; 318 } 319 320 for (i = 0; i < Tegaki.maxSize; ++i) { 321 shape = this.generateShape(i + 1); 322 this.shapeCache[i] = shape; 323 this.setShape(shape); 324 } 325 } 326 327 updateDynamics(t) { 328 var pressure, shape, val; 329 330 pressure = TegakiPressure.lerp(t); 331 332 if (this.sizeDynamicsEnabled) { 333 val = Math.ceil(pressure * this.size); 334 335 if (val === 0) { 336 return false; 337 } 338 339 shape = this.shapeCache[val - 1]; 340 341 this.setShape(shape); 342 } 343 344 if (this.alphaDynamicsEnabled) { 345 val = this.alpha * pressure; 346 347 if (val <= 0) { 348 return false; 349 } 350 351 this.brushAlpha = val; 352 } 353 354 if (this.flowDynamicsEnabled) { 355 val = this.flow * pressure; 356 357 if (val <= 0) { 358 return false; 359 } 360 361 this.brushFlow = this.easeFlow(val); 362 } 363 364 return true; 365 } 366 367 start(posX, posY) { 368 var sampleX, sampleY; 369 370 this.stepAcc = 0; 371 this.posX = posX; 372 this.posY = posY; 373 374 if (this.enabledDynamics()) { 375 if (!this.updateDynamics(1.0)) { 376 return; 377 } 378 } 379 380 sampleX = posX - this.center; 381 sampleY = posY - this.center; 382 383 this.readImageData(sampleX, sampleY, this.brushSize, this.brushSize); 384 385 this.brushFn(0, 0, sampleX, sampleY); 386 387 this.writeImageData(sampleX, sampleY, this.brushSize, this.brushSize); 388 } 389 390 commit() { 391 Tegaki.clearBuffers(); 392 } 393 394 draw(posX, posY) { 395 var mx, my, fromX, fromY, sampleX, sampleY, dx, dy, err, derr, stepAcc, 396 lastX, lastY, distBase, shape, center, brushSize, t, tainted, w, h; 397 398 stepAcc = this.stepAcc; 399 400 fromX = this.posX; 401 fromY = this.posY; 402 403 if (fromX < posX) { dx = posX - fromX; sampleX = fromX; mx = 1; } 404 else { dx = fromX - posX; sampleX = posX; mx = -1; } 405 406 if (fromY < posY) { dy = posY - fromY; sampleY = fromY; my = 1; } 407 else { dy = fromY - posY; sampleY = posY; my = -1; } 408 409 if (this.enabledDynamics()) { 410 distBase = Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)); 411 } 412 413 if (this.sizeDynamicsEnabled) { 414 shape = this.shapeCache[this.size - 1]; 415 center = shape.center; 416 brushSize = shape.brushSize; 417 } 418 else { 419 center = this.center; 420 brushSize = this.brushSize; 421 } 422 423 sampleX -= center; 424 sampleY -= center; 425 426 w = dx + brushSize; 427 h = dy + brushSize; 428 429 this.readImageData(sampleX, sampleY, w, h); 430 431 err = (dx > dy ? dx : (dy !== 0 ? -dy : 0)) / 2; 432 433 if (dx !== 0) { 434 dx = -dx; 435 } 436 437 tainted = false; 438 439 lastX = fromX; 440 lastY = fromY; 441 442 while (true) { 443 stepAcc += Math.max(Math.abs(lastX - fromX), Math.abs(lastY - fromY)); 444 445 lastX = fromX; 446 lastY = fromY; 447 448 if (stepAcc >= this.stepSize) { 449 if (this.enabledDynamics()) { 450 if (distBase > 0) { 451 t = 1.0 - (Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)) / distBase); 452 } 453 else { 454 t = 0.0; 455 } 456 457 if (this.updateDynamics(t)) { 458 this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); 459 tainted = true; 460 } 461 } 462 else { 463 this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); 464 tainted = true; 465 } 466 467 stepAcc = 0; 468 } 469 470 if (fromX === posX && fromY === posY) { 471 break; 472 } 473 474 derr = err; 475 476 if (derr > dx) { err -= dy; fromX += mx; } 477 if (derr < dy) { err -= dx; fromY += my; } 478 } 479 480 this.stepAcc = stepAcc; 481 this.posX = posX; 482 this.posY = posY; 483 484 if (tainted) { 485 this.writeImageData(sampleX, sampleY, w, h); 486 } 487 } 488 489 writeImageData(x, y, w, h) { 490 Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0, x, y, w, h); 491 } 492 493 readImageData(x, y, w, h) {} 494 495 setShape(shape) { 496 this.center = shape.center; 497 this.stepSize = shape.stepSize; 498 this.brushSize = shape.brushSize; 499 this.kernel = shape.kernel; 500 } 501 502 setSize(size) { 503 this.size = size; 504 505 if (this.sizeDynamicsEnabled) { 506 this.generateShapeCache(); 507 } 508 else { 509 this.setShape(this.generateShape(size)); 510 } 511 } 512 513 setSizeDynamics(flag) { 514 if (!this.useSizeDynamics) { 515 return; 516 } 517 518 if (this.sizeDynamicsEnabled === flag) { 519 return; 520 } 521 522 if (flag) { 523 this.generateShapeCache(); 524 } 525 else { 526 this.setShape(this.generateShape(this.size)); 527 } 528 529 this.sizeDynamicsEnabled = flag; 530 } 531 532 setTip(tipId) { 533 this.tipId = tipId; 534 535 if (this.sizeDynamicsEnabled) { 536 this.generateShapeCache(true); 537 } 538 else { 539 this.setShape(this.generateShape(this.size)); 540 } 541 } 542 } 543 class TegakiPencil extends TegakiBrush { 544 constructor() { 545 super(); 546 547 this.id = 1; 548 549 this.name = 'pencil'; 550 551 this.keybind = 'b'; 552 553 this.step = 0.01; 554 555 this.useFlow = false; 556 557 this.size = 1; 558 this.alpha = 1.0; 559 560 this.useSizeDynamics = true; 561 this.useAlphaDynamics = true; 562 this.usePreserveAlpha = true; 563 } 564 565 generateShape(size) { 566 var e, x, y, imageData, data, c, color, r, rr; 567 568 r = 0 | ((size) / 2); 569 570 rr = 0 | ((size + 1) % 2); 571 572 imageData = new ImageData(size, size); 573 574 data = new Uint32Array(imageData.data.buffer); 575 576 color = 0xFF000000; 577 578 x = r; 579 y = 0; 580 e = 1 - r; 581 c = r; 582 583 while (x >= y) { 584 data[c + x - rr + (c + y - rr) * size] = color; 585 data[c + y - rr + (c + x - rr) * size] = color; 586 587 data[c - y + (c + x - rr) * size] = color; 588 data[c - x + (c + y - rr) * size] = color; 589 590 data[c - y + (c - x) * size] = color; 591 data[c - x + (c - y) * size] = color; 592 593 data[c + y - rr + (c - x) * size] = color; 594 data[c + x - rr + (c - y) * size] = color; 595 596 ++y; 597 598 if (e <= 0) { 599 e += 2 * y + 1; 600 } 601 else { 602 x--; 603 e += 2 * (y - x) + 1; 604 } 605 } 606 607 if (r > 0) { 608 Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); 609 } 610 611 return { 612 center: r, 613 stepSize: Math.ceil(size * this.step), 614 brushSize: size, 615 kernel: imageData.data, 616 }; 617 } 618 } 619 class TegakiAirbrush extends TegakiBrush { 620 constructor() { 621 super(); 622 623 this.id = 3; 624 625 this.name = 'airbrush'; 626 627 this.keybind = 'a'; 628 629 this.step = 0.1; 630 631 this.size = 32; 632 this.alpha = 1.0; 633 634 this.useSizeDynamics = true; 635 this.useAlphaDynamics = true; 636 this.useFlowDynamics = true; 637 638 this.usePreserveAlpha = true; 639 } 640 641 easeFlow(flow) { 642 return 1 - Math.sqrt(1 - flow); 643 } 644 645 generateShape(size) { 646 var i, r, data, len, sqd, sqlen, hs, col, row, 647 ecol, erow, a; 648 649 r = size; 650 size = size * 2; 651 652 data = new ImageData(size, size).data; 653 654 len = size * size * 4; 655 sqlen = Math.sqrt(r * r); 656 hs = Math.round(r); 657 col = row = -hs; 658 659 i = 0; 660 while (i < len) { 661 if (col >= hs) { 662 col = -hs; 663 ++row; 664 continue; 665 } 666 667 ecol = col; 668 erow = row; 669 670 if (ecol < 0) { ecol = -ecol; } 671 if (erow < 0) { erow = -erow; } 672 673 sqd = Math.sqrt(ecol * ecol + erow * erow); 674 675 if (sqd >= sqlen) { 676 a = 0; 677 } 678 else if (sqd === 0) { 679 a = 255; 680 } 681 else { 682 a = (sqd / sqlen) + 0.1; 683 684 if (a > 1.0) { 685 a = 1.0; 686 } 687 688 a = (1 - (Math.exp(1 - 1 / a) / a)) * 255; 689 } 690 691 data[i + 3] = a; 692 693 i += 4; 694 695 ++col; 696 } 697 698 return { 699 center: r, 700 stepSize: Math.ceil(size * this.step), 701 brushSize: size, 702 kernel: data, 703 }; 704 } 705 } 706 class TegakiPen extends TegakiBrush { 707 constructor() { 708 super(); 709 710 this.id = 2; 711 712 this.name = 'pen'; 713 714 this.keybind = 'p'; 715 716 this.step = 0.05; 717 718 this.size = 8; 719 this.alpha = 1.0; 720 this.flow = 1.0; 721 722 this.useSizeDynamics = true; 723 this.useAlphaDynamics = true; 724 this.useFlowDynamics = true; 725 726 this.usePreserveAlpha = true; 727 } 728 729 easeFlow(flow) { 730 return 1 - Math.sqrt(1 - Math.pow(flow, 3)); 731 } 732 733 generateShape(size) { 734 var e, x, y, imageData, data, c, color, r, rr, 735 f, ff, bSize, bData, i, ii, xx, yy, center, brushSize; 736 737 center = Math.floor(size / 2) + 1; 738 739 brushSize = size + 2; 740 741 f = 4; 742 743 ff = f * f; 744 745 bSize = brushSize * f; 746 747 r = Math.floor(bSize / 2); 748 749 rr = Math.floor((bSize + 1) % 2); 750 751 imageData = new ImageData(bSize, bSize); 752 bData = new Uint32Array(imageData.data.buffer); 753 754 color = 0x55000000; 755 756 x = r; 757 y = 0; 758 e = 1 - r; 759 c = r; 760 761 while (x >= y) { 762 bData[c + x - rr + (c + y - rr) * bSize] = color; 763 bData[c + y - rr + (c + x - rr) * bSize] = color; 764 765 bData[c - y + (c + x - rr) * bSize] = color; 766 bData[c - x + (c + y - rr) * bSize] = color; 767 768 bData[c - y + (c - x) * bSize] = color; 769 bData[c - x + (c - y) * bSize] = color; 770 771 bData[c + y - rr + (c - x) * bSize] = color; 772 bData[c + x - rr + (c - y) * bSize] = color; 773 774 ++y; 775 776 if (e <= 0) { 777 e += 2 * y + 1; 778 } 779 else { 780 x--; 781 e += 2 * (y - x) + 1; 782 } 783 } 784 785 color = 0xFF000000; 786 787 x = r - 3; 788 y = 0; 789 e = 1 - r; 790 c = r; 791 792 while (x >= y) { 793 bData[c + x - rr + (c + y - rr) * bSize] = color; 794 bData[c + y - rr + (c + x - rr) * bSize] = color; 795 796 bData[c - y + (c + x - rr) * bSize] = color; 797 bData[c - x + (c + y - rr) * bSize] = color; 798 799 bData[c - y + (c - x) * bSize] = color; 800 bData[c - x + (c - y) * bSize] = color; 801 802 bData[c + y - rr + (c - x) * bSize] = color; 803 bData[c + x - rr + (c - y) * bSize] = color; 804 805 ++y; 806 807 if (e <= 0) { 808 e += 2 * y + 1; 809 } 810 else { 811 x--; 812 e += 2 * (y - x) + 1; 813 } 814 } 815 816 if (r > 0) { 817 Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); 818 } 819 820 bData = imageData.data; 821 data = new ImageData(brushSize, brushSize).data; 822 823 for (x = 0; x < brushSize; ++x) { 824 for (y = 0; y < brushSize; ++y) { 825 i = (y * brushSize + x) * 4 + 3; 826 827 color = 0; 828 829 for (xx = 0; xx < f; ++xx) { 830 for (yy = 0; yy < f; ++yy) { 831 ii = ((yy + y * f) * bSize + (xx + x * f)) * 4 + 3; 832 color += bData[ii]; 833 } 834 } 835 836 data[i] = color / ff; 837 } 838 } 839 840 return { 841 center: center, 842 stepSize: Math.ceil(size * this.step), 843 brushSize: brushSize, 844 kernel: data, 845 }; 846 } 847 } 848 class TegakiBucket extends TegakiTool { 849 constructor() { 850 super(); 851 852 this.id = 4; 853 854 this.name = 'bucket'; 855 856 this.keybind = 'g'; 857 858 this.step = 100.0; 859 860 this.useSize = false; 861 this.useFlow = false; 862 863 this.noCursor = true; 864 } 865 866 fill(imageData, x, y, color, alpha) { 867 var r, g, b, px, tr, tg, tb, ta, q, pxMap, yy, xx, yn, ys, 868 yyy, yyn, yys, xd, data, w, h; 869 870 w = imageData.width; 871 h = imageData.height; 872 873 r = color[0]; 874 g = color[1]; 875 b = color[2]; 876 877 px = (y * w + x) * 4; 878 879 data = imageData.data; 880 881 tr = data[px]; 882 tg = data[px + 1]; 883 tb = data[px + 2]; 884 ta = data[px + 3]; 885 886 pxMap = new Uint8Array(w * h * 4); 887 888 q = []; 889 890 q[0] = x; 891 q[1] = y; 892 893 while (q.length) { 894 yy = q.pop(); 895 xx = q.pop(); 896 897 yn = (yy - 1); 898 ys = (yy + 1); 899 900 yyy = yy * w; 901 yyn = yn * w; 902 yys = ys * w; 903 904 xd = xx; 905 906 while (xd >= 0) { 907 px = (yyy + xd) * 4; 908 909 if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 910 break; 911 } 912 913 this.blendPixel(data, px, r, g, b, alpha); 914 915 pxMap[px] = 1; 916 917 if (yn >= 0) { 918 px = (yyn + xd) * 4; 919 920 if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 921 q.push(xd); 922 q.push(yn); 923 } 924 } 925 926 if (ys < h) { 927 px = (yys + xd) * 4; 928 929 if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 930 q.push(xd); 931 q.push(ys); 932 } 933 } 934 935 xd--; 936 } 937 938 xd = xx + 1; 939 940 while (xd < w) { 941 px = (yyy + xd) * 4; 942 943 if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 944 break; 945 } 946 947 this.blendPixel(data, px, r, g, b, alpha); 948 949 pxMap[px] = 1; 950 951 if (yn >= 0) { 952 px = (yyn + xd) * 4; 953 954 if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 955 q.push(xd); 956 q.push(yn); 957 } 958 } 959 960 if (ys < h) { 961 px = (yys + xd) * 4; 962 963 if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { 964 q.push(xd); 965 q.push(ys); 966 } 967 } 968 969 ++xd; 970 } 971 } 972 } 973 974 brushFn(x, y) { 975 if (x < 0 || y < 0 || x >= Tegaki.baseWidth || y >= Tegaki.baseHeight) { 976 return; 977 } 978 979 this.fill(Tegaki.activeLayer.imageData, x, y, this.rgb, this.alpha); 980 981 // TODO: write back only the tainted rect 982 Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0); 983 } 984 985 blendPixel(data, px, r, g, b, a) { 986 var sr, sg, sb, sa, dr, dg, db, da; 987 988 sr = data[px]; 989 sg = data[px + 1]; 990 sb = data[px + 2]; 991 sa = data[px + 3] / 255; 992 993 da = sa + a - sa * a; 994 995 dr = ((r * a) + (sr * sa) * (1 - a)) / da; 996 dg = ((g * a) + (sg * sa) * (1 - a)) / da; 997 db = ((b * a) + (sb * sa) * (1 - a)) / da; 998 999 data[px] = (r > sr) ? Math.ceil(dr) : Math.floor(dr); 1000 data[px + 1] = (g > sg) ? Math.ceil(dg) : Math.floor(dg); 1001 data[px + 2] = (b > sb) ? Math.ceil(db) : Math.floor(db); 1002 data[px + 3] = Math.ceil(da * 255); 1003 } 1004 1005 testPixel(data, px, pxMap, tr, tg, tb, ta) { 1006 return !pxMap[px] && (data[px] == tr 1007 && data[++px] == tg 1008 && data[++px] == tb 1009 && data[++px] == ta) 1010 ; 1011 } 1012 1013 start(x, y) { 1014 this.brushFn(x, y); 1015 } 1016 1017 draw(x, y) { 1018 this.brushFn(x, y); 1019 } 1020 1021 setSize(size) {} 1022 } 1023 class TegakiTone extends TegakiPencil { 1024 constructor() { 1025 super(); 1026 1027 this.id = 5; 1028 1029 this.name = 'tone'; 1030 1031 this.keybind = 't'; 1032 1033 this.step = 0.01; 1034 1035 this.useFlow = false; 1036 1037 this.size = 8; 1038 this.alpha = 0.5; 1039 1040 this.useSizeDynamics = true; 1041 this.useAlphaDynamics = true; 1042 this.usePreserveAlpha = true; 1043 1044 this.matrix = [ 1045 [0, 8, 2, 10], 1046 [12, 4, 14, 6], 1047 [3, 11, 1 ,9], 1048 [15, 7, 13, 5] 1049 ]; 1050 1051 this.mapCache = null; 1052 this.mapWidth = 0; 1053 this.mapHeight = 0; 1054 } 1055 1056 start(x, y) { 1057 if (this.mapWidth !== Tegaki.baseWidth || this.mapHeight !== Tegaki.baseHeight) { 1058 this.generateMapCache(true); 1059 } 1060 1061 super.start(x, y); 1062 } 1063 1064 brushFn(x, y, offsetX, offsetY) { 1065 var data, kernel, brushSize, map, idx, preserveAlpha, 1066 px, mx, mapWidth, xx, yy, ix, iy, canvasWidth, canvasHeight; 1067 1068 data = Tegaki.activeLayer.imageData.data; 1069 1070 canvasWidth = Tegaki.baseWidth; 1071 canvasHeight = Tegaki.baseHeight; 1072 1073 kernel = this.kernel; 1074 1075 brushSize = this.brushSize; 1076 1077 mapWidth = this.mapWidth; 1078 1079 preserveAlpha = this.preserveAlphaEnabled; 1080 1081 idx = Math.round(this.brushAlpha * 16) - 1; 1082 1083 if (idx < 0) { 1084 return; 1085 } 1086 1087 map = this.mapCache[idx]; 1088 1089 for (yy = 0; yy < brushSize; ++yy) { 1090 iy = y + yy + offsetY; 1091 1092 if (iy < 0 || iy >= canvasHeight) { 1093 continue; 1094 } 1095 1096 for (xx = 0; xx < brushSize; ++xx) { 1097 ix = x + xx + offsetX; 1098 1099 if (ix < 0 || ix >= canvasWidth) { 1100 continue; 1101 } 1102 1103 if (kernel[(yy * brushSize + xx) * 4 + 3] === 0) { 1104 continue; 1105 } 1106 1107 mx = iy * canvasWidth + ix; 1108 px = mx * 4; 1109 1110 if (map[mx] === 0) { 1111 data[px] = this.rgb[0]; 1112 data[px + 1] = this.rgb[1]; 1113 data[px + 2] = this.rgb[2]; 1114 1115 if (!preserveAlpha) { 1116 data[px + 3] = 255; 1117 } 1118 } 1119 } 1120 } 1121 } 1122 1123 generateMap(w, h, idx) { 1124 var data, x, y; 1125 1126 data = new Uint8Array(w * h); 1127 1128 for (y = 0; y < h; ++y) { 1129 for (x = 0; x < w; ++x) { 1130 if (idx < this.matrix[y % 4][x % 4]) { 1131 data[w * y + x] = 1; 1132 } 1133 } 1134 } 1135 1136 return data; 1137 } 1138 1139 generateMapCache(force) { 1140 var i, cacheSize; 1141 1142 cacheSize = this.matrix.length * this.matrix[0].length; 1143 1144 if (!this.mapCache) { 1145 this.mapCache = new Array(cacheSize); 1146 } 1147 1148 if (!force && this.mapCache[0] 1149 && this.mapWidth === Tegaki.baseWidth 1150 && this.mapHeight === Tegaki.baseHeight) { 1151 return; 1152 } 1153 1154 this.mapWidth = Tegaki.baseWidth; 1155 this.mapHeight = Tegaki.baseHeight; 1156 1157 for (i = 0; i < cacheSize; ++i) { 1158 this.mapCache[i] = this.generateMap(this.mapWidth, this.mapHeight, i); 1159 } 1160 } 1161 1162 setAlpha(alpha) { 1163 super.setAlpha(alpha); 1164 this.generateMapCache(); 1165 } 1166 } 1167 class TegakiPipette extends TegakiTool { 1168 constructor() { 1169 super(); 1170 1171 this.id = 6; 1172 1173 this.name = 'pipette'; 1174 1175 this.keybind = 'i'; 1176 1177 this.step = 100.0; 1178 1179 this.useSize = false; 1180 this.useAlpha = false; 1181 this.useFlow = false; 1182 1183 this.noCursor = true; 1184 } 1185 1186 start(posX, posY) { 1187 this.draw(posX, posY); 1188 } 1189 1190 draw(posX, posY) { 1191 var c, ctx; 1192 1193 if (true) { 1194 ctx = Tegaki.flatten().getContext('2d'); 1195 } 1196 else { 1197 ctx = Tegaki.activeLayer.ctx; 1198 } 1199 1200 c = $T.getColorAt(ctx, posX, posY); 1201 1202 Tegaki.setToolColor(c); 1203 } 1204 1205 set() { 1206 Tegaki.onToolChanged(this); 1207 } 1208 1209 commit() {} 1210 1211 setSize() {} 1212 1213 setAlpha() {} 1214 } 1215 class TegakiBlur extends TegakiBrush { 1216 constructor() { 1217 super(); 1218 1219 this.id = 7; 1220 1221 this.name = 'blur'; 1222 1223 this.step = 0.25; 1224 1225 this.useFlow = false; 1226 1227 this.size = 32; 1228 this.alpha = 0.5; 1229 1230 this.useAlphaDynamics = true; 1231 this.usePreserveAlpha = false; 1232 } 1233 1234 writeImageData(x, y, w, h) { 1235 var xx, yy, ix, iy, px, canvasWidth, aData, bData; 1236 1237 aData = Tegaki.activeLayer.imageData.data; 1238 bData = Tegaki.blendBuffer.data; 1239 1240 canvasWidth = Tegaki.baseWidth; 1241 1242 for (xx = 0; xx < w; ++xx) { 1243 ix = x + xx; 1244 1245 for (yy = 0; yy < h; ++yy) { 1246 iy = y + yy; 1247 1248 px = (iy * canvasWidth + ix) * 4; 1249 1250 aData[px] = bData[px]; 1251 aData[px + 1] = bData[px + 1]; 1252 aData[px + 2] = bData[px + 2]; 1253 aData[px + 3] = bData[px + 3]; 1254 } 1255 } 1256 1257 super.writeImageData(x, y, w, h); 1258 } 1259 1260 readImageData(x, y, w, h) { 1261 var xx, yy, ix, iy, px, canvasWidth, aData, bData; 1262 1263 aData = Tegaki.activeLayer.imageData.data; 1264 bData = Tegaki.blendBuffer.data; 1265 1266 canvasWidth = Tegaki.baseWidth; 1267 1268 for (xx = 0; xx < w; ++xx) { 1269 ix = x + xx; 1270 1271 for (yy = 0; yy < h; ++yy) { 1272 iy = y + yy; 1273 1274 px = (iy * canvasWidth + ix) * 4; 1275 1276 bData[px] = aData[px]; 1277 bData[px + 1] = aData[px + 1]; 1278 bData[px + 2] = aData[px + 2]; 1279 bData[px + 3] = aData[px + 3]; 1280 } 1281 } 1282 } 1283 1284 brushFn(x, y, offsetX, offsetY) { 1285 var i, j, size, aData, bData, limX, limY, 1286 kernel, alpha, alpha0, ix, iy, canvasWidth, canvasHeight, 1287 sx, sy, r, g, b, a, kx, ky, px, pa, acc, aa; 1288 1289 alpha0 = this.brushAlpha; 1290 alpha = alpha0 * alpha0 * alpha0; 1291 1292 if (alpha <= 0.0) { 1293 return; 1294 } 1295 1296 size = this.brushSize; 1297 1298 kernel = this.kernel; 1299 1300 aData = Tegaki.activeLayer.imageData.data; 1301 bData = Tegaki.blendBuffer.data; 1302 1303 canvasWidth = Tegaki.baseWidth; 1304 canvasHeight = Tegaki.baseHeight; 1305 1306 limX = canvasWidth - 1; 1307 limY = canvasHeight - 1; 1308 1309 for (sx = 0; sx < size; ++sx) { 1310 ix = x + sx + offsetX; 1311 1312 if (ix < 0 || ix >= canvasWidth) { 1313 continue; 1314 } 1315 1316 for (sy = 0; sy < size; ++sy) { 1317 iy = y + sy + offsetY; 1318 1319 if (iy < 0 || iy >= canvasHeight) { 1320 continue; 1321 } 1322 1323 i = (sy * size + sx) * 4; 1324 1325 px = (iy * canvasWidth + ix) * 4; 1326 1327 if (kernel[i + 3] === 0 || ix <= 0 || iy <= 0 || ix >= limX || iy >= limY) { 1328 continue; 1329 } 1330 1331 r = g = b = a = acc = 0; 1332 1333 for (kx = -1; kx < 2; ++kx) { 1334 for (ky = -1; ky < 2; ++ky) { 1335 j = ((iy - ky) * canvasWidth + (ix - kx)) * 4; 1336 1337 pa = aData[j + 3]; 1338 1339 if (kx === 0 && ky === 0) { 1340 aa = pa * alpha0; 1341 acc += alpha0; 1342 } 1343 else { 1344 aa = pa * alpha; 1345 acc += alpha; 1346 } 1347 1348 r = r + aData[j] * aa; ++j; 1349 g = g + aData[j] * aa; ++j; 1350 b = b + aData[j] * aa; 1351 a = a + aa; 1352 } 1353 } 1354 1355 a = a / acc; 1356 1357 if (a <= 0.0) { 1358 continue; 1359 } 1360 1361 bData[px] = Math.round((r / acc) / a); 1362 bData[px + 1] = Math.round((g / acc) / a); 1363 bData[px + 2] = Math.round((b / acc) / a); 1364 bData[px + 3] = Math.round(a); 1365 } 1366 } 1367 } 1368 } 1369 1370 TegakiBlur.prototype.generateShape = TegakiPencil.prototype.generateShape; 1371 class TegakiEraser extends TegakiBrush { 1372 constructor() { 1373 super(); 1374 1375 this.id = 8; 1376 1377 this.name = 'eraser'; 1378 1379 this.keybind = 'e'; 1380 1381 this.step = 0.1; 1382 1383 this.size = 8; 1384 this.alpha = 1.0; 1385 1386 this.useFlow = false; 1387 1388 this.useSizeDynamics = true; 1389 this.useAlphaDynamics = true; 1390 this.usePreserveAlpha = false; 1391 1392 this.tipId = 0; 1393 this.tipList = [ 'pencil', 'pen', 'airbrush' ]; 1394 } 1395 1396 brushFn(x, y, offsetX, offsetY) { 1397 var aData, bData, gData, kernel, canvasWidth, canvasHeight, 1398 ka, ba, px, xx, yy, ix, iy, 1399 brushSize, brushAlpha; 1400 1401 brushAlpha = this.brushAlpha; 1402 brushSize = this.brushSize; 1403 1404 kernel = this.kernel; 1405 1406 aData = Tegaki.activeLayer.imageData.data; 1407 gData = Tegaki.ghostBuffer.data; 1408 bData = Tegaki.blendBuffer.data; 1409 1410 canvasWidth = Tegaki.baseWidth; 1411 canvasHeight = Tegaki.baseHeight; 1412 1413 for (yy = 0; yy < brushSize; ++yy) { 1414 iy = y + yy + offsetY; 1415 1416 if (iy < 0 || iy >= canvasHeight) { 1417 continue; 1418 } 1419 1420 for (xx = 0; xx < brushSize; ++xx) { 1421 ix = x + xx + offsetX; 1422 1423 if (ix < 0 || ix >= canvasWidth) { 1424 continue; 1425 } 1426 1427 ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; 1428 1429 px = (iy * canvasWidth + ix) * 4 + 3; 1430 1431 if (gData[px] === 0) { 1432 gData[px] = aData[px]; 1433 } 1434 1435 ba = bData[px] / 255; 1436 ba = ba + ka * (brushAlpha - ba); 1437 1438 bData[px] = Math.floor(ba * 255); 1439 aData[px] = Math.floor(gData[px] * (1 - ba)); 1440 } 1441 } 1442 } 1443 1444 generateShape(size) { 1445 if (this.tipId === 0) { 1446 return this.generateShapePencil(size); 1447 } 1448 else if (this.tipId === 1) { 1449 return this.generateShapePen(size); 1450 } 1451 else { 1452 return this.generateShapeAirbrush(size); 1453 } 1454 } 1455 } 1456 1457 TegakiEraser.prototype.generateShapePencil = TegakiPencil.prototype.generateShape; 1458 TegakiEraser.prototype.generateShapePen = TegakiPen.prototype.generateShape; 1459 TegakiEraser.prototype.generateShapeAirbrush = TegakiAirbrush.prototype.generateShape; 1460 var $T = { 1461 docEl: document.documentElement, 1462 1463 id: function(id) { 1464 return document.getElementById(id); 1465 }, 1466 1467 cls: function(klass, root) { 1468 return (root || document).getElementsByClassName(klass); 1469 }, 1470 1471 on: function(o, e, h) { 1472 o.addEventListener(e, h, false); 1473 }, 1474 1475 off: function(o, e, h) { 1476 o.removeEventListener(e, h, false); 1477 }, 1478 1479 el: function(name) { 1480 return document.createElement(name); 1481 }, 1482 1483 frag: function() { 1484 return document.createDocumentFragment(); 1485 }, 1486 1487 copyImageData(imageData) { 1488 return new ImageData( 1489 new Uint8ClampedArray(imageData.data), 1490 imageData.width 1491 ); 1492 }, 1493 1494 copyCanvas: function(source, clone) { 1495 var canvas; 1496 1497 if (!clone) { 1498 canvas = $T.el('canvas'); 1499 canvas.width = source.width; 1500 canvas.height = source.height; 1501 } 1502 else { 1503 canvas = source.cloneNode(false); 1504 } 1505 1506 canvas.getContext('2d').drawImage(source, 0, 0); 1507 1508 return canvas; 1509 }, 1510 1511 clearCtx: function(ctx) { 1512 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 1513 }, 1514 1515 hexToRgb: function(hex) { 1516 var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i); 1517 1518 if (c) { 1519 return [ 1520 parseInt(c[1], 16), 1521 parseInt(c[2], 16), 1522 parseInt(c[3], 16) 1523 ]; 1524 } 1525 1526 return null; 1527 }, 1528 1529 RgbToHex: function(r, g, b) { 1530 return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 1531 }, 1532 1533 getColorAt: function(ctx, posX, posY) { 1534 var rgba = ctx.getImageData(posX, posY, 1, 1).data; 1535 1536 return '#' 1537 + ('0' + rgba[0].toString(16)).slice(-2) 1538 + ('0' + rgba[1].toString(16)).slice(-2) 1539 + ('0' + rgba[2].toString(16)).slice(-2); 1540 }, 1541 1542 generateFilename: function() { 1543 return 'tegaki_' + (new Date()).toISOString().split('.')[0].replace(/[^0-9]/g, '_'); 1544 }, 1545 1546 sortAscCb: function(a, b) { 1547 if (a > b) { return 1; } 1548 if (a < b) { return -1; } 1549 return 0; 1550 }, 1551 1552 sortDescCb: function(a, b) { 1553 if (a > b) { return -1; } 1554 if (a < b) { return 1; } 1555 return 0; 1556 }, 1557 1558 msToHms: function(ms) { 1559 var h, m, s, ary; 1560 1561 s = 0 | (ms / 1000); 1562 h = 0 | (s / 3600); 1563 m = 0 | ((s - h * 3600) / 60); 1564 s = s - h * 3600 - m * 60; 1565 1566 ary = []; 1567 1568 if (h) { 1569 ary.push(h < 10 ? ('0' + h) : h); 1570 } 1571 1572 if (m) { 1573 ary.push(m < 10 ? ('0' + m) : m); 1574 } 1575 else { 1576 ary.push('00'); 1577 } 1578 1579 if (s) { 1580 ary.push(s < 10 ? ('0' + s) : s); 1581 } 1582 else { 1583 ary.push('00'); 1584 } 1585 1586 return ary.join(':'); 1587 }, 1588 1589 calcThumbSize(w, h, maxSide) { 1590 var r; 1591 1592 if (w > maxSide) { 1593 r = maxSide / w; 1594 w = maxSide; 1595 h = h * r; 1596 } 1597 1598 if (h > maxSide) { 1599 r = maxSide / h; 1600 h = maxSide; 1601 w = w * r; 1602 } 1603 1604 return [Math.ceil(w), Math.ceil(h)]; 1605 } 1606 }; 1607 class TegakiBinReader { 1608 constructor(buf) { 1609 this.pos = 0; 1610 this.view = new DataView(buf); 1611 this.buf = buf; 1612 } 1613 1614 readInt8() { 1615 var data = this.view.getInt8(this.pos); 1616 this.pos += 1; 1617 return data; 1618 } 1619 1620 readUint8() { 1621 var data = this.view.getUint8(this.pos); 1622 this.pos += 1; 1623 return data; 1624 } 1625 1626 readInt16() { 1627 var data = this.view.getInt16(this.pos); 1628 this.pos += 2; 1629 return data; 1630 } 1631 1632 readUint16() { 1633 var data = this.view.getUint16(this.pos); 1634 this.pos += 2; 1635 return data; 1636 } 1637 1638 readUint32() { 1639 var data = this.view.getUint32(this.pos); 1640 this.pos += 4; 1641 return data; 1642 } 1643 1644 readFloat32() { 1645 var data = this.view.getFloat32(this.pos); 1646 this.pos += 4; 1647 return data; 1648 } 1649 } 1650 1651 class TegakiBinWriter { 1652 constructor(buf) { 1653 this.pos = 0; 1654 this.view = new DataView(buf); 1655 this.buf = buf; 1656 } 1657 1658 writeInt8(val) { 1659 this.view.setInt8(this.pos, val); 1660 this.pos += 1; 1661 } 1662 1663 writeUint8(val) { 1664 this.view.setUint8(this.pos, val); 1665 this.pos += 1; 1666 } 1667 1668 writeInt16(val) { 1669 this.view.setInt16(this.pos, val); 1670 this.pos += 2; 1671 } 1672 1673 writeUint16(val) { 1674 this.view.setUint16(this.pos, val); 1675 this.pos += 2; 1676 } 1677 1678 writeUint32(val) { 1679 this.view.setUint32(this.pos, val); 1680 this.pos += 4; 1681 } 1682 1683 writeFloat32(val) { 1684 this.view.setFloat32(this.pos, val); 1685 this.pos += 4; 1686 } 1687 } 1688 var TegakiCursor = { 1689 size: 0, 1690 radius: 0, 1691 1692 points: null, 1693 1694 tmpCtx: null, 1695 1696 cursorCtx: null, 1697 1698 flatCtxAbove: null, 1699 flatCtxBelow: null, 1700 1701 cached: false, 1702 1703 init: function(w, h) { 1704 var el; 1705 1706 this.tmpCtx = $T.el('canvas').getContext('2d'); 1707 1708 el = $T.el('canvas'); 1709 el.id = 'tegaki-cursor-layer'; 1710 el.width = w; 1711 el.height = h; 1712 Tegaki.layersCnt.appendChild(el); 1713 1714 this.cursorCtx = el.getContext('2d'); 1715 1716 el = $T.el('canvas'); 1717 el.width = w; 1718 el.height = h; 1719 this.flatCtxAbove = el.getContext('2d'); 1720 1721 el = $T.el('canvas'); 1722 el.width = w; 1723 el.height = h; 1724 this.flatCtxBelow = el.getContext('2d'); 1725 }, 1726 1727 updateCanvasSize: function(w, h) { 1728 this.cursorCtx.canvas.width = w; 1729 this.cursorCtx.canvas.height = h; 1730 1731 this.flatCtxAbove.canvas.width = w; 1732 this.flatCtxAbove.canvas.height = h; 1733 1734 this.flatCtxBelow.canvas.width = w; 1735 this.flatCtxBelow.canvas.height = h; 1736 }, 1737 1738 render: function(x, y) { 1739 var i, size, srcImg, srcData, destImg, destData, activeLayer; 1740 1741 if (!this.cached) { 1742 this.buildCache(); 1743 } 1744 1745 size = this.size; 1746 x = x - this.radius; 1747 y = y - this.radius; 1748 1749 $T.clearCtx(this.cursorCtx); 1750 $T.clearCtx(this.tmpCtx); 1751 1752 this.tmpCtx.drawImage(this.flatCtxBelow.canvas, x, y, size, size, 0, 0, size, size); 1753 1754 activeLayer = Tegaki.activeLayer; 1755 1756 if (activeLayer.visible) { 1757 if (activeLayer.alpha < 1.0) { 1758 this.tmpCtx.globalAlpha = activeLayer.alpha; 1759 this.tmpCtx.drawImage(Tegaki.activeLayer.canvas, x, y, size, size, 0, 0, size, size); 1760 this.tmpCtx.globalAlpha = 1.0; 1761 } 1762 else { 1763 this.tmpCtx.drawImage(Tegaki.activeLayer.canvas, x, y, size, size, 0, 0, size, size); 1764 } 1765 } 1766 1767 this.tmpCtx.drawImage(this.flatCtxAbove.canvas, x, y, size, size, 0, 0, size, size); 1768 1769 srcImg = this.tmpCtx.getImageData(0, 0, size, size); 1770 srcData = new Uint32Array(srcImg.data.buffer); 1771 1772 destImg = this.cursorCtx.createImageData(size, size); 1773 destData = new Uint32Array(destImg.data.buffer); 1774 1775 for (i of this.points) { 1776 destData[i] = srcData[i] ^ 0x00FFFF7F; 1777 } 1778 1779 this.cursorCtx.putImageData(destImg, x, y); 1780 }, 1781 1782 buildCache: function() { 1783 var i, layer, ctx, len, layerId; 1784 1785 ctx = this.flatCtxBelow; 1786 ctx.globalAlpha = 1.0; 1787 $T.clearCtx(ctx); 1788 1789 ctx.drawImage(Tegaki.canvas, 0, 0); 1790 1791 layerId = Tegaki.activeLayer.id; 1792 1793 for (i = 0, len = Tegaki.layers.length; i < len; ++i) { 1794 layer = Tegaki.layers[i]; 1795 1796 if (!layer.visible) { 1797 continue; 1798 } 1799 1800 if (layer.id === layerId) { 1801 ctx = this.flatCtxAbove; 1802 ctx.globalAlpha = 1.0; 1803 $T.clearCtx(ctx); 1804 continue; 1805 } 1806 1807 ctx.globalAlpha = layer.alpha; 1808 ctx.drawImage(layer.canvas, 0, 0); 1809 } 1810 1811 this.cached = true; 1812 }, 1813 1814 invalidateCache() { 1815 this.cached = false; 1816 }, 1817 1818 destroy() { 1819 this.size = 0; 1820 this.radius = 0; 1821 this.points = null; 1822 this.tmpCtx = null; 1823 this.cursorCtx = null; 1824 this.flatCtxAbove = null; 1825 this.flatCtxBelow = null; 1826 }, 1827 1828 generate: function(size) { 1829 var e, x, y, c, r, rr, points; 1830 1831 r = 0 | ((size) / 2); 1832 1833 rr = 0 | ((size + 1) % 2); 1834 1835 points = []; 1836 1837 x = r; 1838 y = 0; 1839 e = 1 - r; 1840 c = r; 1841 1842 while (x >= y) { 1843 points.push(c + x - rr + (c + y - rr) * size); 1844 points.push(c + y - rr + (c + x - rr) * size); 1845 1846 points.push(c - y + (c + x - rr) * size); 1847 points.push(c - x + (c + y - rr) * size); 1848 1849 points.push(c - y + (c - x) * size); 1850 points.push(c - x + (c - y) * size); 1851 1852 points.push(c + y - rr + (c - x) * size); 1853 points.push(c + x - rr + (c - y) * size); 1854 1855 ++y; 1856 1857 if (e <= 0) { 1858 e += 2 * y + 1; 1859 } 1860 else { 1861 x--; 1862 e += 2 * (y - x) + 1; 1863 } 1864 } 1865 1866 this.tmpCtx.canvas.width = size; 1867 this.tmpCtx.canvas.height = size; 1868 1869 this.size = size; 1870 this.radius = r; 1871 this.points = points; 1872 } 1873 }; 1874 var TegakiHistory = { 1875 maxSize: 50, 1876 1877 undoStack: [], 1878 redoStack: [], 1879 1880 pendingAction: null, 1881 1882 clear: function() { 1883 this.undoStack = []; 1884 this.redoStack = []; 1885 this.pendingAction = null; 1886 1887 this.onChange(0); 1888 }, 1889 1890 push: function(action) { 1891 if (!action) { 1892 return; 1893 } 1894 1895 if (action.coalesce) { 1896 if (this.undoStack[this.undoStack.length - 1] instanceof action.constructor) { 1897 if (this.undoStack[this.undoStack.length - 1].coalesce(action)) { 1898 return; 1899 } 1900 } 1901 } 1902 1903 this.undoStack.push(action); 1904 1905 if (this.undoStack.length > this.maxSize) { 1906 this.undoStack.shift(); 1907 } 1908 1909 if (this.redoStack.length > 0) { 1910 this.redoStack = []; 1911 } 1912 1913 this.onChange(0); 1914 }, 1915 1916 undo: function() { 1917 var action; 1918 1919 if (!this.undoStack.length) { 1920 return; 1921 } 1922 1923 action = this.undoStack.pop(); 1924 1925 action.undo(); 1926 1927 this.redoStack.push(action); 1928 1929 this.onChange(-1); 1930 }, 1931 1932 redo: function() { 1933 var action; 1934 1935 if (!this.redoStack.length) { 1936 return; 1937 } 1938 1939 action = this.redoStack.pop(); 1940 1941 action.redo(); 1942 1943 this.undoStack.push(action); 1944 1945 this.onChange(1); 1946 }, 1947 1948 onChange: function(type) { 1949 Tegaki.onHistoryChange(this.undoStack.length, this.redoStack.length, type); 1950 }, 1951 1952 sortPosLayerCallback: function(a, b) { 1953 if (a[0] > b[0]) { return 1; } 1954 if (a[0] < b[0]) { return -1; } 1955 return 0; 1956 } 1957 }; 1958 1959 var TegakiHistoryActions = { 1960 Dummy: function() { 1961 this.undo = function() {}; 1962 this.redo = function() {}; 1963 }, 1964 1965 Draw: function(layerId) { 1966 this.coalesce = false; 1967 1968 this.imageDataBefore = null; 1969 this.imageDataAfter = null; 1970 this.layerId = layerId; 1971 }, 1972 1973 DeleteLayers: function(layerPosMap, params) { 1974 var item; 1975 1976 this.coalesce = false; 1977 1978 this.layerPosMap = []; 1979 1980 for (item of layerPosMap.sort(TegakiHistory.sortPosLayerCallback)) { 1981 item[1] = TegakiLayers.cloneLayer(item[1]); 1982 this.layerPosMap.push(item); 1983 } 1984 1985 this.tgtLayerId = null; 1986 1987 this.aLayerIdBefore = null; 1988 this.aLayerIdAfter = null; 1989 1990 this.imageDataBefore = null; 1991 this.imageDataAfter = null; 1992 1993 this.mergeDown = false; 1994 1995 if (params) { 1996 for (let k in params) { 1997 this[k] = params[k]; 1998 } 1999 } 2000 }, 2001 2002 AddLayer: function(params, aLayerIdBefore, aLayerIdAfter) { 2003 this.coalesce = false; 2004 2005 this.layer = params; 2006 this.layerId = params.id; 2007 this.aLayerIdBefore = aLayerIdBefore; 2008 this.aLayerIdAfter = aLayerIdAfter; 2009 }, 2010 2011 MoveLayers: function(layers, belowPos, activeLayerId) { 2012 this.coalesce = false; 2013 2014 this.layers = layers; 2015 this.belowPos = belowPos; 2016 this.aLayerId = activeLayerId; 2017 }, 2018 2019 SetLayersAlpha: function(layerAlphas, newAlpha) { 2020 this.layerAlphas = layerAlphas; 2021 this.newAlpha = newAlpha; 2022 }, 2023 2024 SetLayerName: function(id, oldName, newName) { 2025 this.layerId = id; 2026 this.oldName = oldName; 2027 this.newName = newName; 2028 } 2029 }; 2030 2031 // --- 2032 2033 TegakiHistoryActions.Draw.prototype.addCanvasState = function(imageData, type) { 2034 if (type) { 2035 this.imageDataAfter = $T.copyImageData(imageData); 2036 } 2037 else { 2038 this.imageDataBefore = $T.copyImageData(imageData); 2039 } 2040 }; 2041 2042 TegakiHistoryActions.Draw.prototype.exec = function(type) { 2043 var layer = TegakiLayers.getLayerById(this.layerId); 2044 2045 if (type) { 2046 layer.ctx.putImageData(this.imageDataAfter, 0, 0); 2047 TegakiLayers.syncLayerImageData(layer, this.imageDataAfter); 2048 } 2049 else { 2050 layer.ctx.putImageData(this.imageDataBefore, 0, 0); 2051 TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); 2052 } 2053 2054 TegakiUI.updateLayerPreview(layer); 2055 TegakiLayers.setActiveLayer(this.layerId); 2056 }; 2057 2058 TegakiHistoryActions.Draw.prototype.undo = function() { 2059 this.exec(0); 2060 }; 2061 2062 TegakiHistoryActions.Draw.prototype.redo = function() { 2063 this.exec(1); 2064 }; 2065 2066 TegakiHistoryActions.DeleteLayers.prototype.undo = function() { 2067 var i, lim, refLayer, layer, pos, refId; 2068 2069 for (i = 0, lim = this.layerPosMap.length; i < lim; ++i) { 2070 [pos, layer] = this.layerPosMap[i]; 2071 2072 layer = TegakiLayers.cloneLayer(layer); 2073 2074 refLayer = Tegaki.layers[pos]; 2075 2076 if (refLayer) { 2077 if (refId = TegakiLayers.getLayerBelowId(refLayer.id)) { 2078 refId = refId.id; 2079 } 2080 2081 TegakiUI.updateLayersGridAdd(layer, refId); 2082 TegakiUI.updateLayerPreview(layer); 2083 Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.canvas); 2084 Tegaki.layers.splice(pos, 0, layer); 2085 } 2086 else { 2087 2088 if (!Tegaki.layers[0]) { 2089 refLayer = Tegaki.layersCnt.children[0]; 2090 } 2091 else { 2092 refLayer = Tegaki.layers[Tegaki.layers.length - 1].canvas; 2093 } 2094 2095 TegakiUI.updateLayersGridAdd(layer, TegakiLayers.getTopLayerId()); 2096 TegakiUI.updateLayerPreview(layer); 2097 Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.nextElementSibling); 2098 Tegaki.layers.push(layer); 2099 } 2100 } 2101 2102 if (this.tgtLayerId) { 2103 layer = TegakiLayers.getLayerById(this.tgtLayerId); 2104 layer.ctx.putImageData(this.imageDataBefore, 0, 0); 2105 TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); 2106 TegakiLayers.setLayerAlpha(layer, this.tgtLayerAlpha); 2107 TegakiUI.updateLayerPreview(layer); 2108 } 2109 2110 TegakiLayers.setActiveLayer(this.aLayerIdBefore); 2111 }; 2112 2113 TegakiHistoryActions.DeleteLayers.prototype.redo = function() { 2114 var layer, ids = []; 2115 2116 for (layer of this.layerPosMap) { 2117 ids.unshift(layer[1].id); 2118 } 2119 2120 if (this.tgtLayerId) { 2121 if (!this.mergeDown) { 2122 ids.unshift(this.tgtLayerId); 2123 } 2124 TegakiLayers.mergeLayers(new Set(ids)); 2125 } 2126 else { 2127 TegakiLayers.deleteLayers(ids); 2128 } 2129 2130 TegakiLayers.setActiveLayer(this.aLayerIdAfter); 2131 }; 2132 2133 TegakiHistoryActions.MoveLayers.prototype.undo = function() { 2134 var i, layer, stack, ref, posMap, len; 2135 2136 stack = new Array(Tegaki.layers.length); 2137 2138 posMap = {}; 2139 2140 for (layer of this.layers) { 2141 posMap[layer[1].id] = layer[0]; 2142 } 2143 2144 for (i = 0, len = Tegaki.layers.length; i < len; ++i) { 2145 layer = Tegaki.layers[i]; 2146 2147 if (posMap[layer.id] !== undefined) { 2148 Tegaki.layers.splice(i, 1); 2149 Tegaki.layers.splice(posMap[layer.id], 0, layer); 2150 } 2151 } 2152 2153 TegakiUI.updayeLayersGridOrder(); 2154 2155 ref = Tegaki.layersCnt.children[0]; 2156 2157 for (i = Tegaki.layers.length - 1; i >= 0; i--) { 2158 layer = Tegaki.layers[i]; 2159 Tegaki.layersCnt.insertBefore(layer.canvas, ref.nextElementSibling); 2160 } 2161 2162 TegakiLayers.setActiveLayer(this.aLayerId); 2163 }; 2164 2165 TegakiHistoryActions.MoveLayers.prototype.redo = function() { 2166 var layer, layers = new Set(); 2167 2168 for (layer of this.layers.slice().reverse()) { 2169 layers.add(layer[1].id); 2170 } 2171 2172 TegakiLayers.setActiveLayer(this.aLayerId); 2173 TegakiLayers.moveLayers(layers, this.belowPos); 2174 }; 2175 2176 TegakiHistoryActions.AddLayer.prototype.undo = function() { 2177 TegakiLayers.deleteLayers([this.layer.id]); 2178 TegakiLayers.setActiveLayer(this.aLayerIdBefore); 2179 Tegaki.layerCounter--; 2180 }; 2181 2182 TegakiHistoryActions.AddLayer.prototype.redo = function() { 2183 TegakiLayers.setActiveLayer(this.aLayerIdBefore); 2184 TegakiLayers.addLayer(this.layer); 2185 TegakiLayers.setActiveLayer(this.aLayerIdAfter); 2186 }; 2187 2188 TegakiHistoryActions.SetLayersAlpha.prototype.undo = function() { 2189 var id, layerAlpha, layer; 2190 2191 for (layerAlpha of this.layerAlphas) { 2192 [id, layerAlpha] = layerAlpha; 2193 2194 if (layer = TegakiLayers.getLayerById(id)) { 2195 TegakiLayers.setLayerAlpha(layer, layerAlpha); 2196 } 2197 } 2198 2199 TegakiUI.updateLayerAlphaOpt(); 2200 }; 2201 2202 TegakiHistoryActions.SetLayersAlpha.prototype.redo = function() { 2203 var id, layerAlpha, layer; 2204 2205 for (layerAlpha of this.layerAlphas) { 2206 [id, layerAlpha] = layerAlpha; 2207 2208 if (layer = TegakiLayers.getLayerById(id)) { 2209 TegakiLayers.setLayerAlpha(layer, this.newAlpha); 2210 } 2211 } 2212 2213 TegakiUI.updateLayerAlphaOpt(); 2214 }; 2215 2216 TegakiHistoryActions.SetLayersAlpha.prototype.coalesce = function(action) { 2217 var i; 2218 2219 if (this.layerAlphas.length !== action.layerAlphas.length) { 2220 return false; 2221 } 2222 2223 for (i = 0; i < this.layerAlphas.length; ++i) { 2224 if (this.layerAlphas[i][0] !== action.layerAlphas[i][0]) { 2225 return false; 2226 } 2227 } 2228 2229 this.newAlpha = action.newAlpha; 2230 2231 return true; 2232 }; 2233 2234 TegakiHistoryActions.SetLayerName.prototype.exec = function(type) { 2235 var layer = TegakiLayers.getLayerById(this.layerId); 2236 2237 if (layer) { 2238 layer.name = type ? this.newName : this.oldName; 2239 TegakiUI.updateLayerName(layer); 2240 } 2241 }; 2242 2243 TegakiHistoryActions.SetLayerName.prototype.undo = function() { 2244 this.exec(0); 2245 }; 2246 2247 TegakiHistoryActions.SetLayerName.prototype.redo = function() { 2248 this.exec(1); 2249 }; 2250 var TegakiKeybinds = { 2251 keyMap: {}, 2252 2253 captionMap: {}, 2254 2255 clear: function() { 2256 this.keyMap = {}; 2257 this.captionMap = {}; 2258 }, 2259 2260 bind: function(keys, klass, fn, id, caption) { 2261 this.keyMap[keys] = [klass, fn]; 2262 2263 if (id) { 2264 this.captionMap[id] = caption; 2265 } 2266 }, 2267 2268 getCaption(id) { 2269 return this.captionMap[id]; 2270 }, 2271 2272 resolve: function(e) { 2273 var fn, mods, keys, el; 2274 2275 el = e.target; 2276 2277 if (el.nodeName == 'INPUT' && (el.type === 'text' || el.type === 'number')) { 2278 return; 2279 } 2280 2281 mods = []; 2282 2283 if (e.ctrlKey) { 2284 mods.push('ctrl'); 2285 } 2286 2287 if (e.shiftKey) { 2288 mods.push('shift'); 2289 } 2290 2291 keys = e.key.toLowerCase(); 2292 2293 if (mods[0]) { 2294 keys = mods.join('+') + '+' + keys; 2295 } 2296 2297 fn = TegakiKeybinds.keyMap[keys]; 2298 2299 if (fn && !e.altKey && !e.metaKey) { 2300 e.preventDefault(); 2301 e.stopPropagation(); 2302 fn[0][fn[1]](); 2303 } 2304 }, 2305 }; 2306 var TegakiLayers = { 2307 cloneLayer: function(layer) { 2308 var newLayer = Object.assign({}, layer); 2309 2310 newLayer.canvas = $T.copyCanvas(layer.canvas, true); 2311 newLayer.ctx = newLayer.canvas.getContext('2d'); 2312 newLayer.imageData = $T.copyImageData(layer.imageData); 2313 2314 return newLayer; 2315 }, 2316 2317 getCanvasById: function(id) { 2318 return $T.id('tegaki-canvas-' + id); 2319 }, 2320 2321 getActiveLayer: function() { 2322 return Tegaki.activeLayer; 2323 }, 2324 2325 getLayerPosById: function(id) { 2326 var i, layers = Tegaki.layers; 2327 2328 for (i = 0; i < layers.length; ++i) { 2329 if (layers[i].id === id) { 2330 return i; 2331 } 2332 } 2333 2334 return -1; 2335 }, 2336 2337 getTopFencedLayerId: function() { 2338 var i, id, layer, layers = Tegaki.layers; 2339 2340 for (i = layers.length - 1; i >= 0; i--) { 2341 if (TegakiLayers.selectedLayersHas(layers[i].id)) { 2342 break; 2343 } 2344 } 2345 2346 for (i = i - 1; i >= 0; i--) { 2347 if (!TegakiLayers.selectedLayersHas(layers[i].id)) { 2348 break; 2349 } 2350 } 2351 2352 if (layer = layers[i]) { 2353 id = layer.id; 2354 } 2355 else { 2356 id = 0; 2357 } 2358 2359 return id; 2360 }, 2361 2362 getSelectedEdgeLayerPos: function(top) { 2363 var i, layers = Tegaki.layers; 2364 2365 if (top) { 2366 for (i = Tegaki.layers.length - 1; i >= 0; i--) { 2367 if (TegakiLayers.selectedLayersHas(layers[i].id)) { 2368 break; 2369 } 2370 } 2371 } 2372 else { 2373 for (i = 0; i < layers.length; ++i) { 2374 if (TegakiLayers.selectedLayersHas(layers[i].id)) { 2375 break; 2376 } 2377 } 2378 2379 if (i >= layers.length) { 2380 i = -1; 2381 } 2382 } 2383 2384 return i; 2385 }, 2386 2387 getTopLayer: function() { 2388 return Tegaki.layers[Tegaki.layers.length - 1]; 2389 }, 2390 2391 getTopLayerId: function() { 2392 var layer = TegakiLayers.getTopLayer(); 2393 2394 if (layer) { 2395 return layer.id; 2396 } 2397 else { 2398 return 0; 2399 } 2400 }, 2401 2402 getLayerBelowId: function(belowId) { 2403 var idx; 2404 2405 idx = TegakiLayers.getLayerPosById(belowId); 2406 2407 if (idx < 1) { 2408 return null; 2409 } 2410 2411 return Tegaki.layers[idx - 1]; 2412 }, 2413 2414 getLayerById: function(id) { 2415 return Tegaki.layers[TegakiLayers.getLayerPosById(id)]; 2416 }, 2417 2418 isSameLayerOrder: function(a, b) { 2419 var i, al; 2420 2421 if (a.length !== b.length) { 2422 return false; 2423 } 2424 2425 for (i = 0; al = a[i]; ++i) { 2426 if (al.id !== b[i].id) { 2427 return false; 2428 } 2429 } 2430 2431 return true; 2432 }, 2433 2434 addLayer: function(baseLayer = {}) { 2435 var id, canvas, k, params, layer, afterNode, afterPos, 2436 aLayerIdBefore, ctx; 2437 2438 if (Tegaki.activeLayer) { 2439 aLayerIdBefore = Tegaki.activeLayer.id; 2440 afterPos = TegakiLayers.getLayerPosById(Tegaki.activeLayer.id); 2441 afterNode = $T.cls('tegaki-layer', Tegaki.layersCnt)[afterPos]; 2442 } 2443 else { 2444 afterPos = -1; 2445 afterNode = null; 2446 } 2447 2448 if (!afterNode) { 2449 afterNode = Tegaki.layersCnt.firstElementChild; 2450 } 2451 2452 canvas = $T.el('canvas'); 2453 canvas.className = 'tegaki-layer'; 2454 canvas.width = Tegaki.baseWidth; 2455 canvas.height = Tegaki.baseHeight; 2456 2457 id = ++Tegaki.layerCounter; 2458 2459 canvas.id = 'tegaki-canvas-' + id; 2460 canvas.setAttribute('data-id', id); 2461 2462 params = { 2463 name: TegakiStrings.layer + ' ' + id, 2464 visible: true, 2465 alpha: 1.0, 2466 }; 2467 2468 ctx = canvas.getContext('2d'); 2469 2470 layer = { 2471 id: id, 2472 canvas: canvas, 2473 ctx: ctx, 2474 imageData: ctx.getImageData(0, 0, canvas.width, canvas.height) 2475 }; 2476 2477 for (k in params) { 2478 if (baseLayer[k] !== undefined) { 2479 params[k] = baseLayer[k]; 2480 } 2481 2482 layer[k] = params[k]; 2483 } 2484 2485 Tegaki.layers.splice(afterPos + 1, 0, layer); 2486 2487 TegakiUI.updateLayersGridAdd(layer, aLayerIdBefore); 2488 2489 Tegaki.layersCnt.insertBefore(canvas, afterNode.nextElementSibling); 2490 2491 Tegaki.onLayerStackChanged(); 2492 2493 return new TegakiHistoryActions.AddLayer(layer, aLayerIdBefore, id); 2494 }, 2495 2496 deleteLayers: function(ids, extraParams) { 2497 var id, idx, layer, layers, delIndexes, params; 2498 2499 params = { 2500 aLayerIdBefore: Tegaki.activeLayer ? Tegaki.activeLayer.id : -1, 2501 aLayerIdAfter: TegakiLayers.getTopFencedLayerId() 2502 }; 2503 2504 layers = []; 2505 2506 delIndexes = []; 2507 2508 for (id of ids) { 2509 idx = TegakiLayers.getLayerPosById(id); 2510 layer = Tegaki.layers[idx]; 2511 2512 layers.push([idx, layer]); 2513 2514 Tegaki.layersCnt.removeChild(layer.canvas); 2515 2516 delIndexes.push(idx); 2517 2518 TegakiUI.updateLayersGridRemove(id); 2519 2520 TegakiUI.deleteLayerPreviewCtx(layer); 2521 } 2522 2523 delIndexes = delIndexes.sort($T.sortDescCb); 2524 2525 for (idx of delIndexes) { 2526 Tegaki.layers.splice(idx, 1); 2527 } 2528 2529 if (extraParams) { 2530 Object.assign(params, extraParams); 2531 } 2532 2533 Tegaki.onLayerStackChanged(); 2534 2535 return new TegakiHistoryActions.DeleteLayers(layers, params); 2536 }, 2537 2538 mergeLayers: function(idSet) { 2539 var canvas, ctx, imageDataAfter, imageDataBefore, 2540 targetLayer, action, layer, layers, delIds, mergeDown; 2541 2542 layers = []; 2543 2544 for (layer of Tegaki.layers) { 2545 if (idSet.has(layer.id)) { 2546 layers.push(layer); 2547 } 2548 } 2549 2550 if (layers.length < 1) { 2551 return; 2552 } 2553 2554 if (layers.length === 1) { 2555 targetLayer = TegakiLayers.getLayerBelowId(layers[0].id); 2556 2557 if (!targetLayer) { 2558 return; 2559 } 2560 2561 layers.unshift(targetLayer); 2562 2563 mergeDown = true; 2564 } 2565 else { 2566 targetLayer = layers[layers.length - 1]; 2567 2568 mergeDown = false; 2569 } 2570 2571 canvas = $T.el('canvas'); 2572 canvas.width = Tegaki.baseWidth; 2573 canvas.height = Tegaki.baseHeight; 2574 2575 ctx = canvas.getContext('2d'); 2576 2577 imageDataBefore = $T.copyImageData(targetLayer.imageData); 2578 2579 delIds = []; 2580 2581 for (layer of layers) { 2582 if (layer.id !== targetLayer.id) { 2583 delIds.push(layer.id); 2584 } 2585 2586 ctx.globalAlpha = layer.alpha; 2587 ctx.drawImage(layer.canvas, 0, 0); 2588 } 2589 2590 $T.clearCtx(targetLayer.ctx); 2591 2592 targetLayer.ctx.drawImage(canvas, 0, 0); 2593 2594 TegakiLayers.syncLayerImageData(targetLayer); 2595 2596 imageDataAfter = $T.copyImageData(targetLayer.imageData); 2597 2598 action = TegakiLayers.deleteLayers(delIds, { 2599 tgtLayerId: targetLayer.id, 2600 tgtLayerAlpha: targetLayer.alpha, 2601 aLayerIdAfter: targetLayer.id, 2602 imageDataBefore: imageDataBefore, 2603 imageDataAfter: imageDataAfter, 2604 mergeDown: mergeDown 2605 }); 2606 2607 TegakiLayers.setLayerAlpha(targetLayer, 1.0); 2608 2609 TegakiUI.updateLayerAlphaOpt(); 2610 2611 TegakiUI.updateLayerPreview(targetLayer); 2612 2613 Tegaki.onLayerStackChanged(); 2614 2615 return action; 2616 }, 2617 2618 moveLayers: function(idSet, belowPos) { 2619 var idx, layer, 2620 historyLayers, updLayers, movedLayers, 2621 tgtCanvas, updTgtPos; 2622 2623 if (!idSet.size || !Tegaki.layers.length) { 2624 return; 2625 } 2626 2627 if (belowPos >= Tegaki.layers.length) { 2628 tgtCanvas = TegakiLayers.getTopLayer().canvas.nextElementSibling; 2629 } 2630 else { 2631 layer = Tegaki.layers[belowPos]; 2632 tgtCanvas = layer.canvas; 2633 } 2634 2635 historyLayers = []; 2636 updLayers = []; 2637 movedLayers = []; 2638 2639 updTgtPos = belowPos; 2640 2641 idx = 0; 2642 2643 for (layer of Tegaki.layers) { 2644 if (idSet.has(layer.id)) { 2645 if (belowPos > 0 && idx <= belowPos) { 2646 updTgtPos--; 2647 } 2648 2649 historyLayers.push([idx, layer]); 2650 movedLayers.push(layer); 2651 } 2652 else { 2653 updLayers.push(layer); 2654 } 2655 2656 ++idx; 2657 } 2658 2659 updLayers.splice(updTgtPos, 0, ...movedLayers); 2660 2661 if (TegakiLayers.isSameLayerOrder(updLayers, Tegaki.layers)) { 2662 return; 2663 } 2664 2665 Tegaki.layers = updLayers; 2666 2667 for (layer of historyLayers) { 2668 Tegaki.layersCnt.insertBefore(layer[1].canvas, tgtCanvas); 2669 } 2670 2671 TegakiUI.updayeLayersGridOrder(); 2672 2673 Tegaki.onLayerStackChanged(); 2674 2675 return new TegakiHistoryActions.MoveLayers( 2676 historyLayers, belowPos, 2677 Tegaki.activeLayer ? Tegaki.activeLayer.id : -1 2678 ); 2679 }, 2680 2681 setLayerVisibility: function(layer, flag) { 2682 layer.visible = flag; 2683 2684 if (flag) { 2685 layer.canvas.classList.remove('tegaki-hidden'); 2686 } 2687 else { 2688 layer.canvas.classList.add('tegaki-hidden'); 2689 } 2690 2691 Tegaki.onLayerStackChanged(); 2692 2693 TegakiUI.updateLayersGridVisibility(layer.id, flag); 2694 }, 2695 2696 setLayerAlpha: function(layer, alpha) { 2697 layer.alpha = alpha; 2698 layer.canvas.style.opacity = alpha; 2699 }, 2700 2701 setActiveLayer: function(id) { 2702 var idx, layer; 2703 2704 if (!id) { 2705 id = TegakiLayers.getTopLayerId(); 2706 2707 if (!id) { 2708 Tegaki.activeLayer = null; 2709 return; 2710 } 2711 } 2712 2713 idx = TegakiLayers.getLayerPosById(id); 2714 2715 if (idx < 0) { 2716 return; 2717 } 2718 2719 layer = Tegaki.layers[idx]; 2720 2721 if (Tegaki.activeLayer) { 2722 Tegaki.copyContextState(Tegaki.activeLayer.ctx, layer.ctx); 2723 } 2724 2725 Tegaki.activeLayer = layer; 2726 2727 TegakiLayers.selectedLayersClear(); 2728 TegakiLayers.selectedLayersAdd(id); 2729 2730 TegakiUI.updateLayersGridActive(id); 2731 TegakiUI.updateLayerAlphaOpt(); 2732 2733 Tegaki.onLayerStackChanged(); 2734 }, 2735 2736 syncLayerImageData(layer, imageData = null) { 2737 if (imageData) { 2738 layer.imageData = $T.copyImageData(imageData); 2739 } 2740 else { 2741 layer.imageData = layer.ctx.getImageData( 2742 0, 0, Tegaki.baseWidth, Tegaki.baseHeight 2743 ); 2744 } 2745 }, 2746 2747 selectedLayersHas: function(id) { 2748 return Tegaki.selectedLayers.has(+id); 2749 }, 2750 2751 selectedLayersClear: function() { 2752 Tegaki.selectedLayers.clear(); 2753 TegakiUI.updateLayerAlphaOpt(); 2754 TegakiUI.updateLayersGridSelectedClear(); 2755 }, 2756 2757 selectedLayersAdd: function(id) { 2758 Tegaki.selectedLayers.add(+id); 2759 TegakiUI.updateLayerAlphaOpt(); 2760 TegakiUI.updateLayersGridSelectedSet(id, true); 2761 }, 2762 2763 selectedLayersRemove: function(id) { 2764 Tegaki.selectedLayers.delete(+id); 2765 TegakiUI.updateLayerAlphaOpt(); 2766 TegakiUI.updateLayersGridSelectedSet(id, false); 2767 }, 2768 2769 selectedLayersToggle: function(id) { 2770 if (TegakiLayers.selectedLayersHas(id)) { 2771 TegakiLayers.selectedLayersRemove(id); 2772 } 2773 else { 2774 TegakiLayers.selectedLayersAdd(id); 2775 } 2776 } 2777 }; 2778 var Tegaki = { 2779 VERSION: '0.9.1', 2780 2781 startTimeStamp: 0, 2782 2783 bg: null, 2784 canvas: null, 2785 ctx: null, 2786 layers: [], 2787 2788 layersCnt: null, 2789 canvasCnt: null, 2790 2791 ghostBuffer: null, 2792 blendBuffer: null, 2793 ghostBuffer32: null, 2794 blendBuffer32: null, 2795 2796 activeLayer: null, 2797 2798 layerCounter: 0, 2799 selectedLayers: new Set(), 2800 2801 activePointerId: 0, 2802 activePointerIsPen: false, 2803 2804 isPainting: false, 2805 2806 offsetX: 0, 2807 offsetY: 0, 2808 2809 zoomLevel: 0, 2810 zoomFactor: 1.0, 2811 zoomFactorList: [0.5, 1.0, 2.0, 4.0, 8.0, 16.0], 2812 zoomBaseLevel: 1, 2813 2814 hasCustomCanvas: false, 2815 2816 TWOPI: 2 * Math.PI, 2817 2818 toolList: [ 2819 TegakiPencil, 2820 TegakiPen, 2821 TegakiAirbrush, 2822 TegakiBucket, 2823 TegakiTone, 2824 TegakiPipette, 2825 TegakiBlur, 2826 TegakiEraser 2827 ], 2828 2829 tools: {}, 2830 2831 tool: null, 2832 2833 colorPaletteId: 0, 2834 2835 toolColor: '#000000', 2836 defaultTool: 'pencil', 2837 2838 bgColor: '#ffffff', 2839 maxSize: 64, 2840 maxLayers: 25, 2841 baseWidth: 0, 2842 baseHeight: 0, 2843 2844 replayRecorder: null, 2845 replayViewer: null, 2846 2847 onDoneCb: null, 2848 onCancelCb: null, 2849 2850 replayMode: false, 2851 2852 saveReplay: false, 2853 2854 open: function(opts = {}) { 2855 var self = Tegaki; 2856 2857 if (self.bg) { 2858 if (self.replayMode !== (opts.replayMode ? true : false)) { 2859 self.destroy(); 2860 } 2861 else { 2862 self.resume(); 2863 return; 2864 } 2865 } 2866 2867 self.startTimeStamp = Date.now(); 2868 2869 if (opts.bgColor) { 2870 self.bgColor = opts.bgColor; 2871 } 2872 2873 self.hasCustomCanvas = false; 2874 2875 self.saveReplay = !!opts.saveReplay; 2876 self.replayMode = !!opts.replayMode; 2877 2878 self.onDoneCb = opts.onDone; 2879 self.onCancelCb = opts.onCancel; 2880 2881 self.baseWidth = opts.width || 0; 2882 self.baseHeight = opts.height || 0; 2883 2884 self.createTools(); 2885 2886 self.initKeybinds(); 2887 2888 [self.bg, self.canvasCnt, self.layersCnt] = TegakiUI.buildUI(); 2889 2890 document.body.appendChild(self.bg); 2891 document.body.classList.add('tegaki-backdrop'); 2892 2893 if (!self.replayMode) { 2894 self.init(); 2895 2896 self.setTool(self.defaultTool); 2897 2898 if (self.saveReplay) { 2899 self.replayRecorder = new TegakiReplayRecorder(); 2900 self.replayRecorder.start(); 2901 } 2902 } 2903 else { 2904 TegakiUI.setReplayMode(true); 2905 2906 self.replayViewer = new TegakiReplayViewer(); 2907 2908 if (opts.replayURL) { 2909 self.loadReplayFromURL(opts.replayURL); 2910 } 2911 } 2912 }, 2913 2914 init: function() { 2915 var self = Tegaki; 2916 2917 self.createCanvas(); 2918 2919 self.centerLayersCnt(); 2920 2921 self.createBuffers(); 2922 2923 self.updatePosOffset(); 2924 2925 self.resetLayers(); 2926 2927 self.bindGlobalEvents(); 2928 2929 TegakiCursor.init(self.baseWidth, self.baseHeight); 2930 2931 TegakiUI.updateUndoRedo(0, 0); 2932 TegakiUI.updateZoomLevel(); 2933 }, 2934 2935 initFromReplay: function() { 2936 var self, r; 2937 2938 self = Tegaki; 2939 r = self.replayViewer; 2940 2941 self.initToolsFromReplay(); 2942 2943 self.baseWidth = r.canvasWidth; 2944 self.baseHeight = r.canvasHeight; 2945 self.bgColor = $T.RgbToHex(...r.bgColor); 2946 2947 self.toolColor = $T.RgbToHex(...r.toolColor); 2948 }, 2949 2950 initToolsFromReplay: function() { 2951 var self, r, name, tool, rTool, prop, props; 2952 2953 self = Tegaki; 2954 r = self.replayViewer; 2955 2956 for (name in self.tools) { 2957 tool = self.tools[name]; 2958 2959 if (tool.id === r.toolId) { 2960 self.defaultTool = name; 2961 } 2962 2963 rTool = r.toolMap[tool.id]; 2964 2965 props = ['step', 'size', 'alpha', 'flow', 'tipId']; 2966 2967 for (prop of props) { 2968 if (rTool[prop] !== undefined) { 2969 tool[prop] = rTool[prop]; 2970 } 2971 } 2972 2973 props = [ 2974 'sizeDynamicsEnabled', 'alphaDynamicsEnabled', 'flowDynamicsEnabled', 2975 'usePreserveAlpha' 2976 ]; 2977 2978 for (prop of props) { 2979 if (rTool[prop] !== undefined) { 2980 tool[prop] = !!rTool[prop]; 2981 } 2982 } 2983 } 2984 }, 2985 2986 resetLayers: function() { 2987 var i, len; 2988 2989 if (Tegaki.layers.length) { 2990 for (i = 0, len = Tegaki.layers.length; i < len; ++i) { 2991 Tegaki.layersCnt.removeChild(Tegaki.layers[i].canvas); 2992 } 2993 2994 Tegaki.layers = []; 2995 Tegaki.layerCounter = 0; 2996 2997 TegakiUI.updateLayersGridClear(); 2998 } 2999 3000 TegakiLayers.addLayer(); 3001 TegakiLayers.setActiveLayer(0); 3002 }, 3003 3004 createCanvas: function() { 3005 var canvas, self = Tegaki; 3006 3007 canvas = $T.el('canvas'); 3008 canvas.id = 'tegaki-canvas'; 3009 canvas.width = self.baseWidth; 3010 canvas.height = self.baseHeight; 3011 3012 self.canvas = canvas; 3013 3014 self.ctx = canvas.getContext('2d'); 3015 self.ctx.fillStyle = self.bgColor; 3016 self.ctx.fillRect(0, 0, self.baseWidth, self.baseHeight); 3017 3018 self.layersCnt.appendChild(canvas); 3019 }, 3020 3021 createTools: function() { 3022 var klass, tool; 3023 3024 for (klass of Tegaki.toolList) { 3025 tool = new klass(); 3026 Tegaki.tools[tool.name] = tool; 3027 } 3028 }, 3029 3030 bindGlobalEvents: function() { 3031 var self = Tegaki; 3032 3033 if (!self.replayMode) { 3034 $T.on(self.canvasCnt, 'pointermove', self.onPointerMove); 3035 $T.on(self.canvasCnt, 'pointerdown', self.onPointerDown); 3036 $T.on(document, 'pointerup', self.onPointerUp); 3037 $T.on(document, 'pointercancel', self.onPointerUp); 3038 3039 $T.on(document, 'keydown', TegakiKeybinds.resolve); 3040 3041 $T.on(window, 'beforeunload', Tegaki.onTabClose); 3042 } 3043 else { 3044 $T.on(document, 'visibilitychange', Tegaki.onVisibilityChange); 3045 } 3046 3047 $T.on(self.bg, 'contextmenu', self.onDummy); 3048 $T.on(window, 'resize', self.updatePosOffset); 3049 $T.on(window, 'scroll', self.updatePosOffset); 3050 }, 3051 3052 unBindGlobalEvents: function() { 3053 var self = Tegaki; 3054 3055 if (!self.replayMode) { 3056 $T.off(self.canvasCnt, 'pointermove', self.onPointerMove); 3057 $T.off(self.canvasCnt, 'pointerdown', self.onPointerDown); 3058 $T.off(document, 'pointerup', self.onPointerUp); 3059 $T.off(document, 'pointercancel', self.onPointerUp); 3060 3061 $T.off(document, 'keydown', TegakiKeybinds.resolve); 3062 3063 $T.off(window, 'beforeunload', Tegaki.onTabClose); 3064 } 3065 else { 3066 $T.off(document, 'visibilitychange', Tegaki.onVisibilityChange); 3067 } 3068 3069 $T.off(self.bg, 'contextmenu', self.onDummy); 3070 $T.off(window, 'resize', self.updatePosOffset); 3071 $T.off(window, 'scroll', self.updatePosOffset); 3072 }, 3073 3074 createBuffers() { 3075 Tegaki.ghostBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); 3076 Tegaki.blendBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); 3077 Tegaki.ghostBuffer32 = new Uint32Array(Tegaki.ghostBuffer.data.buffer); 3078 Tegaki.blendBuffer32 = new Uint32Array(Tegaki.blendBuffer.data.buffer); 3079 }, 3080 3081 clearBuffers() { 3082 Tegaki.ghostBuffer32.fill(0); 3083 Tegaki.blendBuffer32.fill(0); 3084 }, 3085 3086 destroyBuffers() { 3087 Tegaki.ghostBuffer = null; 3088 Tegaki.blendBuffer = null; 3089 Tegaki.ghostBuffer32 = null; 3090 Tegaki.blendBuffer32 = null; 3091 }, 3092 3093 disableSmoothing: function(ctx) { 3094 ctx.mozImageSmoothingEnabled = false; 3095 ctx.webkitImageSmoothingEnabled = false; 3096 ctx.msImageSmoothingEnabled = false; 3097 ctx.imageSmoothingEnabled = false; 3098 }, 3099 3100 centerLayersCnt: function() { 3101 var style = Tegaki.layersCnt.style; 3102 3103 style.width = Tegaki.baseWidth + 'px'; 3104 style.height = Tegaki.baseHeight + 'px'; 3105 }, 3106 3107 onTabClose: function(e) { 3108 e.preventDefault(); 3109 e.returnValue = ''; 3110 }, 3111 3112 onVisibilityChange: function(e) { 3113 if (!Tegaki.replayMode) { 3114 return; 3115 } 3116 3117 if (document.visibilityState === 'visible') { 3118 if (Tegaki.replayViewer.autoPaused) { 3119 Tegaki.replayViewer.play(); 3120 } 3121 } 3122 else { 3123 if (Tegaki.replayViewer.playing) { 3124 Tegaki.replayViewer.autoPause(); 3125 } 3126 } 3127 }, 3128 3129 initKeybinds: function() { 3130 var cls, tool; 3131 3132 if (Tegaki.replayMode) { 3133 return; 3134 } 3135 3136 TegakiKeybinds.bind('ctrl+z', TegakiHistory, 'undo', 'undo', 'Ctrl+Z'); 3137 TegakiKeybinds.bind('ctrl+y', TegakiHistory, 'redo', 'redo', 'Ctrl+Y'); 3138 3139 TegakiKeybinds.bind('+', Tegaki, 'setToolSizeUp', 'toolSize', 'Numpad +/-'); 3140 TegakiKeybinds.bind('-', Tegaki, 'setToolSizeDown'); 3141 3142 for (tool in Tegaki.tools) { 3143 cls = Tegaki.tools[tool]; 3144 3145 if (cls.keybind) { 3146 TegakiKeybinds.bind(cls.keybind, cls, 'set'); 3147 } 3148 } 3149 }, 3150 3151 getCursorPos: function(e, axis) { 3152 if (axis === 0) { 3153 return 0 | (( 3154 e.clientX 3155 + window.pageXOffset 3156 + Tegaki.canvasCnt.scrollLeft 3157 - Tegaki.offsetX 3158 ) / Tegaki.zoomFactor); 3159 } 3160 else { 3161 return 0 | (( 3162 e.clientY 3163 + window.pageYOffset 3164 + Tegaki.canvasCnt.scrollTop 3165 - Tegaki.offsetY 3166 ) / Tegaki.zoomFactor); 3167 } 3168 }, 3169 3170 resume: function() { 3171 if (Tegaki.saveReplay) { 3172 Tegaki.replayRecorder.start(); 3173 } 3174 3175 Tegaki.bg.classList.remove('tegaki-hidden'); 3176 document.body.classList.add('tegaki-backdrop'); 3177 Tegaki.setZoom(0); 3178 Tegaki.centerLayersCnt(); 3179 Tegaki.updatePosOffset(); 3180 Tegaki.bindGlobalEvents(); 3181 }, 3182 3183 hide: function() { 3184 if (Tegaki.saveReplay) { 3185 Tegaki.replayRecorder.stop(); 3186 } 3187 3188 Tegaki.bg.classList.add('tegaki-hidden'); 3189 document.body.classList.remove('tegaki-backdrop'); 3190 Tegaki.unBindGlobalEvents(); 3191 }, 3192 3193 destroy: function() { 3194 Tegaki.unBindGlobalEvents(); 3195 3196 TegakiKeybinds.clear(); 3197 3198 TegakiHistory.clear(); 3199 3200 Tegaki.bg.parentNode.removeChild(Tegaki.bg); 3201 3202 document.body.classList.remove('tegaki-backdrop'); 3203 3204 Tegaki.startTimeStamp = 0; 3205 3206 Tegaki.bg = null; 3207 Tegaki.canvasCnt = null; 3208 Tegaki.layersCnt = null; 3209 Tegaki.canvas = null; 3210 Tegaki.ctx = null; 3211 Tegaki.layers = []; 3212 Tegaki.layerCounter = 0; 3213 Tegaki.zoomLevel = 0; 3214 Tegaki.zoomFactor = 1.0; 3215 Tegaki.activeLayer = null; 3216 3217 Tegaki.tool = null; 3218 3219 TegakiCursor.destroy(); 3220 3221 Tegaki.replayRecorder = null; 3222 Tegaki.replayViewer = null; 3223 3224 Tegaki.destroyBuffers(); 3225 }, 3226 3227 flatten: function(ctx) { 3228 var i, layer, canvas, len; 3229 3230 if (!ctx) { 3231 canvas = $T.el('canvas'); 3232 ctx = canvas.getContext('2d'); 3233 } 3234 else { 3235 canvas = ctx.canvas; 3236 } 3237 3238 canvas.width = Tegaki.canvas.width; 3239 canvas.height = Tegaki.canvas.height; 3240 3241 ctx.drawImage(Tegaki.canvas, 0, 0); 3242 3243 for (i = 0, len = Tegaki.layers.length; i < len; ++i) { 3244 layer = Tegaki.layers[i]; 3245 3246 if (!layer.visible) { 3247 continue; 3248 } 3249 3250 ctx.globalAlpha = layer.alpha; 3251 ctx.drawImage(layer.canvas, 0, 0); 3252 } 3253 3254 return canvas; 3255 }, 3256 3257 onReplayLoaded: function() { 3258 TegakiUI.clearMsg(); 3259 Tegaki.initFromReplay(); 3260 Tegaki.init(); 3261 Tegaki.setTool(Tegaki.defaultTool); 3262 TegakiUI.updateReplayControls(); 3263 TegakiUI.updateReplayTime(true); 3264 TegakiUI.enableReplayControls(true); 3265 Tegaki.replayViewer.play(); 3266 }, 3267 3268 onReplayGaplessClick: function() { 3269 Tegaki.replayViewer.toggleGapless(); 3270 TegakiUI.updateReplayGapless(); 3271 }, 3272 3273 onReplayPlayPauseClick: function() { 3274 Tegaki.replayViewer.togglePlayPause(); 3275 }, 3276 3277 onReplayRewindClick: function() { 3278 Tegaki.replayViewer.rewind(); 3279 }, 3280 3281 onReplaySlowDownClick: function() { 3282 Tegaki.replayViewer.slowDown(); 3283 TegakiUI.updateReplaySpeed(); 3284 }, 3285 3286 onReplaySpeedUpClick: function() { 3287 Tegaki.replayViewer.speedUp(); 3288 TegakiUI.updateReplaySpeed(); 3289 }, 3290 3291 onReplayTimeChanged: function() { 3292 TegakiUI.updateReplayTime(); 3293 }, 3294 3295 onReplayPlayPauseChanged: function() { 3296 TegakiUI.updateReplayPlayPause(); 3297 }, 3298 3299 onReplayReset: function() { 3300 Tegaki.initFromReplay(); 3301 Tegaki.setTool(Tegaki.defaultTool); 3302 Tegaki.resizeCanvas(Tegaki.baseWidth, Tegaki.baseHeight); 3303 TegakiUI.updateReplayControls(); 3304 TegakiUI.updateReplayTime(); 3305 }, 3306 3307 onMainColorClick: function(e) { 3308 var el; 3309 e.preventDefault(); 3310 el = $T.id('tegaki-colorpicker'); 3311 el.click(); 3312 }, 3313 3314 onPaletteColorClick: function(e) { 3315 if (e.button === 2) { 3316 this.style.backgroundColor = Tegaki.toolColor; 3317 this.setAttribute('data-color', Tegaki.toolColor); 3318 } 3319 else if (e.button === 0) { 3320 Tegaki.setToolColor(this.getAttribute('data-color')); 3321 } 3322 }, 3323 3324 onColorPicked: function(e) { 3325 $T.id('tegaki-color').style.backgroundColor = this.value; 3326 Tegaki.setToolColor(this.value); 3327 }, 3328 3329 onSwitchPaletteClick: function(e) { 3330 var id; 3331 3332 if (e.target.hasAttribute('data-prev')) { 3333 id = Tegaki.colorPaletteId - 1; 3334 } 3335 else { 3336 id = Tegaki.colorPaletteId + 1; 3337 } 3338 3339 Tegaki.setColorPalette(id); 3340 }, 3341 3342 setColorPalette: function(id) { 3343 if (id < 0 || id >= TegakiColorPalettes.length) { 3344 return; 3345 } 3346 3347 Tegaki.colorPaletteId = id; 3348 TegakiUI.updateColorPalette(); 3349 }, 3350 3351 setToolSizeUp: function() { 3352 Tegaki.setToolSize(Tegaki.tool.size + 1); 3353 }, 3354 3355 setToolSizeDown: function() { 3356 Tegaki.setToolSize(Tegaki.tool.size - 1); 3357 }, 3358 3359 setToolSize: function(size) { 3360 if (size > 0 && size <= Tegaki.maxSize) { 3361 Tegaki.tool.setSize(size); 3362 Tegaki.updateCursorStatus(); 3363 Tegaki.recordEvent(TegakiEventSetToolSize, performance.now(), size); 3364 TegakiUI.updateToolSize(); 3365 } 3366 }, 3367 3368 setToolAlpha: function(alpha) { 3369 alpha = Math.fround(alpha); 3370 3371 if (alpha >= 0.0 && alpha <= 1.0) { 3372 Tegaki.tool.setAlpha(alpha); 3373 Tegaki.recordEvent(TegakiEventSetToolAlpha, performance.now(), alpha); 3374 TegakiUI.updateToolAlpha(); 3375 } 3376 }, 3377 3378 setToolFlow: function(flow) { 3379 flow = Math.fround(flow); 3380 3381 if (flow >= 0.0 && flow <= 1.0) { 3382 Tegaki.tool.setFlow(flow); 3383 Tegaki.recordEvent(TegakiEventSetToolFlow, performance.now(), flow); 3384 TegakiUI.updateToolFlow(); 3385 } 3386 }, 3387 3388 setToolColor: function(color) { 3389 Tegaki.toolColor = color; 3390 $T.id('tegaki-color').style.backgroundColor = color; 3391 $T.id('tegaki-colorpicker').value = color; 3392 Tegaki.tool.setColor(color); 3393 Tegaki.recordEvent(TegakiEventSetColor, performance.now(), Tegaki.tool.rgb); 3394 }, 3395 3396 setToolColorRGB: function(r, g, b) { 3397 Tegaki.setToolColor($T.RgbToHex(r, g, b)); 3398 }, 3399 3400 setTool: function(tool) { 3401 Tegaki.tools[tool].set(); 3402 }, 3403 3404 setToolById: function(id) { 3405 var tool; 3406 3407 for (tool in Tegaki.tools) { 3408 if (Tegaki.tools[tool].id === id) { 3409 Tegaki.setTool(tool); 3410 return; 3411 } 3412 } 3413 }, 3414 3415 setZoom: function(level) { 3416 var idx; 3417 3418 idx = level + Tegaki.zoomBaseLevel; 3419 3420 if (idx >= Tegaki.zoomFactorList.length || idx < 0 || !Tegaki.canvas) { 3421 return; 3422 } 3423 3424 Tegaki.zoomLevel = level; 3425 Tegaki.zoomFactor = Tegaki.zoomFactorList[idx]; 3426 3427 TegakiUI.updateZoomLevel(); 3428 3429 Tegaki.layersCnt.style.width = Math.ceil(Tegaki.baseWidth * Tegaki.zoomFactor) + 'px'; 3430 Tegaki.layersCnt.style.height = Math.ceil(Tegaki.baseHeight * Tegaki.zoomFactor) + 'px'; 3431 3432 if (level < 0) { 3433 Tegaki.layersCnt.classList.add('tegaki-smooth-layers'); 3434 } 3435 else { 3436 Tegaki.layersCnt.classList.remove('tegaki-smooth-layers'); 3437 } 3438 3439 Tegaki.updatePosOffset(); 3440 }, 3441 3442 onZoomChange: function() { 3443 if (this.hasAttribute('data-in')) { 3444 Tegaki.setZoom(Tegaki.zoomLevel + 1); 3445 } 3446 else { 3447 Tegaki.setZoom(Tegaki.zoomLevel - 1); 3448 } 3449 }, 3450 3451 onNewClick: function() { 3452 var width, height, tmp, self = Tegaki; 3453 3454 width = prompt(TegakiStrings.promptWidth, self.canvas.width); 3455 3456 if (!width) { return; } 3457 3458 height = prompt(TegakiStrings.promptHeight, self.canvas.height); 3459 3460 if (!height) { return; } 3461 3462 width = +width; 3463 height = +height; 3464 3465 if (width < 1 || height < 1) { 3466 TegakiUI.printMsg(TegakiStrings.badDimensions); 3467 return; 3468 } 3469 3470 tmp = {}; 3471 self.copyContextState(self.activeLayer.ctx, tmp); 3472 self.resizeCanvas(width, height); 3473 self.copyContextState(tmp, self.activeLayer.ctx); 3474 3475 self.setZoom(0); 3476 TegakiHistory.clear(); 3477 3478 TegakiUI.updateLayerPreviewSize(); 3479 3480 self.startTimeStamp = Date.now(); 3481 3482 if (self.saveReplay) { 3483 self.createTools(); 3484 self.setTool(self.defaultTool); 3485 self.replayRecorder = new TegakiReplayRecorder(); 3486 self.replayRecorder.start(); 3487 } 3488 }, 3489 3490 onOpenClick: function() { 3491 var el, tainted; 3492 3493 tainted = TegakiHistory.undoStack[0] || TegakiHistory.redoStack[0]; 3494 3495 if (tainted || Tegaki.saveReplay) { 3496 if (!confirm(TegakiStrings.confirmChangeCanvas)) { 3497 return; 3498 } 3499 } 3500 3501 el = $T.id('tegaki-filepicker'); 3502 el.click(); 3503 }, 3504 3505 loadReplayFromFile: function() { 3506 Tegaki.replayViewer.debugLoadLocal(); 3507 }, 3508 3509 loadReplayFromURL: function(url) { 3510 TegakiUI.printMsg(TegakiStrings.loadingReplay, 0); 3511 Tegaki.replayViewer.loadFromURL(url); 3512 }, 3513 3514 onExportClick: function() { 3515 Tegaki.flatten().toBlob(function(b) { 3516 var el = $T.el('a'); 3517 el.className = 'tegaki-hidden'; 3518 el.download = $T.generateFilename() + '.png'; 3519 el.href = URL.createObjectURL(b); 3520 Tegaki.bg.appendChild(el); 3521 el.click(); 3522 Tegaki.bg.removeChild(el); 3523 }, 'image/png'); 3524 }, 3525 3526 onUndoClick: function() { 3527 TegakiHistory.undo(); 3528 }, 3529 3530 onRedoClick: function() { 3531 TegakiHistory.redo(); 3532 }, 3533 3534 onHistoryChange: function(undoSize, redoSize, type = 0) { 3535 TegakiUI.updateUndoRedo(undoSize, redoSize); 3536 3537 if (type === -1) { 3538 Tegaki.recordEvent(TegakiEventUndo, performance.now()); 3539 } 3540 else if (type === 1) { 3541 Tegaki.recordEvent(TegakiEventRedo, performance.now()); 3542 } 3543 }, 3544 3545 onDoneClick: function() { 3546 Tegaki.hide(); 3547 Tegaki.onDoneCb(); 3548 }, 3549 3550 onCancelClick: function() { 3551 if (!confirm(TegakiStrings.confirmCancel)) { 3552 return; 3553 } 3554 3555 Tegaki.destroy(); 3556 Tegaki.onCancelCb(); 3557 }, 3558 3559 onCloseViewerClick: function() { 3560 Tegaki.replayViewer.destroy(); 3561 Tegaki.destroy(); 3562 }, 3563 3564 onToolSizeChange: function() { 3565 var val = +this.value; 3566 3567 if (val < 1) { 3568 val = 1; 3569 } 3570 else if (val > Tegaki.maxSize) { 3571 val = Tegaki.maxSize; 3572 } 3573 3574 Tegaki.setToolSize(val); 3575 }, 3576 3577 onToolAlphaChange: function(e) { 3578 var val = +this.value; 3579 3580 val = val / 100; 3581 3582 if (val < 0.0) { 3583 val = 0.0; 3584 } 3585 else if (val > 1.0) { 3586 val = 1.0; 3587 } 3588 3589 Tegaki.setToolAlpha(val); 3590 }, 3591 3592 onToolFlowChange: function(e) { 3593 var val = +this.value; 3594 3595 val = val / 100; 3596 3597 if (val < 0.0) { 3598 val = 0.0; 3599 } 3600 else if (val > 1.0) { 3601 val = 1.0; 3602 } 3603 3604 Tegaki.setToolFlow(val); 3605 }, 3606 3607 onToolPressureSizeClick: function(e) { 3608 if (!Tegaki.tool.useSizeDynamics) { 3609 return; 3610 } 3611 3612 Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled); 3613 }, 3614 3615 setToolSizeDynamics: function(flag) { 3616 Tegaki.tool.setSizeDynamics(flag); 3617 TegakiUI.updateToolDynamics(); 3618 Tegaki.recordEvent(TegakiEventSetToolSizeDynamics, performance.now(), +flag); 3619 }, 3620 3621 onToolPressureAlphaClick: function(e) { 3622 if (!Tegaki.tool.useAlphaDynamics) { 3623 return; 3624 } 3625 3626 Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled); 3627 }, 3628 3629 setToolAlphaDynamics: function(flag) { 3630 Tegaki.tool.setAlphaDynamics(flag); 3631 TegakiUI.updateToolDynamics(); 3632 Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics, performance.now(), +flag); 3633 }, 3634 3635 onToolPressureFlowClick: function(e) { 3636 if (!Tegaki.tool.useFlowDynamics) { 3637 return; 3638 } 3639 3640 Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled); 3641 }, 3642 3643 setToolFlowDynamics: function(flag) { 3644 Tegaki.tool.setFlowDynamics(flag); 3645 TegakiUI.updateToolDynamics(); 3646 Tegaki.recordEvent(TegakiEventSetToolFlowDynamics, performance.now(), +flag); 3647 }, 3648 3649 onToolPreserveAlphaClick: function(e) { 3650 if (!Tegaki.tool.usePreserveAlpha) { 3651 return; 3652 } 3653 3654 Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled); 3655 }, 3656 3657 setToolPreserveAlpha: function(flag) { 3658 Tegaki.tool.setPreserveAlpha(flag); 3659 TegakiUI.updateToolPreserveAlpha(); 3660 Tegaki.recordEvent(TegakiEventPreserveAlpha, performance.now(), +flag); 3661 }, 3662 3663 onToolTipClick: function(e) { 3664 var tipId = +e.target.getAttribute('data-id'); 3665 3666 if (tipId !== Tegaki.tool.tipId) { 3667 Tegaki.setToolTip(tipId); 3668 } 3669 }, 3670 3671 setToolTip: function(id) { 3672 Tegaki.tool.setTip(id); 3673 TegakiUI.updateToolShape(); 3674 Tegaki.recordEvent(TegakiEventSetToolTip, performance.now(), id); 3675 }, 3676 3677 onLayerSelectorClick: function(e) { 3678 var id = +this.getAttribute('data-id'); 3679 3680 if (!id || e.target.classList.contains('tegaki-ui-cb')) { 3681 return; 3682 } 3683 3684 if (e.ctrlKey) { 3685 Tegaki.toggleSelectedLayer(id); 3686 } 3687 else { 3688 Tegaki.setActiveLayer(id); 3689 } 3690 }, 3691 3692 toggleSelectedLayer: function(id) { 3693 TegakiLayers.selectedLayersToggle(id); 3694 Tegaki.recordEvent(TegakiEventToggleLayerSelection, performance.now(), id); 3695 }, 3696 3697 setActiveLayer: function(id) { 3698 TegakiLayers.setActiveLayer(id); 3699 Tegaki.recordEvent(TegakiEventSetActiveLayer, performance.now(), id); 3700 }, 3701 3702 onLayerAlphaDragStart: function(e) { 3703 TegakiUI.setupDragLabel(e, Tegaki.onLayerAlphaDragMove); 3704 }, 3705 3706 onLayerAlphaDragMove: function(delta) { 3707 var val; 3708 3709 if (!delta) { 3710 return; 3711 } 3712 3713 val = Tegaki.activeLayer.alpha + delta / 100 ; 3714 3715 if (val < 0.0) { 3716 val = 0.0; 3717 } 3718 else if (val > 1.0) { 3719 val = 1.0; 3720 } 3721 3722 Tegaki.setSelectedLayersAlpha(val); 3723 }, 3724 3725 onLayerAlphaChange: function() { 3726 var val = +this.value; 3727 3728 val = val / 100; 3729 3730 if (val < 0.0) { 3731 val = 0.0; 3732 } 3733 else if (val > 1.0) { 3734 val = 1.0; 3735 } 3736 3737 Tegaki.setSelectedLayersAlpha(val); 3738 }, 3739 3740 setSelectedLayersAlpha: function(alpha) { 3741 var layer, id, layerAlphas; 3742 3743 alpha = Math.fround(alpha); 3744 3745 if (alpha >= 0.0 && alpha <= 1.0 && Tegaki.selectedLayers.size > 0) { 3746 layerAlphas = []; 3747 3748 for (id of Tegaki.selectedLayers) { 3749 if (layer = TegakiLayers.getLayerById(id)) { 3750 layerAlphas.push([layer.id, layer.alpha]); 3751 TegakiLayers.setLayerAlpha(layer, alpha); 3752 } 3753 } 3754 3755 TegakiUI.updateLayerAlphaOpt(); 3756 3757 TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(layerAlphas, alpha)); 3758 3759 Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha, performance.now(), alpha); 3760 } 3761 }, 3762 3763 onLayerNameChangeClick: function(e) { 3764 var id, name, layer; 3765 3766 id = +this.getAttribute('data-id'); 3767 3768 layer = TegakiLayers.getLayerById(id); 3769 3770 if (!layer) { 3771 return; 3772 } 3773 3774 if (name = prompt(undefined, layer.name)) { 3775 Tegaki.setLayerName(id, name); 3776 } 3777 }, 3778 3779 setLayerName: function(id, name) { 3780 var oldName, layer; 3781 3782 name = name.trim().slice(0, 25); 3783 3784 layer = TegakiLayers.getLayerById(id); 3785 3786 if (!layer || !name || name === layer.name) { 3787 return; 3788 } 3789 3790 oldName = layer.name; 3791 3792 layer.name = name; 3793 3794 TegakiUI.updateLayerName(layer); 3795 3796 TegakiHistory.push(new TegakiHistoryActions.SetLayerName(id, oldName, name)); 3797 3798 Tegaki.recordEvent(TegakiEventHistoryDummy, performance.now()); 3799 }, 3800 3801 onLayerAddClick: function() { 3802 Tegaki.addLayer(); 3803 }, 3804 3805 addLayer: function() { 3806 var action; 3807 3808 if (Tegaki.layers.length >= Tegaki.maxLayers) { 3809 TegakiUI.printMsg(TegakiStrings.tooManyLayers); 3810 return; 3811 } 3812 3813 TegakiHistory.push(action = TegakiLayers.addLayer()); 3814 TegakiLayers.setActiveLayer(action.aLayerIdAfter); 3815 Tegaki.recordEvent(TegakiEventAddLayer, performance.now()); 3816 }, 3817 3818 onLayerDeleteClick: function() { 3819 Tegaki.deleteSelectedLayers(); 3820 }, 3821 3822 deleteSelectedLayers: function() { 3823 var action, layerSet; 3824 3825 layerSet = Tegaki.selectedLayers; 3826 3827 if (layerSet.size === Tegaki.layers.length) { 3828 return; 3829 } 3830 3831 if (!layerSet.size || Tegaki.layers.length < 2) { 3832 return; 3833 } 3834 3835 TegakiHistory.push(action = TegakiLayers.deleteLayers(layerSet)); 3836 TegakiLayers.selectedLayersClear(); 3837 TegakiLayers.setActiveLayer(action.aLayerIdAfter); 3838 Tegaki.recordEvent(TegakiEventDeleteLayers, performance.now()); 3839 }, 3840 3841 onLayerToggleVisibilityClick: function() { 3842 Tegaki.toggleLayerVisibility(+this.getAttribute('data-id')); 3843 }, 3844 3845 toggleLayerVisibility: function(id) { 3846 var layer = TegakiLayers.getLayerById(id); 3847 TegakiLayers.setLayerVisibility(layer, !layer.visible); 3848 Tegaki.recordEvent(TegakiEventToggleLayerVisibility, performance.now(), id); 3849 }, 3850 3851 onMergeLayersClick: function() { 3852 Tegaki.mergeSelectedLayers(); 3853 }, 3854 3855 mergeSelectedLayers: function() { 3856 var action; 3857 3858 if (Tegaki.selectedLayers.size) { 3859 if (action = TegakiLayers.mergeLayers(Tegaki.selectedLayers)) { 3860 TegakiHistory.push(action); 3861 TegakiLayers.setActiveLayer(action.aLayerIdAfter); 3862 Tegaki.recordEvent(TegakiEventMergeLayers, performance.now()); 3863 } 3864 } 3865 }, 3866 3867 onMoveLayerClick: function(e) { 3868 var belowPos, up; 3869 3870 if (!Tegaki.selectedLayers.size) { 3871 return; 3872 } 3873 3874 up = e.target.hasAttribute('data-up'); 3875 3876 belowPos = TegakiLayers.getSelectedEdgeLayerPos(up); 3877 3878 if (belowPos < 0) { 3879 return; 3880 } 3881 3882 if (up) { 3883 belowPos += 2; 3884 } 3885 else if (belowPos >= 1) { 3886 belowPos--; 3887 } 3888 3889 Tegaki.moveSelectedLayers(belowPos); 3890 }, 3891 3892 moveSelectedLayers: function(belowPos) { 3893 TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers, belowPos)); 3894 Tegaki.recordEvent(TegakiEventMoveLayers, performance.now(), belowPos); 3895 }, 3896 3897 onToolClick: function() { 3898 Tegaki.setTool(this.getAttribute('data-tool')); 3899 }, 3900 3901 onToolChanged: function(tool) { 3902 var el; 3903 3904 Tegaki.tool = tool; 3905 3906 if (el = $T.cls('tegaki-tool-active')[0]) { 3907 el.classList.remove('tegaki-tool-active'); 3908 } 3909 3910 $T.id('tegaki-tool-btn-' + tool.name).classList.add('tegaki-tool-active'); 3911 3912 Tegaki.recordEvent(TegakiEventSetTool, performance.now(), Tegaki.tool.id); 3913 3914 TegakiUI.onToolChanged(); 3915 Tegaki.updateCursorStatus(); 3916 }, 3917 3918 onLayerStackChanged: function() { 3919 TegakiCursor.invalidateCache(); 3920 }, 3921 3922 onOpenFileSelected: function() { 3923 var img; 3924 3925 if (this.files && this.files[0]) { 3926 img = new Image(); 3927 img.onload = Tegaki.onOpenImageLoaded; 3928 img.onerror = Tegaki.onOpenImageError; 3929 3930 img.src = URL.createObjectURL(this.files[0]); 3931 } 3932 }, 3933 3934 onOpenImageLoaded: function() { 3935 var tmp = {}, self = Tegaki; 3936 3937 self.hasCustomCanvas = true; 3938 3939 self.copyContextState(self.activeLayer.ctx, tmp); 3940 self.resizeCanvas(this.naturalWidth, this.naturalHeight); 3941 self.activeLayer.ctx.drawImage(this, 0, 0); 3942 TegakiLayers.syncLayerImageData(self.activeLayer); 3943 self.copyContextState(tmp, self.activeLayer.ctx); 3944 3945 self.setZoom(0); 3946 3947 TegakiHistory.clear(); 3948 3949 TegakiUI.updateLayerPreviewSize(true); 3950 3951 self.startTimeStamp = Date.now(); 3952 3953 if (self.saveReplay) { 3954 self.replayRecorder.stop(); 3955 self.replayRecorder = null; 3956 self.saveReplay = false; 3957 TegakiUI.setRecordingStatus(false); 3958 } 3959 }, 3960 3961 onOpenImageError: function() { 3962 TegakiUI.printMsg(TegakiStrings.errorLoadImage); 3963 }, 3964 3965 resizeCanvas: function(width, height) { 3966 Tegaki.baseWidth = width; 3967 Tegaki.baseHeight = height; 3968 3969 Tegaki.createBuffers(); 3970 3971 Tegaki.canvas.width = width; 3972 Tegaki.canvas.height = height; 3973 3974 TegakiCursor.updateCanvasSize(width, height); 3975 3976 Tegaki.ctx.fillStyle = Tegaki.bgColor; 3977 Tegaki.ctx.fillRect(0, 0, width, height); 3978 3979 Tegaki.activeLayer = null; 3980 3981 Tegaki.resetLayers(); 3982 3983 Tegaki.centerLayersCnt(); 3984 Tegaki.updatePosOffset(); 3985 }, 3986 3987 copyContextState: function(src, dest) { 3988 var i, p, props = [ 3989 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha', 3990 'lineWidth', 'globalCompositeOperation' 3991 ]; 3992 3993 for (i = 0; p = props[i]; ++i) { 3994 dest[p] = src[p]; 3995 } 3996 }, 3997 3998 updateCursorStatus: function() { 3999 if (!Tegaki.tool.noCursor && Tegaki.tool.size > 1) { 4000 Tegaki.cursor = true; 4001 TegakiCursor.generate(Tegaki.tool.size); 4002 } 4003 else { 4004 Tegaki.cursor = false; 4005 $T.clearCtx(TegakiCursor.cursorCtx); 4006 } 4007 }, 4008 4009 updatePosOffset: function() { 4010 var aabb = Tegaki.canvas.getBoundingClientRect(); 4011 4012 Tegaki.offsetX = aabb.left + window.pageXOffset 4013 + Tegaki.canvasCnt.scrollLeft + Tegaki.layersCnt.scrollLeft; 4014 Tegaki.offsetY = aabb.top + window.pageYOffset 4015 + Tegaki.canvasCnt.scrollTop + Tegaki.layersCnt.scrollTop; 4016 }, 4017 4018 isScrollbarClick: function(e) { 4019 var sbwh, scbv; 4020 4021 sbwh = Tegaki.canvasCnt.offsetWidth - Tegaki.canvasCnt.clientWidth; 4022 scbv = Tegaki.canvasCnt.offsetHeight - Tegaki.canvasCnt.clientHeight; 4023 4024 if (sbwh > 0 4025 && e.clientX >= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth 4026 && e.clientX <= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth 4027 + sbwh) { 4028 return true; 4029 } 4030 4031 if (scbv > 0 4032 && e.clientY >= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight 4033 && e.clientY <= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight 4034 + sbwh) { 4035 return true; 4036 } 4037 4038 return false; 4039 }, 4040 4041 onPointerMove: function(e) { 4042 var events, x, y, tool, ts, p; 4043 4044 if (e.mozInputSource !== undefined) { 4045 // Firefox thing where mouse events fire for no reason when the pointer is a pen 4046 if (Tegaki.activePointerIsPen && e.pointerType === 'mouse') { 4047 return; 4048 } 4049 } 4050 else { 4051 // Webkit thing where a pointermove event is fired at pointerdown location after a pointerup 4052 if (Tegaki.activePointerId !== e.pointerId) { 4053 Tegaki.activePointerId = e.pointerId; 4054 return; 4055 } 4056 } 4057 4058 if (Tegaki.isPainting) { 4059 tool = Tegaki.tool; 4060 4061 if (Tegaki.activePointerIsPen && e.getCoalescedEvents) { 4062 events = e.getCoalescedEvents(); 4063 4064 ts = e.timeStamp; 4065 4066 for (e of events) { 4067 x = Tegaki.getCursorPos(e, 0); 4068 y = Tegaki.getCursorPos(e, 1); 4069 4070 if (!tool.enabledDynamics()) { 4071 Tegaki.recordEvent(TegakiEventDrawNoP, ts, x, y); 4072 } 4073 else { 4074 p = TegakiPressure.toShort(e.pressure); 4075 TegakiPressure.push(p); 4076 Tegaki.recordEvent(TegakiEventDraw, ts, x, y, p); 4077 } 4078 4079 tool.draw(x, y); 4080 } 4081 } 4082 else { 4083 x = Tegaki.getCursorPos(e, 0); 4084 y = Tegaki.getCursorPos(e, 1); 4085 p = TegakiPressure.toShort(e.pressure); 4086 Tegaki.recordEvent(TegakiEventDraw, e.timeStamp, x, y, p); 4087 TegakiPressure.push(p); 4088 tool.draw(x, y); 4089 } 4090 } 4091 else { 4092 x = Tegaki.getCursorPos(e, 0); 4093 y = Tegaki.getCursorPos(e, 1); 4094 } 4095 4096 if (Tegaki.cursor) { 4097 TegakiCursor.render(x, y); 4098 } 4099 }, 4100 4101 onPointerDown: function(e) { 4102 var x, y, tool, p; 4103 4104 if (Tegaki.isScrollbarClick(e)) { 4105 return; 4106 } 4107 4108 Tegaki.activePointerId = e.pointerId; 4109 4110 Tegaki.activePointerIsPen = e.pointerType === 'pen'; 4111 4112 if (Tegaki.activeLayer === null) { 4113 if (e.target.parentNode === Tegaki.layersCnt) { 4114 TegakiUI.printMsg(TegakiStrings.noActiveLayer); 4115 } 4116 4117 return; 4118 } 4119 4120 if (!TegakiLayers.getActiveLayer().visible) { 4121 if (e.target.parentNode === Tegaki.layersCnt) { 4122 TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer); 4123 } 4124 4125 return; 4126 } 4127 4128 x = Tegaki.getCursorPos(e, 0); 4129 y = Tegaki.getCursorPos(e, 1); 4130 4131 if (e.button === 2 || e.altKey) { 4132 e.preventDefault(); 4133 4134 Tegaki.isPainting = false; 4135 4136 Tegaki.tools.pipette.draw(x, y); 4137 } 4138 else if (e.button === 0) { 4139 e.preventDefault(); 4140 4141 tool = Tegaki.tool; 4142 4143 if (!tool.enabledDynamics()) { 4144 Tegaki.recordEvent(TegakiEventDrawStartNoP, e.timeStamp, x, y); 4145 } 4146 else { 4147 p = TegakiPressure.toShort(e.pressure); 4148 TegakiPressure.push(p); 4149 Tegaki.recordEvent(TegakiEventDrawStart, e.timeStamp, x, y, p); 4150 } 4151 4152 Tegaki.isPainting = true; 4153 4154 TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( 4155 Tegaki.activeLayer.id 4156 ); 4157 4158 TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); 4159 4160 tool.start(x, y); 4161 } 4162 4163 if (Tegaki.cursor) { 4164 TegakiCursor.render(x, y); 4165 } 4166 }, 4167 4168 onPointerUp: function(e) { 4169 Tegaki.activePointerId = e.pointerId; 4170 4171 Tegaki.activePointerIsPen = false; 4172 4173 if (Tegaki.isPainting) { 4174 Tegaki.recordEvent(TegakiEventDrawCommit, e.timeStamp); 4175 Tegaki.tool.commit(); 4176 TegakiUI.updateLayerPreview(Tegaki.activeLayer); 4177 TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); 4178 TegakiHistory.push(TegakiHistory.pendingAction); 4179 Tegaki.isPainting = false; 4180 } 4181 }, 4182 4183 onDummy: function(e) { 4184 e.preventDefault(); 4185 e.stopPropagation(); 4186 }, 4187 4188 recordEvent(klass, ...args) { 4189 if (Tegaki.replayRecorder) { 4190 Tegaki.replayRecorder.push(new klass(...args)); 4191 } 4192 } 4193 }; 4194 var TegakiColorPalettes = [ 4195 [ 4196 '#ffffff', '#000000', '#888888', '#b47575', '#c096c0', 4197 '#fa9696', '#8080ff', '#ffb6ff', '#e7e58d', '#25c7c9', 4198 '#99cb7b', '#e7962d', '#f9ddcf', '#fcece2' 4199 ], 4200 4201 [ 4202 '#000000', '#ffffff', '#7f7f7f', '#c3c3c3', '#880015', '#b97a57', '#ed1c24', 4203 '#ffaec9', '#ff7f27', '#ffc90e', '#fff200', '#efe4b0', '#22b14c', '#b5e61d', 4204 '#00a2e8', '#99d9ea', '#3f48cc', '#7092be', '#a349a4', '#c8bfe7' 4205 ], 4206 4207 [ 4208 '#000000', '#ffffff', '#8a8a8a', '#cacaca', '#fcece2', '#f9ddcf', '#e0a899', '#a05b53', 4209 '#7a444a', '#960018', '#c41e3a', '#de4537', '#ff3300', '#ff9800', '#ffc107', 4210 '#ffd700', '#ffeb3b', '#ffffcc', '#f3e5ab', '#cddc39', '#8bc34a', '#4caf50', '#3e8948', 4211 '#355e3b', '#3eb489', '#f0f8ff', '#87ceeb', '#6699cc', '#007fff', '#2d68c4', '#364478', 4212 '#352c4a', '#9c27b0', '#da70d6', '#ff0090', '#fa8072', '#f19cbb', '#c78b95' 4213 ] 4214 ]; 4215 var TegakiPressure = { 4216 pressureNow: 0.0, 4217 pressureThen: 0.0, 4218 4219 toShort: function(pressure) { 4220 return 0 | (pressure * 65535); 4221 }, 4222 4223 get: function() { 4224 return this.pressureNow; 4225 }, 4226 4227 lerp: function(t) { 4228 return this.pressureThen * (1.0 - t) + this.pressureNow * t; 4229 }, 4230 4231 push: function(p) { 4232 this.pressureThen = this.pressureNow; 4233 this.pressureNow = p / 65535; 4234 }, 4235 4236 set: function(p) { 4237 this.pressureThen = this.pressureNow = p / 65535; 4238 } 4239 }; 4240 class TegakiEvent_void { 4241 constructor() { 4242 this.size = 5; 4243 } 4244 4245 pack(w) { 4246 w.writeUint8(this.type); 4247 w.writeUint32(this.timeStamp); 4248 } 4249 4250 static unpack(r) { 4251 return new this(r.readUint32()); 4252 } 4253 } 4254 4255 class TegakiEvent_c { 4256 constructor() { 4257 this.size = 6; 4258 } 4259 4260 pack(w) { 4261 w.writeUint8(this.type); 4262 w.writeUint32(this.timeStamp); 4263 w.writeUint8(this.value); 4264 } 4265 4266 static unpack(r) { 4267 return new this(r.readUint32(), r.readUint8()); 4268 } 4269 } 4270 4271 // --- 4272 4273 class TegakiEventPrelude extends TegakiEvent_void { 4274 constructor(timeStamp) { 4275 super(); 4276 this.timeStamp = timeStamp; 4277 this.type = TegakiEvents[this.constructor.name][0]; 4278 } 4279 4280 dispatch() {} 4281 } 4282 4283 class TegakiEventConclusion extends TegakiEvent_void { 4284 constructor(timeStamp) { 4285 super(); 4286 this.timeStamp = timeStamp; 4287 this.type = TegakiEvents[this.constructor.name][0]; 4288 } 4289 4290 dispatch() {} 4291 } 4292 4293 class TegakiEventHistoryDummy extends TegakiEvent_void { 4294 constructor(timeStamp) { 4295 super(); 4296 this.timeStamp = timeStamp; 4297 this.type = TegakiEvents[this.constructor.name][0]; 4298 } 4299 4300 dispatch() { 4301 TegakiHistory.push(new TegakiHistoryActions.Dummy()); 4302 } 4303 } 4304 4305 class TegakiEventDrawStart { 4306 constructor(timeStamp, x, y, pressure) { 4307 this.timeStamp = timeStamp; 4308 this.x = x; 4309 this.y = y; 4310 this.pressure = pressure; 4311 this.type = TegakiEvents[this.constructor.name][0]; 4312 this.size = 11; 4313 } 4314 4315 pack(w) { 4316 w.writeUint8(this.type); 4317 w.writeUint32(this.timeStamp); 4318 w.writeInt16(this.x); 4319 w.writeInt16(this.y); 4320 w.writeUint16(this.pressure); 4321 } 4322 4323 static unpack(r) { 4324 var timeStamp, x, y, pressure; 4325 4326 timeStamp = r.readUint32(); 4327 x = r.readInt16(); 4328 y = r.readInt16(); 4329 pressure = r.readUint16(); 4330 4331 return new TegakiEventDrawStart(timeStamp, x, y, pressure); 4332 } 4333 4334 dispatch() { 4335 TegakiPressure.set(this.pressure); 4336 4337 TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( 4338 Tegaki.activeLayer.id 4339 ); 4340 4341 TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); 4342 4343 Tegaki.tool.start(this.x, this.y); 4344 } 4345 } 4346 4347 class TegakiEventDrawStartNoP { 4348 constructor(timeStamp, x, y) { 4349 this.timeStamp = timeStamp; 4350 this.x = x; 4351 this.y = y; 4352 this.type = TegakiEvents[this.constructor.name][0]; 4353 this.size = 9; 4354 } 4355 4356 pack(w) { 4357 w.writeUint8(this.type); 4358 w.writeUint32(this.timeStamp); 4359 w.writeInt16(this.x); 4360 w.writeInt16(this.y); 4361 } 4362 4363 static unpack(r) { 4364 var timeStamp, x, y; 4365 4366 timeStamp = r.readUint32(); 4367 x = r.readInt16(); 4368 y = r.readInt16(); 4369 4370 return new TegakiEventDrawStartNoP(timeStamp, x, y); 4371 } 4372 4373 dispatch() { 4374 TegakiPressure.set(0.5); 4375 4376 TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( 4377 Tegaki.activeLayer.id 4378 ); 4379 4380 TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); 4381 4382 Tegaki.tool.start(this.x, this.y); 4383 } 4384 } 4385 4386 class TegakiEventDraw { 4387 constructor(timeStamp, x, y, pressure) { 4388 this.timeStamp = timeStamp; 4389 this.x = x; 4390 this.y = y; 4391 this.pressure = pressure; 4392 this.type = TegakiEvents[this.constructor.name][0]; 4393 this.size = 11; 4394 } 4395 4396 pack(w) { 4397 w.writeUint8(this.type); 4398 w.writeUint32(this.timeStamp); 4399 w.writeInt16(this.x); 4400 w.writeInt16(this.y); 4401 w.writeUint16(this.pressure); 4402 } 4403 4404 static unpack(r) { 4405 var timeStamp, x, y, pressure; 4406 4407 timeStamp = r.readUint32(); 4408 x = r.readInt16(); 4409 y = r.readInt16(); 4410 pressure = r.readUint16(); 4411 4412 return new TegakiEventDraw(timeStamp, x, y, pressure); 4413 } 4414 4415 dispatch() { 4416 TegakiPressure.push(this.pressure); 4417 Tegaki.tool.draw(this.x, this.y); 4418 } 4419 } 4420 4421 class TegakiEventDrawNoP { 4422 constructor(timeStamp, x, y) { 4423 this.timeStamp = timeStamp; 4424 this.x = x; 4425 this.y = y; 4426 this.type = TegakiEvents[this.constructor.name][0]; 4427 this.size = 9; 4428 } 4429 4430 pack(w) { 4431 w.writeUint8(this.type); 4432 w.writeUint32(this.timeStamp); 4433 w.writeInt16(this.x); 4434 w.writeInt16(this.y); 4435 } 4436 4437 static unpack(r) { 4438 var timeStamp, x, y; 4439 4440 timeStamp = r.readUint32(); 4441 x = r.readInt16(); 4442 y = r.readInt16(); 4443 4444 return new TegakiEventDraw(timeStamp, x, y); 4445 } 4446 4447 dispatch() { 4448 TegakiPressure.push(0.5); 4449 Tegaki.tool.draw(this.x, this.y); 4450 } 4451 } 4452 4453 class TegakiEventDrawCommit extends TegakiEvent_void { 4454 constructor(timeStamp) { 4455 super(); 4456 this.timeStamp = timeStamp; 4457 this.type = TegakiEvents[this.constructor.name][0]; 4458 } 4459 4460 dispatch() { 4461 Tegaki.tool.commit(); 4462 TegakiUI.updateLayerPreview(Tegaki.activeLayer); 4463 TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); 4464 TegakiHistory.push(TegakiHistory.pendingAction); 4465 Tegaki.isPainting = false; 4466 } 4467 } 4468 4469 class TegakiEventUndo extends TegakiEvent_void { 4470 constructor(timeStamp) { 4471 super(); 4472 this.timeStamp = timeStamp; 4473 this.type = TegakiEvents[this.constructor.name][0]; 4474 } 4475 4476 dispatch() { 4477 TegakiHistory.undo(); 4478 } 4479 } 4480 4481 class TegakiEventRedo extends TegakiEvent_void { 4482 constructor(timeStamp) { 4483 super(); 4484 this.timeStamp = timeStamp; 4485 this.type = TegakiEvents[this.constructor.name][0]; 4486 } 4487 4488 dispatch() { 4489 TegakiHistory.redo(); 4490 } 4491 } 4492 4493 class TegakiEventSetColor { 4494 constructor(timeStamp, rgb) { 4495 this.timeStamp = timeStamp; 4496 [this.r, this.g, this.b] = rgb; 4497 this.type = TegakiEvents[this.constructor.name][0]; 4498 this.size = 8; 4499 this.coalesce = true; 4500 } 4501 4502 pack(w) { 4503 w.writeUint8(this.type); 4504 w.writeUint32(this.timeStamp); 4505 w.writeUint8(this.r); 4506 w.writeUint8(this.g); 4507 w.writeUint8(this.b); 4508 } 4509 4510 static unpack(r) { 4511 var timeStamp, rgb; 4512 4513 timeStamp = r.readUint32(); 4514 4515 rgb = [r.readUint8(), r.readUint8(), r.readUint8()]; 4516 4517 return new TegakiEventSetColor(timeStamp, rgb); 4518 } 4519 4520 dispatch() { 4521 Tegaki.setToolColorRGB(this.r, this.g, this.b); 4522 } 4523 } 4524 4525 class TegakiEventSetTool extends TegakiEvent_c { 4526 constructor(timeStamp, value) { 4527 super(); 4528 this.timeStamp = timeStamp; 4529 this.value = value; 4530 this.type = TegakiEvents[this.constructor.name][0]; 4531 this.coalesce = true; 4532 } 4533 4534 dispatch() { 4535 Tegaki.setToolById(this.value); 4536 } 4537 } 4538 4539 class TegakiEventSetToolSize extends TegakiEvent_c { 4540 constructor(timeStamp, value) { 4541 super(); 4542 this.timeStamp = timeStamp; 4543 this.value = value; 4544 this.type = TegakiEvents[this.constructor.name][0]; 4545 this.coalesce = true; 4546 } 4547 4548 dispatch() { 4549 Tegaki.setToolSize(this.value); 4550 } 4551 } 4552 4553 class TegakiEventSetToolAlpha { 4554 constructor(timeStamp, value) { 4555 this.timeStamp = timeStamp; 4556 this.value = value; 4557 this.type = TegakiEvents[this.constructor.name][0]; 4558 this.coalesce = true; 4559 this.size = 9; 4560 } 4561 4562 pack(w) { 4563 w.writeUint8(this.type); 4564 w.writeUint32(this.timeStamp); 4565 w.writeFloat32(this.value); 4566 } 4567 4568 static unpack(r) { 4569 return new this(r.readUint32(), r.readFloat32()); 4570 } 4571 4572 dispatch() { 4573 Tegaki.setToolAlpha(this.value); 4574 } 4575 } 4576 4577 class TegakiEventSetToolFlow { 4578 constructor(timeStamp, value) { 4579 this.timeStamp = timeStamp; 4580 this.value = value; 4581 this.type = TegakiEvents[this.constructor.name][0]; 4582 this.coalesce = true; 4583 this.size = 9; 4584 } 4585 4586 pack(w) { 4587 w.writeUint8(this.type); 4588 w.writeUint32(this.timeStamp); 4589 w.writeFloat32(this.value); 4590 } 4591 4592 static unpack(r) { 4593 return new this(r.readUint32(), r.readFloat32()); 4594 } 4595 4596 dispatch() { 4597 Tegaki.setToolFlow(this.value); 4598 } 4599 } 4600 4601 class TegakiEventPreserveAlpha extends TegakiEvent_c { 4602 constructor(timeStamp, value) { 4603 super(); 4604 this.timeStamp = timeStamp; 4605 this.value = value; 4606 this.type = TegakiEvents[this.constructor.name][0]; 4607 this.coalesce = true; 4608 } 4609 4610 dispatch() { 4611 Tegaki.setToolPreserveAlpha(!!this.value); 4612 } 4613 } 4614 4615 class TegakiEventSetToolSizeDynamics extends TegakiEvent_c { 4616 constructor(timeStamp, value) { 4617 super(); 4618 this.timeStamp = timeStamp; 4619 this.value = value; 4620 this.type = TegakiEvents[this.constructor.name][0]; 4621 this.coalesce = true; 4622 } 4623 4624 dispatch() { 4625 Tegaki.setToolSizeDynamics(!!this.value); 4626 } 4627 } 4628 4629 class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c { 4630 constructor(timeStamp, value) { 4631 super(); 4632 this.timeStamp = timeStamp; 4633 this.value = value; 4634 this.type = TegakiEvents[this.constructor.name][0]; 4635 this.coalesce = true; 4636 } 4637 4638 dispatch() { 4639 Tegaki.setToolAlphaDynamics(!!this.value); 4640 } 4641 } 4642 4643 class TegakiEventSetToolFlowDynamics extends TegakiEvent_c { 4644 constructor(timeStamp, value) { 4645 super(); 4646 this.timeStamp = timeStamp; 4647 this.value = value; 4648 this.type = TegakiEvents[this.constructor.name][0]; 4649 this.coalesce = true; 4650 } 4651 4652 dispatch() { 4653 Tegaki.setToolFlowDynamics(!!this.value); 4654 } 4655 } 4656 4657 class TegakiEventSetToolTip extends TegakiEvent_c { 4658 constructor(timeStamp, value) { 4659 super(); 4660 this.timeStamp = timeStamp; 4661 this.value = value; 4662 this.type = TegakiEvents[this.constructor.name][0]; 4663 this.coalesce = true; 4664 } 4665 4666 dispatch() { 4667 Tegaki.setToolTip(this.value); 4668 } 4669 } 4670 4671 class TegakiEventAddLayer extends TegakiEvent_void { 4672 constructor(timeStamp) { 4673 super(); 4674 this.timeStamp = timeStamp; 4675 this.type = TegakiEvents[this.constructor.name][0]; 4676 } 4677 4678 dispatch() { 4679 Tegaki.addLayer(); 4680 } 4681 } 4682 4683 class TegakiEventDeleteLayers extends TegakiEvent_void { 4684 constructor(timeStamp) { 4685 super(); 4686 this.timeStamp = timeStamp; 4687 this.type = TegakiEvents[this.constructor.name][0]; 4688 } 4689 4690 dispatch() { 4691 Tegaki.deleteSelectedLayers(); 4692 } 4693 } 4694 4695 class TegakiEventMoveLayers extends TegakiEvent_c { 4696 constructor(timeStamp, value) { 4697 super(); 4698 this.timeStamp = timeStamp; 4699 this.value = value; 4700 this.type = TegakiEvents[this.constructor.name][0]; 4701 } 4702 4703 dispatch() { 4704 Tegaki.moveSelectedLayers(this.value); 4705 } 4706 } 4707 4708 class TegakiEventMergeLayers extends TegakiEvent_void { 4709 constructor(timeStamp) { 4710 super(); 4711 this.timeStamp = timeStamp; 4712 this.type = TegakiEvents[this.constructor.name][0]; 4713 } 4714 4715 dispatch() { 4716 Tegaki.mergeSelectedLayers(); 4717 } 4718 } 4719 4720 class TegakiEventToggleLayerVisibility extends TegakiEvent_c { 4721 constructor(timeStamp, value) { 4722 super(); 4723 this.timeStamp = timeStamp; 4724 this.value = value; 4725 this.type = TegakiEvents[this.constructor.name][0]; 4726 } 4727 4728 dispatch() { 4729 Tegaki.toggleLayerVisibility(this.value); 4730 } 4731 } 4732 4733 class TegakiEventSetActiveLayer extends TegakiEvent_c { 4734 constructor(timeStamp, value) { 4735 super(); 4736 this.timeStamp = timeStamp; 4737 this.value = value; 4738 this.type = TegakiEvents[this.constructor.name][0]; 4739 } 4740 4741 dispatch() { 4742 Tegaki.setActiveLayer(this.value); 4743 } 4744 } 4745 4746 class TegakiEventToggleLayerSelection extends TegakiEvent_c { 4747 constructor(timeStamp, value) { 4748 super(); 4749 this.timeStamp = timeStamp; 4750 this.value = value; 4751 this.type = TegakiEvents[this.constructor.name][0]; 4752 } 4753 4754 dispatch() { 4755 Tegaki.toggleSelectedLayer(this.value); 4756 } 4757 } 4758 4759 class TegakiEventSetSelectedLayersAlpha { 4760 constructor(timeStamp, value) { 4761 this.timeStamp = timeStamp; 4762 this.value = value; 4763 this.type = TegakiEvents[this.constructor.name][0]; 4764 this.coalesce = true; 4765 this.size = 9; 4766 } 4767 4768 pack(w) { 4769 w.writeUint8(this.type); 4770 w.writeUint32(this.timeStamp); 4771 w.writeFloat32(this.value); 4772 } 4773 4774 static unpack(r) { 4775 return new this(r.readUint32(), r.readFloat32()); 4776 } 4777 4778 dispatch() { 4779 Tegaki.setSelectedLayersAlpha(this.value); 4780 } 4781 } 4782 4783 const TegakiEvents = Object.freeze({ 4784 TegakiEventPrelude: [0, TegakiEventPrelude], 4785 4786 TegakiEventDrawStart: [1, TegakiEventDrawStart], 4787 TegakiEventDraw: [2, TegakiEventDraw], 4788 TegakiEventDrawCommit: [3, TegakiEventDrawCommit], 4789 TegakiEventUndo: [4, TegakiEventUndo], 4790 TegakiEventRedo: [5, TegakiEventRedo], 4791 TegakiEventSetColor: [6, TegakiEventSetColor], 4792 TegakiEventDrawStartNoP: [7, TegakiEventDrawStartNoP], 4793 TegakiEventDrawNoP: [8, TegakiEventDrawNoP], 4794 4795 TegakiEventSetTool: [10, TegakiEventSetTool], 4796 TegakiEventSetToolSize: [11, TegakiEventSetToolSize], 4797 TegakiEventSetToolAlpha: [12, TegakiEventSetToolAlpha], 4798 TegakiEventSetToolSizeDynamics: [13, TegakiEventSetToolSizeDynamics], 4799 TegakiEventSetToolAlphaDynamics: [14, TegakiEventSetToolAlphaDynamics], 4800 TegakiEventSetToolTip: [15, TegakiEventSetToolTip], 4801 TegakiEventPreserveAlpha: [16, TegakiEventPreserveAlpha], 4802 TegakiEventSetToolFlowDynamics: [17, TegakiEventSetToolFlowDynamics], 4803 TegakiEventSetToolFlow: [18, TegakiEventSetToolFlow], 4804 4805 TegakiEventAddLayer: [20, TegakiEventAddLayer], 4806 TegakiEventDeleteLayers: [21, TegakiEventDeleteLayers], 4807 TegakiEventMoveLayers: [22, TegakiEventMoveLayers], 4808 TegakiEventMergeLayers: [23, TegakiEventMergeLayers], 4809 TegakiEventToggleLayerVisibility: [24, TegakiEventToggleLayerVisibility], 4810 TegakiEventSetActiveLayer: [25, TegakiEventSetActiveLayer], 4811 TegakiEventToggleLayerSelection: [26, TegakiEventToggleLayerSelection], 4812 TegakiEventSetSelectedLayersAlpha: [27, TegakiEventSetSelectedLayersAlpha], 4813 4814 TegakiEventHistoryDummy: [254, TegakiEventHistoryDummy], 4815 4816 TegakiEventConclusion: [255, TegakiEventConclusion] 4817 }); 4818 class TegakiReplayRecorder { 4819 constructor() { 4820 this.formatVersion = 1; 4821 4822 this.compressed = true; 4823 4824 this.tegakiVersion = Tegaki.VERSION.split('.').map((v) => +v); 4825 4826 this.canvasWidth = Tegaki.baseWidth; 4827 this.canvasHeight = Tegaki.baseHeight; 4828 4829 this.bgColor = $T.hexToRgb(Tegaki.bgColor); 4830 this.toolColor = $T.hexToRgb(Tegaki.toolColor); 4831 4832 this.toolId = Tegaki.tools[Tegaki.defaultTool].id; 4833 4834 this.toolList = this.buildToolList(Tegaki.tools); 4835 4836 this.startTimeStamp = 0; 4837 this.endTimeStamp = 0; 4838 4839 this.recording = false; 4840 4841 this.events = []; 4842 4843 this.mimeType = 'application/octet-stream'; 4844 } 4845 4846 buildToolList(tools) { 4847 var k, tool, toolMap; 4848 4849 toolMap = []; 4850 4851 for (k in tools) { 4852 tool = tools[k]; 4853 4854 toolMap.push({ 4855 id: tool.id, 4856 size: tool.size, 4857 alpha: tool.alpha, 4858 flow: tool.flow, 4859 step: tool.step, 4860 sizeDynamicsEnabled: +tool.sizeDynamicsEnabled, 4861 alphaDynamicsEnabled: +tool.alphaDynamicsEnabled, 4862 flowDynamicsEnabled: +tool.flowDynamicsEnabled, 4863 usePreserveAlpha: +tool.usePreserveAlpha, 4864 tipId: tool.tipId 4865 }); 4866 } 4867 4868 return toolMap; 4869 } 4870 4871 start() { 4872 if (this.recording) { 4873 return; 4874 } 4875 4876 if (this.endTimeStamp > 0) { 4877 this.events.pop(); 4878 this.endTimeStamp = 0; 4879 } 4880 4881 if (this.startTimeStamp === 0) { 4882 this.events.push(new TegakiEventPrelude(performance.now())); 4883 this.startTimeStamp = Date.now(); 4884 } 4885 4886 this.recording = true; 4887 } 4888 4889 stop() { 4890 if (this.startTimeStamp === 0 || !this.recording) { 4891 return; 4892 } 4893 4894 this.events.push(new TegakiEventConclusion(performance.now())); 4895 this.endTimeStamp = Date.now(); 4896 this.recording = false; 4897 } 4898 4899 push(e) { 4900 if (this.recording) { 4901 if (e.coalesce && this.events[this.events.length - 1].type === e.type) { 4902 this.events[this.events.length - 1] = e; 4903 } 4904 else { 4905 this.events.push(e); 4906 } 4907 } 4908 } 4909 4910 getEventStackSize() { 4911 var e, size; 4912 4913 size = 4; 4914 4915 for (e of this.events) { 4916 size += e.size; 4917 } 4918 4919 return size; 4920 } 4921 4922 getHeaderSize() { 4923 return 12; 4924 } 4925 4926 getMetaSize() { 4927 return 21; 4928 } 4929 4930 getToolSize() { 4931 return 19; 4932 } 4933 4934 getToolListSize() { 4935 return 2 + (this.toolList.length * this.getToolSize()); 4936 } 4937 4938 writeToolList(w) { 4939 var tool, field, fields; 4940 4941 fields = [ 4942 ['id', 'Uint8'], 4943 ['size', 'Uint8'], 4944 ['alpha', 'Float32'], 4945 ['step', 'Float32'], 4946 ['sizeDynamicsEnabled', 'Uint8'], 4947 ['alphaDynamicsEnabled', 'Uint8'], 4948 ['usePreserveAlpha', 'Uint8'], 4949 ['tipId', 'Int8'], 4950 ['flow', 'Float32'], 4951 ['flowDynamicsEnabled', 'Uint8'], 4952 ]; 4953 4954 w.writeUint8(this.toolList.length); 4955 4956 w.writeUint8(this.getToolSize()); 4957 4958 for (tool of this.toolList) { 4959 for (field of fields) { 4960 w['write' + field[1]](tool[field[0]]); 4961 } 4962 } 4963 } 4964 4965 writeMeta(w) { 4966 w.writeUint16(this.getMetaSize()); 4967 4968 w.writeUint32(Math.ceil(this.startTimeStamp / 1000)); 4969 w.writeUint32(Math.ceil(this.endTimeStamp / 1000)); 4970 4971 w.writeUint16(this.canvasWidth); 4972 w.writeUint16(this.canvasHeight); 4973 4974 w.writeUint8(this.bgColor[0]); 4975 w.writeUint8(this.bgColor[1]); 4976 w.writeUint8(this.bgColor[2]); 4977 4978 w.writeUint8(this.toolColor[0]); 4979 w.writeUint8(this.toolColor[1]); 4980 w.writeUint8(this.toolColor[2]); 4981 4982 w.writeUint8(this.toolId); 4983 } 4984 4985 writeEventStack(w) { 4986 var event; 4987 4988 w.writeUint32(this.events.length); 4989 4990 for (event of this.events) { 4991 event.pack(w); 4992 } 4993 } 4994 4995 writeHeader(w, dataSize) { 4996 w.writeUint8(0x54); 4997 w.writeUint8(0x47); 4998 w.writeUint8(0x4B); 4999 5000 w.writeUint8(+this.compressed); 5001 5002 w.writeUint32(dataSize); 5003 5004 w.writeUint8(this.tegakiVersion[0]); 5005 w.writeUint8(this.tegakiVersion[1]); 5006 w.writeUint8(this.tegakiVersion[2]); 5007 w.writeUint8(this.formatVersion); 5008 } 5009 5010 compressData(w) { 5011 return UZIP.deflateRaw(new Uint8Array(w.buf), { level: 9 }); 5012 } 5013 5014 toUint8Array() { 5015 var headerSize, dataSize, data, w, compData, bytes; 5016 5017 if (!this.startTimeStamp || !this.endTimeStamp) { 5018 return null; 5019 } 5020 5021 headerSize = this.getHeaderSize(); 5022 dataSize = this.getMetaSize() + this.getToolListSize() + this.getEventStackSize(); 5023 5024 data = new ArrayBuffer(dataSize); 5025 5026 w = new TegakiBinWriter(data); 5027 5028 this.writeMeta(w); 5029 5030 this.writeToolList(w); 5031 5032 this.writeEventStack(w); 5033 5034 compData = this.compressData(w); 5035 //compData = new Uint8Array(data.slice(0)); 5036 5037 w = new TegakiBinWriter(new ArrayBuffer(headerSize + compData.length)); 5038 5039 this.writeHeader(w, dataSize); 5040 5041 bytes = new Uint8Array(w.buf); 5042 5043 bytes.set(compData, headerSize); 5044 5045 return bytes; 5046 } 5047 5048 toBlob() { 5049 var ary = this.toUint8Array(); 5050 5051 if (!ary) { 5052 return null; 5053 } 5054 5055 return new Blob([ary.buffer], { type: this.mimeType }); 5056 } 5057 } 5058 class TegakiReplayViewer { 5059 constructor() { 5060 this.formatVersion = 1; 5061 5062 this.compressed = true; 5063 5064 this.tegakiVersion = [0, 0, 0]; 5065 5066 this.dataSize = 0; 5067 5068 this.canvasWidth = 0; 5069 this.canvasHeight = 0; 5070 5071 this.bgColor = [0, 0, 0]; 5072 this.toolColor = [0, 0, 0]; 5073 5074 this.toolId = 1; 5075 5076 this.toolMap = {}; 5077 5078 this.startTimeStamp = 0; 5079 this.endTimeStamp = 0; 5080 5081 this.loaded = false; 5082 this.playing = false; 5083 this.gapless = true; 5084 5085 this.autoPaused = false; 5086 5087 this.destroyed = false; 5088 5089 this.speedIndex = 1; 5090 this.speedList = [0.5, 1.0, 2.0, 5.0, 10.0]; 5091 this.speed = this.speedList[this.speedIndex]; 5092 5093 this.maxEventsPerFrame = 25; 5094 5095 this.maxEventCount = 8640000; 5096 5097 this.events = []; 5098 5099 this.preludePos = 0.0; 5100 this.currentPos = 0.0; 5101 this.conclusionPos = 0.0; 5102 this.duration = 0.0; 5103 5104 this.playTimeStart = 0.0; 5105 this.playTimeCurrent = 0.0; 5106 5107 this.eventIndex = 0; 5108 5109 this.maxCanvasWH = 8192; 5110 5111 this.maxGapTime = 3000; 5112 5113 this.uiAccTime = 0; 5114 5115 this.onFrameThis = this.onFrame.bind(this); 5116 } 5117 5118 destroy() { 5119 this.destroyed = true; 5120 this.pause(); 5121 this.events = null; 5122 } 5123 5124 speedUp() { 5125 if (this.speedIndex + 1 < this.speedList.length) { 5126 this.speed = this.speedList[++this.speedIndex]; 5127 } 5128 } 5129 5130 slowDown() { 5131 if (this.speedIndex - 1 >= 0) { 5132 this.speed = this.speedList[--this.speedIndex]; 5133 } 5134 } 5135 5136 toggleGapless() { 5137 this.gapless = !this.gapless; 5138 } 5139 5140 getCurrentPos() { 5141 return this.currentPos; 5142 } 5143 5144 getDuration() { 5145 return this.duration; 5146 } 5147 5148 loadFromURL(url) { 5149 fetch(url) 5150 .then((resp) => this.onResponseReady(resp)) 5151 .catch((err) => this.onLoadError(err)); 5152 } 5153 5154 onResponseReady(resp) { 5155 if (resp.ok) { 5156 resp.arrayBuffer() 5157 .then((buf) => this.onResponseBodyReady(buf)) 5158 .catch((err) => this.onLoadError(err)); 5159 } 5160 else { 5161 this.onLoadError(resp.statusText); 5162 } 5163 } 5164 5165 onResponseBodyReady(data) { 5166 this.loadFromBuffer(data); 5167 Tegaki.onReplayLoaded(); 5168 } 5169 5170 onLoadError(err) { 5171 TegakiUI.printMsg(TegakiStrings.errorLoadReplay + err, 0); 5172 } 5173 5174 autoPause() { 5175 this.autoPaused = true; 5176 this.pause(); 5177 } 5178 5179 pause(rewind) { 5180 window.cancelAnimationFrame(this.onFrameThis); 5181 5182 this.playing = false; 5183 5184 if (rewind) { 5185 this.currentPos = 0; 5186 this.eventIndex = 0; 5187 } 5188 5189 Tegaki.onReplayTimeChanged(); 5190 Tegaki.onReplayPlayPauseChanged(); 5191 } 5192 5193 rewind() { 5194 this.autoPaused = false; 5195 this.pause(true); 5196 Tegaki.onReplayReset(); 5197 } 5198 5199 play() { 5200 this.playTimeStart = performance.now(); 5201 this.playTimeCurrent = this.playTimeStart; 5202 5203 this.playing = true; 5204 this.autoPaused = false; 5205 5206 this.uiAccTime = 0; 5207 5208 Tegaki.onReplayPlayPauseChanged(); 5209 5210 window.requestAnimationFrame(this.onFrameThis); 5211 } 5212 5213 togglePlayPause() { 5214 if (this.playing) { 5215 this.pause(); 5216 } 5217 else { 5218 this.play(); 5219 } 5220 } 5221 5222 onFrame(ts) { 5223 var delta = ts - this.playTimeCurrent; 5224 5225 if (!this.playing) { 5226 return; 5227 } 5228 5229 this.playTimeCurrent = ts; 5230 5231 this.step(delta); 5232 5233 this.uiAccTime += delta; 5234 5235 if (this.uiAccTime > 1000) { 5236 Tegaki.onReplayTimeChanged(); 5237 this.uiAccTime = 0; 5238 } 5239 5240 if (this.currentPos < this.duration) { 5241 window.requestAnimationFrame(this.onFrameThis); 5242 } 5243 else { 5244 this.pause(); 5245 } 5246 } 5247 5248 step(delta) { 5249 var event, currentEventTime, i; 5250 5251 this.currentPos += (delta * this.speed); 5252 5253 currentEventTime = this.currentPos + this.preludePos; 5254 5255 if (this.gapless && this.eventIndex < this.events.length) { 5256 event = this.events[this.eventIndex]; 5257 5258 if (event.timeStamp - currentEventTime > this.maxGapTime) { 5259 this.currentPos = event.timeStamp - this.preludePos; 5260 currentEventTime = event.timeStamp; 5261 } 5262 } 5263 5264 i = 0; 5265 5266 while (this.eventIndex < this.events.length) { 5267 event = this.events[this.eventIndex]; 5268 5269 if (event.timeStamp <= currentEventTime) { 5270 if (i >= this.maxEventsPerFrame) { 5271 this.currentPos = event.timeStamp - this.preludePos; 5272 break; 5273 } 5274 5275 event.dispatch(); 5276 5277 ++this.eventIndex; 5278 ++i; 5279 } 5280 else { 5281 break; 5282 } 5283 } 5284 } 5285 5286 getEventIdMap() { 5287 var map, key, val; 5288 5289 map = {}; 5290 5291 for (key in TegakiEvents) { 5292 val = TegakiEvents[key]; 5293 map[val[0]] = val[1]; 5294 } 5295 5296 return map; 5297 } 5298 5299 readToolMap(r) { 5300 var i, len, size, tool, field, fields, pos; 5301 5302 this.toolMap = {}; 5303 5304 fields = [ 5305 ['id', 'Uint8'], 5306 ['size', 'Uint8'], 5307 ['alpha', 'Float32'], 5308 ['step', 'Float32'], 5309 ['sizeDynamicsEnabled', 'Uint8'], 5310 ['alphaDynamicsEnabled', 'Uint8'], 5311 ['usePreserveAlpha', 'Uint8'], 5312 ['tipId', 'Int8'], 5313 ['flow', 'Float32'], 5314 ['flowDynamicsEnabled', 'Uint8'], 5315 ]; 5316 5317 len = r.readUint8(); 5318 5319 size = r.readUint8(); 5320 5321 for (i = 0; i < len; ++i) { 5322 pos = r.pos + size; 5323 5324 tool = {}; 5325 5326 for (field of fields) { 5327 if (r.pos >= pos) { 5328 break; 5329 } 5330 5331 tool[field[0]] = r['read' + field[1]](); 5332 } 5333 5334 this.toolMap[tool.id] = tool; 5335 5336 r.pos = pos; 5337 } 5338 } 5339 5340 readHeader(r) { 5341 var tgk; 5342 5343 tgk = String.fromCharCode(r.readUint8(), r.readUint8(), r.readUint8()); 5344 5345 if (tgk !== 'TGK') { 5346 throw 'invalid header'; 5347 } 5348 5349 this.compressed = r.readUint8() === 1; 5350 5351 this.dataSize = r.readUint32(); 5352 5353 this.tegakiVersion[0] = r.readUint8(); 5354 this.tegakiVersion[1] = r.readUint8(); 5355 this.tegakiVersion[2] = r.readUint8(); 5356 5357 this.formatVersion = r.readUint8(); 5358 } 5359 5360 decompressData(r) { 5361 return UZIP.inflateRaw( 5362 new Uint8Array(r.buf, r.pos), 5363 new Uint8Array(this.dataSize) 5364 ); 5365 } 5366 5367 readMeta(r) { 5368 var pos, size; 5369 5370 size = r.readUint16(); 5371 5372 pos = r.pos + size - 2; 5373 5374 this.startTimeStamp = r.readUint32() * 1000; 5375 this.endTimeStamp = r.readUint32() * 1000; 5376 5377 this.canvasWidth = r.readUint16(); 5378 this.canvasHeight = r.readUint16(); 5379 5380 if (this.canvasWidth > this.maxCanvasWH 5381 || this.canvasHeight > this.maxCanvasWH) { 5382 throw 'canvas too large'; 5383 } 5384 5385 this.bgColor[0] = r.readUint8(); 5386 this.bgColor[1] = r.readUint8(); 5387 this.bgColor[2] = r.readUint8(); 5388 5389 this.toolColor[0] = r.readUint8(); 5390 this.toolColor[1] = r.readUint8(); 5391 this.toolColor[2] = r.readUint8(); 5392 5393 this.toolId = r.readUint8(); 5394 5395 r.pos = pos; 5396 } 5397 5398 readEventStack(r) { 5399 var i, len, type, klass, event, eventMap; 5400 5401 eventMap = this.getEventIdMap(); 5402 5403 len = r.readUint32(); 5404 5405 if (len < 1 || len > this.maxEventCount) { 5406 throw 'invalid event count'; 5407 } 5408 5409 for (i = 0; i < len; ++i) { 5410 type = r.readUint8(); 5411 5412 klass = eventMap[type]; 5413 5414 if (!klass) { 5415 throw 'invalid event id'; 5416 } 5417 5418 event = klass.unpack(r); 5419 5420 this.events.push(event); 5421 } 5422 5423 if (this.events[0].type !== TegakiEvents.TegakiEventPrelude[0]) { 5424 throw 'invalid prelude'; 5425 } 5426 5427 if (this.events[len - 1].type !== TegakiEvents.TegakiEventConclusion[0]) { 5428 throw 'invalid conclusion'; 5429 } 5430 5431 this.preludePos = this.events[0].timeStamp; 5432 this.conclusionPos = this.events[len - 1].timeStamp; 5433 5434 this.duration = this.conclusionPos - this.preludePos; 5435 5436 if (this.duration <= 0) { 5437 throw 'invalid duration'; 5438 } 5439 } 5440 5441 loadFromBuffer(buffer) { 5442 var r, data; 5443 5444 if (this.destroyed || this.loaded) { 5445 return false; 5446 } 5447 5448 r = new TegakiBinReader(buffer); 5449 5450 this.readHeader(r); 5451 5452 data = this.decompressData(r); 5453 5454 r = new TegakiBinReader(data.buffer); 5455 5456 this.readMeta(r); 5457 5458 this.readToolMap(r); 5459 5460 this.readEventStack(r); 5461 5462 this.loaded = true; 5463 5464 return true; 5465 } 5466 } 5467 var TegakiUI = { 5468 draggedNode: null, 5469 5470 draggedLabelLastX: 0, 5471 draggedLabelFn: null, 5472 5473 statusTimeout: 0, 5474 5475 layerPreviewCtxCache: new WeakMap(), 5476 5477 getLayerPreviewSize: function() { 5478 return $T.calcThumbSize(Tegaki.baseWidth, Tegaki.baseHeight, 24); 5479 }, 5480 5481 setupDragLabel: function(e, moveFn) { 5482 TegakiUI.draggedLabelFn = moveFn; 5483 TegakiUI.draggedLabelLastX = e.clientX; 5484 $T.on(document, 'pointermove', TegakiUI.processDragLabel); 5485 $T.on(document, 'pointerup', TegakiUI.clearDragLabel); 5486 }, 5487 5488 processDragLabel: function(e) { 5489 TegakiUI.draggedLabelFn.call(Tegaki, e.clientX - TegakiUI.draggedLabelLastX); 5490 TegakiUI.draggedLabelLastX = e.clientX; 5491 }, 5492 5493 clearDragLabel: function(e) { 5494 $T.off(document, 'pointermove', TegakiUI.processDragLabel); 5495 $T.off(document, 'pointerup', TegakiUI.clearDragLabel); 5496 }, 5497 5498 printMsg: function(str, timeout = 5000) { 5499 TegakiUI.clearMsg(); 5500 5501 $T.id('tegaki-status-output').textContent = str; 5502 5503 if (timeout > 0) { 5504 TegakiUI.statusTimeout = setTimeout(TegakiUI.clearMsg, 5000); 5505 } 5506 }, 5507 5508 clearMsg: function() { 5509 if (TegakiUI.statusTimeout) { 5510 clearTimeout(TegakiUI.statusTimeout); 5511 TegakiUI.statusTimeout = 0; 5512 } 5513 5514 $T.id('tegaki-status-output').textContent = ''; 5515 }, 5516 5517 buildUI: function() { 5518 var bg, cnt, el, ctrl, layersCnt, canvasCnt; 5519 5520 // 5521 // Grid container 5522 // 5523 bg = $T.el('div'); 5524 bg.id = 'tegaki'; 5525 5526 // 5527 // Menu area 5528 // 5529 el = $T.el('div'); 5530 el.id = 'tegaki-menu-cnt'; 5531 5532 if (!Tegaki.replayMode) { 5533 el.appendChild(TegakiUI.buildMenuBar()); 5534 } 5535 else { 5536 el.appendChild(TegakiUI.buildViewerMenuBar()); 5537 el.appendChild(TegakiUI.buildReplayControls()); 5538 } 5539 5540 el.appendChild(TegakiUI.buildToolModeBar()); 5541 5542 bg.appendChild(el); 5543 5544 bg.appendChild(TegakiUI.buildDummyFilePicker()); 5545 5546 // 5547 // Tools area 5548 // 5549 cnt = $T.el('div'); 5550 cnt.id = 'tegaki-tools-cnt'; 5551 5552 cnt.appendChild(TegakiUI.buildToolsMenu()); 5553 5554 bg.appendChild(cnt); 5555 5556 // 5557 // Canvas area 5558 // 5559 [canvasCnt, layersCnt] = TegakiUI.buildCanvasCnt(); 5560 5561 bg.appendChild(canvasCnt); 5562 5563 // 5564 // Controls area 5565 // 5566 ctrl = $T.el('div'); 5567 ctrl.id = 'tegaki-ctrl-cnt'; 5568 5569 // Zoom control 5570 ctrl.appendChild(TegakiUI.buildZoomCtrlGroup()); 5571 5572 // Colorpicker 5573 ctrl.appendChild( 5574 TegakiUI.buildColorCtrlGroup(Tegaki.toolColor) 5575 ); 5576 5577 // Size control 5578 ctrl.appendChild(TegakiUI.buildSizeCtrlGroup()); 5579 5580 // Alpha control 5581 ctrl.appendChild(TegakiUI.buildAlphaCtrlGroup()); 5582 5583 // Flow control 5584 ctrl.appendChild(TegakiUI.buildFlowCtrlGroup()); 5585 5586 // Layers control 5587 ctrl.appendChild(TegakiUI.buildLayersCtrlGroup()); 5588 5589 // --- 5590 5591 bg.appendChild(ctrl); 5592 5593 // 5594 // Status area 5595 // 5596 bg.appendChild(TegakiUI.buildStatusCnt()); 5597 5598 return [bg, canvasCnt, layersCnt]; 5599 }, 5600 5601 buildDummyFilePicker: function() { 5602 var el = $T.el('input'); 5603 5604 el.type = 'file'; 5605 el.id = 'tegaki-filepicker'; 5606 el.className = 'tegaki-hidden'; 5607 el.accept = 'image/png, image/jpeg'; 5608 $T.on(el, 'change', Tegaki.onOpenFileSelected); 5609 5610 return el; 5611 }, 5612 5613 buildMenuBar: function() { 5614 var frag, btn; 5615 5616 frag = $T.el('div'); 5617 frag.id = 'tegaki-menu-bar'; 5618 5619 btn = $T.el('span'); 5620 btn.className = 'tegaki-mb-btn'; 5621 btn.textContent = TegakiStrings.newCanvas; 5622 $T.on(btn, 'click', Tegaki.onNewClick); 5623 frag.appendChild(btn); 5624 5625 btn = $T.el('span'); 5626 btn.className = 'tegaki-mb-btn'; 5627 btn.textContent = TegakiStrings.open; 5628 $T.on(btn, 'click', Tegaki.onOpenClick); 5629 frag.appendChild(btn); 5630 5631 btn = $T.el('span'); 5632 btn.className = 'tegaki-mb-btn'; 5633 btn.textContent = TegakiStrings.export; 5634 $T.on(btn, 'click', Tegaki.onExportClick); 5635 frag.appendChild(btn); 5636 5637 btn = $T.el('span'); 5638 btn.id = 'tegaki-undo-btn'; 5639 btn.className = 'tegaki-mb-btn'; 5640 btn.textContent = TegakiStrings.undo; 5641 btn.title = TegakiKeybinds.getCaption('undo'); 5642 $T.on(btn, 'click', Tegaki.onUndoClick); 5643 frag.appendChild(btn); 5644 5645 btn = $T.el('span'); 5646 btn.id = 'tegaki-redo-btn'; 5647 btn.className = 'tegaki-mb-btn'; 5648 btn.textContent = TegakiStrings.redo; 5649 btn.title = TegakiKeybinds.getCaption('redo'); 5650 $T.on(btn, 'click', Tegaki.onRedoClick); 5651 frag.appendChild(btn); 5652 5653 btn = $T.el('span'); 5654 btn.className = 'tegaki-mb-btn'; 5655 btn.textContent = TegakiStrings.close; 5656 $T.on(btn, 'click', Tegaki.onCancelClick); 5657 frag.appendChild(btn); 5658 5659 btn = $T.el('span'); 5660 btn.id = 'tegaki-finish-btn'; 5661 btn.className = 'tegaki-mb-btn'; 5662 btn.textContent = TegakiStrings.finish; 5663 $T.on(btn, 'click', Tegaki.onDoneClick); 5664 frag.appendChild(btn); 5665 5666 return frag; 5667 }, 5668 5669 buildViewerMenuBar: function() { 5670 var frag, btn; 5671 5672 frag = $T.el('div'); 5673 frag.id = 'tegaki-menu-bar'; 5674 5675 btn = $T.el('span'); 5676 btn.id = 'tegaki-finish-btn'; 5677 btn.className = 'tegaki-mb-btn'; 5678 btn.textContent = TegakiStrings.close; 5679 $T.on(btn, 'click', Tegaki.onCloseViewerClick); 5680 frag.appendChild(btn); 5681 5682 return frag; 5683 }, 5684 5685 buildToolModeBar: function() { 5686 var cnt, grp, el, btn; 5687 5688 cnt = $T.el('div'); 5689 cnt.id = 'tegaki-toolmode-bar'; 5690 5691 if (!Tegaki.tool) { 5692 cnt.classList.add('tegaki-hidden'); 5693 } 5694 5695 // Dynamics 5696 grp = $T.el('span'); 5697 grp.id = 'tegaki-tool-mode-dynamics'; 5698 grp.className = 'tegaki-toolmode-grp'; 5699 5700 el = $T.el('span'); 5701 el.className = 'tegaki-toolmode-lbl'; 5702 el.textContent = TegakiStrings.pressure; 5703 grp.appendChild(el); 5704 5705 el = $T.el('span'); 5706 el.id = 'tegaki-tool-mode-dynamics-ctrl'; 5707 el.className = 'tegaki-toolmode-ctrl'; 5708 5709 btn = $T.el('span'); 5710 btn.id = 'tegaki-tool-mode-dynamics-size'; 5711 btn.className = 'tegaki-sw-btn'; 5712 btn.textContent = TegakiStrings.size; 5713 $T.on(btn, 'mousedown', Tegaki.onToolPressureSizeClick); 5714 el.appendChild(btn); 5715 5716 btn = $T.el('span'); 5717 btn.id = 'tegaki-tool-mode-dynamics-alpha'; 5718 btn.className = 'tegaki-sw-btn'; 5719 btn.textContent = TegakiStrings.alpha; 5720 $T.on(btn, 'mousedown', Tegaki.onToolPressureAlphaClick); 5721 el.appendChild(btn); 5722 5723 btn = $T.el('span'); 5724 btn.id = 'tegaki-tool-mode-dynamics-flow'; 5725 btn.className = 'tegaki-sw-btn'; 5726 btn.textContent = TegakiStrings.flow; 5727 $T.on(btn, 'mousedown', Tegaki.onToolPressureFlowClick); 5728 el.appendChild(btn); 5729 5730 grp.appendChild(el); 5731 5732 cnt.appendChild(grp); 5733 5734 // Preserve Alpha 5735 grp = $T.el('span'); 5736 grp.id = 'tegaki-tool-mode-mask'; 5737 grp.className = 'tegaki-toolmode-grp'; 5738 5739 el = $T.el('span'); 5740 el.id = 'tegaki-toolmode-ctrl-tip'; 5741 el.className = 'tegaki-toolmode-ctrl'; 5742 5743 btn = $T.el('span'); 5744 btn.id = 'tegaki-tool-mode-mask-alpha'; 5745 btn.className = 'tegaki-sw-btn'; 5746 btn.textContent = TegakiStrings.preserveAlpha; 5747 $T.on(btn, 'mousedown', Tegaki.onToolPreserveAlphaClick); 5748 el.appendChild(btn); 5749 5750 grp.appendChild(el); 5751 5752 cnt.appendChild(grp); 5753 5754 // Tip 5755 grp = $T.el('span'); 5756 grp.id = 'tegaki-tool-mode-tip'; 5757 grp.className = 'tegaki-toolmode-grp'; 5758 5759 el = $T.el('span'); 5760 el.className = 'tegaki-toolmode-lbl'; 5761 el.textContent = TegakiStrings.tip; 5762 grp.appendChild(el); 5763 5764 el = $T.el('span'); 5765 el.id = 'tegaki-tool-mode-tip-ctrl'; 5766 el.className = 'tegaki-toolmode-ctrl'; 5767 grp.appendChild(el); 5768 5769 cnt.appendChild(grp); 5770 5771 return cnt; 5772 }, 5773 5774 buildToolsMenu: function() { 5775 var grp, el, lbl, name; 5776 5777 grp = $T.el('div'); 5778 grp.id = 'tegaki-tools-grid'; 5779 5780 for (name in Tegaki.tools) { 5781 el = $T.el('span'); 5782 el.setAttribute('data-tool', name); 5783 5784 lbl = TegakiStrings[name]; 5785 5786 if (Tegaki.tools[name].keybind) { 5787 lbl += ' (' + Tegaki.tools[name].keybind.toUpperCase() + ')'; 5788 } 5789 5790 el.setAttribute('title', lbl); 5791 el.id = 'tegaki-tool-btn-' + name; 5792 el.className = 'tegaki-tool-btn tegaki-icon tegaki-' + name; 5793 5794 $T.on(el, 'click', Tegaki.onToolClick); 5795 5796 grp.appendChild(el); 5797 } 5798 5799 return grp; 5800 }, 5801 5802 buildCanvasCnt: function() { 5803 var canvasCnt, wrap, layersCnt; 5804 5805 canvasCnt = $T.el('div'); 5806 canvasCnt.id = 'tegaki-canvas-cnt'; 5807 5808 wrap = $T.el('div'); 5809 wrap.id = 'tegaki-layers-wrap'; 5810 5811 layersCnt = $T.el('div'); 5812 layersCnt.id = 'tegaki-layers'; 5813 5814 wrap.appendChild(layersCnt); 5815 5816 canvasCnt.appendChild(wrap); 5817 5818 return [canvasCnt, layersCnt]; 5819 }, 5820 5821 buildCtrlGroup: function(id, title) { 5822 var cnt, el; 5823 5824 cnt = $T.el('div'); 5825 cnt.className = 'tegaki-ctrlgrp'; 5826 5827 if (id) { 5828 cnt.id = 'tegaki-ctrlgrp-' + id; 5829 } 5830 5831 if (title !== undefined) { 5832 el = $T.el('div'); 5833 el.className = 'tegaki-ctrlgrp-title'; 5834 el.textContent = title; 5835 cnt.appendChild(el); 5836 } 5837 5838 return cnt; 5839 }, 5840 5841 buildLayersCtrlGroup: function() { 5842 var el, ctrl, row, cnt; 5843 5844 ctrl = this.buildCtrlGroup('layers', TegakiStrings.layers); 5845 5846 // Layer options row 5847 row = $T.el('div'); 5848 row.id = 'tegaki-layers-opts'; 5849 5850 // Alpha 5851 cnt = $T.el('div'); 5852 cnt.id = 'tegaki-layer-alpha-cell'; 5853 5854 el = $T.el('span'); 5855 el.className = 'tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl'; 5856 el.textContent = TegakiStrings.alpha; 5857 $T.on(el, 'pointerdown', Tegaki.onLayerAlphaDragStart); 5858 cnt.appendChild(el); 5859 5860 el = $T.el('input'); 5861 el.id = 'tegaki-layer-alpha-opt'; 5862 el.className = 'tegaki-stealth-input tegaki-range-lbl-xs'; 5863 el.setAttribute('maxlength', 3); 5864 $T.on(el, 'input', Tegaki.onLayerAlphaChange); 5865 cnt.appendChild(el); 5866 5867 row.appendChild(cnt); 5868 5869 ctrl.appendChild(row); 5870 5871 el = $T.el('div'); 5872 el.id = 'tegaki-layers-grid'; 5873 ctrl.appendChild(el); 5874 5875 row = $T.el('div'); 5876 row.id = 'tegaki-layers-ctrl'; 5877 5878 el = $T.el('span'); 5879 el.title = TegakiStrings.addLayer; 5880 el.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; 5881 $T.on(el, 'click', Tegaki.onLayerAddClick); 5882 row.appendChild(el); 5883 5884 el = $T.el('span'); 5885 el.title = TegakiStrings.delLayers; 5886 el.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; 5887 $T.on(el, 'click', Tegaki.onLayerDeleteClick); 5888 row.appendChild(el); 5889 5890 el = $T.el('span'); 5891 el.id = 'tegaki-layer-merge'; 5892 el.title = TegakiStrings.mergeLayers; 5893 el.className = 'tegaki-ui-btn tegaki-icon tegaki-level-down'; 5894 $T.on(el, 'click', Tegaki.onMergeLayersClick); 5895 row.appendChild(el); 5896 5897 el = $T.el('span'); 5898 el.id = 'tegaki-layer-up'; 5899 el.title = TegakiStrings.moveLayerUp; 5900 el.setAttribute('data-up', '1'); 5901 el.className = 'tegaki-ui-btn tegaki-icon tegaki-up-open'; 5902 $T.on(el, 'click', Tegaki.onMoveLayerClick); 5903 row.appendChild(el); 5904 5905 el = $T.el('span'); 5906 el.id = 'tegaki-layer-down'; 5907 el.title = TegakiStrings.moveLayerDown; 5908 el.className = 'tegaki-ui-btn tegaki-icon tegaki-down-open'; 5909 $T.on(el, 'click', Tegaki.onMoveLayerClick); 5910 row.appendChild(el); 5911 5912 ctrl.appendChild(row); 5913 5914 return ctrl; 5915 }, 5916 5917 buildSizeCtrlGroup: function() { 5918 var el, ctrl, row; 5919 5920 ctrl = this.buildCtrlGroup('size', TegakiStrings.size); 5921 5922 row = $T.el('div'); 5923 row.className = 'tegaki-ctrlrow'; 5924 5925 el = $T.el('input'); 5926 el.id = 'tegaki-size'; 5927 el.className = 'tegaki-ctrl-range'; 5928 el.min = 1; 5929 el.max = Tegaki.maxSize; 5930 el.type = 'range'; 5931 el.title = TegakiKeybinds.getCaption('toolSize'); 5932 $T.on(el, 'input', Tegaki.onToolSizeChange); 5933 row.appendChild(el); 5934 5935 el = $T.el('input'); 5936 el.id = 'tegaki-size-lbl'; 5937 el.setAttribute('maxlength', 3); 5938 el.className = 'tegaki-stealth-input tegaki-range-lbl'; 5939 $T.on(el, 'input', Tegaki.onToolSizeChange); 5940 row.appendChild(el); 5941 5942 ctrl.appendChild(row); 5943 5944 return ctrl; 5945 }, 5946 5947 buildAlphaCtrlGroup: function() { 5948 var el, ctrl, row; 5949 5950 ctrl = this.buildCtrlGroup('alpha', TegakiStrings.alpha); 5951 5952 row = $T.el('div'); 5953 row.className = 'tegaki-ctrlrow'; 5954 5955 el = $T.el('input'); 5956 el.id = 'tegaki-alpha'; 5957 el.className = 'tegaki-ctrl-range'; 5958 el.min = 0; 5959 el.max = 100; 5960 el.step = 1; 5961 el.type = 'range'; 5962 $T.on(el, 'input', Tegaki.onToolAlphaChange); 5963 row.appendChild(el); 5964 5965 el = $T.el('input'); 5966 el.id = 'tegaki-alpha-lbl'; 5967 el.setAttribute('maxlength', 3); 5968 el.className = 'tegaki-stealth-input tegaki-range-lbl'; 5969 $T.on(el, 'input', Tegaki.onToolAlphaChange); 5970 row.appendChild(el); 5971 5972 ctrl.appendChild(row); 5973 5974 return ctrl; 5975 }, 5976 5977 buildFlowCtrlGroup: function() { 5978 var el, ctrl, row; 5979 5980 ctrl = this.buildCtrlGroup('flow', TegakiStrings.flow); 5981 5982 row = $T.el('div'); 5983 row.className = 'tegaki-ctrlrow'; 5984 5985 el = $T.el('input'); 5986 el.id = 'tegaki-flow'; 5987 el.className = 'tegaki-ctrl-range'; 5988 el.min = 0; 5989 el.max = 100; 5990 el.step = 1; 5991 el.type = 'range'; 5992 $T.on(el, 'input', Tegaki.onToolFlowChange); 5993 row.appendChild(el); 5994 5995 el = $T.el('input'); 5996 el.id = 'tegaki-flow-lbl'; 5997 el.setAttribute('maxlength', 3); 5998 el.className = 'tegaki-stealth-input tegaki-range-lbl'; 5999 $T.on(el, 'input', Tegaki.onToolFlowChange); 6000 row.appendChild(el); 6001 6002 ctrl.appendChild(row); 6003 6004 return ctrl; 6005 }, 6006 6007 buildZoomCtrlGroup: function() { 6008 var el, btn, ctrl; 6009 6010 ctrl = this.buildCtrlGroup('zoom', TegakiStrings.zoom); 6011 6012 btn = $T.el('div'); 6013 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; 6014 btn.id = 'tegaki-zoomin-btn'; 6015 btn.setAttribute('data-in', 1); 6016 $T.on(btn, 'click', Tegaki.onZoomChange); 6017 ctrl.appendChild(btn); 6018 6019 btn = $T.el('div'); 6020 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; 6021 btn.id = 'tegaki-zoomout-btn'; 6022 btn.setAttribute('data-out', 1); 6023 $T.on(btn, 'click', Tegaki.onZoomChange); 6024 ctrl.appendChild(btn); 6025 6026 el = $T.el('div'); 6027 el.id = 'tegaki-zoom-lbl'; 6028 ctrl.appendChild(el); 6029 6030 return ctrl; 6031 }, 6032 6033 buildColorCtrlGroup: function(mainColor) { 6034 var el, cnt, btn, ctrl, color, edge, i, palette, cls; 6035 6036 edge = / Edge\//i.test(window.navigator.userAgent); 6037 6038 ctrl = this.buildCtrlGroup('color', TegakiStrings.color); 6039 6040 cnt = $T.el('div'); 6041 cnt.id = 'tegaki-color-ctrl'; 6042 6043 el = $T.el('div'); 6044 el.id = 'tegaki-color'; 6045 edge && el.classList.add('tegaki-hidden'); 6046 el.style.backgroundColor = mainColor; 6047 $T.on(el, 'mousedown', Tegaki.onMainColorClick); 6048 cnt.appendChild(el); 6049 6050 el = $T.el('div'); 6051 el.id = 'tegaki-palette-switcher'; 6052 6053 btn = $T.el('span'); 6054 btn.id = 'tegaki-palette-prev-btn'; 6055 btn.title = TegakiStrings.switchPalette; 6056 btn.setAttribute('data-prev', '1'); 6057 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled'; 6058 $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); 6059 el.appendChild(btn); 6060 6061 btn = $T.el('span'); 6062 btn.id = 'tegaki-palette-next-btn'; 6063 btn.title = TegakiStrings.switchPalette; 6064 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-right-open'; 6065 $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); 6066 el.appendChild(btn); 6067 6068 cnt.appendChild(el); 6069 6070 ctrl.appendChild(cnt); 6071 6072 cnt = $T.el('div'); 6073 cnt.id = 'tegaki-color-grids'; 6074 6075 for (i = 0; i < TegakiColorPalettes.length; ++i) { 6076 el = $T.el('div'); 6077 6078 el.setAttribute('data-id', i); 6079 6080 cls = 'tegaki-color-grid'; 6081 6082 palette = TegakiColorPalettes[i]; 6083 6084 if (palette.length <= 18) { 6085 cls += ' tegaki-color-grid-20'; 6086 } 6087 else { 6088 cls += ' tegaki-color-grid-15'; 6089 } 6090 6091 if (i > 0) { 6092 cls += ' tegaki-hidden'; 6093 } 6094 6095 el.className = cls; 6096 6097 for (color of palette) { 6098 btn = $T.el('div'); 6099 btn.title = TegakiStrings.paletteSlotReplace; 6100 btn.className = 'tegaki-color-btn'; 6101 btn.setAttribute('data-color', color); 6102 btn.style.backgroundColor = color; 6103 $T.on(btn, 'mousedown', Tegaki.onPaletteColorClick); 6104 el.appendChild(btn); 6105 } 6106 6107 cnt.appendChild(el); 6108 } 6109 6110 ctrl.appendChild(cnt); 6111 6112 el = $T.el('input'); 6113 el.id = 'tegaki-colorpicker'; 6114 !edge && el.classList.add('tegaki-hidden'); 6115 el.value = color; 6116 el.type = 'color'; 6117 $T.on(el, 'change', Tegaki.onColorPicked); 6118 6119 ctrl.appendChild(el); 6120 6121 return ctrl; 6122 }, 6123 6124 buildStatusCnt: function() { 6125 var cnt, el; 6126 6127 cnt = $T.el('div'); 6128 cnt.id = 'tegaki-status-cnt'; 6129 6130 if (Tegaki.saveReplay) { 6131 el = $T.el('div'); 6132 el.id = 'tegaki-status-replay'; 6133 el.textContent = '⬤'; 6134 el.setAttribute('title', TegakiStrings.recordingEnabled); 6135 cnt.appendChild(el); 6136 } 6137 6138 el = $T.el('div'); 6139 el.id = 'tegaki-status-output'; 6140 cnt.appendChild(el); 6141 6142 el = $T.el('div'); 6143 el.id = 'tegaki-status-version'; 6144 el.textContent = 'tegaki.js v' + Tegaki.VERSION; 6145 cnt.appendChild(el); 6146 6147 return cnt; 6148 }, 6149 6150 buildReplayControls: function() { 6151 var cnt, btn, el; 6152 6153 cnt = $T.el('div'); 6154 cnt.id = 'tegaki-replay-controls'; 6155 cnt.className = 'tegaki-hidden'; 6156 6157 btn = $T.el('span'); 6158 btn.id = 'tegaki-replay-gapless-btn'; 6159 btn.className = 'tegaki-ui-cb-w'; 6160 $T.on(btn, 'click', Tegaki.onReplayGaplessClick); 6161 6162 el = $T.el('span'); 6163 el.id = 'tegaki-replay-gapless-cb'; 6164 el.className = 'tegaki-ui-cb'; 6165 btn.appendChild(el); 6166 6167 el = $T.el('span'); 6168 el.className = 'tegaki-menu-lbl'; 6169 el.textContent = TegakiStrings.gapless; 6170 btn.appendChild(el); 6171 6172 cnt.appendChild(btn); 6173 6174 btn = $T.el('span'); 6175 btn.id = 'tegaki-replay-play-btn'; 6176 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-play'; 6177 btn.setAttribute('title', TegakiStrings.play); 6178 $T.on(btn, 'click', Tegaki.onReplayPlayPauseClick); 6179 cnt.appendChild(btn); 6180 6181 btn = $T.el('span'); 6182 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-to-start'; 6183 btn.setAttribute('title', TegakiStrings.rewind); 6184 $T.on(btn, 'click', Tegaki.onReplayRewindClick); 6185 cnt.appendChild(btn); 6186 6187 btn = $T.el('span'); 6188 btn.id = 'tegaki-replay-slower-btn'; 6189 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-bw'; 6190 btn.setAttribute('title', TegakiStrings.slower); 6191 $T.on(btn, 'click', Tegaki.onReplaySlowDownClick); 6192 cnt.appendChild(btn); 6193 6194 el = $T.el('span'); 6195 el.id = 'tegaki-replay-speed-lbl'; 6196 el.className = 'tegaki-menu-lbl'; 6197 el.textContent = '1.0'; 6198 cnt.appendChild(el); 6199 6200 btn = $T.el('span'); 6201 btn.id = 'tegaki-replay-faster-btn'; 6202 btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-fw'; 6203 btn.setAttribute('title', TegakiStrings.faster); 6204 $T.on(btn, 'click', Tegaki.onReplaySpeedUpClick); 6205 cnt.appendChild(btn); 6206 6207 el = $T.el('span'); 6208 el.id = 'tegaki-replay-now-lbl'; 6209 el.className = 'tegaki-menu-lbl'; 6210 el.textContent = '00:00'; 6211 cnt.appendChild(el); 6212 6213 el = $T.el('span'); 6214 el.id = 'tegaki-replay-end-lbl'; 6215 el.className = 'tegaki-menu-lbl'; 6216 el.textContent = '00:00'; 6217 cnt.appendChild(el); 6218 6219 return cnt; 6220 }, 6221 6222 buildLayerGridCell: function(layer) { 6223 var cnt, el, cell; 6224 6225 cnt = $T.el('div'); 6226 cnt.id = 'tegaki-layers-cell-' + layer.id; 6227 cnt.className = 'tegaki-layers-cell'; 6228 cnt.setAttribute('data-id', layer.id); 6229 cnt.draggable = true; 6230 cnt.setAttribute('data-id', layer.id); 6231 6232 $T.on(cnt, 'pointerdown', TegakiUI.onLayerSelectorPtrDown); 6233 $T.on(cnt, 'pointerup', Tegaki.onLayerSelectorClick); 6234 6235 $T.on(cnt, 'dragstart', TegakiUI.onLayerDragStart); 6236 $T.on(cnt, 'dragover', TegakiUI.onLayerDragOver); 6237 $T.on(cnt, 'drop', TegakiUI.onLayerDragDrop); 6238 $T.on(cnt, 'dragend', TegakiUI.onLayerDragEnd); 6239 $T.on(cnt, 'dragleave', TegakiUI.onLayerDragLeave); 6240 $T.on(cnt, 'dragexit', TegakiUI.onLayerDragLeave); 6241 6242 // visibility toggle 6243 cell = $T.el('div'); 6244 cell.className = 'tegaki-layers-cell-v'; 6245 6246 el = $T.el('span'); 6247 el.id = 'tegaki-layers-cb-v-' + layer.id; 6248 el.className = 'tegaki-ui-cb'; 6249 el.setAttribute('data-id', layer.id); 6250 el.title = TegakiStrings.toggleVisibility; 6251 $T.on(el, 'click', Tegaki.onLayerToggleVisibilityClick); 6252 6253 if (layer.visible) { 6254 el.className += ' tegaki-ui-cb-a'; 6255 } 6256 6257 cell.appendChild(el); 6258 cnt.appendChild(cell); 6259 6260 // preview 6261 cell = $T.el('div'); 6262 cell.className = 'tegaki-layers-cell-p'; 6263 6264 el = $T.el('canvas'); 6265 el.id = 'tegaki-layers-p-canvas-' + layer.id; 6266 el.className = 'tegaki-alpha-bg-xs'; 6267 [el.width, el.height] = TegakiUI.getLayerPreviewSize(); 6268 6269 cell.appendChild(el); 6270 cnt.appendChild(cell); 6271 6272 // name 6273 cell = $T.el('div'); 6274 cell.className = 'tegaki-layers-cell-n'; 6275 6276 el = $T.el('div'); 6277 el.id = 'tegaki-layer-name-' + layer.id; 6278 el.className = 'tegaki-ellipsis'; 6279 el.setAttribute('data-id', layer.id); 6280 el.textContent = layer.name; 6281 $T.on(el, 'dblclick', Tegaki.onLayerNameChangeClick); 6282 6283 cell.appendChild(el); 6284 cnt.appendChild(cell); 6285 6286 return cnt; 6287 }, 6288 6289 // --- 6290 6291 onLayerSelectorPtrDown: function(e) { 6292 if (e.pointerType === 'mouse') { 6293 if (this.hasAttribute('data-nodrag')) { 6294 this.removeAttribute('data-nodrag'); 6295 $T.on(this, 'dragstart', TegakiUI.onLayerDragStart); 6296 } 6297 } 6298 else if (!this.hasAttribute('data-nodrag')) { 6299 this.setAttribute('data-nodrag', 1); 6300 $T.off(this, 'dragstart', TegakiUI.onLayerDragStart); 6301 } 6302 }, 6303 6304 onLayerDragStart: function(e) { 6305 var el, id; 6306 6307 if (e.ctrlKey) { 6308 return; 6309 } 6310 6311 TegakiUI.draggedNode = null; 6312 6313 if (!$T.id('tegaki-layers-grid').children[1]) { 6314 e.preventDefault(); 6315 return; 6316 } 6317 6318 id = +e.target.getAttribute('data-id'); 6319 6320 el = $T.el('div'); 6321 el.className = 'tegaki-invis'; 6322 e.dataTransfer.setDragImage(el, 0, 0); 6323 e.dataTransfer.setData('text/plain', id); 6324 e.dataTransfer.effectAllowed = 'move'; 6325 6326 TegakiUI.draggedNode = e.target; 6327 6328 TegakiUI.updateLayersGridDragExt(true); 6329 }, 6330 6331 onLayerDragOver: function(e) { 6332 e.preventDefault(); 6333 6334 e.dataTransfer.dropEffect = 'move'; 6335 6336 TegakiUI.updateLayersGridDragEffect( 6337 e.target, 6338 +TegakiUI.draggedNode.getAttribute('data-id') 6339 ); 6340 }, 6341 6342 onLayerDragLeave: function(e) { 6343 TegakiUI.updateLayersGridDragEffect(); 6344 }, 6345 6346 onLayerDragEnd: function(e) { 6347 TegakiUI.draggedNode = null; 6348 TegakiUI.updateLayersGridDragExt(false); 6349 TegakiUI.updateLayersGridDragEffect(); 6350 }, 6351 6352 onLayerDragDrop: function(e) { 6353 var tgtId, srcId, belowPos; 6354 6355 e.preventDefault(); 6356 6357 TegakiUI.draggedNode = null; 6358 6359 [tgtId] = TegakiUI.layersGridFindDropTgt(e.target); 6360 srcId = +e.dataTransfer.getData('text/plain'); 6361 6362 TegakiUI.updateLayersGridDragEffect(e.target.parentNode); 6363 TegakiUI.updateLayersGridDragExt(false); 6364 6365 if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { 6366 return; 6367 } 6368 6369 if (!tgtId) { 6370 belowPos = Tegaki.layers.length; 6371 } 6372 else { 6373 belowPos = TegakiLayers.getLayerPosById(tgtId); 6374 } 6375 6376 if (!TegakiLayers.selectedLayersHas(srcId)) { 6377 Tegaki.setActiveLayer(srcId); 6378 } 6379 6380 Tegaki.moveSelectedLayers(belowPos); 6381 }, 6382 6383 updateLayersGridDragExt: function(flag) { 6384 var cnt, el; 6385 6386 cnt = $T.id('tegaki-layers-grid'); 6387 6388 if (!cnt.children[1]) { 6389 return; 6390 } 6391 6392 if (flag) { 6393 el = $T.el('div'); 6394 el.id = 'tegaki-layers-cell-dx'; 6395 el.draggable = true; 6396 $T.on(el, 'dragover', TegakiUI.onLayerDragOver); 6397 $T.on(el, 'drop', TegakiUI.onLayerDragDrop); 6398 cnt.parentNode.insertBefore(el, cnt); 6399 } 6400 else { 6401 if (el = $T.id('tegaki-layers-cell-dx')) { 6402 el.parentNode.removeChild(el); 6403 } 6404 } 6405 }, 6406 6407 updateLayersGridDragEffect: function(tgt, srcId) { 6408 var el, nodes, tgtId; 6409 6410 nodes = $T.cls('tegaki-layers-cell-d', $T.id('tegaki-ctrlgrp-layers')); 6411 6412 for (el of nodes) { 6413 el.classList.remove('tegaki-layers-cell-d'); 6414 } 6415 6416 if (!tgt || !srcId) { 6417 return; 6418 } 6419 6420 [tgtId, tgt] = TegakiUI.layersGridFindDropTgt(tgt); 6421 6422 if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { 6423 return; 6424 } 6425 6426 if (!tgt) { 6427 tgt = $T.id('tegaki-layers-grid'); 6428 } 6429 6430 tgt.classList.add('tegaki-layers-cell-d'); 6431 }, 6432 6433 layersGridFindDropTgt: function(tgt) { 6434 var tgtId, cnt; 6435 6436 tgtId = +tgt.getAttribute('data-id'); 6437 6438 cnt = $T.id('tegaki-ctrlgrp-layers'); 6439 6440 while (!tgt.draggable && tgt !== cnt) { 6441 tgt = tgt.parentNode; 6442 tgtId = +tgt.getAttribute('data-id'); 6443 } 6444 6445 if (tgt === cnt || !tgt.draggable) { 6446 return [0, null]; 6447 } 6448 6449 return [tgtId, tgt]; 6450 }, 6451 6452 layersGridCanDrop: function(tgtId, srcId) { 6453 var srcEl; 6454 6455 if (tgtId === srcId) { 6456 return false; 6457 } 6458 6459 srcEl = $T.id('tegaki-layers-cell-' + srcId); 6460 6461 if (!srcEl.previousElementSibling) { 6462 if (!tgtId) { 6463 return false; 6464 } 6465 } 6466 else if (+srcEl.previousElementSibling.getAttribute('data-id') === tgtId) { 6467 return false; 6468 } 6469 6470 return true; 6471 }, 6472 6473 // --- 6474 6475 setReplayMode: function(flag) { 6476 Tegaki.bg.classList[flag ? 'add' : 'remove']('tegaki-replay-mode'); 6477 }, 6478 6479 // --- 6480 6481 onToolChanged: function() { 6482 $T.id('tegaki-toolmode-bar').classList.remove('tegaki-hidden'); 6483 TegakiUI.updateToolSize(); 6484 TegakiUI.updateToolAlpha(); 6485 TegakiUI.updateToolFlow(); 6486 TegakiUI.updateToolModes(); 6487 }, 6488 6489 // --- 6490 6491 updateLayerAlphaOpt: function() { 6492 var el = $T.id('tegaki-layer-alpha-opt'); 6493 el.value = Math.round(Tegaki.activeLayer.alpha * 100); 6494 }, 6495 6496 updateLayerName: function(layer) { 6497 var el; 6498 6499 if (el = $T.id('tegaki-layer-name-' + layer.id)) { 6500 el.textContent = layer.name; 6501 } 6502 }, 6503 6504 updateLayerPreview: function(layer) { 6505 var canvas, ctx; 6506 6507 canvas = $T.id('tegaki-layers-p-canvas-' + layer.id); 6508 6509 if (!canvas) { 6510 return; 6511 } 6512 6513 ctx = TegakiUI.getLayerPreviewCtx(layer); 6514 6515 if (!ctx) { 6516 ctx = canvas.getContext('2d'); 6517 ctx.imageSmoothingEnabled = false; 6518 TegakiUI.setLayerPreviewCtx(layer, ctx); 6519 } 6520 6521 $T.clearCtx(ctx); 6522 ctx.drawImage(layer.canvas, 0, 0, canvas.width, canvas.height); 6523 }, 6524 6525 updateLayerPreviewSize: function(regen) { 6526 var el, layer, size; 6527 6528 size = TegakiUI.getLayerPreviewSize(); 6529 6530 for (layer of Tegaki.layers) { 6531 if (el = $T.id('tegaki-layers-p-canvas-' + layer.id)) { 6532 [el.width, el.height] = size; 6533 6534 if (regen) { 6535 TegakiUI.updateLayerPreview(layer); 6536 } 6537 } 6538 } 6539 }, 6540 6541 getLayerPreviewCtx: function(layer) { 6542 TegakiUI.layerPreviewCtxCache.get(layer); 6543 }, 6544 6545 setLayerPreviewCtx: function(layer, ctx) { 6546 TegakiUI.layerPreviewCtxCache.set(layer, ctx); 6547 }, 6548 6549 deleteLayerPreviewCtx: function(layer) { 6550 TegakiUI.layerPreviewCtxCache.delete(layer); 6551 }, 6552 6553 updateLayersGridClear: function() { 6554 $T.id('tegaki-layers-grid').innerHTML = ''; 6555 }, 6556 6557 updateLayersGrid: function() { 6558 var layer, el, frag, cnt; 6559 6560 frag = $T.frag(); 6561 6562 for (layer of Tegaki.layers) { 6563 el = TegakiUI.buildLayerGridCell(layer); 6564 frag.insertBefore(el, frag.firstElementChild); 6565 } 6566 6567 TegakiUI.updateLayersGridClear(); 6568 6569 cnt.appendChild(frag); 6570 }, 6571 6572 updateLayersGridActive: function(layerId) { 6573 var el; 6574 6575 el = $T.cls('tegaki-layers-cell-a', $T.id('tegaki-layers-grid'))[0]; 6576 6577 if (el) { 6578 el.classList.remove('tegaki-layers-cell-a'); 6579 } 6580 6581 el = $T.id('tegaki-layers-cell-' + layerId); 6582 6583 if (el) { 6584 el.classList.add('tegaki-layers-cell-a'); 6585 } 6586 6587 TegakiUI.updateLayerAlphaOpt(); 6588 }, 6589 6590 updateLayersGridAdd: function(layer, aboveId) { 6591 var el, cnt, ref; 6592 6593 el = TegakiUI.buildLayerGridCell(layer); 6594 6595 cnt = $T.id('tegaki-layers-grid'); 6596 6597 if (aboveId) { 6598 ref = $T.id('tegaki-layers-cell-' + aboveId); 6599 } 6600 else { 6601 ref = null; 6602 } 6603 6604 cnt.insertBefore(el, ref); 6605 }, 6606 6607 updateLayersGridRemove: function(id) { 6608 var el; 6609 6610 if (el = $T.id('tegaki-layers-cell-' + id)) { 6611 el.parentNode.removeChild(el); 6612 } 6613 }, 6614 6615 updayeLayersGridOrder: function() { 6616 var layer, cnt, el; 6617 6618 cnt = $T.id('tegaki-layers-grid'); 6619 6620 for (layer of Tegaki.layers) { 6621 el = $T.id('tegaki-layers-cell-' + layer.id); 6622 cnt.insertBefore(el, cnt.firstElementChild); 6623 } 6624 }, 6625 6626 updateLayersGridVisibility: function(id, flag) { 6627 var el; 6628 6629 el = $T.id('tegaki-layers-cb-v-' + id); 6630 6631 if (!el) { 6632 return; 6633 } 6634 6635 if (flag) { 6636 el.classList.add('tegaki-ui-cb-a'); 6637 } 6638 else { 6639 el.classList.remove('tegaki-ui-cb-a'); 6640 } 6641 }, 6642 6643 updateLayersGridSelectedClear: function() { 6644 var layer, el; 6645 6646 for (layer of Tegaki.layers) { 6647 if (el = $T.id('tegaki-layers-cell-' + layer.id)) { 6648 el.classList.remove('tegaki-layers-cell-s'); 6649 } 6650 } 6651 }, 6652 6653 updateLayersGridSelectedSet: function(id, flag) { 6654 var el; 6655 6656 if (el = $T.id('tegaki-layers-cell-' + id)) { 6657 if (flag) { 6658 el.classList.add('tegaki-layers-cell-s'); 6659 } 6660 else { 6661 el.classList.remove('tegaki-layers-cell-s'); 6662 } 6663 } 6664 }, 6665 6666 updateToolSize: function() { 6667 var el = $T.id('tegaki-ctrlgrp-size'); 6668 6669 if (Tegaki.tool.useSize) { 6670 el.classList.remove('tegaki-hidden'); 6671 6672 $T.id('tegaki-size-lbl').value = Tegaki.tool.size; 6673 $T.id('tegaki-size').value = Tegaki.tool.size; 6674 } 6675 else { 6676 el.classList.add('tegaki-hidden'); 6677 } 6678 }, 6679 6680 updateToolAlpha: function() { 6681 var val, el = $T.id('tegaki-ctrlgrp-alpha'); 6682 6683 if (Tegaki.tool.useAlpha) { 6684 el.classList.remove('tegaki-hidden'); 6685 6686 val = Math.round(Tegaki.tool.alpha * 100); 6687 $T.id('tegaki-alpha-lbl').value = val; 6688 $T.id('tegaki-alpha').value = val; 6689 } 6690 else { 6691 el.classList.add('tegaki-hidden'); 6692 } 6693 }, 6694 6695 updateToolFlow: function() { 6696 var val, el = $T.id('tegaki-ctrlgrp-flow'); 6697 6698 if (Tegaki.tool.useFlow) { 6699 el.classList.remove('tegaki-hidden'); 6700 6701 val = Math.round(Tegaki.tool.flow * 100); 6702 $T.id('tegaki-flow-lbl').value = val; 6703 $T.id('tegaki-flow').value = val; 6704 } 6705 else { 6706 el.classList.add('tegaki-hidden'); 6707 } 6708 }, 6709 6710 updateToolDynamics: function() { 6711 var ctrl, cb; 6712 6713 ctrl = $T.id('tegaki-tool-mode-dynamics'); 6714 6715 if (!Tegaki.tool.usesDynamics()) { 6716 ctrl.classList.add('tegaki-hidden'); 6717 } 6718 else { 6719 cb = $T.id('tegaki-tool-mode-dynamics-size'); 6720 6721 if (Tegaki.tool.useSizeDynamics) { 6722 if (Tegaki.tool.sizeDynamicsEnabled) { 6723 cb.classList.add('tegaki-sw-btn-a'); 6724 } 6725 else { 6726 cb.classList.remove('tegaki-sw-btn-a'); 6727 } 6728 6729 cb.classList.remove('tegaki-hidden'); 6730 } 6731 else { 6732 cb.classList.add('tegaki-hidden'); 6733 } 6734 6735 cb = $T.id('tegaki-tool-mode-dynamics-alpha'); 6736 6737 if (Tegaki.tool.useAlphaDynamics) { 6738 if (Tegaki.tool.alphaDynamicsEnabled) { 6739 cb.classList.add('tegaki-sw-btn-a'); 6740 } 6741 else { 6742 cb.classList.remove('tegaki-sw-btn-a'); 6743 } 6744 6745 cb.classList.remove('tegaki-hidden'); 6746 } 6747 else { 6748 cb.classList.add('tegaki-hidden'); 6749 } 6750 6751 cb = $T.id('tegaki-tool-mode-dynamics-flow'); 6752 6753 if (Tegaki.tool.useFlowDynamics) { 6754 if (Tegaki.tool.flowDynamicsEnabled) { 6755 cb.classList.add('tegaki-sw-btn-a'); 6756 } 6757 else { 6758 cb.classList.remove('tegaki-sw-btn-a'); 6759 } 6760 6761 cb.classList.remove('tegaki-hidden'); 6762 } 6763 else { 6764 cb.classList.add('tegaki-hidden'); 6765 } 6766 6767 ctrl.classList.remove('tegaki-hidden'); 6768 } 6769 }, 6770 6771 updateToolShape: function() { 6772 var tipId, ctrl, cnt, btn, tipList; 6773 6774 ctrl = $T.id('tegaki-tool-mode-tip'); 6775 6776 if (!Tegaki.tool.tipList) { 6777 ctrl.classList.add('tegaki-hidden'); 6778 } 6779 else { 6780 tipList = Tegaki.tool.tipList; 6781 6782 cnt = $T.id('tegaki-tool-mode-tip-ctrl'); 6783 6784 cnt.innerHTML = ''; 6785 6786 for (tipId = 0; tipId < tipList.length; ++tipId) { 6787 btn = $T.el('span'); 6788 btn.id = 'tegaki-tool-mode-tip-' + tipId; 6789 btn.className = 'tegaki-sw-btn'; 6790 btn.setAttribute('data-id', tipId); 6791 btn.textContent = TegakiStrings[tipList[tipId]]; 6792 6793 $T.on(btn, 'mousedown', Tegaki.onToolTipClick); 6794 6795 cnt.appendChild(btn); 6796 6797 if (Tegaki.tool.tipId === tipId) { 6798 btn.classList.add('tegaki-sw-btn-a'); 6799 } 6800 } 6801 6802 ctrl.classList.remove('tegaki-hidden'); 6803 } 6804 }, 6805 6806 updateToolPreserveAlpha: function() { 6807 var cb, ctrl; 6808 6809 ctrl = $T.id('tegaki-tool-mode-mask'); 6810 6811 if (!Tegaki.tool.usePreserveAlpha) { 6812 ctrl.classList.add('tegaki-hidden'); 6813 } 6814 else { 6815 cb = $T.id('tegaki-tool-mode-mask-alpha'); 6816 6817 if (Tegaki.tool.preserveAlphaEnabled) { 6818 cb.classList.add('tegaki-sw-btn-a'); 6819 } 6820 else { 6821 cb.classList.remove('tegaki-sw-btn-a'); 6822 } 6823 6824 ctrl.classList.remove('tegaki-hidden'); 6825 } 6826 }, 6827 6828 updateToolModes: function() { 6829 var el, flag; 6830 6831 TegakiUI.updateToolShape(); 6832 TegakiUI.updateToolDynamics(); 6833 TegakiUI.updateToolPreserveAlpha(); 6834 6835 flag = false; 6836 6837 for (el of $T.id('tegaki-toolmode-bar').children) { 6838 if (!flag && !el.classList.contains('tegaki-hidden')) { 6839 el.classList.add('tegaki-ui-borderless'); 6840 flag = true; 6841 } 6842 else { 6843 el.classList.remove('tegaki-ui-borderless'); 6844 } 6845 } 6846 }, 6847 6848 updateUndoRedo: function(undoSize, redoSize) { 6849 var u, r; 6850 6851 if (Tegaki.replayMode) { 6852 return; 6853 } 6854 6855 u = $T.id('tegaki-undo-btn').classList; 6856 r = $T.id('tegaki-redo-btn').classList; 6857 6858 if (undoSize) { 6859 if (u.contains('tegaki-disabled')) { 6860 u.remove('tegaki-disabled'); 6861 } 6862 } 6863 else { 6864 if (!u.contains('tegaki-disabled')) { 6865 u.add('tegaki-disabled'); 6866 } 6867 } 6868 6869 if (redoSize) { 6870 if (r.contains('tegaki-disabled')) { 6871 r.remove('tegaki-disabled'); 6872 } 6873 } 6874 else { 6875 if (!r.contains('tegaki-disabled')) { 6876 r.add('tegaki-disabled'); 6877 } 6878 } 6879 }, 6880 6881 updateZoomLevel: function() { 6882 $T.id('tegaki-zoom-lbl').textContent = (Tegaki.zoomFactor * 100) + '%'; 6883 6884 if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel >= Tegaki.zoomFactorList.length) { 6885 $T.id('tegaki-zoomin-btn').classList.add('tegaki-disabled'); 6886 } 6887 else { 6888 $T.id('tegaki-zoomin-btn').classList.remove('tegaki-disabled'); 6889 } 6890 6891 if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel <= 0) { 6892 $T.id('tegaki-zoomout-btn').classList.add('tegaki-disabled'); 6893 } 6894 else { 6895 $T.id('tegaki-zoomout-btn').classList.remove('tegaki-disabled'); 6896 } 6897 }, 6898 6899 updateColorPalette: function() { 6900 var el, nodes, id; 6901 6902 id = Tegaki.colorPaletteId; 6903 6904 nodes = $T.cls('tegaki-color-grid', $T.id('tegaki-color-grids')); 6905 6906 for (el of nodes) { 6907 if (+el.getAttribute('data-id') === id) { 6908 el.classList.remove('tegaki-hidden'); 6909 } 6910 else { 6911 el.classList.add('tegaki-hidden'); 6912 } 6913 } 6914 6915 el = $T.id('tegaki-palette-prev-btn'); 6916 6917 if (id === 0) { 6918 el.classList.add('tegaki-disabled'); 6919 } 6920 else { 6921 el.classList.remove('tegaki-disabled'); 6922 } 6923 6924 el = $T.id('tegaki-palette-next-btn'); 6925 6926 if (id === TegakiColorPalettes.length - 1) { 6927 el.classList.add('tegaki-disabled'); 6928 } 6929 else { 6930 el.classList.remove('tegaki-disabled'); 6931 } 6932 }, 6933 6934 updateReplayTime: function(full) { 6935 var now, end, r = Tegaki.replayViewer; 6936 6937 now = r.getCurrentPos(); 6938 6939 end = r.getDuration(); 6940 6941 if (now > end) { 6942 now = end; 6943 } 6944 6945 $T.id('tegaki-replay-now-lbl').textContent = $T.msToHms(now); 6946 6947 if (full) { 6948 $T.id('tegaki-replay-end-lbl').textContent = $T.msToHms(end); 6949 } 6950 }, 6951 6952 updateReplayControls: function() { 6953 TegakiUI.updateReplayGapless(); 6954 TegakiUI.updateReplayPlayPause(); 6955 TegakiUI.updateReplaySpeed(); 6956 }, 6957 6958 updateReplayGapless: function() { 6959 var el, r = Tegaki.replayViewer; 6960 6961 el = $T.id('tegaki-replay-gapless-cb'); 6962 6963 if (r.gapless) { 6964 el.classList.add('tegaki-ui-cb-a'); 6965 } 6966 else { 6967 el.classList.remove('tegaki-ui-cb-a'); 6968 } 6969 }, 6970 6971 updateReplayPlayPause: function() { 6972 var el, r = Tegaki.replayViewer; 6973 6974 el = $T.id('tegaki-replay-play-btn'); 6975 6976 if (r.playing) { 6977 el.classList.remove('tegaki-play'); 6978 el.classList.add('tegaki-pause'); 6979 el.setAttribute('title', TegakiStrings.pause); 6980 } 6981 else { 6982 el.classList.add('tegaki-play'); 6983 el.classList.remove('tegaki-pause'); 6984 el.setAttribute('title', TegakiStrings.play); 6985 6986 if (r.getCurrentPos() < r.getDuration()) { 6987 el.classList.remove('tegaki-disabled'); 6988 } 6989 else { 6990 el.classList.add('tegaki-disabled'); 6991 } 6992 } 6993 }, 6994 6995 updateReplaySpeed: function() { 6996 var el, r = Tegaki.replayViewer; 6997 6998 $T.id('tegaki-replay-speed-lbl').textContent = r.speed.toFixed(1); 6999 7000 el = $T.id('tegaki-replay-slower-btn'); 7001 7002 if (r.speedIndex === 0) { 7003 el.classList.add('tegaki-disabled'); 7004 } 7005 else { 7006 el.classList.remove('tegaki-disabled'); 7007 } 7008 7009 el = $T.id('tegaki-replay-faster-btn'); 7010 7011 if (r.speedIndex === r.speedList.length - 1) { 7012 el.classList.add('tegaki-disabled'); 7013 } 7014 else { 7015 el.classList.remove('tegaki-disabled'); 7016 } 7017 }, 7018 7019 enableReplayControls: function(flag) { 7020 if (flag) { 7021 $T.id('tegaki-replay-controls').classList.remove('tegaki-hidden'); 7022 } 7023 else { 7024 $T.id('tegaki-replay-controls').classList.add('tegaki-hidden'); 7025 } 7026 }, 7027 7028 setRecordingStatus: function(flag) { 7029 var el = $T.id('tegaki-status-replay'); 7030 7031 if (flag) { 7032 el.classList.remove('tegaki-hidden'); 7033 } 7034 else { 7035 el.classList.add('tegaki-hidden'); 7036 } 7037 } 7038 }; 7039 /*! UZIP.js, © 2018 Photopea, MIT License */ 7040 7041 var UZIP = {}; 7042 if(typeof module == "object") module.exports = UZIP; 7043 7044 UZIP.inflateRaw = function(file, buf) { return UZIP.F.inflate(file, buf); } 7045 7046 UZIP.deflateRaw = function(data, opts) { 7047 if(opts==null) opts={level:6}; 7048 var buf=new Uint8Array(50+Math.floor(data.length*1.1)); 7049 var off = UZIP.F.deflateRaw(data, buf, off, opts.level); 7050 return new Uint8Array(buf.buffer, 0, off); 7051 } 7052 7053 UZIP.bin = { 7054 readUshort : function(buff,p) { return (buff[p]) | (buff[p+1]<<8); }, 7055 writeUshort: function(buff,p,n){ buff[p] = (n)&255; buff[p+1] = (n>>8)&255; }, 7056 readUint : function(buff,p) { return (buff[p+3]*(256*256*256)) + ((buff[p+2]<<16) | (buff[p+1]<< 8) | buff[p]); }, 7057 writeUint : function(buff,p,n){ buff[p]=n&255; buff[p+1]=(n>>8)&255; buff[p+2]=(n>>16)&255; buff[p+3]=(n>>24)&255; }, 7058 readASCII : function(buff,p,l){ var s = ""; for(var i=0; i<l; i++) s += String.fromCharCode(buff[p+i]); return s; }, 7059 writeASCII : function(data,p,s){ for(var i=0; i<s.length; i++) data[p+i] = s.charCodeAt(i); }, 7060 pad : function(n) { return n.length < 2 ? "0" + n : n; }, 7061 readUTF8 : function(buff, p, l) { 7062 var s = "", ns; 7063 for(var i=0; i<l; i++) s += "%" + UZIP.bin.pad(buff[p+i].toString(16)); 7064 try { ns = decodeURIComponent(s); } 7065 catch(e) { return UZIP.bin.readASCII(buff, p, l); } 7066 return ns; 7067 }, 7068 writeUTF8 : function(buff, p, str) { 7069 var strl = str.length, i=0; 7070 for(var ci=0; ci<strl; ci++) 7071 { 7072 var code = str.charCodeAt(ci); 7073 if ((code&(0xffffffff-(1<< 7)+1))==0) { buff[p+i] = ( code ); i++; } 7074 else if((code&(0xffffffff-(1<<11)+1))==0) { buff[p+i] = (192|(code>> 6)); buff[p+i+1] = (128|((code>> 0)&63)); i+=2; } 7075 else if((code&(0xffffffff-(1<<16)+1))==0) { buff[p+i] = (224|(code>>12)); buff[p+i+1] = (128|((code>> 6)&63)); buff[p+i+2] = (128|((code>>0)&63)); i+=3; } 7076 else if((code&(0xffffffff-(1<<21)+1))==0) { buff[p+i] = (240|(code>>18)); buff[p+i+1] = (128|((code>>12)&63)); buff[p+i+2] = (128|((code>>6)&63)); buff[p+i+3] = (128|((code>>0)&63)); i+=4; } 7077 else throw "e"; 7078 } 7079 return i; 7080 }, 7081 sizeUTF8 : function(str) { 7082 var strl = str.length, i=0; 7083 for(var ci=0; ci<strl; ci++) 7084 { 7085 var code = str.charCodeAt(ci); 7086 if ((code&(0xffffffff-(1<< 7)+1))==0) { i++ ; } 7087 else if((code&(0xffffffff-(1<<11)+1))==0) { i+=2; } 7088 else if((code&(0xffffffff-(1<<16)+1))==0) { i+=3; } 7089 else if((code&(0xffffffff-(1<<21)+1))==0) { i+=4; } 7090 else throw "e"; 7091 } 7092 return i; 7093 } 7094 } 7095 7096 7097 7098 7099 7100 7101 7102 UZIP.F = {}; 7103 7104 UZIP.F.deflateRaw = function(data, out, opos, lvl) { 7105 var opts = [ 7106 /* 7107 ush good_length; /* reduce lazy search above this match length 7108 ush max_lazy; /* do not perform lazy search above this match length 7109 ush nice_length; /* quit search above this match length 7110 */ 7111 /* good lazy nice chain */ 7112 /* 0 */ [ 0, 0, 0, 0,0], /* store only */ 7113 /* 1 */ [ 4, 4, 8, 4,0], /* max speed, no lazy matches */ 7114 /* 2 */ [ 4, 5, 16, 8,0], 7115 /* 3 */ [ 4, 6, 16, 16,0], 7116 7117 /* 4 */ [ 4, 10, 16, 32,0], /* lazy matches */ 7118 /* 5 */ [ 8, 16, 32, 32,0], 7119 /* 6 */ [ 8, 16, 128, 128,0], 7120 /* 7 */ [ 8, 32, 128, 256,0], 7121 /* 8 */ [32, 128, 258, 1024,1], 7122 /* 9 */ [32, 258, 258, 4096,1]]; /* max compression */ 7123 7124 var opt = opts[lvl]; 7125 7126 7127 var U = UZIP.F.U, goodIndex = UZIP.F._goodIndex, hash = UZIP.F._hash, putsE = UZIP.F._putsE; 7128 var i = 0, pos = opos<<3, cvrd = 0, dlen = data.length; 7129 7130 if(lvl==0) { 7131 while(i<dlen) { var len = Math.min(0xffff, dlen-i); 7132 putsE(out, pos, (i+len==dlen ? 1 : 0)); pos = UZIP.F._copyExact(data, i, len, out, pos+8); i += len; } 7133 return pos>>>3; 7134 } 7135 7136 var lits = U.lits, strt=U.strt, prev=U.prev, li=0, lc=0, bs=0, ebits=0, c=0, nc=0; // last_item, literal_count, block_start 7137 if(dlen>2) { nc=UZIP.F._hash(data,0); strt[nc]=0; } 7138 var nmch=0,nmci=0; 7139 7140 for(i=0; i<dlen; i++) { 7141 c = nc; 7142 //* 7143 if(i+1<dlen-2) { 7144 nc = UZIP.F._hash(data, i+1); 7145 var ii = ((i+1)&0x7fff); 7146 prev[ii]=strt[nc]; 7147 strt[nc]=ii; 7148 } //*/ 7149 if(cvrd<=i) { 7150 if((li>14000 || lc>26697) && (dlen-i)>100) { 7151 if(cvrd<i) { lits[li]=i-cvrd; li+=2; cvrd=i; } 7152 pos = UZIP.F._writeBlock(((i==dlen-1) || (cvrd==dlen))?1:0, lits, li, ebits, data,bs,i-bs, out, pos); li=lc=ebits=0; bs=i; 7153 } 7154 7155 var mch = 0; 7156 //if(nmci==i) mch= nmch; else 7157 if(i<dlen-2) mch = UZIP.F._bestMatch(data, i, prev, c, Math.min(opt[2],dlen-i), opt[3]); 7158 /* 7159 if(mch!=0 && opt[4]==1 && (mch>>>16)<opt[1] && i+1<dlen-2) { 7160 nmch = UZIP.F._bestMatch(data, i+1, prev, nc, opt[2], opt[3]); nmci=i+1; 7161 //var mch2 = UZIP.F._bestMatch(data, i+2, prev, nnc); //nmci=i+1; 7162 if((nmch>>>16)>(mch>>>16)) mch=0; 7163 }//*/ 7164 var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; 7165 if(mch!=0) { 7166 var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; 7167 var lgi = goodIndex(len, U.of0); U.lhst[257+lgi]++; 7168 var dgi = goodIndex(dst, U.df0); U.dhst[ dgi]++; ebits += U.exb[lgi] + U.dxb[dgi]; 7169 lits[li] = (len<<23)|(i-cvrd); lits[li+1] = (dst<<16)|(lgi<<8)|dgi; li+=2; 7170 cvrd = i + len; 7171 } 7172 else { U.lhst[data[i]]++; } 7173 lc++; 7174 } 7175 } 7176 if(bs!=i || data.length==0) { 7177 if(cvrd<i) { lits[li]=i-cvrd; li+=2; cvrd=i; } 7178 pos = UZIP.F._writeBlock(1, lits, li, ebits, data,bs,i-bs, out, pos); li=0; lc=0; li=lc=ebits=0; bs=i; 7179 } 7180 while((pos&7)!=0) pos++; 7181 return pos>>>3; 7182 } 7183 UZIP.F._bestMatch = function(data, i, prev, c, nice, chain) { 7184 var ci = (i&0x7fff), pi=prev[ci]; 7185 //console.log("----", i); 7186 var dif = ((ci-pi + (1<<15)) & 0x7fff); if(pi==ci || c!=UZIP.F._hash(data,i-dif)) return 0; 7187 var tl=0, td=0; // top length, top distance 7188 var dlim = Math.min(0x7fff, i); 7189 while(dif<=dlim && --chain!=0 && pi!=ci /*&& c==UZIP.F._hash(data,i-dif)*/) { 7190 if(tl==0 || (data[i+tl]==data[i+tl-dif])) { 7191 var cl = UZIP.F._howLong(data, i, dif); 7192 if(cl>tl) { 7193 tl=cl; td=dif; if(tl>=nice) break; //* 7194 if(dif+2<cl) cl = dif+2; 7195 var maxd = 0; // pi does not point to the start of the word 7196 for(var j=0; j<cl-2; j++) { 7197 var ei = (i-dif+j+ (1<<15)) & 0x7fff; 7198 var li = prev[ei]; 7199 var curd = (ei-li + (1<<15)) & 0x7fff; 7200 if(curd>maxd) { maxd=curd; pi = ei; } 7201 } //*/ 7202 } 7203 } 7204 7205 ci=pi; pi = prev[ci]; 7206 dif += ((ci-pi + (1<<15)) & 0x7fff); 7207 } 7208 return (tl<<16)|td; 7209 } 7210 UZIP.F._howLong = function(data, i, dif) { 7211 if(data[i]!=data[i-dif] || data[i+1]!=data[i+1-dif] || data[i+2]!=data[i+2-dif]) return 0; 7212 var oi=i, l = Math.min(data.length, i+258); i+=3; 7213 //while(i+4<l && data[i]==data[i-dif] && data[i+1]==data[i+1-dif] && data[i+2]==data[i+2-dif] && data[i+3]==data[i+3-dif]) i+=4; 7214 while(i<l && data[i]==data[i-dif]) i++; 7215 return i-oi; 7216 } 7217 UZIP.F._hash = function(data, i) { 7218 return (((data[i]<<8) | data[i+1])+(data[i+2]<<4))&0xffff; 7219 //var hash_shift = 0, hash_mask = 255; 7220 //var h = data[i+1] % 251; 7221 //h = (((h << 8) + data[i+2]) % 251); 7222 //h = (((h << 8) + data[i+2]) % 251); 7223 //h = ((h<<hash_shift) ^ (c) ) & hash_mask; 7224 //return h | (data[i]<<8); 7225 //return (data[i] | (data[i+1]<<8)); 7226 } 7227 //UZIP.___toth = 0; 7228 UZIP.saved = 0; 7229 UZIP.F._writeBlock = function(BFINAL, lits, li, ebits, data,o0,l0, out, pos) { 7230 var U = UZIP.F.U, putsF = UZIP.F._putsF, putsE = UZIP.F._putsE; 7231 7232 //* 7233 var T, ML, MD, MH, numl, numd, numh, lset, dset; U.lhst[256]++; 7234 T = UZIP.F.getTrees(); ML=T[0]; MD=T[1]; MH=T[2]; numl=T[3]; numd=T[4]; numh=T[5]; lset=T[6]; dset=T[7]; 7235 7236 var cstSize = (((pos+3)&7)==0 ? 0 : 8-((pos+3)&7)) + 32 + (l0<<3); 7237 var fxdSize = ebits + UZIP.F.contSize(U.fltree, U.lhst) + UZIP.F.contSize(U.fdtree, U.dhst); 7238 var dynSize = ebits + UZIP.F.contSize(U.ltree , U.lhst) + UZIP.F.contSize(U.dtree , U.dhst); 7239 dynSize += 14 + 3*numh + UZIP.F.contSize(U.itree, U.ihst) + (U.ihst[16]*2 + U.ihst[17]*3 + U.ihst[18]*7); 7240 7241 for(var j=0; j<286; j++) U.lhst[j]=0; for(var j=0; j<30; j++) U.dhst[j]=0; for(var j=0; j<19; j++) U.ihst[j]=0; 7242 //*/ 7243 var BTYPE = (cstSize<fxdSize && cstSize<dynSize) ? 0 : ( fxdSize<dynSize ? 1 : 2 ); 7244 putsF(out, pos, BFINAL); putsF(out, pos+1, BTYPE); pos+=3; 7245 7246 var opos = pos; 7247 if(BTYPE==0) { 7248 while((pos&7)!=0) pos++; 7249 pos = UZIP.F._copyExact(data, o0, l0, out, pos); 7250 } 7251 else { 7252 var ltree, dtree; 7253 if(BTYPE==1) { ltree=U.fltree; dtree=U.fdtree; } 7254 if(BTYPE==2) { 7255 UZIP.F.makeCodes(U.ltree, ML); UZIP.F.revCodes(U.ltree, ML); 7256 UZIP.F.makeCodes(U.dtree, MD); UZIP.F.revCodes(U.dtree, MD); 7257 UZIP.F.makeCodes(U.itree, MH); UZIP.F.revCodes(U.itree, MH); 7258 7259 ltree = U.ltree; dtree = U.dtree; 7260 7261 putsE(out, pos,numl-257); pos+=5; // 286 7262 putsE(out, pos,numd- 1); pos+=5; // 30 7263 putsE(out, pos,numh- 4); pos+=4; // 19 7264 7265 for(var i=0; i<numh; i++) putsE(out, pos+i*3, U.itree[(U.ordr[i]<<1)+1]); pos+=3* numh; 7266 pos = UZIP.F._codeTiny(lset, U.itree, out, pos); 7267 pos = UZIP.F._codeTiny(dset, U.itree, out, pos); 7268 } 7269 7270 var off=o0; 7271 for(var si=0; si<li; si+=2) { 7272 var qb=lits[si], len=(qb>>>23), end = off+(qb&((1<<23)-1)); 7273 while(off<end) pos = UZIP.F._writeLit(data[off++], ltree, out, pos); 7274 7275 if(len!=0) { 7276 var qc = lits[si+1], dst=(qc>>16), lgi=(qc>>8)&255, dgi=(qc&255); 7277 pos = UZIP.F._writeLit(257+lgi, ltree, out, pos); 7278 putsE(out, pos, len-U.of0[lgi]); pos+=U.exb[lgi]; 7279 7280 pos = UZIP.F._writeLit(dgi, dtree, out, pos); 7281 putsF(out, pos, dst-U.df0[dgi]); pos+=U.dxb[dgi]; off+=len; 7282 } 7283 } 7284 pos = UZIP.F._writeLit(256, ltree, out, pos); 7285 } 7286 //console.log(pos-opos, fxdSize, dynSize, cstSize); 7287 return pos; 7288 } 7289 UZIP.F._copyExact = function(data,off,len,out,pos) { 7290 var p8 = (pos>>>3); 7291 out[p8]=(len); out[p8+1]=(len>>>8); out[p8+2]=255-out[p8]; out[p8+3]=255-out[p8+1]; p8+=4; 7292 out.set(new Uint8Array(data.buffer, off, len), p8); 7293 //for(var i=0; i<len; i++) out[p8+i]=data[off+i]; 7294 return pos + ((len+4)<<3); 7295 } 7296 /* 7297 Interesting facts: 7298 - decompressed block can have bytes, which do not occur in a Huffman tree (copied from the previous block by reference) 7299 */ 7300 7301 UZIP.F.getTrees = function() { 7302 var U = UZIP.F.U; 7303 var ML = UZIP.F._hufTree(U.lhst, U.ltree, 15); 7304 var MD = UZIP.F._hufTree(U.dhst, U.dtree, 15); 7305 var lset = [], numl = UZIP.F._lenCodes(U.ltree, lset); 7306 var dset = [], numd = UZIP.F._lenCodes(U.dtree, dset); 7307 for(var i=0; i<lset.length; i+=2) U.ihst[lset[i]]++; 7308 for(var i=0; i<dset.length; i+=2) U.ihst[dset[i]]++; 7309 var MH = UZIP.F._hufTree(U.ihst, U.itree, 7); 7310 var numh = 19; while(numh>4 && U.itree[(U.ordr[numh-1]<<1)+1]==0) numh--; 7311 return [ML, MD, MH, numl, numd, numh, lset, dset]; 7312 } 7313 UZIP.F.getSecond= function(a) { var b=[]; for(var i=0; i<a.length; i+=2) b.push (a[i+1]); return b; } 7314 UZIP.F.nonZero = function(a) { var b= ""; for(var i=0; i<a.length; i+=2) if(a[i+1]!=0)b+=(i>>1)+","; return b; } 7315 UZIP.F.contSize = function(tree, hst) { var s=0; for(var i=0; i<hst.length; i++) s+= hst[i]*tree[(i<<1)+1]; return s; } 7316 UZIP.F._codeTiny = function(set, tree, out, pos) { 7317 for(var i=0; i<set.length; i+=2) { 7318 var l = set[i], rst = set[i+1]; //console.log(l, pos, tree[(l<<1)+1]); 7319 pos = UZIP.F._writeLit(l, tree, out, pos); 7320 var rsl = l==16 ? 2 : (l==17 ? 3 : 7); 7321 if(l>15) { UZIP.F._putsE(out, pos, rst, rsl); pos+=rsl; } 7322 } 7323 return pos; 7324 } 7325 UZIP.F._lenCodes = function(tree, set) { 7326 var len=tree.length; while(len!=2 && tree[len-1]==0) len-=2; // when no distances, keep one code with length 0 7327 for(var i=0; i<len; i+=2) { 7328 var l = tree[i+1], nxt = (i+3<len ? tree[i+3]:-1), nnxt = (i+5<len ? tree[i+5]:-1), prv = (i==0 ? -1 : tree[i-1]); 7329 if(l==0 && nxt==l && nnxt==l) { 7330 var lz = i+5; 7331 while(lz+2<len && tree[lz+2]==l) lz+=2; 7332 var zc = Math.min((lz+1-i)>>>1, 138); 7333 if(zc<11) set.push(17, zc-3); 7334 else set.push(18, zc-11); 7335 i += zc*2-2; 7336 } 7337 else if(l==prv && nxt==l && nnxt==l) { 7338 var lz = i+5; 7339 while(lz+2<len && tree[lz+2]==l) lz+=2; 7340 var zc = Math.min((lz+1-i)>>>1, 6); 7341 set.push(16, zc-3); 7342 i += zc*2-2; 7343 } 7344 else set.push(l, 0); 7345 } 7346 return len>>>1; 7347 } 7348 UZIP.F._hufTree = function(hst, tree, MAXL) { 7349 var list=[], hl = hst.length, tl=tree.length, i=0; 7350 for(i=0; i<tl; i+=2) { tree[i]=0; tree[i+1]=0; } 7351 for(i=0; i<hl; i++) if(hst[i]!=0) list.push({lit:i, f:hst[i]}); 7352 var end = list.length, l2=list.slice(0); 7353 if(end==0) return 0; // empty histogram (usually for dist) 7354 if(end==1) { var lit=list[0].lit, l2=lit==0?1:0; tree[(lit<<1)+1]=1; tree[(l2<<1)+1]=1; return 1; } 7355 list.sort(function(a,b){return a.f-b.f;}); 7356 var a=list[0], b=list[1], i0=0, i1=1, i2=2; list[0]={lit:-1,f:a.f+b.f,l:a,r:b,d:0}; 7357 while(i1!=end-1) { 7358 if(i0!=i1 && (i2==end || list[i0].f<list[i2].f)) { a=list[i0++]; } else { a=list[i2++]; } 7359 if(i0!=i1 && (i2==end || list[i0].f<list[i2].f)) { b=list[i0++]; } else { b=list[i2++]; } 7360 list[i1++]={lit:-1,f:a.f+b.f, l:a,r:b}; 7361 } 7362 var maxl = UZIP.F.setDepth(list[i1-1], 0); 7363 if(maxl>MAXL) { UZIP.F.restrictDepth(l2, MAXL, maxl); maxl = MAXL; } 7364 for(i=0; i<end; i++) tree[(l2[i].lit<<1)+1]=l2[i].d; 7365 return maxl; 7366 } 7367 7368 UZIP.F.setDepth = function(t, d) { 7369 if(t.lit!=-1) { t.d=d; return d; } 7370 return Math.max( UZIP.F.setDepth(t.l, d+1), UZIP.F.setDepth(t.r, d+1) ); 7371 } 7372 7373 UZIP.F.restrictDepth = function(dps, MD, maxl) { 7374 var i=0, bCost=1<<(maxl-MD), dbt=0; 7375 dps.sort(function(a,b){return b.d==a.d ? a.f-b.f : b.d-a.d;}); 7376 7377 for(i=0; i<dps.length; i++) if(dps[i].d>MD) { var od=dps[i].d; dps[i].d=MD; dbt+=bCost-(1<<(maxl-od)); } else break; 7378 dbt = dbt>>>(maxl-MD); 7379 while(dbt>0) { var od=dps[i].d; if(od<MD) { dps[i].d++; dbt-=(1<<(MD-od-1)); } else i++; } 7380 for(; i>=0; i--) if(dps[i].d==MD && dbt<0) { dps[i].d--; dbt++; } if(dbt!=0) console.log("debt left"); 7381 } 7382 7383 UZIP.F._goodIndex = function(v, arr) { 7384 var i=0; if(arr[i|16]<=v) i|=16; if(arr[i|8]<=v) i|=8; if(arr[i|4]<=v) i|=4; if(arr[i|2]<=v) i|=2; if(arr[i|1]<=v) i|=1; return i; 7385 } 7386 UZIP.F._writeLit = function(ch, ltree, out, pos) { 7387 UZIP.F._putsF(out, pos, ltree[ch<<1]); 7388 return pos+ltree[(ch<<1)+1]; 7389 } 7390 7391 7392 7393 7394 7395 7396 7397 7398 UZIP.F.inflate = function(data, buf) { 7399 var u8=Uint8Array; 7400 if(data[0]==3 && data[1]==0) return (buf ? buf : new u8(0)); 7401 var F=UZIP.F, bitsF = F._bitsF, bitsE = F._bitsE, decodeTiny = F._decodeTiny, makeCodes = F.makeCodes, codes2map=F.codes2map, get17 = F._get17; 7402 var U = F.U; 7403 7404 var noBuf = (buf==null); 7405 if(noBuf) buf = new u8((data.length>>>2)<<3); 7406 7407 var BFINAL=0, BTYPE=0, HLIT=0, HDIST=0, HCLEN=0, ML=0, MD=0; 7408 var off = 0, pos = 0; 7409 var lmap, dmap; 7410 7411 while(BFINAL==0) { 7412 BFINAL = bitsF(data, pos , 1); 7413 BTYPE = bitsF(data, pos+1, 2); pos+=3; 7414 //console.log(BFINAL, BTYPE); 7415 7416 if(BTYPE==0) { 7417 if((pos&7)!=0) pos+=8-(pos&7); 7418 var p8 = (pos>>>3)+4, len = data[p8-4]|(data[p8-3]<<8); //console.log(len);//bitsF(data, pos, 16), 7419 if(noBuf) buf=UZIP.F._check(buf, off+len); 7420 buf.set(new u8(data.buffer, data.byteOffset+p8, len), off); 7421 //for(var i=0; i<len; i++) buf[off+i] = data[p8+i]; 7422 //for(var i=0; i<len; i++) if(buf[off+i] != data[p8+i]) throw "e"; 7423 pos = ((p8+len)<<3); off+=len; continue; 7424 } 7425 if(noBuf) buf=UZIP.F._check(buf, off+(1<<17)); // really not enough in many cases (but PNG and ZIP provide buffer in advance) 7426 if(BTYPE==1) { lmap = U.flmap; dmap = U.fdmap; ML = (1<<9)-1; MD = (1<<5)-1; } 7427 if(BTYPE==2) { 7428 HLIT = bitsE(data, pos , 5)+257; 7429 HDIST = bitsE(data, pos+ 5, 5)+ 1; 7430 HCLEN = bitsE(data, pos+10, 4)+ 4; pos+=14; 7431 7432 var ppos = pos; 7433 for(var i=0; i<38; i+=2) { U.itree[i]=0; U.itree[i+1]=0; } 7434 var tl = 1; 7435 for(var i=0; i<HCLEN; i++) { var l=bitsE(data, pos+i*3, 3); U.itree[(U.ordr[i]<<1)+1] = l; if(l>tl)tl=l; } pos+=3*HCLEN; //console.log(itree); 7436 makeCodes(U.itree, tl); 7437 codes2map(U.itree, tl, U.imap); 7438 7439 lmap = U.lmap; dmap = U.dmap; 7440 7441 pos = decodeTiny(U.imap, (1<<tl)-1, HLIT+HDIST, data, pos, U.ttree); 7442 var mx0 = F._copyOut(U.ttree, 0, HLIT , U.ltree); ML = (1<<mx0)-1; 7443 var mx1 = F._copyOut(U.ttree, HLIT, HDIST, U.dtree); MD = (1<<mx1)-1; 7444 7445 //var ml = decodeTiny(U.imap, (1<<tl)-1, HLIT , data, pos, U.ltree); ML = (1<<(ml>>>24))-1; pos+=(ml&0xffffff); 7446 makeCodes(U.ltree, mx0); 7447 codes2map(U.ltree, mx0, lmap); 7448 7449 //var md = decodeTiny(U.imap, (1<<tl)-1, HDIST, data, pos, U.dtree); MD = (1<<(md>>>24))-1; pos+=(md&0xffffff); 7450 makeCodes(U.dtree, mx1); 7451 codes2map(U.dtree, mx1, dmap); 7452 } 7453 //var ooff=off, opos=pos; 7454 while(true) { 7455 var code = lmap[get17(data, pos) & ML]; pos += code&15; 7456 var lit = code>>>4; //U.lhst[lit]++; 7457 if((lit>>>8)==0) { buf[off++] = lit; } 7458 else if(lit==256) { break; } 7459 else { 7460 var end = off+lit-254; 7461 if(lit>264) { var ebs = U.ldef[lit-257]; end = off + (ebs>>>3) + bitsE(data, pos, ebs&7); pos += ebs&7; } 7462 //UZIP.F.dst[end-off]++; 7463 7464 var dcode = dmap[get17(data, pos) & MD]; pos += dcode&15; 7465 var dlit = dcode>>>4; 7466 var dbs = U.ddef[dlit], dst = (dbs>>>4) + bitsF(data, pos, dbs&15); pos += dbs&15; 7467 7468 //var o0 = off-dst, stp = Math.min(end-off, dst); 7469 //if(stp>20) while(off<end) { buf.copyWithin(off, o0, o0+stp); off+=stp; } else 7470 //if(end-dst<=off) buf.copyWithin(off, off-dst, end-dst); else 7471 //if(dst==1) buf.fill(buf[off-1], off, end); else 7472 if(noBuf) buf=UZIP.F._check(buf, off+(1<<17)); 7473 while(off<end) { buf[off]=buf[off++-dst]; buf[off]=buf[off++-dst]; buf[off]=buf[off++-dst]; buf[off]=buf[off++-dst]; } 7474 off=end; 7475 //while(off!=end) { buf[off]=buf[off++-dst]; } 7476 } 7477 } 7478 //console.log(off-ooff, (pos-opos)>>>3); 7479 } 7480 //console.log(UZIP.F.dst); 7481 //console.log(tlen, dlen, off-tlen+tcnt); 7482 return buf.length==off ? buf : buf.slice(0,off); 7483 } 7484 UZIP.F._check=function(buf, len) { 7485 var bl=buf.length; if(len<=bl) return buf; 7486 var nbuf = new Uint8Array(Math.max(bl<<1,len)); nbuf.set(buf,0); 7487 //for(var i=0; i<bl; i+=4) { nbuf[i]=buf[i]; nbuf[i+1]=buf[i+1]; nbuf[i+2]=buf[i+2]; nbuf[i+3]=buf[i+3]; } 7488 return nbuf; 7489 } 7490 7491 UZIP.F._decodeTiny = function(lmap, LL, len, data, pos, tree) { 7492 var bitsE = UZIP.F._bitsE, get17 = UZIP.F._get17; 7493 var i = 0; 7494 while(i<len) { 7495 var code = lmap[get17(data, pos)&LL]; pos+=code&15; 7496 var lit = code>>>4; 7497 if(lit<=15) { tree[i]=lit; i++; } 7498 else { 7499 var ll = 0, n = 0; 7500 if(lit==16) { 7501 n = (3 + bitsE(data, pos, 2)); pos += 2; ll = tree[i-1]; 7502 } 7503 else if(lit==17) { 7504 n = (3 + bitsE(data, pos, 3)); pos += 3; 7505 } 7506 else if(lit==18) { 7507 n = (11 + bitsE(data, pos, 7)); pos += 7; 7508 } 7509 var ni = i+n; 7510 while(i<ni) { tree[i]=ll; i++; } 7511 } 7512 } 7513 return pos; 7514 } 7515 UZIP.F._copyOut = function(src, off, len, tree) { 7516 var mx=0, i=0, tl=tree.length>>>1; 7517 while(i<len) { var v=src[i+off]; tree[(i<<1)]=0; tree[(i<<1)+1]=v; if(v>mx)mx=v; i++; } 7518 while(i<tl ) { tree[(i<<1)]=0; tree[(i<<1)+1]=0; i++; } 7519 return mx; 7520 } 7521 7522 UZIP.F.makeCodes = function(tree, MAX_BITS) { // code, length 7523 var U = UZIP.F.U; 7524 var max_code = tree.length; 7525 var code, bits, n, i, len; 7526 7527 var bl_count = U.bl_count; for(var i=0; i<=MAX_BITS; i++) bl_count[i]=0; 7528 for(i=1; i<max_code; i+=2) bl_count[tree[i]]++; 7529 7530 var next_code = U.next_code; // smallest code for each length 7531 7532 code = 0; 7533 bl_count[0] = 0; 7534 for (bits = 1; bits <= MAX_BITS; bits++) { 7535 code = (code + bl_count[bits-1]) << 1; 7536 next_code[bits] = code; 7537 } 7538 7539 for (n = 0; n < max_code; n+=2) { 7540 len = tree[n+1]; 7541 if (len != 0) { 7542 tree[n] = next_code[len]; 7543 next_code[len]++; 7544 } 7545 } 7546 } 7547 UZIP.F.codes2map = function(tree, MAX_BITS, map) { 7548 var max_code = tree.length; 7549 var U=UZIP.F.U, r15 = U.rev15; 7550 for(var i=0; i<max_code; i+=2) if(tree[i+1]!=0) { 7551 var lit = i>>1; 7552 var cl = tree[i+1], val = (lit<<4)|cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl); 7553 var rest = (MAX_BITS-cl), i0 = tree[i]<<rest, i1 = i0 + (1<<rest); 7554 //tree[i]=r15[i0]>>>(15-MAX_BITS); 7555 while(i0!=i1) { 7556 var p0 = r15[i0]>>>(15-MAX_BITS); 7557 map[p0]=val; i0++; 7558 } 7559 } 7560 } 7561 UZIP.F.revCodes = function(tree, MAX_BITS) { 7562 var r15 = UZIP.F.U.rev15, imb = 15-MAX_BITS; 7563 for(var i=0; i<tree.length; i+=2) { var i0 = (tree[i]<<(MAX_BITS-tree[i+1])); tree[i] = r15[i0]>>>imb; } 7564 } 7565 7566 // used only in deflate 7567 UZIP.F._putsE= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); } 7568 UZIP.F._putsF= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); dt[o+2]|=(val>>>16); } 7569 7570 UZIP.F._bitsE= function(dt, pos, length) { return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) )>>>(pos&7))&((1<<length)-1); } 7571 UZIP.F._bitsF= function(dt, pos, length) { return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16))>>>(pos&7))&((1<<length)-1); } 7572 /* 7573 UZIP.F._get9 = function(dt, pos) { 7574 return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8))>>>(pos&7))&511; 7575 } */ 7576 UZIP.F._get17= function(dt, pos) { // return at least 17 meaningful bytes 7577 return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) )>>>(pos&7); 7578 } 7579 UZIP.F._get25= function(dt, pos) { // return at least 17 meaningful bytes 7580 return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) | (dt[(pos>>>3)+3]<<24) )>>>(pos&7); 7581 } 7582 UZIP.F.U = function(){ 7583 var u16=Uint16Array, u32=Uint32Array; 7584 return { 7585 next_code : new u16(16), 7586 bl_count : new u16(16), 7587 ordr : [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ], 7588 of0 : [3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999], 7589 exb : [0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0], 7590 ldef : new u16(32), 7591 df0 : [1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 65535, 65535], 7592 dxb : [0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0], 7593 ddef : new u32(32), 7594 flmap: new u16( 512), fltree: [], 7595 fdmap: new u16( 32), fdtree: [], 7596 lmap : new u16(32768), ltree : [], ttree:[], 7597 dmap : new u16(32768), dtree : [], 7598 imap : new u16( 512), itree : [], 7599 //rev9 : new u16( 512) 7600 rev15: new u16(1<<15), 7601 lhst : new u32(286), dhst : new u32( 30), ihst : new u32(19), 7602 lits : new u32(15000), 7603 strt : new u16(1<<16), 7604 prev : new u16(1<<15) 7605 }; 7606 } (); 7607 7608 (function(){ 7609 var U = UZIP.F.U; 7610 var len = 1<<15; 7611 for(var i=0; i<len; i++) { 7612 var x = i; 7613 x = (((x & 0xaaaaaaaa) >>> 1) | ((x & 0x55555555) << 1)); 7614 x = (((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2)); 7615 x = (((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4)); 7616 x = (((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8)); 7617 U.rev15[i] = (((x >>> 16) | (x << 16)))>>>17; 7618 } 7619 7620 function pushV(tgt, n, sv) { while(n--!=0) tgt.push(0,sv); } 7621 7622 for(var i=0; i<32; i++) { U.ldef[i]=(U.of0[i]<<3)|U.exb[i]; U.ddef[i]=(U.df0[i]<<4)|U.dxb[i]; } 7623 7624 pushV(U.fltree, 144, 8); pushV(U.fltree, 255-143, 9); pushV(U.fltree, 279-255, 7); pushV(U.fltree,287-279,8); 7625 /* 7626 var i = 0; 7627 for(; i<=143; i++) U.fltree.push(0,8); 7628 for(; i<=255; i++) U.fltree.push(0,9); 7629 for(; i<=279; i++) U.fltree.push(0,7); 7630 for(; i<=287; i++) U.fltree.push(0,8); 7631 */ 7632 UZIP.F.makeCodes(U.fltree, 9); 7633 UZIP.F.codes2map(U.fltree, 9, U.flmap); 7634 UZIP.F.revCodes (U.fltree, 9) 7635 7636 pushV(U.fdtree,32,5); 7637 //for(i=0;i<32; i++) U.fdtree.push(0,5); 7638 UZIP.F.makeCodes(U.fdtree, 5); 7639 UZIP.F.codes2map(U.fdtree, 5, U.fdmap); 7640 UZIP.F.revCodes (U.fdtree, 5) 7641 7642 pushV(U.itree,19,0); pushV(U.ltree,286,0); pushV(U.dtree,30,0); pushV(U.ttree,320,0); 7643 /* 7644 for(var i=0; i< 19; i++) U.itree.push(0,0); 7645 for(var i=0; i<286; i++) U.ltree.push(0,0); 7646 for(var i=0; i< 30; i++) U.dtree.push(0,0); 7647 for(var i=0; i<320; i++) U.ttree.push(0,0); 7648 */ 7649 })() 7650 7651 7652 7653 7654 7655 7656 7657 7658 7659 7660 7661 7662 7663 7664 7665