/ js / tegaki-test.js
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