diff --git a/gulp/res/js/tegaki.js b/gulp/res/js/tegaki.js index 61d2550b..e88df54f 100644 --- a/gulp/res/js/tegaki.js +++ b/gulp/res/js/tegaki.js @@ -1,7665 +1,7664 @@ -/*! tegaki.js, MIT License */'use strict'; - -var TegakiStrings = { - // Messages - badDimensions: __('Invalid dimensions.'), - promptWidth: __('Canvas width in pixels'), - promptHeight: __('Canvas height in pixels'), - confirmDelLayers: __('Delete selected layers?'), - confirmMergeLayers: __('Merge selected layers?'), - tooManyLayers: __('Layer limit reached.'), - errorLoadImage: __('Could not load the image.'), - noActiveLayer: __('No active layer.'), - hiddenActiveLayer: __('The active layer is not visible.'), - confirmCancel: __('Are you sure? Your work will be lost.'), - confirmChangeCanvas: __('Are you sure? Changing the canvas will clear all layers and history and disable replay recording.'), - - // Controls - color: __('Color'), - size: __('Size'), - alpha: __('Opacity'), - flow: __('Flow'), - zoom: __('Zoom'), - layers: __('Layers'), - switchPalette: __('Switch color palette'), - paletteSlotReplace: __('Right click to replace with the current color'), - - // Layers - layer: __('Layer'), - addLayer: __('Add layer'), - delLayers: __('Delete layers'), - mergeLayers: __('Merge layers'), - moveLayerUp: __('Move up'), - moveLayerDown: __('Move down'), - toggleVisibility: __('Toggle visibility'), - - // Menu bar - newCanvas: __('New'), - open: __('Open'), - save: __('Save'), - saveAs: __('Save As'), - export: __('Export'), - undo: __('Undo'), - redo: __('Redo'), - close: __('Close'), - finish: __('Finish'), - - // Tool modes - tip: __('Tip'), - pressure: __('Pressure'), - preserveAlpha: __('Preserve Alpha'), - - // Tools - pen: __('Pen'), - pencil: __('Pencil'), - airbrush: __('Airbrush'), - pipette: __('Pipette'), - blur: __('Blur'), - eraser: __('Eraser'), - bucket: __('Bucket'), - tone: __('Tone'), - - // Replay - gapless: __('Gapless'), - play: __('Play'), - pause: __('Pause'), - rewind: __('Rewind'), - slower: __('Slower'), - faster: __('Faster'), - recordingEnabled: __('Recording replay'), - errorLoadReplay: __('Could not load the replay: '), - loadingReplay: __('Loading replay…'), -}; -class TegakiTool { - constructor() { - this.id = 0; - - this.name = null; - - this.keybind = null; - - this.useFlow = false; - - this.useSizeDynamics = false; - this.useAlphaDynamics = false; - this.useFlowDynamics = false; - - this.usePreserveAlpha = false; - - this.step = 0.0; - - this.size = 1; - this.alpha = 1.0; - this.flow = 1.0; - - this.useSize = true; - this.useAlpha = true; - this.useFlow = true; - - this.noCursor = false; - - this.color = '#000000'; - this.rgb = [0, 0, 0]; - - this.brushSize = 0; - this.brushAlpha = 0.0; - this.brushFlow = 0.0; - this.stepSize = 0.0; - this.center = 0.0; - - this.sizeDynamicsEnabled = false; - this.alphaDynamicsEnabled = false; - this.flowDynamicsEnabled = false; - this.preserveAlphaEnabled = false; - - this.tip = -1; - this.tipList = null; - - this.stepAcc = 0; - - this.shapeCache = null; - - this.kernel = null; - } - - brushFn(x, y, offsetX, offsetY) {} - - start(posX, posY) {} - - commit() {} - - draw(posX, posY) {} - - usesDynamics() { - return this.useSizeDynamics || this.useAlphaDynamics || this.useFlowDynamics; - } - - enabledDynamics() { - return this.sizeDynamicsEnabled || this.alphaDynamicsEnabled || this.flowDynamicsEnabled; - } - - setSize(size) { - this.size = size; - } - - setAlpha(alpha) { - this.alpha = alpha; - this.brushAlpha = alpha; - } - - setFlow(flow) { - this.flow = flow; - this.brushFlow = this.easeFlow(flow); - } - - easeFlow(flow) { - return flow; - } - - setColor(hex) { - this.rgb = $T.hexToRgb(hex); - } - - setSizeDynamics(flag) { - if (!this.useSizeDynamics) { - return; - } - - if (!flag) { - this.setSize(this.size); - } - - this.sizeDynamicsEnabled = flag; - } - - setAlphaDynamics(flag) { - if (!this.useAlphaDynamics) { - return; - } - - if (!flag) { - this.setAlpha(this.alpha); - } - - this.alphaDynamicsEnabled = flag; - } - - setFlowDynamics(flag) { - if (!this.useFlowDynamics) { - return; - } - - if (!flag) { - this.setFlow(this.flow); - } - - this.flowDynamicsEnabled = flag; - } - - setPreserveAlpha(flag) { - this.preserveAlphaEnabled = flag; - } - - set() { - this.setAlpha(this.alpha); - this.setFlow(this.flow); - this.setSize(this.size); - this.setColor(Tegaki.toolColor); - - Tegaki.onToolChanged(this); - } -} -class TegakiBrush extends TegakiTool { - constructor() { - super(); - } - - generateShape(size) {} - - brushFn(x, y, offsetX, offsetY) { - var aData, gData, bData, aWidth, canvasWidth, canvasHeight, - kernel, xx, yy, ix, iy, - pa, ka, a, sa, - kr, kg, kb, - r, g, b, - pr, pg, pb, - px, ba, - brushSize, brushAlpha, brushFlow, preserveAlpha; - - preserveAlpha = this.preserveAlphaEnabled; - - kernel = this.kernel; - - brushAlpha = this.brushAlpha; - brushFlow = this.brushFlow; - brushSize = this.brushSize; - - aData = Tegaki.activeLayer.imageData.data; - gData = Tegaki.ghostBuffer.data; - bData = Tegaki.blendBuffer.data; - - canvasWidth = Tegaki.baseWidth; - canvasHeight = Tegaki.baseHeight; - - aWidth = canvasWidth; - - kr = this.rgb[0]; - kg = this.rgb[1]; - kb = this.rgb[2]; - - for (yy = 0; yy < brushSize; ++yy) { - iy = y + yy + offsetY; - - if (iy < 0 || iy >= canvasHeight) { - continue; - } - - for (xx = 0; xx < brushSize; ++xx) { - ix = x + xx + offsetX; - - if (ix < 0 || ix >= canvasWidth) { - continue; - } - - ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; - - if (ka <= 0.0) { - continue; - } - - px = (iy * canvasWidth + ix) * 4; - - sa = bData[px + 3] / 255; - sa = sa + ka * brushFlow * (brushAlpha - sa); - - ba = Math.ceil(sa * 255); - - if (ba > bData[px + 3]) { - if (bData[px] === 0) { - gData[px] = aData[px]; - gData[px + 1] = aData[px + 1]; - gData[px + 2] = aData[px + 2]; - gData[px + 3] = aData[px + 3]; - } - - bData[px] = 1; - bData[px + 3] = ba; - - pr = gData[px]; - pg = gData[px + 1]; - pb = gData[px + 2]; - pa = gData[px + 3] / 255; - - a = pa + sa - pa * sa; - - r = ((kr * sa) + (pr * pa) * (1 - sa)) / a; - g = ((kg * sa) + (pg * pa) * (1 - sa)) / a; - b = ((kb * sa) + (pb * pa) * (1 - sa)) / a; - - aData[px] = (kr > pr) ? Math.ceil(r) : Math.floor(r); - aData[px + 1] = (kg > pg) ? Math.ceil(g) : Math.floor(g); - aData[px + 2] = (kb > pb) ? Math.ceil(b) : Math.floor(b); - - if (!preserveAlpha) { - aData[px + 3] = Math.ceil(a * 255); - } - } - } - } - } - - generateShapeCache(force) { - var i, shape; - - if (!this.shapeCache) { - this.shapeCache = new Array(Tegaki.maxSize); - } - - if (this.shapeCache[0] && !force) { - return; - } - - for (i = 0; i < Tegaki.maxSize; ++i) { - shape = this.generateShape(i + 1); - this.shapeCache[i] = shape; - this.setShape(shape); - } - } - - updateDynamics(t) { - var pressure, shape, val; - - pressure = TegakiPressure.lerp(t); - - if (this.sizeDynamicsEnabled) { - val = Math.ceil(pressure * this.size); - - if (val === 0) { - return false; - } - - shape = this.shapeCache[val - 1]; - - this.setShape(shape); - } - - if (this.alphaDynamicsEnabled) { - val = this.alpha * pressure; - - if (val <= 0) { - return false; - } - - this.brushAlpha = val; - } - - if (this.flowDynamicsEnabled) { - val = this.flow * pressure; - - if (val <= 0) { - return false; - } - - this.brushFlow = this.easeFlow(val); - } - - return true; - } - - start(posX, posY) { - var sampleX, sampleY; - - this.stepAcc = 0; - this.posX = posX; - this.posY = posY; - - if (this.enabledDynamics()) { - if (!this.updateDynamics(1.0)) { - return; - } - } - - sampleX = posX - this.center; - sampleY = posY - this.center; - - this.readImageData(sampleX, sampleY, this.brushSize, this.brushSize); - - this.brushFn(0, 0, sampleX, sampleY); - - this.writeImageData(sampleX, sampleY, this.brushSize, this.brushSize); - } - - commit() { - Tegaki.clearBuffers(); - } - - draw(posX, posY) { - var mx, my, fromX, fromY, sampleX, sampleY, dx, dy, err, derr, stepAcc, - lastX, lastY, distBase, shape, center, brushSize, t, tainted, w, h; - - stepAcc = this.stepAcc; - - fromX = this.posX; - fromY = this.posY; - - if (fromX < posX) { dx = posX - fromX; sampleX = fromX; mx = 1; } - else { dx = fromX - posX; sampleX = posX; mx = -1; } - - if (fromY < posY) { dy = posY - fromY; sampleY = fromY; my = 1; } - else { dy = fromY - posY; sampleY = posY; my = -1; } - - if (this.enabledDynamics()) { - distBase = Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)); - } - - if (this.sizeDynamicsEnabled) { - shape = this.shapeCache[this.size - 1]; - center = shape.center; - brushSize = shape.brushSize; - } - else { - center = this.center; - brushSize = this.brushSize; - } - - sampleX -= center; - sampleY -= center; - - w = dx + brushSize; - h = dy + brushSize; - - this.readImageData(sampleX, sampleY, w, h); - - err = (dx > dy ? dx : (dy !== 0 ? -dy : 0)) / 2; - - if (dx !== 0) { - dx = -dx; - } - - tainted = false; - - lastX = fromX; - lastY = fromY; - - while (true) { - stepAcc += Math.max(Math.abs(lastX - fromX), Math.abs(lastY - fromY)); - - lastX = fromX; - lastY = fromY; - - if (stepAcc >= this.stepSize) { - if (this.enabledDynamics()) { - if (distBase > 0) { - t = 1.0 - (Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)) / distBase); - } - else { - t = 0.0; - } - - if (this.updateDynamics(t)) { - this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); - tainted = true; - } - } - else { - this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); - tainted = true; - } - - stepAcc = 0; - } - - if (fromX === posX && fromY === posY) { - break; - } - - derr = err; - - if (derr > dx) { err -= dy; fromX += mx; } - if (derr < dy) { err -= dx; fromY += my; } - } - - this.stepAcc = stepAcc; - this.posX = posX; - this.posY = posY; - - if (tainted) { - this.writeImageData(sampleX, sampleY, w, h); - } - } - - writeImageData(x, y, w, h) { - Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0, x, y, w, h); - } - - readImageData(x, y, w, h) {} - - setShape(shape) { - this.center = shape.center; - this.stepSize = shape.stepSize; - this.brushSize = shape.brushSize; - this.kernel = shape.kernel; - } - - setSize(size) { - this.size = size; - - if (this.sizeDynamicsEnabled) { - this.generateShapeCache(); - } - else { - this.setShape(this.generateShape(size)); - } - } - - setSizeDynamics(flag) { - if (!this.useSizeDynamics) { - return; - } - - if (this.sizeDynamicsEnabled === flag) { - return; - } - - if (flag) { - this.generateShapeCache(); - } - else { - this.setShape(this.generateShape(this.size)); - } - - this.sizeDynamicsEnabled = flag; - } - - setTip(tipId) { - this.tipId = tipId; - - if (this.sizeDynamicsEnabled) { - this.generateShapeCache(true); - } - else { - this.setShape(this.generateShape(this.size)); - } - } -} -class TegakiPencil extends TegakiBrush { - constructor() { - super(); - - this.id = 1; - - this.name = 'pencil'; - - this.keybind = 'b'; - - this.step = 0.01; - - this.useFlow = false; - - this.size = 1; - this.alpha = 1.0; - - this.useSizeDynamics = true; - this.useAlphaDynamics = true; - this.usePreserveAlpha = true; - } - - generateShape(size) { - var e, x, y, imageData, data, c, color, r, rr; - - r = 0 | ((size) / 2); - - rr = 0 | ((size + 1) % 2); - - imageData = new ImageData(size, size); - - data = new Uint32Array(imageData.data.buffer); - - color = 0xFF000000; - - x = r; - y = 0; - e = 1 - r; - c = r; - - while (x >= y) { - data[c + x - rr + (c + y - rr) * size] = color; - data[c + y - rr + (c + x - rr) * size] = color; - - data[c - y + (c + x - rr) * size] = color; - data[c - x + (c + y - rr) * size] = color; - - data[c - y + (c - x) * size] = color; - data[c - x + (c - y) * size] = color; - - data[c + y - rr + (c - x) * size] = color; - data[c + x - rr + (c - y) * size] = color; - - ++y; - - if (e <= 0) { - e += 2 * y + 1; - } - else { - x--; - e += 2 * (y - x) + 1; - } - } - - if (r > 0) { - Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); - } - - return { - center: r, - stepSize: Math.ceil(size * this.step), - brushSize: size, - kernel: imageData.data, - }; - } -} -class TegakiAirbrush extends TegakiBrush { - constructor() { - super(); - - this.id = 3; - - this.name = 'airbrush'; - - this.keybind = 'a'; - - this.step = 0.1; - - this.size = 32; - this.alpha = 1.0; - - this.useSizeDynamics = true; - this.useAlphaDynamics = true; - this.useFlowDynamics = true; - - this.usePreserveAlpha = true; - } - - easeFlow(flow) { - return 1 - Math.sqrt(1 - flow); - } - - generateShape(size) { - var i, r, data, len, sqd, sqlen, hs, col, row, - ecol, erow, a; - - r = size; - size = size * 2; - - data = new ImageData(size, size).data; - - len = size * size * 4; - sqlen = Math.sqrt(r * r); - hs = Math.round(r); - col = row = -hs; - - i = 0; - while (i < len) { - if (col >= hs) { - col = -hs; - ++row; - continue; - } - - ecol = col; - erow = row; - - if (ecol < 0) { ecol = -ecol; } - if (erow < 0) { erow = -erow; } - - sqd = Math.sqrt(ecol * ecol + erow * erow); - - if (sqd >= sqlen) { - a = 0; - } - else if (sqd === 0) { - a = 255; - } - else { - a = (sqd / sqlen) + 0.1; - - if (a > 1.0) { - a = 1.0; - } - - a = (1 - (Math.exp(1 - 1 / a) / a)) * 255; - } - - data[i + 3] = a; - - i += 4; - - ++col; - } - - return { - center: r, - stepSize: Math.ceil(size * this.step), - brushSize: size, - kernel: data, - }; - } -} -class TegakiPen extends TegakiBrush { - constructor() { - super(); - - this.id = 2; - - this.name = 'pen'; - - this.keybind = 'p'; - - this.step = 0.05; - - this.size = 8; - this.alpha = 1.0; - this.flow = 1.0; - - this.useSizeDynamics = true; - this.useAlphaDynamics = true; - this.useFlowDynamics = true; - - this.usePreserveAlpha = true; - } - - easeFlow(flow) { - return 1 - Math.sqrt(1 - Math.pow(flow, 3)); - } - - generateShape(size) { - var e, x, y, imageData, data, c, color, r, rr, - f, ff, bSize, bData, i, ii, xx, yy, center, brushSize; - - center = Math.floor(size / 2) + 1; - - brushSize = size + 2; - - f = 4; - - ff = f * f; - - bSize = brushSize * f; - - r = Math.floor(bSize / 2); - - rr = Math.floor((bSize + 1) % 2); - - imageData = new ImageData(bSize, bSize); - bData = new Uint32Array(imageData.data.buffer); - - color = 0x55000000; - - x = r; - y = 0; - e = 1 - r; - c = r; - - while (x >= y) { - bData[c + x - rr + (c + y - rr) * bSize] = color; - bData[c + y - rr + (c + x - rr) * bSize] = color; - - bData[c - y + (c + x - rr) * bSize] = color; - bData[c - x + (c + y - rr) * bSize] = color; - - bData[c - y + (c - x) * bSize] = color; - bData[c - x + (c - y) * bSize] = color; - - bData[c + y - rr + (c - x) * bSize] = color; - bData[c + x - rr + (c - y) * bSize] = color; - - ++y; - - if (e <= 0) { - e += 2 * y + 1; - } - else { - x--; - e += 2 * (y - x) + 1; - } - } - - color = 0xFF000000; - - x = r - 3; - y = 0; - e = 1 - r; - c = r; - - while (x >= y) { - bData[c + x - rr + (c + y - rr) * bSize] = color; - bData[c + y - rr + (c + x - rr) * bSize] = color; - - bData[c - y + (c + x - rr) * bSize] = color; - bData[c - x + (c + y - rr) * bSize] = color; - - bData[c - y + (c - x) * bSize] = color; - bData[c - x + (c - y) * bSize] = color; - - bData[c + y - rr + (c - x) * bSize] = color; - bData[c + x - rr + (c - y) * bSize] = color; - - ++y; - - if (e <= 0) { - e += 2 * y + 1; - } - else { - x--; - e += 2 * (y - x) + 1; - } - } - - if (r > 0) { - Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); - } - - bData = imageData.data; - data = new ImageData(brushSize, brushSize).data; - - for (x = 0; x < brushSize; ++x) { - for (y = 0; y < brushSize; ++y) { - i = (y * brushSize + x) * 4 + 3; - - color = 0; - - for (xx = 0; xx < f; ++xx) { - for (yy = 0; yy < f; ++yy) { - ii = ((yy + y * f) * bSize + (xx + x * f)) * 4 + 3; - color += bData[ii]; - } - } - - data[i] = color / ff; - } - } - - return { - center: center, - stepSize: Math.ceil(size * this.step), - brushSize: brushSize, - kernel: data, - }; - } -} -class TegakiBucket extends TegakiTool { - constructor() { - super(); - - this.id = 4; - - this.name = 'bucket'; - - this.keybind = 'g'; - - this.step = 100.0; - - this.useSize = false; - this.useFlow = false; - - this.noCursor = true; - } - - fill(imageData, x, y, color, alpha) { - var r, g, b, px, tr, tg, tb, ta, q, pxMap, yy, xx, yn, ys, - yyy, yyn, yys, xd, data, w, h; - - w = imageData.width; - h = imageData.height; - - r = color[0]; - g = color[1]; - b = color[2]; - - px = (y * w + x) * 4; - - data = imageData.data; - - tr = data[px]; - tg = data[px + 1]; - tb = data[px + 2]; - ta = data[px + 3]; - - pxMap = new Uint8Array(w * h * 4); - - q = []; - - q[0] = x; - q[1] = y; - - while (q.length) { - yy = q.pop(); - xx = q.pop(); - - yn = (yy - 1); - ys = (yy + 1); - - yyy = yy * w; - yyn = yn * w; - yys = ys * w; - - xd = xx; - - while (xd >= 0) { - px = (yyy + xd) * 4; - - if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - break; - } - - this.blendPixel(data, px, r, g, b, alpha); - - pxMap[px] = 1; - - if (yn >= 0) { - px = (yyn + xd) * 4; - - if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - q.push(xd); - q.push(yn); - } - } - - if (ys < h) { - px = (yys + xd) * 4; - - if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - q.push(xd); - q.push(ys); - } - } - - xd--; - } - - xd = xx + 1; - - while (xd < w) { - px = (yyy + xd) * 4; - - if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - break; - } - - this.blendPixel(data, px, r, g, b, alpha); - - pxMap[px] = 1; - - if (yn >= 0) { - px = (yyn + xd) * 4; - - if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - q.push(xd); - q.push(yn); - } - } - - if (ys < h) { - px = (yys + xd) * 4; - - if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { - q.push(xd); - q.push(ys); - } - } - - ++xd; - } - } - } - - brushFn(x, y) { - if (x < 0 || y < 0 || x >= Tegaki.baseWidth || y >= Tegaki.baseHeight) { - return; - } - - this.fill(Tegaki.activeLayer.imageData, x, y, this.rgb, this.alpha); - - // TODO: write back only the tainted rect - Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0); - } - - blendPixel(data, px, r, g, b, a) { - var sr, sg, sb, sa, dr, dg, db, da; - - sr = data[px]; - sg = data[px + 1]; - sb = data[px + 2]; - sa = data[px + 3] / 255; - - da = sa + a - sa * a; - - dr = ((r * a) + (sr * sa) * (1 - a)) / da; - dg = ((g * a) + (sg * sa) * (1 - a)) / da; - db = ((b * a) + (sb * sa) * (1 - a)) / da; - - data[px] = (r > sr) ? Math.ceil(dr) : Math.floor(dr); - data[px + 1] = (g > sg) ? Math.ceil(dg) : Math.floor(dg); - data[px + 2] = (b > sb) ? Math.ceil(db) : Math.floor(db); - data[px + 3] = Math.ceil(da * 255); - } - - testPixel(data, px, pxMap, tr, tg, tb, ta) { - return !pxMap[px] && (data[px] == tr - && data[++px] == tg - && data[++px] == tb - && data[++px] == ta) - ; - } - - start(x, y) { - this.brushFn(x, y); - } - - draw(x, y) { - this.brushFn(x, y); - } - - setSize(size) {} -} -class TegakiTone extends TegakiPencil { - constructor() { - super(); - - this.id = 5; - - this.name = 'tone'; - - this.keybind = 't'; - - this.step = 0.01; - - this.useFlow = false; - - this.size = 8; - this.alpha = 0.5; - - this.useSizeDynamics = true; - this.useAlphaDynamics = true; - this.usePreserveAlpha = true; - - this.matrix = [ - [0, 8, 2, 10], - [12, 4, 14, 6], - [3, 11, 1 ,9], - [15, 7, 13, 5] - ]; - - this.mapCache = null; - this.mapWidth = 0; - this.mapHeight = 0; - } - - start(x, y) { - if (this.mapWidth !== Tegaki.baseWidth || this.mapHeight !== Tegaki.baseHeight) { - this.generateMapCache(true); - } - - super.start(x, y); - } - - brushFn(x, y, offsetX, offsetY) { - var data, kernel, brushSize, map, idx, preserveAlpha, - px, mx, mapWidth, xx, yy, ix, iy, canvasWidth, canvasHeight; - - data = Tegaki.activeLayer.imageData.data; - - canvasWidth = Tegaki.baseWidth; - canvasHeight = Tegaki.baseHeight; - - kernel = this.kernel; - - brushSize = this.brushSize; - - mapWidth = this.mapWidth; - - preserveAlpha = this.preserveAlphaEnabled; - - idx = Math.round(this.brushAlpha * 16) - 1; - - if (idx < 0) { - return; - } - - map = this.mapCache[idx]; - - for (yy = 0; yy < brushSize; ++yy) { - iy = y + yy + offsetY; - - if (iy < 0 || iy >= canvasHeight) { - continue; - } - - for (xx = 0; xx < brushSize; ++xx) { - ix = x + xx + offsetX; - - if (ix < 0 || ix >= canvasWidth) { - continue; - } - - if (kernel[(yy * brushSize + xx) * 4 + 3] === 0) { - continue; - } - - mx = iy * canvasWidth + ix; - px = mx * 4; - - if (map[mx] === 0) { - data[px] = this.rgb[0]; - data[px + 1] = this.rgb[1]; - data[px + 2] = this.rgb[2]; - - if (!preserveAlpha) { - data[px + 3] = 255; - } - } - } - } - } - - generateMap(w, h, idx) { - var data, x, y; - - data = new Uint8Array(w * h); - - for (y = 0; y < h; ++y) { - for (x = 0; x < w; ++x) { - if (idx < this.matrix[y % 4][x % 4]) { - data[w * y + x] = 1; - } - } - } - - return data; - } - - generateMapCache(force) { - var i, cacheSize; - - cacheSize = this.matrix.length * this.matrix[0].length; - - if (!this.mapCache) { - this.mapCache = new Array(cacheSize); - } - - if (!force && this.mapCache[0] - && this.mapWidth === Tegaki.baseWidth - && this.mapHeight === Tegaki.baseHeight) { - return; - } - - this.mapWidth = Tegaki.baseWidth; - this.mapHeight = Tegaki.baseHeight; - - for (i = 0; i < cacheSize; ++i) { - this.mapCache[i] = this.generateMap(this.mapWidth, this.mapHeight, i); - } - } - - setAlpha(alpha) { - super.setAlpha(alpha); - this.generateMapCache(); - } -} -class TegakiPipette extends TegakiTool { - constructor() { - super(); - - this.id = 6; - - this.name = 'pipette'; - - this.keybind = 'i'; - - this.step = 100.0; - - this.useSize = false; - this.useAlpha = false; - this.useFlow = false; - - this.noCursor = true; - } - - start(posX, posY) { - this.draw(posX, posY); - } - - draw(posX, posY) { - var c, ctx; - - if (true) { - ctx = Tegaki.flatten().getContext('2d'); - } - else { - ctx = Tegaki.activeLayer.ctx; - } - - c = $T.getColorAt(ctx, posX, posY); - - Tegaki.setToolColor(c); - } - - set() { - Tegaki.onToolChanged(this); - } - - commit() {} - - setSize() {} - - setAlpha() {} -} -class TegakiBlur extends TegakiBrush { - constructor() { - super(); - - this.id = 7; - - this.name = 'blur'; - - this.step = 0.25; - - this.useFlow = false; - - this.size = 32; - this.alpha = 0.5; - - this.useAlphaDynamics = true; - this.usePreserveAlpha = false; - } - - writeImageData(x, y, w, h) { - var xx, yy, ix, iy, px, canvasWidth, aData, bData; - - aData = Tegaki.activeLayer.imageData.data; - bData = Tegaki.blendBuffer.data; - - canvasWidth = Tegaki.baseWidth; - - for (xx = 0; xx < w; ++xx) { - ix = x + xx; - - for (yy = 0; yy < h; ++yy) { - iy = y + yy; - - px = (iy * canvasWidth + ix) * 4; - - aData[px] = bData[px]; - aData[px + 1] = bData[px + 1]; - aData[px + 2] = bData[px + 2]; - aData[px + 3] = bData[px + 3]; - } - } - - super.writeImageData(x, y, w, h); - } - - readImageData(x, y, w, h) { - var xx, yy, ix, iy, px, canvasWidth, aData, bData; - - aData = Tegaki.activeLayer.imageData.data; - bData = Tegaki.blendBuffer.data; - - canvasWidth = Tegaki.baseWidth; - - for (xx = 0; xx < w; ++xx) { - ix = x + xx; - - for (yy = 0; yy < h; ++yy) { - iy = y + yy; - - px = (iy * canvasWidth + ix) * 4; - - bData[px] = aData[px]; - bData[px + 1] = aData[px + 1]; - bData[px + 2] = aData[px + 2]; - bData[px + 3] = aData[px + 3]; - } - } - } - - brushFn(x, y, offsetX, offsetY) { - var i, j, size, aData, bData, limX, limY, - kernel, alpha, alpha0, ix, iy, canvasWidth, canvasHeight, - sx, sy, r, g, b, a, kx, ky, px, pa, acc, aa; - - alpha0 = this.brushAlpha; - alpha = alpha0 * alpha0 * alpha0; - - if (alpha <= 0.0) { - return; - } - - size = this.brushSize; - - kernel = this.kernel; - - aData = Tegaki.activeLayer.imageData.data; - bData = Tegaki.blendBuffer.data; - - canvasWidth = Tegaki.baseWidth; - canvasHeight = Tegaki.baseHeight; - - limX = canvasWidth - 1; - limY = canvasHeight - 1; - - for (sx = 0; sx < size; ++sx) { - ix = x + sx + offsetX; - - if (ix < 0 || ix >= canvasWidth) { - continue; - } - - for (sy = 0; sy < size; ++sy) { - iy = y + sy + offsetY; - - if (iy < 0 || iy >= canvasHeight) { - continue; - } - - i = (sy * size + sx) * 4; - - px = (iy * canvasWidth + ix) * 4; - - if (kernel[i + 3] === 0 || ix <= 0 || iy <= 0 || ix >= limX || iy >= limY) { - continue; - } - - r = g = b = a = acc = 0; - - for (kx = -1; kx < 2; ++kx) { - for (ky = -1; ky < 2; ++ky) { - j = ((iy - ky) * canvasWidth + (ix - kx)) * 4; - - pa = aData[j + 3]; - - if (kx === 0 && ky === 0) { - aa = pa * alpha0; - acc += alpha0; - } - else { - aa = pa * alpha; - acc += alpha; - } - - r = r + aData[j] * aa; ++j; - g = g + aData[j] * aa; ++j; - b = b + aData[j] * aa; - a = a + aa; - } - } - - a = a / acc; - - if (a <= 0.0) { - continue; - } - - bData[px] = Math.round((r / acc) / a); - bData[px + 1] = Math.round((g / acc) / a); - bData[px + 2] = Math.round((b / acc) / a); - bData[px + 3] = Math.round(a); - } - } - } -} - -TegakiBlur.prototype.generateShape = TegakiPencil.prototype.generateShape; -class TegakiEraser extends TegakiBrush { - constructor() { - super(); - - this.id = 8; - - this.name = 'eraser'; - - this.keybind = 'e'; - - this.step = 0.1; - - this.size = 8; - this.alpha = 1.0; - - this.useFlow = false; - - this.useSizeDynamics = true; - this.useAlphaDynamics = true; - this.usePreserveAlpha = false; - - this.tipId = 0; - this.tipList = [ 'pencil', 'pen', 'airbrush' ]; - } - - brushFn(x, y, offsetX, offsetY) { - var aData, bData, gData, kernel, canvasWidth, canvasHeight, - ka, ba, px, xx, yy, ix, iy, - brushSize, brushAlpha; - - brushAlpha = this.brushAlpha; - brushSize = this.brushSize; - - kernel = this.kernel; - - aData = Tegaki.activeLayer.imageData.data; - gData = Tegaki.ghostBuffer.data; - bData = Tegaki.blendBuffer.data; - - canvasWidth = Tegaki.baseWidth; - canvasHeight = Tegaki.baseHeight; - - for (yy = 0; yy < brushSize; ++yy) { - iy = y + yy + offsetY; - - if (iy < 0 || iy >= canvasHeight) { - continue; - } - - for (xx = 0; xx < brushSize; ++xx) { - ix = x + xx + offsetX; - - if (ix < 0 || ix >= canvasWidth) { - continue; - } - - ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; - - px = (iy * canvasWidth + ix) * 4 + 3; - - if (gData[px] === 0) { - gData[px] = aData[px]; - } - - ba = bData[px] / 255; - ba = ba + ka * (brushAlpha - ba); - - bData[px] = Math.floor(ba * 255); - aData[px] = Math.floor(gData[px] * (1 - ba)); - } - } - } - - generateShape(size) { - if (this.tipId === 0) { - return this.generateShapePencil(size); - } - else if (this.tipId === 1) { - return this.generateShapePen(size); - } - else { - return this.generateShapeAirbrush(size); - } - } -} - -TegakiEraser.prototype.generateShapePencil = TegakiPencil.prototype.generateShape; -TegakiEraser.prototype.generateShapePen = TegakiPen.prototype.generateShape; -TegakiEraser.prototype.generateShapeAirbrush = TegakiAirbrush.prototype.generateShape; -var $T = { - docEl: document.documentElement, - - id: function(id) { - return document.getElementById(id); - }, - - cls: function(klass, root) { - return (root || document).getElementsByClassName(klass); - }, - - on: function(o, e, h) { - o.addEventListener(e, h, false); - }, - - off: function(o, e, h) { - o.removeEventListener(e, h, false); - }, - - el: function(name) { - return document.createElement(name); - }, - - frag: function() { - return document.createDocumentFragment(); - }, - - copyImageData(imageData) { - return new ImageData( - new Uint8ClampedArray(imageData.data), - imageData.width - ); - }, - - copyCanvas: function(source, clone) { - var canvas; - - if (!clone) { - canvas = $T.el('canvas'); - canvas.width = source.width; - canvas.height = source.height; - } - else { - canvas = source.cloneNode(false); - } - - canvas.getContext('2d').drawImage(source, 0, 0); - - return canvas; - }, - - clearCtx: function(ctx) { - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - }, - - hexToRgb: function(hex) { - var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i); - - if (c) { - return [ - parseInt(c[1], 16), - parseInt(c[2], 16), - parseInt(c[3], 16) - ]; - } - - return null; - }, - - RgbToHex: function(r, g, b) { - return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); - }, - - getColorAt: function(ctx, posX, posY) { - var rgba = ctx.getImageData(posX, posY, 1, 1).data; - - return '#' - + ('0' + rgba[0].toString(16)).slice(-2) - + ('0' + rgba[1].toString(16)).slice(-2) - + ('0' + rgba[2].toString(16)).slice(-2); - }, - - generateFilename: function() { - return 'tegaki_' + (new Date()).toISOString().split('.')[0].replace(/[^0-9]/g, '_'); - }, - - sortAscCb: function(a, b) { - if (a > b) { return 1; } - if (a < b) { return -1; } - return 0; - }, - - sortDescCb: function(a, b) { - if (a > b) { return -1; } - if (a < b) { return 1; } - return 0; - }, - - msToHms: function(ms) { - var h, m, s, ary; - - s = 0 | (ms / 1000); - h = 0 | (s / 3600); - m = 0 | ((s - h * 3600) / 60); - s = s - h * 3600 - m * 60; - - ary = []; - - if (h) { - ary.push(h < 10 ? ('0' + h) : h); - } - - if (m) { - ary.push(m < 10 ? ('0' + m) : m); - } - else { - ary.push('00'); - } - - if (s) { - ary.push(s < 10 ? ('0' + s) : s); - } - else { - ary.push('00'); - } - - return ary.join(':'); - }, - - calcThumbSize(w, h, maxSide) { - var r; - - if (w > maxSide) { - r = maxSide / w; - w = maxSide; - h = h * r; - } - - if (h > maxSide) { - r = maxSide / h; - h = maxSide; - w = w * r; - } - - return [Math.ceil(w), Math.ceil(h)]; - } -}; -class TegakiBinReader { - constructor(buf) { - this.pos = 0; - this.view = new DataView(buf); - this.buf = buf; - } - - readInt8() { - var data = this.view.getInt8(this.pos); - this.pos += 1; - return data; - } - - readUint8() { - var data = this.view.getUint8(this.pos); - this.pos += 1; - return data; - } - - readInt16() { - var data = this.view.getInt16(this.pos); - this.pos += 2; - return data; - } - - readUint16() { - var data = this.view.getUint16(this.pos); - this.pos += 2; - return data; - } - - readUint32() { - var data = this.view.getUint32(this.pos); - this.pos += 4; - return data; - } - - readFloat32() { - var data = this.view.getFloat32(this.pos); - this.pos += 4; - return data; - } -} - -class TegakiBinWriter { - constructor(buf) { - this.pos = 0; - this.view = new DataView(buf); - this.buf = buf; - } - - writeInt8(val) { - this.view.setInt8(this.pos, val); - this.pos += 1; - } - - writeUint8(val) { - this.view.setUint8(this.pos, val); - this.pos += 1; - } - - writeInt16(val) { - this.view.setInt16(this.pos, val); - this.pos += 2; - } - - writeUint16(val) { - this.view.setUint16(this.pos, val); - this.pos += 2; - } - - writeUint32(val) { - this.view.setUint32(this.pos, val); - this.pos += 4; - } - - writeFloat32(val) { - this.view.setFloat32(this.pos, val); - this.pos += 4; - } -} -var TegakiCursor = { - size: 0, - radius: 0, - - points: null, - - cursorCtx: null, - - offsetX: 0, - offsetY: 0, - - lastX: 0, - lastY: 0, - lastSize: 0, - - init: function(w, h) { - var el; - - el = $T.el('canvas'); - el.id = 'tegaki-cursor-layer'; - [ el.width, el.height ] = TegakiCursor.getMaxCanvasSize() - - Tegaki.canvasCnt.appendChild(el); - - this.offsetX = el.offsetLeft; - this.offsetY = el.offsetTop; - - this.cursorCtx = el.getContext('2d'); - }, - - getCanvas: function() { - if (this.cursorCtx) { - return this.cursorCtx.canvas; - } - else { - return null; - } - }, - - getMaxCanvasSize: function() { - let w = Tegaki.canvasCnt.offsetWidth; - let h = Tegaki.canvasCnt.offsetHeight; - let [ sbwh, scbv ] = Tegaki.getScrollbarSizes(); - return [ w - sbwh, h - scbv ]; - }, - - updateCanvasSize: function() { - let canvas = this.cursorCtx.canvas; - - let [w, h] = this.getMaxCanvasSize(); - - if (w !== canvas.width || h !== canvas.height) { - canvas.width = w; - canvas.height = h; - - this.offsetX = canvas.offsetLeft; - this.offsetY = canvas.offsetTop; - } - }, - - render: function(rawX, rawY) { - var x, y, i, destImg, destData; - - x = rawX - this.offsetX - this.radius; - y = rawY - this.offsetY - this.radius; - - this.clear(); - - this.lastX = x; - this.lastY = y; - this.lastSize = this.size; - - destImg = this.cursorCtx.createImageData(this.size, this.size); - destData = new Uint32Array(destImg.data.buffer); - - for (i = 0; i < this.points.length; ++i) { - destData[this.points[i]] = 0xFFFFFF7F; - } - - this.cursorCtx.putImageData(destImg, x, y); - }, - - clear: function() { - this.cursorCtx.clearRect(this.lastX, this.lastY, this.lastSize, this.lastSize); - }, - - clearAll: function() { - var canvas = this.cursorCtx.canvas; - this.cursorCtx.clearRect(0, 0, canvas.width, canvas.height); - }, - - destroy() { - this.size = 0; - this.radius = 0; - this.offsetX = 0; - this.offsetY = 0; - this.lastX = 0; - this.lastY = 0; - this.lastSize = 0; - this.points = null; - this.cursorCtx = null; - }, - - generate: function(size) { - var e, x, y, c, r, rr, points; - - size = 0 | (size * Tegaki.zoomFactor); - - if (size < 2) { - return false; - } - - r = 0 | ((size) / 2); - - rr = 0 | ((size + 1) % 2); - - points = []; - - x = r; - y = 0; - e = 1 - r; - c = r; - - while (x >= y) { - points.push(c + x - rr + (c + y - rr) * size); - points.push(c + y - rr + (c + x - rr) * size); - - points.push(c - y + (c + x - rr) * size); - points.push(c - x + (c + y - rr) * size); - - points.push(c - y + (c - x) * size); - points.push(c - x + (c - y) * size); - - points.push(c + y - rr + (c - x) * size); - points.push(c + x - rr + (c - y) * size); - - ++y; - - if (e <= 0) { - e += 2 * y + 1; - } - else { - x--; - e += 2 * (y - x) + 1; - } - } - - this.size = size; - this.radius = r; - this.points = points; - - return true; - } -}; -var TegakiHistory = { - maxSize: 50, - - undoStack: [], - redoStack: [], - - pendingAction: null, - - clear: function() { - this.undoStack = []; - this.redoStack = []; - this.pendingAction = null; - - this.onChange(0); - }, - - push: function(action) { - if (!action) { - return; - } - - if (action.coalesce) { - if (this.undoStack[this.undoStack.length - 1] instanceof action.constructor) { - if (this.undoStack[this.undoStack.length - 1].coalesce(action)) { - return; - } - } - } - - this.undoStack.push(action); - - if (this.undoStack.length > this.maxSize) { - this.undoStack.shift(); - } - - if (this.redoStack.length > 0) { - this.redoStack = []; - } - - this.onChange(0); - }, - - undo: function() { - var action; - - if (!this.undoStack.length) { - return; - } - - action = this.undoStack.pop(); - - action.undo(); - - this.redoStack.push(action); - - this.onChange(-1); - }, - - redo: function() { - var action; - - if (!this.redoStack.length) { - return; - } - - action = this.redoStack.pop(); - - action.redo(); - - this.undoStack.push(action); - - this.onChange(1); - }, - - onChange: function(type) { - Tegaki.onHistoryChange(this.undoStack.length, this.redoStack.length, type); - }, - - sortPosLayerCallback: function(a, b) { - if (a[0] > b[0]) { return 1; } - if (a[0] < b[0]) { return -1; } - return 0; - } -}; - -var TegakiHistoryActions = { - Dummy: function() { - this.undo = function() {}; - this.redo = function() {}; - }, - - Draw: function(layerId) { - this.coalesce = false; - - this.imageDataBefore = null; - this.imageDataAfter = null; - this.layerId = layerId; - }, - - DeleteLayers: function(layerPosMap, params) { - var item; - - this.coalesce = false; - - this.layerPosMap = []; - - for (item of layerPosMap.sort(TegakiHistory.sortPosLayerCallback)) { - item[1] = TegakiLayers.cloneLayer(item[1]); - this.layerPosMap.push(item); - } - - this.tgtLayerId = null; - - this.aLayerIdBefore = null; - this.aLayerIdAfter = null; - - this.imageDataBefore = null; - this.imageDataAfter = null; - - this.mergeDown = false; - - if (params) { - for (let k in params) { - this[k] = params[k]; - } - } - }, - - AddLayer: function(params, aLayerIdBefore, aLayerIdAfter) { - this.coalesce = false; - - this.layer = params; - this.layerId = params.id; - this.aLayerIdBefore = aLayerIdBefore; - this.aLayerIdAfter = aLayerIdAfter; - }, - - MoveLayers: function(layers, belowPos, activeLayerId) { - this.coalesce = false; - - this.layers = layers; - this.belowPos = belowPos; - this.aLayerId = activeLayerId; - }, - - SetLayersAlpha: function(layerAlphas, newAlpha) { - this.layerAlphas = layerAlphas; - this.newAlpha = newAlpha; - }, - - SetLayerName: function(id, oldName, newName) { - this.layerId = id; - this.oldName = oldName; - this.newName = newName; - } -}; - -// --- - -TegakiHistoryActions.Draw.prototype.addCanvasState = function(imageData, type) { - if (type) { - this.imageDataAfter = $T.copyImageData(imageData); - } - else { - this.imageDataBefore = $T.copyImageData(imageData); - } -}; - -TegakiHistoryActions.Draw.prototype.exec = function(type) { - var layer = TegakiLayers.getLayerById(this.layerId); - - if (type) { - layer.ctx.putImageData(this.imageDataAfter, 0, 0); - TegakiLayers.syncLayerImageData(layer, this.imageDataAfter); - } - else { - layer.ctx.putImageData(this.imageDataBefore, 0, 0); - TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); - } - - TegakiUI.updateLayerPreview(layer); - TegakiLayers.setActiveLayer(this.layerId); -}; - -TegakiHistoryActions.Draw.prototype.undo = function() { - this.exec(0); -}; - -TegakiHistoryActions.Draw.prototype.redo = function() { - this.exec(1); -}; - -TegakiHistoryActions.DeleteLayers.prototype.undo = function() { - var i, lim, refLayer, layer, pos, refId; - - for (i = 0, lim = this.layerPosMap.length; i < lim; ++i) { - [pos, layer] = this.layerPosMap[i]; - - layer = TegakiLayers.cloneLayer(layer); - - refLayer = Tegaki.layers[pos]; - - if (refLayer) { - if (refId = TegakiLayers.getLayerBelowId(refLayer.id)) { - refId = refId.id; - } - - TegakiUI.updateLayersGridAdd(layer, refId); - TegakiUI.updateLayerPreview(layer); - Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.canvas); - Tegaki.layers.splice(pos, 0, layer); - } - else { - - if (!Tegaki.layers[0]) { - refLayer = Tegaki.layersCnt.children[0]; - } - else { - refLayer = Tegaki.layers[Tegaki.layers.length - 1].canvas; - } - - TegakiUI.updateLayersGridAdd(layer, TegakiLayers.getTopLayerId()); - TegakiUI.updateLayerPreview(layer); - Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.nextElementSibling); - Tegaki.layers.push(layer); - } - } - - if (this.tgtLayerId) { - layer = TegakiLayers.getLayerById(this.tgtLayerId); - layer.ctx.putImageData(this.imageDataBefore, 0, 0); - TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); - TegakiLayers.setLayerAlpha(layer, this.tgtLayerAlpha); - TegakiUI.updateLayerPreview(layer); - } - - TegakiLayers.setActiveLayer(this.aLayerIdBefore); -}; - -TegakiHistoryActions.DeleteLayers.prototype.redo = function() { - var layer, ids = []; - - for (layer of this.layerPosMap) { - ids.unshift(layer[1].id); - } - - if (this.tgtLayerId) { - if (!this.mergeDown) { - ids.unshift(this.tgtLayerId); - } - TegakiLayers.mergeLayers(new Set(ids)); - } - else { - TegakiLayers.deleteLayers(ids); - } - - TegakiLayers.setActiveLayer(this.aLayerIdAfter); -}; - -TegakiHistoryActions.MoveLayers.prototype.undo = function() { - var i, layer, stack, ref, posMap, len; - - stack = new Array(Tegaki.layers.length); - - posMap = {}; - - for (layer of this.layers) { - posMap[layer[1].id] = layer[0]; - } - - for (i = 0, len = Tegaki.layers.length; i < len; ++i) { - layer = Tegaki.layers[i]; - - if (posMap[layer.id] !== undefined) { - Tegaki.layers.splice(i, 1); - Tegaki.layers.splice(posMap[layer.id], 0, layer); - } - } - - TegakiUI.updayeLayersGridOrder(); - - ref = Tegaki.layersCnt.children[0]; - - for (i = Tegaki.layers.length - 1; i >= 0; i--) { - layer = Tegaki.layers[i]; - Tegaki.layersCnt.insertBefore(layer.canvas, ref.nextElementSibling); - } - - TegakiLayers.setActiveLayer(this.aLayerId); -}; - -TegakiHistoryActions.MoveLayers.prototype.redo = function() { - var layer, layers = new Set(); - - for (layer of this.layers.slice().reverse()) { - layers.add(layer[1].id); - } - - TegakiLayers.setActiveLayer(this.aLayerId); - TegakiLayers.moveLayers(layers, this.belowPos); -}; - -TegakiHistoryActions.AddLayer.prototype.undo = function() { - TegakiLayers.deleteLayers([this.layer.id]); - TegakiLayers.setActiveLayer(this.aLayerIdBefore); - Tegaki.layerCounter--; -}; - -TegakiHistoryActions.AddLayer.prototype.redo = function() { - TegakiLayers.setActiveLayer(this.aLayerIdBefore); - TegakiLayers.addLayer(this.layer); - TegakiLayers.setActiveLayer(this.aLayerIdAfter); -}; - -TegakiHistoryActions.SetLayersAlpha.prototype.undo = function() { - var id, layerAlpha, layer; - - for (layerAlpha of this.layerAlphas) { - [id, layerAlpha] = layerAlpha; - - if (layer = TegakiLayers.getLayerById(id)) { - TegakiLayers.setLayerAlpha(layer, layerAlpha); - } - } - - TegakiUI.updateLayerAlphaOpt(); -}; - -TegakiHistoryActions.SetLayersAlpha.prototype.redo = function() { - var id, layerAlpha, layer; - - for (layerAlpha of this.layerAlphas) { - [id, layerAlpha] = layerAlpha; - - if (layer = TegakiLayers.getLayerById(id)) { - TegakiLayers.setLayerAlpha(layer, this.newAlpha); - } - } - - TegakiUI.updateLayerAlphaOpt(); -}; - -TegakiHistoryActions.SetLayersAlpha.prototype.coalesce = function(action) { - var i; - - if (this.layerAlphas.length !== action.layerAlphas.length) { - return false; - } - - for (i = 0; i < this.layerAlphas.length; ++i) { - if (this.layerAlphas[i][0] !== action.layerAlphas[i][0]) { - return false; - } - } - - this.newAlpha = action.newAlpha; - - return true; -}; - -TegakiHistoryActions.SetLayerName.prototype.exec = function(type) { - var layer = TegakiLayers.getLayerById(this.layerId); - - if (layer) { - layer.name = type ? this.newName : this.oldName; - TegakiUI.updateLayerName(layer); - } -}; - -TegakiHistoryActions.SetLayerName.prototype.undo = function() { - this.exec(0); -}; - -TegakiHistoryActions.SetLayerName.prototype.redo = function() { - this.exec(1); -}; -var TegakiKeybinds = { - keyMap: {}, - - captionMap: {}, - - clear: function() { - this.keyMap = {}; - this.captionMap = {}; - }, - - bind: function(keys, klass, fn, id, caption) { - this.keyMap[keys] = [klass, fn]; - - if (id) { - this.captionMap[id] = caption; - } - }, - - getCaption(id) { - return this.captionMap[id]; - }, - - resolve: function(e) { - var fn, mods, keys, el; - - el = e.target; - - if (el.nodeName == 'INPUT' && (el.type === 'text' || el.type === 'number')) { - return; - } - - mods = []; - - if (e.ctrlKey) { - mods.push('ctrl'); - } - - if (e.shiftKey) { - mods.push('shift'); - } - - keys = e.key.toLowerCase(); - - if (mods[0]) { - keys = mods.join('+') + '+' + keys; - } - - fn = TegakiKeybinds.keyMap[keys]; - - if (fn && !e.altKey && !e.metaKey) { - e.preventDefault(); - e.stopPropagation(); - fn[0][fn[1]](); - } - }, -}; -var TegakiLayers = { - cloneLayer: function(layer) { - var newLayer = Object.assign({}, layer); - - newLayer.canvas = $T.copyCanvas(layer.canvas, true); - newLayer.ctx = newLayer.canvas.getContext('2d'); - newLayer.imageData = $T.copyImageData(layer.imageData); - - return newLayer; - }, - - getCanvasById: function(id) { - return $T.id('tegaki-canvas-' + id); - }, - - getActiveLayer: function() { - return Tegaki.activeLayer; - }, - - getLayerPosById: function(id) { - var i, layers = Tegaki.layers; - - for (i = 0; i < layers.length; ++i) { - if (layers[i].id === id) { - return i; - } - } - - return -1; - }, - - getTopFencedLayerId: function() { - var i, id, layer, layers = Tegaki.layers; - - for (i = layers.length - 1; i >= 0; i--) { - if (TegakiLayers.selectedLayersHas(layers[i].id)) { - break; - } - } - - for (i = i - 1; i >= 0; i--) { - if (!TegakiLayers.selectedLayersHas(layers[i].id)) { - break; - } - } - - if (layer = layers[i]) { - id = layer.id; - } - else { - id = 0; - } - - return id; - }, - - getSelectedEdgeLayerPos: function(top) { - var i, layers = Tegaki.layers; - - if (top) { - for (i = Tegaki.layers.length - 1; i >= 0; i--) { - if (TegakiLayers.selectedLayersHas(layers[i].id)) { - break; - } - } - } - else { - for (i = 0; i < layers.length; ++i) { - if (TegakiLayers.selectedLayersHas(layers[i].id)) { - break; - } - } - - if (i >= layers.length) { - i = -1; - } - } - - return i; - }, - - getTopLayer: function() { - return Tegaki.layers[Tegaki.layers.length - 1]; - }, - - getTopLayerId: function() { - var layer = TegakiLayers.getTopLayer(); - - if (layer) { - return layer.id; - } - else { - return 0; - } - }, - - getLayerBelowId: function(belowId) { - var idx; - - idx = TegakiLayers.getLayerPosById(belowId); - - if (idx < 1) { - return null; - } - - return Tegaki.layers[idx - 1]; - }, - - getLayerById: function(id) { - return Tegaki.layers[TegakiLayers.getLayerPosById(id)]; - }, - - isSameLayerOrder: function(a, b) { - var i, al; - - if (a.length !== b.length) { - return false; - } - - for (i = 0; al = a[i]; ++i) { - if (al.id !== b[i].id) { - return false; - } - } - - return true; - }, - - addLayer: function(baseLayer = {}) { - var id, canvas, k, params, layer, afterNode, afterPos, - aLayerIdBefore, ctx; - - if (Tegaki.activeLayer) { - aLayerIdBefore = Tegaki.activeLayer.id; - afterPos = TegakiLayers.getLayerPosById(Tegaki.activeLayer.id); - afterNode = $T.cls('tegaki-layer', Tegaki.layersCnt)[afterPos]; - } - else { - afterPos = -1; - afterNode = null; - } - - if (!afterNode) { - afterNode = Tegaki.layersCnt.firstElementChild; - } - - canvas = $T.el('canvas'); - canvas.className = 'tegaki-layer'; - canvas.width = Tegaki.baseWidth; - canvas.height = Tegaki.baseHeight; - - id = ++Tegaki.layerCounter; - - canvas.id = 'tegaki-canvas-' + id; - canvas.setAttribute('data-id', id); - - params = { - name: TegakiStrings.layer + ' ' + id, - visible: true, - alpha: 1.0, - }; - - ctx = canvas.getContext('2d'); - - layer = { - id: id, - canvas: canvas, - ctx: ctx, - imageData: ctx.getImageData(0, 0, canvas.width, canvas.height) - }; - - for (k in params) { - if (baseLayer[k] !== undefined) { - params[k] = baseLayer[k]; - } - - layer[k] = params[k]; - } - - Tegaki.layers.splice(afterPos + 1, 0, layer); - - TegakiUI.updateLayersGridAdd(layer, aLayerIdBefore); - - Tegaki.layersCnt.insertBefore(canvas, afterNode.nextElementSibling); - - Tegaki.onLayerStackChanged(); - - return new TegakiHistoryActions.AddLayer(layer, aLayerIdBefore, id); - }, - - deleteLayers: function(ids, extraParams) { - var id, idx, layer, layers, delIndexes, params; - - params = { - aLayerIdBefore: Tegaki.activeLayer ? Tegaki.activeLayer.id : -1, - aLayerIdAfter: TegakiLayers.getTopFencedLayerId() - }; - - layers = []; - - delIndexes = []; - - for (id of ids) { - idx = TegakiLayers.getLayerPosById(id); - layer = Tegaki.layers[idx]; - - layers.push([idx, layer]); - - Tegaki.layersCnt.removeChild(layer.canvas); - - delIndexes.push(idx); - - TegakiUI.updateLayersGridRemove(id); - - TegakiUI.deleteLayerPreviewCtx(layer); - } - - delIndexes = delIndexes.sort($T.sortDescCb); - - for (idx of delIndexes) { - Tegaki.layers.splice(idx, 1); - } - - if (extraParams) { - Object.assign(params, extraParams); - } - - Tegaki.onLayerStackChanged(); - - return new TegakiHistoryActions.DeleteLayers(layers, params); - }, - - mergeLayers: function(idSet) { - var canvas, ctx, imageDataAfter, imageDataBefore, - targetLayer, action, layer, layers, delIds, mergeDown; - - layers = []; - - for (layer of Tegaki.layers) { - if (idSet.has(layer.id)) { - layers.push(layer); - } - } - - if (layers.length < 1) { - return; - } - - if (layers.length === 1) { - targetLayer = TegakiLayers.getLayerBelowId(layers[0].id); - - if (!targetLayer) { - return; - } - - layers.unshift(targetLayer); - - mergeDown = true; - } - else { - targetLayer = layers[layers.length - 1]; - - mergeDown = false; - } - - canvas = $T.el('canvas'); - canvas.width = Tegaki.baseWidth; - canvas.height = Tegaki.baseHeight; - - ctx = canvas.getContext('2d'); - - imageDataBefore = $T.copyImageData(targetLayer.imageData); - - delIds = []; - - for (layer of layers) { - if (layer.id !== targetLayer.id) { - delIds.push(layer.id); - } - - ctx.globalAlpha = layer.alpha; - ctx.drawImage(layer.canvas, 0, 0); - } - - $T.clearCtx(targetLayer.ctx); - - targetLayer.ctx.drawImage(canvas, 0, 0); - - TegakiLayers.syncLayerImageData(targetLayer); - - imageDataAfter = $T.copyImageData(targetLayer.imageData); - - action = TegakiLayers.deleteLayers(delIds, { - tgtLayerId: targetLayer.id, - tgtLayerAlpha: targetLayer.alpha, - aLayerIdAfter: targetLayer.id, - imageDataBefore: imageDataBefore, - imageDataAfter: imageDataAfter, - mergeDown: mergeDown - }); - - TegakiLayers.setLayerAlpha(targetLayer, 1.0); - - TegakiUI.updateLayerAlphaOpt(); - - TegakiUI.updateLayerPreview(targetLayer); - - Tegaki.onLayerStackChanged(); - - return action; - }, - - moveLayers: function(idSet, belowPos) { - var idx, layer, - historyLayers, updLayers, movedLayers, - tgtCanvas, updTgtPos; - - if (!idSet.size || !Tegaki.layers.length) { - return; - } - - if (belowPos >= Tegaki.layers.length) { - tgtCanvas = TegakiLayers.getTopLayer().canvas.nextElementSibling; - } - else { - layer = Tegaki.layers[belowPos]; - tgtCanvas = layer.canvas; - } - - historyLayers = []; - updLayers = []; - movedLayers = []; - - updTgtPos = belowPos; - - idx = 0; - - for (layer of Tegaki.layers) { - if (idSet.has(layer.id)) { - if (belowPos > 0 && idx <= belowPos) { - updTgtPos--; - } - - historyLayers.push([idx, layer]); - movedLayers.push(layer); - } - else { - updLayers.push(layer); - } - - ++idx; - } - - updLayers.splice(updTgtPos, 0, ...movedLayers); - - if (TegakiLayers.isSameLayerOrder(updLayers, Tegaki.layers)) { - return; - } - - Tegaki.layers = updLayers; - - for (layer of historyLayers) { - Tegaki.layersCnt.insertBefore(layer[1].canvas, tgtCanvas); - } - - TegakiUI.updayeLayersGridOrder(); - - Tegaki.onLayerStackChanged(); - - return new TegakiHistoryActions.MoveLayers( - historyLayers, belowPos, - Tegaki.activeLayer ? Tegaki.activeLayer.id : -1 - ); - }, - - setLayerVisibility: function(layer, flag) { - layer.visible = flag; - - if (flag) { - layer.canvas.classList.remove('tegaki-hidden'); - } - else { - layer.canvas.classList.add('tegaki-hidden'); - } - - Tegaki.onLayerStackChanged(); - - TegakiUI.updateLayersGridVisibility(layer.id, flag); - }, - - setLayerAlpha: function(layer, alpha) { - layer.alpha = alpha; - layer.canvas.style.opacity = alpha; - }, - - setActiveLayer: function(id) { - var idx, layer; - - if (!id) { - id = TegakiLayers.getTopLayerId(); - - if (!id) { - Tegaki.activeLayer = null; - return; - } - } - - idx = TegakiLayers.getLayerPosById(id); - - if (idx < 0) { - return; - } - - layer = Tegaki.layers[idx]; - - if (Tegaki.activeLayer) { - Tegaki.copyContextState(Tegaki.activeLayer.ctx, layer.ctx); - } - - Tegaki.activeLayer = layer; - - TegakiLayers.selectedLayersClear(); - TegakiLayers.selectedLayersAdd(id); - - TegakiUI.updateLayersGridActive(id); - TegakiUI.updateLayerAlphaOpt(); - - Tegaki.onLayerStackChanged(); - }, - - syncLayerImageData(layer, imageData = null) { - if (imageData) { - layer.imageData = $T.copyImageData(imageData); - } - else { - layer.imageData = layer.ctx.getImageData( - 0, 0, Tegaki.baseWidth, Tegaki.baseHeight - ); - } - }, - - selectedLayersHas: function(id) { - return Tegaki.selectedLayers.has(+id); - }, - - selectedLayersClear: function() { - Tegaki.selectedLayers.clear(); - TegakiUI.updateLayerAlphaOpt(); - TegakiUI.updateLayersGridSelectedClear(); - }, - - selectedLayersAdd: function(id) { - Tegaki.selectedLayers.add(+id); - TegakiUI.updateLayerAlphaOpt(); - TegakiUI.updateLayersGridSelectedSet(id, true); - }, - - selectedLayersRemove: function(id) { - Tegaki.selectedLayers.delete(+id); - TegakiUI.updateLayerAlphaOpt(); - TegakiUI.updateLayersGridSelectedSet(id, false); - }, - - selectedLayersToggle: function(id) { - if (TegakiLayers.selectedLayersHas(id)) { - TegakiLayers.selectedLayersRemove(id); - } - else { - TegakiLayers.selectedLayersAdd(id); - } - } -}; -var Tegaki = { - VERSION: '0.9.2', - - startTimeStamp: 0, - - bg: null, - canvas: null, - ctx: null, - layers: [], - - layersCnt: null, - canvasCnt: null, - - ghostBuffer: null, - blendBuffer: null, - ghostBuffer32: null, - blendBuffer32: null, - - activeLayer: null, - - layerCounter: 0, - selectedLayers: new Set(), - - activePointerId: 0, - activePointerIsPen: false, - - isPainting: false, - - offsetX: 0, - offsetY: 0, - - zoomLevel: 0, - zoomFactor: 1.0, - zoomFactorList: [0.5, 1.0, 2.0, 4.0, 8.0, 16.0], - zoomBaseLevel: 1, - - hasCustomCanvas: false, - - TWOPI: 2 * Math.PI, - - toolList: [ - TegakiPencil, - TegakiPen, - TegakiAirbrush, - TegakiBucket, - TegakiTone, - TegakiPipette, - TegakiBlur, - TegakiEraser - ], - - tools: {}, - - tool: null, - - colorPaletteId: 0, - - toolColor: '#000000', - defaultTool: 'pencil', - - bgColor: '#ffffff', - maxSize: 64, - maxLayers: 25, - baseWidth: 0, - baseHeight: 0, - - replayRecorder: null, - replayViewer: null, - - onDoneCb: null, - onCancelCb: null, - - replayMode: false, - - saveReplay: false, - - open: function(opts = {}) { - var self = Tegaki; - - if (self.bg) { - if (self.replayMode !== (opts.replayMode ? true : false)) { - self.destroy(); - } - else { - self.resume(); - return; - } - } - - self.startTimeStamp = Date.now(); - - if (opts.bgColor) { - self.bgColor = opts.bgColor; - } - - self.hasCustomCanvas = false; - - self.saveReplay = !!opts.saveReplay; - self.replayMode = !!opts.replayMode; - - self.onDoneCb = opts.onDone; - self.onCancelCb = opts.onCancel; - - self.baseWidth = opts.width || 0; - self.baseHeight = opts.height || 0; - - self.createTools(); - - self.initKeybinds(); - - [self.bg, self.canvasCnt, self.layersCnt] = TegakiUI.buildUI(); - - document.body.appendChild(self.bg); - document.body.classList.add('tegaki-backdrop'); - - if (!self.replayMode) { - self.init(); - - self.setTool(self.defaultTool); - - if (self.saveReplay) { - self.replayRecorder = new TegakiReplayRecorder(); - self.replayRecorder.start(); - } - } - else { - TegakiUI.setReplayMode(true); - - self.replayViewer = new TegakiReplayViewer(); - - if (opts.replayURL) { - self.loadReplayFromURL(opts.replayURL); - } - } - }, - - init: function() { - var self = Tegaki; - - self.createCanvas(); - - self.updateLayersCntSize(); - - self.createBuffers(); - - self.updatePosOffset(); - - self.resetLayers(); - - TegakiCursor.init(self.baseWidth, self.baseHeight); - - self.bindGlobalEvents(); - - TegakiUI.updateUndoRedo(0, 0); - TegakiUI.updateZoomLevel(); - }, - - initFromReplay: function() { - var self, r; - - self = Tegaki; - r = self.replayViewer; - - self.initToolsFromReplay(); - - self.baseWidth = r.canvasWidth; - self.baseHeight = r.canvasHeight; - self.bgColor = $T.RgbToHex(...r.bgColor); - - self.toolColor = $T.RgbToHex(...r.toolColor); - }, - - initToolsFromReplay: function() { - var self, r, name, tool, rTool, prop, props; - - self = Tegaki; - r = self.replayViewer; - - for (name in self.tools) { - tool = self.tools[name]; - - if (tool.id === r.toolId) { - self.defaultTool = name; - } - - rTool = r.toolMap[tool.id]; - - props = ['step', 'size', 'alpha', 'flow', 'tipId']; - - for (prop of props) { - if (rTool[prop] !== undefined) { - tool[prop] = rTool[prop]; - } - } - - props = [ - 'sizeDynamicsEnabled', 'alphaDynamicsEnabled', 'flowDynamicsEnabled', - 'usePreserveAlpha' - ]; - - for (prop of props) { - if (rTool[prop] !== undefined) { - tool[prop] = !!rTool[prop]; - } - } - } - }, - - resetLayers: function() { - var i, len; - - if (Tegaki.layers.length) { - for (i = 0, len = Tegaki.layers.length; i < len; ++i) { - Tegaki.layersCnt.removeChild(Tegaki.layers[i].canvas); - } - - Tegaki.layers = []; - Tegaki.layerCounter = 0; - - TegakiUI.updateLayersGridClear(); - } - - TegakiLayers.addLayer(); - TegakiLayers.setActiveLayer(0); - }, - - createCanvas: function() { - var canvas, self = Tegaki; - - canvas = $T.el('canvas'); - canvas.id = 'tegaki-canvas'; - canvas.width = self.baseWidth; - canvas.height = self.baseHeight; - - self.canvas = canvas; - - self.ctx = canvas.getContext('2d'); - self.ctx.fillStyle = self.bgColor; - self.ctx.fillRect(0, 0, self.baseWidth, self.baseHeight); - - self.layersCnt.appendChild(canvas); - }, - - createTools: function() { - var klass, tool; - - for (klass of Tegaki.toolList) { - tool = new klass(); - Tegaki.tools[tool.name] = tool; - } - }, - - bindGlobalEvents: function() { - var self = Tegaki; - - if (!self.replayMode) { - let cursorNode = TegakiCursor.getCanvas(); - $T.on(cursorNode, 'pointermove', self.onPointerMove); - $T.on(cursorNode, 'pointerdown', self.onPointerDown); - $T.on(cursorNode, 'pointerout', self.onPointerOut); - - $T.on(document, 'pointerup', self.onPointerUp); - $T.on(document, 'pointercancel', self.onPointerUp); - - $T.on(document, 'keydown', TegakiKeybinds.resolve); - - $T.on(window, 'beforeunload', Tegaki.onTabClose); - } - else { - $T.on(document, 'visibilitychange', Tegaki.onVisibilityChange); - } - - $T.on(self.bg, 'contextmenu', self.onDummy); - $T.on(window, 'resize', self.onWindowResized); - $T.on(window, 'scroll', self.updatePosOffset); - }, - - unBindGlobalEvents: function() { - var self = Tegaki; - - if (!self.replayMode) { - let cursorNode = TegakiCursor.getCanvas(); - $T.off(cursorNode, 'pointermove', self.onPointerMove); - $T.off(cursorNode, 'pointerdown', self.onPointerDown); - $T.off(cursorNode, 'pointerout', self.onPointerOut); - - $T.off(document, 'pointerup', self.onPointerUp); - $T.off(document, 'pointercancel', self.onPointerUp); - - $T.off(document, 'keydown', TegakiKeybinds.resolve); - - $T.off(window, 'beforeunload', Tegaki.onTabClose); - } - else { - $T.off(document, 'visibilitychange', Tegaki.onVisibilityChange); - } - - $T.off(self.bg, 'contextmenu', self.onDummy); - $T.off(window, 'resize', self.onWindowResized); - $T.off(window, 'scroll', self.updatePosOffset); - }, - - createBuffers() { - Tegaki.ghostBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); - Tegaki.blendBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); - Tegaki.ghostBuffer32 = new Uint32Array(Tegaki.ghostBuffer.data.buffer); - Tegaki.blendBuffer32 = new Uint32Array(Tegaki.blendBuffer.data.buffer); - }, - - clearBuffers() { - Tegaki.ghostBuffer32.fill(0); - Tegaki.blendBuffer32.fill(0); - }, - - destroyBuffers() { - Tegaki.ghostBuffer = null; - Tegaki.blendBuffer = null; - Tegaki.ghostBuffer32 = null; - Tegaki.blendBuffer32 = null; - }, - - disableSmoothing: function(ctx) { - ctx.mozImageSmoothingEnabled = false; - ctx.webkitImageSmoothingEnabled = false; - ctx.msImageSmoothingEnabled = false; - ctx.imageSmoothingEnabled = false; - }, - - updateLayersCntSize: function() { - var style = Tegaki.layersCnt.style; - - style.width = Tegaki.baseWidth + 'px'; - style.height = Tegaki.baseHeight + 'px'; - }, - - onTabClose: function(e) { - e.preventDefault(); - e.returnValue = ''; - }, - - onVisibilityChange: function(e) { - if (!Tegaki.replayMode) { - return; - } - - if (document.visibilityState === 'visible') { - if (Tegaki.replayViewer.autoPaused) { - Tegaki.replayViewer.play(); - } - } - else { - if (Tegaki.replayViewer.playing) { - Tegaki.replayViewer.autoPause(); - } - } - }, - - onWindowResized: function() { - Tegaki.updatePosOffset(); - TegakiCursor.updateCanvasSize(); - }, - - initKeybinds: function() { - var cls, tool; - - if (Tegaki.replayMode) { - return; - } - - TegakiKeybinds.bind('ctrl+z', TegakiHistory, 'undo', 'undo', 'Ctrl+Z'); - TegakiKeybinds.bind('ctrl+y', TegakiHistory, 'redo', 'redo', 'Ctrl+Y'); - - TegakiKeybinds.bind('+', Tegaki, 'setToolSizeUp', 'toolSize', 'Numpad +/-'); - TegakiKeybinds.bind('-', Tegaki, 'setToolSizeDown'); - - for (tool in Tegaki.tools) { - cls = Tegaki.tools[tool]; - - if (cls.keybind) { - TegakiKeybinds.bind(cls.keybind, cls, 'set'); - } - } - }, - - getPointerPos: function(e, axis) { - if (axis === 0) { - return 0 | (( - e.clientX - + window.pageXOffset - + Tegaki.canvasCnt.scrollLeft - - Tegaki.offsetX - ) / Tegaki.zoomFactor); - } - else { - return 0 | (( - e.clientY - + window.pageYOffset - + Tegaki.canvasCnt.scrollTop - - Tegaki.offsetY - ) / Tegaki.zoomFactor); - } - }, - - resume: function() { - if (Tegaki.saveReplay) { - Tegaki.replayRecorder.start(); - } - - Tegaki.bg.classList.remove('tegaki-hidden'); - document.body.classList.add('tegaki-backdrop'); - Tegaki.setZoom(0); - Tegaki.updateLayersCntSize(); - Tegaki.updatePosOffset(); - TegakiCursor.updateCanvasSize(); - Tegaki.bindGlobalEvents(); - }, - - hide: function() { - if (Tegaki.saveReplay) { - Tegaki.replayRecorder.stop(); - } - - Tegaki.bg.classList.add('tegaki-hidden'); - document.body.classList.remove('tegaki-backdrop'); - Tegaki.unBindGlobalEvents(); - }, - - destroy: function() { - Tegaki.unBindGlobalEvents(); - - TegakiKeybinds.clear(); - - TegakiHistory.clear(); - - Tegaki.bg.parentNode.removeChild(Tegaki.bg); - - document.body.classList.remove('tegaki-backdrop'); - - Tegaki.startTimeStamp = 0; - - Tegaki.bg = null; - Tegaki.canvasCnt = null; - Tegaki.layersCnt = null; - Tegaki.canvas = null; - Tegaki.ctx = null; - Tegaki.layers = []; - Tegaki.layerCounter = 0; - Tegaki.zoomLevel = 0; - Tegaki.zoomFactor = 1.0; - Tegaki.activeLayer = null; - - Tegaki.tool = null; - - TegakiCursor.destroy(); - - Tegaki.replayRecorder = null; - Tegaki.replayViewer = null; - - Tegaki.destroyBuffers(); - }, - - flatten: function(ctx) { - var i, layer, canvas, len; - - if (!ctx) { - canvas = $T.el('canvas'); - ctx = canvas.getContext('2d'); - } - else { - canvas = ctx.canvas; - } - - canvas.width = Tegaki.canvas.width; - canvas.height = Tegaki.canvas.height; - - ctx.drawImage(Tegaki.canvas, 0, 0); - - for (i = 0, len = Tegaki.layers.length; i < len; ++i) { - layer = Tegaki.layers[i]; - - if (!layer.visible) { - continue; - } - - ctx.globalAlpha = layer.alpha; - ctx.drawImage(layer.canvas, 0, 0); - } - - return canvas; - }, - - onReplayLoaded: function() { - TegakiUI.clearMsg(); - Tegaki.initFromReplay(); - Tegaki.init(); - Tegaki.setTool(Tegaki.defaultTool); - TegakiUI.updateReplayControls(); - TegakiUI.updateReplayTime(true); - TegakiUI.enableReplayControls(true); - Tegaki.replayViewer.play(); - }, - - onReplayGaplessClick: function() { - Tegaki.replayViewer.toggleGapless(); - TegakiUI.updateReplayGapless(); - }, - - onReplayPlayPauseClick: function() { - Tegaki.replayViewer.togglePlayPause(); - }, - - onReplayRewindClick: function() { - Tegaki.replayViewer.rewind(); - }, - - onReplaySlowDownClick: function() { - Tegaki.replayViewer.slowDown(); - TegakiUI.updateReplaySpeed(); - }, - - onReplaySpeedUpClick: function() { - Tegaki.replayViewer.speedUp(); - TegakiUI.updateReplaySpeed(); - }, - - onReplayTimeChanged: function() { - TegakiUI.updateReplayTime(); - }, - - onReplayPlayPauseChanged: function() { - TegakiUI.updateReplayPlayPause(); - }, - - onReplayReset: function() { - Tegaki.initFromReplay(); - Tegaki.setTool(Tegaki.defaultTool); - Tegaki.resizeCanvas(Tegaki.baseWidth, Tegaki.baseHeight); - TegakiUI.updateReplayControls(); - TegakiUI.updateReplayTime(); - }, - - onMainColorClick: function(e) { - var el; - e.preventDefault(); - el = $T.id('tegaki-colorpicker'); - el.click(); - }, - - onPaletteColorClick: function(e) { - if (e.button === 2) { - this.style.backgroundColor = Tegaki.toolColor; - this.setAttribute('data-color', Tegaki.toolColor); - } - else if (e.button === 0) { - Tegaki.setToolColor(this.getAttribute('data-color')); - } - }, - - onColorPicked: function(e) { - $T.id('tegaki-color').style.backgroundColor = this.value; - Tegaki.setToolColor(this.value); - }, - - onSwitchPaletteClick: function(e) { - var id; - - if (e.target.hasAttribute('data-prev')) { - id = Tegaki.colorPaletteId - 1; - } - else { - id = Tegaki.colorPaletteId + 1; - } - - Tegaki.setColorPalette(id); - }, - - setColorPalette: function(id) { - if (id < 0 || id >= TegakiColorPalettes.length) { - return; - } - - Tegaki.colorPaletteId = id; - TegakiUI.updateColorPalette(); - }, - - setToolSizeUp: function() { - Tegaki.setToolSize(Tegaki.tool.size + 1); - }, - - setToolSizeDown: function() { - Tegaki.setToolSize(Tegaki.tool.size - 1); - }, - - setToolSize: function(size) { - if (size > 0 && size <= Tegaki.maxSize) { - Tegaki.tool.setSize(size); - Tegaki.updateCursorStatus(); - Tegaki.recordEvent(TegakiEventSetToolSize, performance.now(), size); - TegakiUI.updateToolSize(); - } - }, - - setToolAlpha: function(alpha) { - alpha = Math.fround(alpha); - - if (alpha >= 0.0 && alpha <= 1.0) { - Tegaki.tool.setAlpha(alpha); - Tegaki.recordEvent(TegakiEventSetToolAlpha, performance.now(), alpha); - TegakiUI.updateToolAlpha(); - } - }, - - setToolFlow: function(flow) { - flow = Math.fround(flow); - - if (flow >= 0.0 && flow <= 1.0) { - Tegaki.tool.setFlow(flow); - Tegaki.recordEvent(TegakiEventSetToolFlow, performance.now(), flow); - TegakiUI.updateToolFlow(); - } - }, - - setToolColor: function(color) { - Tegaki.toolColor = color; - $T.id('tegaki-color').style.backgroundColor = color; - $T.id('tegaki-colorpicker').value = color; - Tegaki.tool.setColor(color); - Tegaki.recordEvent(TegakiEventSetColor, performance.now(), Tegaki.tool.rgb); - }, - - setToolColorRGB: function(r, g, b) { - Tegaki.setToolColor($T.RgbToHex(r, g, b)); - }, - - setTool: function(tool) { - Tegaki.tools[tool].set(); - }, - - setToolById: function(id) { - var tool; - - for (tool in Tegaki.tools) { - if (Tegaki.tools[tool].id === id) { - Tegaki.setTool(tool); - return; - } - } - }, - - setZoom: function(level) { - var idx; - - idx = level + Tegaki.zoomBaseLevel; - - if (idx >= Tegaki.zoomFactorList.length || idx < 0 || !Tegaki.canvas) { - return; - } - - Tegaki.zoomLevel = level; - Tegaki.zoomFactor = Tegaki.zoomFactorList[idx]; - - TegakiUI.updateZoomLevel(); - - Tegaki.layersCnt.style.width = Math.ceil(Tegaki.baseWidth * Tegaki.zoomFactor) + 'px'; - Tegaki.layersCnt.style.height = Math.ceil(Tegaki.baseHeight * Tegaki.zoomFactor) + 'px'; - - Tegaki.updateCursorStatus(); - Tegaki.updatePosOffset(); - }, - - onZoomChange: function() { - if (this.hasAttribute('data-in')) { - Tegaki.setZoom(Tegaki.zoomLevel + 1); - } - else { - Tegaki.setZoom(Tegaki.zoomLevel - 1); - } - - TegakiCursor.updateCanvasSize(); - }, - - onNewClick: function() { - var width, height, tmp, self = Tegaki; - - width = prompt(TegakiStrings.promptWidth, self.canvas.width); - - if (!width) { return; } - - height = prompt(TegakiStrings.promptHeight, self.canvas.height); - - if (!height) { return; } - - width = +width; - height = +height; - - if (width < 1 || height < 1) { - TegakiUI.printMsg(TegakiStrings.badDimensions); - return; - } - - tmp = {}; - self.copyContextState(self.activeLayer.ctx, tmp); - self.resizeCanvas(width, height); - self.copyContextState(tmp, self.activeLayer.ctx); - - self.setZoom(0); - TegakiHistory.clear(); - - TegakiUI.updateLayerPreviewSize(); - - self.startTimeStamp = Date.now(); - - if (self.saveReplay) { - self.createTools(); - self.setTool(self.defaultTool); - self.replayRecorder = new TegakiReplayRecorder(); - self.replayRecorder.start(); - } - }, - - onOpenClick: function() { - var el, tainted; - - tainted = TegakiHistory.undoStack[0] || TegakiHistory.redoStack[0]; - - if (tainted || Tegaki.saveReplay) { - if (!confirm(TegakiStrings.confirmChangeCanvas)) { - return; - } - } - - el = $T.id('tegaki-filepicker'); - el.click(); - }, - - loadReplayFromFile: function() { - Tegaki.replayViewer.debugLoadLocal(); - }, - - loadReplayFromURL: function(url) { - TegakiUI.printMsg(TegakiStrings.loadingReplay, 0); - Tegaki.replayViewer.loadFromURL(url); - }, - - onExportClick: function() { - Tegaki.flatten().toBlob(function(b) { - var el = $T.el('a'); - el.className = 'tegaki-hidden'; - el.download = $T.generateFilename() + '.png'; - el.href = URL.createObjectURL(b); - Tegaki.bg.appendChild(el); - el.click(); - Tegaki.bg.removeChild(el); - }, 'image/png'); - }, - - onUndoClick: function() { - TegakiHistory.undo(); - }, - - onRedoClick: function() { - TegakiHistory.redo(); - }, - - onHistoryChange: function(undoSize, redoSize, type = 0) { - TegakiUI.updateUndoRedo(undoSize, redoSize); - - if (type === -1) { - Tegaki.recordEvent(TegakiEventUndo, performance.now()); - } - else if (type === 1) { - Tegaki.recordEvent(TegakiEventRedo, performance.now()); - } - }, - - onDoneClick: function() { - Tegaki.hide(); - Tegaki.onDoneCb(); - }, - - onCancelClick: function() { - if (!confirm(TegakiStrings.confirmCancel)) { - return; - } - - Tegaki.destroy(); - Tegaki.onCancelCb(); - }, - - onCloseViewerClick: function() { - Tegaki.replayViewer.destroy(); - Tegaki.destroy(); - }, - - onToolSizeChange: function() { - var val = +this.value; - - if (val < 1) { - val = 1; - } - else if (val > Tegaki.maxSize) { - val = Tegaki.maxSize; - } - - Tegaki.setToolSize(val); - }, - - onToolAlphaChange: function(e) { - var val = +this.value; - - val = val / 100; - - if (val < 0.0) { - val = 0.0; - } - else if (val > 1.0) { - val = 1.0; - } - - Tegaki.setToolAlpha(val); - }, - - onToolFlowChange: function(e) { - var val = +this.value; - - val = val / 100; - - if (val < 0.0) { - val = 0.0; - } - else if (val > 1.0) { - val = 1.0; - } - - Tegaki.setToolFlow(val); - }, - - onToolPressureSizeClick: function(e) { - if (!Tegaki.tool.useSizeDynamics) { - return; - } - - Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled); - }, - - setToolSizeDynamics: function(flag) { - Tegaki.tool.setSizeDynamics(flag); - TegakiUI.updateToolDynamics(); - Tegaki.recordEvent(TegakiEventSetToolSizeDynamics, performance.now(), +flag); - }, - - onToolPressureAlphaClick: function(e) { - if (!Tegaki.tool.useAlphaDynamics) { - return; - } - - Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled); - }, - - setToolAlphaDynamics: function(flag) { - Tegaki.tool.setAlphaDynamics(flag); - TegakiUI.updateToolDynamics(); - Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics, performance.now(), +flag); - }, - - onToolPressureFlowClick: function(e) { - if (!Tegaki.tool.useFlowDynamics) { - return; - } - - Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled); - }, - - setToolFlowDynamics: function(flag) { - Tegaki.tool.setFlowDynamics(flag); - TegakiUI.updateToolDynamics(); - Tegaki.recordEvent(TegakiEventSetToolFlowDynamics, performance.now(), +flag); - }, - - onToolPreserveAlphaClick: function(e) { - if (!Tegaki.tool.usePreserveAlpha) { - return; - } - - Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled); - }, - - setToolPreserveAlpha: function(flag) { - Tegaki.tool.setPreserveAlpha(flag); - TegakiUI.updateToolPreserveAlpha(); - Tegaki.recordEvent(TegakiEventPreserveAlpha, performance.now(), +flag); - }, - - onToolTipClick: function(e) { - var tipId = +e.target.getAttribute('data-id'); - - if (tipId !== Tegaki.tool.tipId) { - Tegaki.setToolTip(tipId); - } - }, - - setToolTip: function(id) { - Tegaki.tool.setTip(id); - TegakiUI.updateToolShape(); - Tegaki.recordEvent(TegakiEventSetToolTip, performance.now(), id); - }, - - onLayerSelectorClick: function(e) { - var id = +this.getAttribute('data-id'); - - if (!id || e.target.classList.contains('tegaki-ui-cb')) { - return; - } - - if (e.ctrlKey) { - Tegaki.toggleSelectedLayer(id); - } - else { - Tegaki.setActiveLayer(id); - } - }, - - toggleSelectedLayer: function(id) { - TegakiLayers.selectedLayersToggle(id); - Tegaki.recordEvent(TegakiEventToggleLayerSelection, performance.now(), id); - }, - - setActiveLayer: function(id) { - TegakiLayers.setActiveLayer(id); - Tegaki.recordEvent(TegakiEventSetActiveLayer, performance.now(), id); - }, - - onLayerAlphaDragStart: function(e) { - TegakiUI.setupDragLabel(e, Tegaki.onLayerAlphaDragMove); - }, - - onLayerAlphaDragMove: function(delta) { - var val; - - if (!delta) { - return; - } - - val = Tegaki.activeLayer.alpha + delta / 100 ; - - if (val < 0.0) { - val = 0.0; - } - else if (val > 1.0) { - val = 1.0; - } - - Tegaki.setSelectedLayersAlpha(val); - }, - - onLayerAlphaChange: function() { - var val = +this.value; - - val = val / 100; - - if (val < 0.0) { - val = 0.0; - } - else if (val > 1.0) { - val = 1.0; - } - - Tegaki.setSelectedLayersAlpha(val); - }, - - setSelectedLayersAlpha: function(alpha) { - var layer, id, layerAlphas; - - alpha = Math.fround(alpha); - - if (alpha >= 0.0 && alpha <= 1.0 && Tegaki.selectedLayers.size > 0) { - layerAlphas = []; - - for (id of Tegaki.selectedLayers) { - if (layer = TegakiLayers.getLayerById(id)) { - layerAlphas.push([layer.id, layer.alpha]); - TegakiLayers.setLayerAlpha(layer, alpha); - } - } - - TegakiUI.updateLayerAlphaOpt(); - - TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(layerAlphas, alpha)); - - Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha, performance.now(), alpha); - } - }, - - onLayerNameChangeClick: function(e) { - var id, name, layer; - - id = +this.getAttribute('data-id'); - - layer = TegakiLayers.getLayerById(id); - - if (!layer) { - return; - } - - if (name = prompt(undefined, layer.name)) { - Tegaki.setLayerName(id, name); - } - }, - - setLayerName: function(id, name) { - var oldName, layer; - - name = name.trim().slice(0, 25); - - layer = TegakiLayers.getLayerById(id); - - if (!layer || !name || name === layer.name) { - return; - } - - oldName = layer.name; - - layer.name = name; - - TegakiUI.updateLayerName(layer); - - TegakiHistory.push(new TegakiHistoryActions.SetLayerName(id, oldName, name)); - - Tegaki.recordEvent(TegakiEventHistoryDummy, performance.now()); - }, - - onLayerAddClick: function() { - Tegaki.addLayer(); - }, - - addLayer: function() { - var action; - - if (Tegaki.layers.length >= Tegaki.maxLayers) { - TegakiUI.printMsg(TegakiStrings.tooManyLayers); - return; - } - - TegakiHistory.push(action = TegakiLayers.addLayer()); - TegakiLayers.setActiveLayer(action.aLayerIdAfter); - Tegaki.recordEvent(TegakiEventAddLayer, performance.now()); - }, - - onLayerDeleteClick: function() { - Tegaki.deleteSelectedLayers(); - }, - - deleteSelectedLayers: function() { - var action, layerSet; - - layerSet = Tegaki.selectedLayers; - - if (layerSet.size === Tegaki.layers.length) { - return; - } - - if (!layerSet.size || Tegaki.layers.length < 2) { - return; - } - - TegakiHistory.push(action = TegakiLayers.deleteLayers(layerSet)); - TegakiLayers.selectedLayersClear(); - TegakiLayers.setActiveLayer(action.aLayerIdAfter); - Tegaki.recordEvent(TegakiEventDeleteLayers, performance.now()); - }, - - onLayerToggleVisibilityClick: function() { - Tegaki.toggleLayerVisibility(+this.getAttribute('data-id')); - }, - - toggleLayerVisibility: function(id) { - var layer = TegakiLayers.getLayerById(id); - TegakiLayers.setLayerVisibility(layer, !layer.visible); - Tegaki.recordEvent(TegakiEventToggleLayerVisibility, performance.now(), id); - }, - - onMergeLayersClick: function() { - Tegaki.mergeSelectedLayers(); - }, - - mergeSelectedLayers: function() { - var action; - - if (Tegaki.selectedLayers.size) { - if (action = TegakiLayers.mergeLayers(Tegaki.selectedLayers)) { - TegakiHistory.push(action); - TegakiLayers.setActiveLayer(action.aLayerIdAfter); - Tegaki.recordEvent(TegakiEventMergeLayers, performance.now()); - } - } - }, - - onMoveLayerClick: function(e) { - var belowPos, up; - - if (!Tegaki.selectedLayers.size) { - return; - } - - up = e.target.hasAttribute('data-up'); - - belowPos = TegakiLayers.getSelectedEdgeLayerPos(up); - - if (belowPos < 0) { - return; - } - - if (up) { - belowPos += 2; - } - else if (belowPos >= 1) { - belowPos--; - } - - Tegaki.moveSelectedLayers(belowPos); - }, - - moveSelectedLayers: function(belowPos) { - TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers, belowPos)); - Tegaki.recordEvent(TegakiEventMoveLayers, performance.now(), belowPos); - }, - - onToolClick: function() { - Tegaki.setTool(this.getAttribute('data-tool')); - }, - - onToolChanged: function(tool) { - var el; - - Tegaki.tool = tool; - - if (el = $T.cls('tegaki-tool-active')[0]) { - el.classList.remove('tegaki-tool-active'); - } - - $T.id('tegaki-tool-btn-' + tool.name).classList.add('tegaki-tool-active'); - - Tegaki.recordEvent(TegakiEventSetTool, performance.now(), Tegaki.tool.id); - - TegakiUI.onToolChanged(); - Tegaki.updateCursorStatus(); - }, - - onLayerStackChanged: function() {}, - - onOpenFileSelected: function() { - var img; - - if (this.files && this.files[0]) { - img = new Image(); - img.onload = Tegaki.onOpenImageLoaded; - img.onerror = Tegaki.onOpenImageError; - - img.src = URL.createObjectURL(this.files[0]); - } - }, - - onOpenImageLoaded: function() { - var tmp = {}, self = Tegaki; - - self.hasCustomCanvas = true; - - self.copyContextState(self.activeLayer.ctx, tmp); - self.resizeCanvas(this.naturalWidth, this.naturalHeight); - self.activeLayer.ctx.drawImage(this, 0, 0); - TegakiLayers.syncLayerImageData(self.activeLayer); - self.copyContextState(tmp, self.activeLayer.ctx); - - self.setZoom(0); - - TegakiHistory.clear(); - - TegakiUI.updateLayerPreviewSize(true); - - self.startTimeStamp = Date.now(); - - if (self.saveReplay) { - self.replayRecorder.stop(); - self.replayRecorder = null; - self.saveReplay = false; - TegakiUI.setRecordingStatus(false); - } - }, - - onOpenImageError: function() { - TegakiUI.printMsg(TegakiStrings.errorLoadImage); - }, - - resizeCanvas: function(width, height) { - Tegaki.baseWidth = width; - Tegaki.baseHeight = height; - - Tegaki.createBuffers(); - - Tegaki.canvas.width = width; - Tegaki.canvas.height = height; - - Tegaki.ctx.fillStyle = Tegaki.bgColor; - Tegaki.ctx.fillRect(0, 0, width, height); - - Tegaki.activeLayer = null; - - Tegaki.resetLayers(); - - Tegaki.updateLayersCntSize(); - Tegaki.updatePosOffset(); - TegakiCursor.updateCanvasSize(); - }, - - copyContextState: function(src, dest) { - var i, p, props = [ - 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha', - 'lineWidth', 'globalCompositeOperation' - ]; - - for (i = 0; p = props[i]; ++i) { - dest[p] = src[p]; - } - }, - - updateCursorStatus: function() { - if (!Tegaki.tool.noCursor && Tegaki.tool.size > 1) { - Tegaki.cursor = TegakiCursor.generate(Tegaki.tool.size); - } - else { - Tegaki.cursor = false; - TegakiCursor.clear(); - } - }, - - updatePosOffset: function() { - var aabb = Tegaki.canvas.getBoundingClientRect(); - - Tegaki.offsetX = aabb.left + window.pageXOffset - + Tegaki.canvasCnt.scrollLeft + Tegaki.layersCnt.scrollLeft; - Tegaki.offsetY = aabb.top + window.pageYOffset - + Tegaki.canvasCnt.scrollTop + Tegaki.layersCnt.scrollTop; - }, - - getScrollbarSizes: function() { - return [ - Tegaki.canvasCnt.offsetWidth - Tegaki.canvasCnt.clientWidth, - Tegaki.canvasCnt.offsetHeight - Tegaki.canvasCnt.clientHeight - ]; - }, - - isScrollbarClick: function(e) { - var [ sbwh, scbv ] = Tegaki.getScrollbarSizes(); - - if (sbwh > 0 - && e.clientX >= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth - && e.clientX <= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth - + sbwh) { - return true; - } - - if (scbv > 0 - && e.clientY >= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight - && e.clientY <= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight - + sbwh) { - return true; - } - - return false; - }, - - onPointerMove: function(e) { - var events, x, y, tool, ts, p; - - if (e.mozInputSource !== undefined) { - // Firefox thing where mouse events fire for no reason when the pointer is a pen - if (Tegaki.activePointerIsPen && e.pointerType === 'mouse') { - return; - } - } - else { - // Webkit thing where a pointermove event is fired at pointerdown location after a pointerup - if (Tegaki.activePointerId !== e.pointerId) { - Tegaki.activePointerId = e.pointerId; - return; - } - } - - if (Tegaki.isPainting) { - tool = Tegaki.tool; - - if (Tegaki.activePointerIsPen && e.getCoalescedEvents) { - events = e.getCoalescedEvents(); - - ts = e.timeStamp; - - for (e of events) { - x = Tegaki.getPointerPos(e, 0); - y = Tegaki.getPointerPos(e, 1); - - if (!tool.enabledDynamics()) { - Tegaki.recordEvent(TegakiEventDrawNoP, ts, x, y); - } - else { - p = TegakiPressure.toShort(e.pressure); - TegakiPressure.push(p); - Tegaki.recordEvent(TegakiEventDraw, ts, x, y, p); - } - - tool.draw(x, y); - } - } - else { - x = Tegaki.getPointerPos(e, 0); - y = Tegaki.getPointerPos(e, 1); - p = TegakiPressure.toShort(e.pressure); - Tegaki.recordEvent(TegakiEventDraw, e.timeStamp, x, y, p); - TegakiPressure.push(p); - tool.draw(x, y); - } - } - else { - x = Tegaki.getPointerPos(e, 0); - y = Tegaki.getPointerPos(e, 1); - } - - if (Tegaki.cursor) { - TegakiCursor.render(e.clientX, e.clientY); - } - }, - - onPointerDown: function(e) { - var x, y, tool, p; - - if (Tegaki.isScrollbarClick(e)) { - return; - } - - Tegaki.activePointerId = e.pointerId; - - Tegaki.activePointerIsPen = e.pointerType === 'pen'; - - if (Tegaki.activeLayer === null) { - if (e.target.parentNode === Tegaki.layersCnt) { - TegakiUI.printMsg(TegakiStrings.noActiveLayer); - } - - return; - } - - if (!TegakiLayers.getActiveLayer().visible) { - if (e.target.parentNode === Tegaki.layersCnt) { - TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer); - } - - return; - } - - x = Tegaki.getPointerPos(e, 0); - y = Tegaki.getPointerPos(e, 1); - - if (e.button === 2 || e.altKey) { - e.preventDefault(); - - Tegaki.isPainting = false; - - Tegaki.tools.pipette.draw(x, y); - } - else if (e.button === 0) { - e.preventDefault(); - - tool = Tegaki.tool; - - if (!tool.enabledDynamics()) { - Tegaki.recordEvent(TegakiEventDrawStartNoP, e.timeStamp, x, y); - } - else { - p = TegakiPressure.toShort(e.pressure); - TegakiPressure.push(p); - Tegaki.recordEvent(TegakiEventDrawStart, e.timeStamp, x, y, p); - } - - Tegaki.isPainting = true; - - TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( - Tegaki.activeLayer.id - ); - - TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); - - tool.start(x, y); - } - - if (Tegaki.cursor) { - TegakiCursor.render(e.clientX, e.clientY); - } - }, - - onPointerUp: function(e) { - Tegaki.activePointerId = e.pointerId; - - Tegaki.activePointerIsPen = false; - - if (Tegaki.isPainting) { - Tegaki.recordEvent(TegakiEventDrawCommit, e.timeStamp); - Tegaki.tool.commit(); - TegakiUI.updateLayerPreview(Tegaki.activeLayer); - TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); - TegakiHistory.push(TegakiHistory.pendingAction); - Tegaki.isPainting = false; - } - }, - - onPointerOut: function(e) { - TegakiCursor.clearAll(); - }, - - onDummy: function(e) { - e.preventDefault(); - e.stopPropagation(); - }, - - recordEvent(klass, ...args) { - if (Tegaki.replayRecorder) { - Tegaki.replayRecorder.push(new klass(...args)); - } - } -}; -var TegakiColorPalettes = [ - [ - '#ffffff', '#000000', '#888888', '#b47575', '#c096c0', - '#fa9696', '#8080ff', '#ffb6ff', '#e7e58d', '#25c7c9', - '#99cb7b', '#e7962d', '#f9ddcf', '#fcece2' - ], - - [ - '#000000', '#ffffff', '#7f7f7f', '#c3c3c3', '#880015', '#b97a57', '#ed1c24', - '#ffaec9', '#ff7f27', '#ffc90e', '#fff200', '#efe4b0', '#22b14c', '#b5e61d', - '#00a2e8', '#99d9ea', '#3f48cc', '#7092be', '#a349a4', '#c8bfe7' - ], - - [ - '#000000', '#ffffff', '#8a8a8a', '#cacaca', '#fcece2', '#f9ddcf', '#e0a899', '#a05b53', - '#7a444a', '#960018', '#c41e3a', '#de4537', '#ff3300', '#ff9800', '#ffc107', - '#ffd700', '#ffeb3b', '#ffffcc', '#f3e5ab', '#cddc39', '#8bc34a', '#4caf50', '#3e8948', - '#355e3b', '#3eb489', '#f0f8ff', '#87ceeb', '#6699cc', '#007fff', '#2d68c4', '#364478', - '#352c4a', '#9c27b0', '#da70d6', '#ff0090', '#fa8072', '#f19cbb', '#c78b95' - ] -]; -var TegakiPressure = { - pressureNow: 0.0, - pressureThen: 0.0, - - toShort: function(pressure) { - return 0 | (pressure * 65535); - }, - - get: function() { - return this.pressureNow; - }, - - lerp: function(t) { - return this.pressureThen * (1.0 - t) + this.pressureNow * t; - }, - - push: function(p) { - this.pressureThen = this.pressureNow; - this.pressureNow = p / 65535; - }, - - set: function(p) { - this.pressureThen = this.pressureNow = p / 65535; - } -}; -class TegakiEvent_void { - constructor() { - this.size = 5; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - } - - static unpack(r) { - return new this(r.readUint32()); - } -} - -class TegakiEvent_c { - constructor() { - this.size = 6; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeUint8(this.value); - } - - static unpack(r) { - return new this(r.readUint32(), r.readUint8()); - } -} - -// --- - -class TegakiEventPrelude extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() {} -} - -class TegakiEventConclusion extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() {} -} - -class TegakiEventHistoryDummy extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - TegakiHistory.push(new TegakiHistoryActions.Dummy()); - } -} - -class TegakiEventDrawStart { - constructor(timeStamp, x, y, pressure) { - this.timeStamp = timeStamp; - this.x = x; - this.y = y; - this.pressure = pressure; - this.type = TegakiEvents[this.constructor.name][0]; - this.size = 11; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeInt16(this.x); - w.writeInt16(this.y); - w.writeUint16(this.pressure); - } - - static unpack(r) { - var timeStamp, x, y, pressure; - - timeStamp = r.readUint32(); - x = r.readInt16(); - y = r.readInt16(); - pressure = r.readUint16(); - - return new TegakiEventDrawStart(timeStamp, x, y, pressure); - } - - dispatch() { - TegakiPressure.set(this.pressure); - - TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( - Tegaki.activeLayer.id - ); - - TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); - - Tegaki.tool.start(this.x, this.y); - } -} - -class TegakiEventDrawStartNoP { - constructor(timeStamp, x, y) { - this.timeStamp = timeStamp; - this.x = x; - this.y = y; - this.type = TegakiEvents[this.constructor.name][0]; - this.size = 9; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeInt16(this.x); - w.writeInt16(this.y); - } - - static unpack(r) { - var timeStamp, x, y; - - timeStamp = r.readUint32(); - x = r.readInt16(); - y = r.readInt16(); - - return new TegakiEventDrawStartNoP(timeStamp, x, y); - } - - dispatch() { - TegakiPressure.set(0.5); - - TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( - Tegaki.activeLayer.id - ); - - TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); - - Tegaki.tool.start(this.x, this.y); - } -} - -class TegakiEventDraw { - constructor(timeStamp, x, y, pressure) { - this.timeStamp = timeStamp; - this.x = x; - this.y = y; - this.pressure = pressure; - this.type = TegakiEvents[this.constructor.name][0]; - this.size = 11; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeInt16(this.x); - w.writeInt16(this.y); - w.writeUint16(this.pressure); - } - - static unpack(r) { - var timeStamp, x, y, pressure; - - timeStamp = r.readUint32(); - x = r.readInt16(); - y = r.readInt16(); - pressure = r.readUint16(); - - return new TegakiEventDraw(timeStamp, x, y, pressure); - } - - dispatch() { - TegakiPressure.push(this.pressure); - Tegaki.tool.draw(this.x, this.y); - } -} - -class TegakiEventDrawNoP { - constructor(timeStamp, x, y) { - this.timeStamp = timeStamp; - this.x = x; - this.y = y; - this.type = TegakiEvents[this.constructor.name][0]; - this.size = 9; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeInt16(this.x); - w.writeInt16(this.y); - } - - static unpack(r) { - var timeStamp, x, y; - - timeStamp = r.readUint32(); - x = r.readInt16(); - y = r.readInt16(); - - return new TegakiEventDraw(timeStamp, x, y); - } - - dispatch() { - TegakiPressure.push(0.5); - Tegaki.tool.draw(this.x, this.y); - } -} - -class TegakiEventDrawCommit extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - Tegaki.tool.commit(); - TegakiUI.updateLayerPreview(Tegaki.activeLayer); - TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); - TegakiHistory.push(TegakiHistory.pendingAction); - Tegaki.isPainting = false; - } -} - -class TegakiEventUndo extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - TegakiHistory.undo(); - } -} - -class TegakiEventRedo extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - TegakiHistory.redo(); - } -} - -class TegakiEventSetColor { - constructor(timeStamp, rgb) { - this.timeStamp = timeStamp; - [this.r, this.g, this.b] = rgb; - this.type = TegakiEvents[this.constructor.name][0]; - this.size = 8; - this.coalesce = true; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeUint8(this.r); - w.writeUint8(this.g); - w.writeUint8(this.b); - } - - static unpack(r) { - var timeStamp, rgb; - - timeStamp = r.readUint32(); - - rgb = [r.readUint8(), r.readUint8(), r.readUint8()]; - - return new TegakiEventSetColor(timeStamp, rgb); - } - - dispatch() { - Tegaki.setToolColorRGB(this.r, this.g, this.b); - } -} - -class TegakiEventSetTool extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolById(this.value); - } -} - -class TegakiEventSetToolSize extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolSize(this.value); - } -} - -class TegakiEventSetToolAlpha { - constructor(timeStamp, value) { - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - this.size = 9; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeFloat32(this.value); - } - - static unpack(r) { - return new this(r.readUint32(), r.readFloat32()); - } - - dispatch() { - Tegaki.setToolAlpha(this.value); - } -} - -class TegakiEventSetToolFlow { - constructor(timeStamp, value) { - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - this.size = 9; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeFloat32(this.value); - } - - static unpack(r) { - return new this(r.readUint32(), r.readFloat32()); - } - - dispatch() { - Tegaki.setToolFlow(this.value); - } -} - -class TegakiEventPreserveAlpha extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolPreserveAlpha(!!this.value); - } -} - -class TegakiEventSetToolSizeDynamics extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolSizeDynamics(!!this.value); - } -} - -class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolAlphaDynamics(!!this.value); - } -} - -class TegakiEventSetToolFlowDynamics extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolFlowDynamics(!!this.value); - } -} - -class TegakiEventSetToolTip extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - } - - dispatch() { - Tegaki.setToolTip(this.value); - } -} - -class TegakiEventAddLayer extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - Tegaki.addLayer(); - } -} - -class TegakiEventDeleteLayers extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - Tegaki.deleteSelectedLayers(); - } -} - -class TegakiEventMoveLayers extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - } - - dispatch() { - Tegaki.moveSelectedLayers(this.value); - } -} - -class TegakiEventMergeLayers extends TegakiEvent_void { - constructor(timeStamp) { - super(); - this.timeStamp = timeStamp; - this.type = TegakiEvents[this.constructor.name][0]; - } - - static unpack(r) { return super.unpack(r); } // FF bug 1628719 - - dispatch() { - Tegaki.mergeSelectedLayers(); - } -} - -class TegakiEventToggleLayerVisibility extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - } - - dispatch() { - Tegaki.toggleLayerVisibility(this.value); - } -} - -class TegakiEventSetActiveLayer extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - } - - dispatch() { - Tegaki.setActiveLayer(this.value); - } -} - -class TegakiEventToggleLayerSelection extends TegakiEvent_c { - constructor(timeStamp, value) { - super(); - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - } - - dispatch() { - Tegaki.toggleSelectedLayer(this.value); - } -} - -class TegakiEventSetSelectedLayersAlpha { - constructor(timeStamp, value) { - this.timeStamp = timeStamp; - this.value = value; - this.type = TegakiEvents[this.constructor.name][0]; - this.coalesce = true; - this.size = 9; - } - - pack(w) { - w.writeUint8(this.type); - w.writeUint32(this.timeStamp); - w.writeFloat32(this.value); - } - - static unpack(r) { - return new this(r.readUint32(), r.readFloat32()); - } - - dispatch() { - Tegaki.setSelectedLayersAlpha(this.value); - } -} - -const TegakiEvents = Object.freeze({ - TegakiEventPrelude: [0, TegakiEventPrelude], - - TegakiEventDrawStart: [1, TegakiEventDrawStart], - TegakiEventDraw: [2, TegakiEventDraw], - TegakiEventDrawCommit: [3, TegakiEventDrawCommit], - TegakiEventUndo: [4, TegakiEventUndo], - TegakiEventRedo: [5, TegakiEventRedo], - TegakiEventSetColor: [6, TegakiEventSetColor], - TegakiEventDrawStartNoP: [7, TegakiEventDrawStartNoP], - TegakiEventDrawNoP: [8, TegakiEventDrawNoP], - - TegakiEventSetTool: [10, TegakiEventSetTool], - TegakiEventSetToolSize: [11, TegakiEventSetToolSize], - TegakiEventSetToolAlpha: [12, TegakiEventSetToolAlpha], - TegakiEventSetToolSizeDynamics: [13, TegakiEventSetToolSizeDynamics], - TegakiEventSetToolAlphaDynamics: [14, TegakiEventSetToolAlphaDynamics], - TegakiEventSetToolTip: [15, TegakiEventSetToolTip], - TegakiEventPreserveAlpha: [16, TegakiEventPreserveAlpha], - TegakiEventSetToolFlowDynamics: [17, TegakiEventSetToolFlowDynamics], - TegakiEventSetToolFlow: [18, TegakiEventSetToolFlow], - - TegakiEventAddLayer: [20, TegakiEventAddLayer], - TegakiEventDeleteLayers: [21, TegakiEventDeleteLayers], - TegakiEventMoveLayers: [22, TegakiEventMoveLayers], - TegakiEventMergeLayers: [23, TegakiEventMergeLayers], - TegakiEventToggleLayerVisibility: [24, TegakiEventToggleLayerVisibility], - TegakiEventSetActiveLayer: [25, TegakiEventSetActiveLayer], - TegakiEventToggleLayerSelection: [26, TegakiEventToggleLayerSelection], - TegakiEventSetSelectedLayersAlpha: [27, TegakiEventSetSelectedLayersAlpha], - - TegakiEventHistoryDummy: [254, TegakiEventHistoryDummy], - - TegakiEventConclusion: [255, TegakiEventConclusion] -}); -class TegakiReplayRecorder { - constructor() { - this.formatVersion = 1; - - this.compressed = true; - - this.tegakiVersion = Tegaki.VERSION.split('.').map((v) => +v); - - this.canvasWidth = Tegaki.baseWidth; - this.canvasHeight = Tegaki.baseHeight; - - this.bgColor = $T.hexToRgb(Tegaki.bgColor); - this.toolColor = $T.hexToRgb(Tegaki.toolColor); - - this.toolId = Tegaki.tools[Tegaki.defaultTool].id; - - this.toolList = this.buildToolList(Tegaki.tools); - - this.startTimeStamp = 0; - this.endTimeStamp = 0; - - this.recording = false; - - this.events = []; - - this.mimeType = 'application/octet-stream'; - } - - buildToolList(tools) { - var k, tool, toolMap; - - toolMap = []; - - for (k in tools) { - tool = tools[k]; - - toolMap.push({ - id: tool.id, - size: tool.size, - alpha: tool.alpha, - flow: tool.flow, - step: tool.step, - sizeDynamicsEnabled: +tool.sizeDynamicsEnabled, - alphaDynamicsEnabled: +tool.alphaDynamicsEnabled, - flowDynamicsEnabled: +tool.flowDynamicsEnabled, - usePreserveAlpha: +tool.usePreserveAlpha, - tipId: tool.tipId - }); - } - - return toolMap; - } - - start() { - if (this.recording) { - return; - } - - if (this.endTimeStamp > 0) { - this.events.pop(); - this.endTimeStamp = 0; - } - - if (this.startTimeStamp === 0) { - this.events.push(new TegakiEventPrelude(performance.now())); - this.startTimeStamp = Date.now(); - } - - this.recording = true; - } - - stop() { - if (this.startTimeStamp === 0 || !this.recording) { - return; - } - - this.events.push(new TegakiEventConclusion(performance.now())); - this.endTimeStamp = Date.now(); - this.recording = false; - } - - push(e) { - if (this.recording) { - if (e.coalesce && this.events[this.events.length - 1].type === e.type) { - this.events[this.events.length - 1] = e; - } - else { - this.events.push(e); - } - } - } - - getEventStackSize() { - var e, size; - - size = 4; - - for (e of this.events) { - size += e.size; - } - - return size; - } - - getHeaderSize() { - return 12; - } - - getMetaSize() { - return 21; - } - - getToolSize() { - return 19; - } - - getToolListSize() { - return 2 + (this.toolList.length * this.getToolSize()); - } - - writeToolList(w) { - var tool, field, fields; - - fields = [ - ['id', 'Uint8'], - ['size', 'Uint8'], - ['alpha', 'Float32'], - ['step', 'Float32'], - ['sizeDynamicsEnabled', 'Uint8'], - ['alphaDynamicsEnabled', 'Uint8'], - ['usePreserveAlpha', 'Uint8'], - ['tipId', 'Int8'], - ['flow', 'Float32'], - ['flowDynamicsEnabled', 'Uint8'], - ]; - - w.writeUint8(this.toolList.length); - - w.writeUint8(this.getToolSize()); - - for (tool of this.toolList) { - for (field of fields) { - w['write' + field[1]](tool[field[0]]); - } - } - } - - writeMeta(w) { - w.writeUint16(this.getMetaSize()); - - w.writeUint32(Math.ceil(this.startTimeStamp / 1000)); - w.writeUint32(Math.ceil(this.endTimeStamp / 1000)); - - w.writeUint16(this.canvasWidth); - w.writeUint16(this.canvasHeight); - - w.writeUint8(this.bgColor[0]); - w.writeUint8(this.bgColor[1]); - w.writeUint8(this.bgColor[2]); - - w.writeUint8(this.toolColor[0]); - w.writeUint8(this.toolColor[1]); - w.writeUint8(this.toolColor[2]); - - w.writeUint8(this.toolId); - } - - writeEventStack(w) { - var event; - - w.writeUint32(this.events.length); - - for (event of this.events) { - event.pack(w); - } - } - - writeHeader(w, dataSize) { - w.writeUint8(0x54); - w.writeUint8(0x47); - w.writeUint8(0x4B); - - w.writeUint8(+this.compressed); - - w.writeUint32(dataSize); - - w.writeUint8(this.tegakiVersion[0]); - w.writeUint8(this.tegakiVersion[1]); - w.writeUint8(this.tegakiVersion[2]); - w.writeUint8(this.formatVersion); - } - - compressData(w) { - return UZIP.deflateRaw(new Uint8Array(w.buf), { level: 9 }); - } - - toUint8Array() { - var headerSize, dataSize, data, w, compData, bytes; - - if (!this.startTimeStamp || !this.endTimeStamp) { - return null; - } - - headerSize = this.getHeaderSize(); - dataSize = this.getMetaSize() + this.getToolListSize() + this.getEventStackSize(); - - data = new ArrayBuffer(dataSize); - - w = new TegakiBinWriter(data); - - this.writeMeta(w); - - this.writeToolList(w); - - this.writeEventStack(w); - - compData = this.compressData(w); - //compData = new Uint8Array(data.slice(0)); - - w = new TegakiBinWriter(new ArrayBuffer(headerSize + compData.length)); - - this.writeHeader(w, dataSize); - - bytes = new Uint8Array(w.buf); - - bytes.set(compData, headerSize); - - return bytes; - } - - toBlob() { - var ary = this.toUint8Array(); - - if (!ary) { - return null; - } - - return new Blob([ary.buffer], { type: this.mimeType }); - } -} -class TegakiReplayViewer { - constructor() { - this.formatVersion = 1; - - this.compressed = true; - - this.tegakiVersion = [0, 0, 0]; - - this.dataSize = 0; - - this.canvasWidth = 0; - this.canvasHeight = 0; - - this.bgColor = [0, 0, 0]; - this.toolColor = [0, 0, 0]; - - this.toolId = 1; - - this.toolMap = {}; - - this.startTimeStamp = 0; - this.endTimeStamp = 0; - - this.loaded = false; - this.playing = false; - this.gapless = true; - - this.autoPaused = false; - - this.destroyed = false; - - this.speedIndex = 1; - this.speedList = [0.5, 1.0, 2.0, 5.0, 10.0, 25.0]; - this.speed = this.speedList[this.speedIndex]; - - this.maxEventsPerFrame = 50; - - this.maxEventCount = 8640000; - - this.events = []; - - this.preludePos = 0.0; - this.currentPos = 0.0; - this.conclusionPos = 0.0; - this.duration = 0.0; - - this.playTimeStart = 0.0; - this.playTimeCurrent = 0.0; - - this.eventIndex = 0; - - this.maxCanvasWH = 8192; - - this.maxGapTime = 3000; - - this.uiAccTime = 0; - - this.onFrameThis = this.onFrame.bind(this); - } - - destroy() { - this.destroyed = true; - this.pause(); - this.events = null; - } - - speedUp() { - if (this.speedIndex + 1 < this.speedList.length) { - this.speed = this.speedList[++this.speedIndex]; - } - } - - slowDown() { - if (this.speedIndex - 1 >= 0) { - this.speed = this.speedList[--this.speedIndex]; - } - } - - toggleGapless() { - this.gapless = !this.gapless; - } - - getCurrentPos() { - return this.currentPos; - } - - getDuration() { - return this.duration; - } - - loadFromURL(url) { - fetch(url) - .then((resp) => this.onResponseReady(resp)) - .catch((err) => this.onLoadError(err)); - } - - onResponseReady(resp) { - if (resp.ok) { - resp.arrayBuffer() - .then((buf) => this.onResponseBodyReady(buf)) - .catch((err) => this.onLoadError(err)); - } - else { - this.onLoadError(resp.statusText); - } - } - - onResponseBodyReady(data) { - this.loadFromBuffer(data); - Tegaki.onReplayLoaded(); - } - - onLoadError(err) { - TegakiUI.printMsg(TegakiStrings.errorLoadReplay + err, 0); - } - - autoPause() { - this.autoPaused = true; - this.pause(); - } - - pause(rewind) { - window.cancelAnimationFrame(this.onFrameThis); - - this.playing = false; - - if (rewind) { - this.currentPos = 0; - this.eventIndex = 0; - } - - Tegaki.onReplayTimeChanged(); - Tegaki.onReplayPlayPauseChanged(); - } - - rewind() { - this.autoPaused = false; - this.pause(true); - Tegaki.onReplayReset(); - } - - play() { - this.playTimeStart = performance.now(); - this.playTimeCurrent = this.playTimeStart; - - this.playing = true; - this.autoPaused = false; - - this.uiAccTime = 0; - - Tegaki.onReplayPlayPauseChanged(); - - window.requestAnimationFrame(this.onFrameThis); - } - - togglePlayPause() { - if (this.playing) { - this.pause(); - } - else { - this.play(); - } - } - - onFrame(ts) { - var delta = ts - this.playTimeCurrent; - - if (!this.playing) { - return; - } - - this.playTimeCurrent = ts; - - this.step(delta); - - this.uiAccTime += delta; - - if (this.uiAccTime > 1000) { - Tegaki.onReplayTimeChanged(); - this.uiAccTime = 0; - } - - if (this.currentPos < this.duration) { - window.requestAnimationFrame(this.onFrameThis); - } - else { - this.pause(); - } - } - - step(delta) { - var event, currentEventTime, i; - - this.currentPos += (delta * this.speed); - - currentEventTime = this.currentPos + this.preludePos; - - if (this.gapless && this.eventIndex < this.events.length) { - event = this.events[this.eventIndex]; - - if (event.timeStamp - currentEventTime > this.maxGapTime) { - this.currentPos = event.timeStamp - this.preludePos; - currentEventTime = event.timeStamp; - } - } - - i = 0; - - while (this.eventIndex < this.events.length) { - event = this.events[this.eventIndex]; - - if (event.timeStamp <= currentEventTime) { - if (i >= this.maxEventsPerFrame) { - this.currentPos = event.timeStamp - this.preludePos; - break; - } - - event.dispatch(); - - ++this.eventIndex; - ++i; - } - else { - break; - } - } - } - - getEventIdMap() { - var map, key, val; - - map = {}; - - for (key in TegakiEvents) { - val = TegakiEvents[key]; - map[val[0]] = val[1]; - } - - return map; - } - - readToolMap(r) { - var i, len, size, tool, field, fields, pos; - - this.toolMap = {}; - - fields = [ - ['id', 'Uint8'], - ['size', 'Uint8'], - ['alpha', 'Float32'], - ['step', 'Float32'], - ['sizeDynamicsEnabled', 'Uint8'], - ['alphaDynamicsEnabled', 'Uint8'], - ['usePreserveAlpha', 'Uint8'], - ['tipId', 'Int8'], - ['flow', 'Float32'], - ['flowDynamicsEnabled', 'Uint8'], - ]; - - len = r.readUint8(); - - size = r.readUint8(); - - for (i = 0; i < len; ++i) { - pos = r.pos + size; - - tool = {}; - - for (field of fields) { - if (r.pos >= pos) { - break; - } - - tool[field[0]] = r['read' + field[1]](); - } - - this.toolMap[tool.id] = tool; - - r.pos = pos; - } - } - - readHeader(r) { - var tgk; - - tgk = String.fromCharCode(r.readUint8(), r.readUint8(), r.readUint8()); - - if (tgk !== 'TGK') { - throw 'invalid header'; - } - - this.compressed = r.readUint8() === 1; - - this.dataSize = r.readUint32(); - - this.tegakiVersion[0] = r.readUint8(); - this.tegakiVersion[1] = r.readUint8(); - this.tegakiVersion[2] = r.readUint8(); - - this.formatVersion = r.readUint8(); - } - - decompressData(r) { - return UZIP.inflateRaw( - new Uint8Array(r.buf, r.pos), - new Uint8Array(this.dataSize) - ); - } - - readMeta(r) { - var pos, size; - - size = r.readUint16(); - - pos = r.pos + size - 2; - - this.startTimeStamp = r.readUint32() * 1000; - this.endTimeStamp = r.readUint32() * 1000; - - this.canvasWidth = r.readUint16(); - this.canvasHeight = r.readUint16(); - - if (this.canvasWidth > this.maxCanvasWH - || this.canvasHeight > this.maxCanvasWH) { - throw 'canvas too large'; - } - - this.bgColor[0] = r.readUint8(); - this.bgColor[1] = r.readUint8(); - this.bgColor[2] = r.readUint8(); - - this.toolColor[0] = r.readUint8(); - this.toolColor[1] = r.readUint8(); - this.toolColor[2] = r.readUint8(); - - this.toolId = r.readUint8(); - - r.pos = pos; - } - - readEventStack(r) { - var i, len, type, klass, event, eventMap; - - eventMap = this.getEventIdMap(); - - len = r.readUint32(); - - if (len < 1 || len > this.maxEventCount) { - throw 'invalid event count'; - } - - for (i = 0; i < len; ++i) { - type = r.readUint8(); - - klass = eventMap[type]; - - if (!klass) { - throw 'invalid event id'; - } - - event = klass.unpack(r); - - this.events.push(event); - } - - if (this.events[0].type !== TegakiEvents.TegakiEventPrelude[0]) { - throw 'invalid prelude'; - } - - if (this.events[len - 1].type !== TegakiEvents.TegakiEventConclusion[0]) { - throw 'invalid conclusion'; - } - - this.preludePos = this.events[0].timeStamp; - this.conclusionPos = this.events[len - 1].timeStamp; - - this.duration = this.conclusionPos - this.preludePos; - - if (this.duration <= 0) { - throw 'invalid duration'; - } - } - - loadFromBuffer(buffer) { - var r, data; - - if (this.destroyed || this.loaded) { - return false; - } - - r = new TegakiBinReader(buffer); - - this.readHeader(r); - - data = this.decompressData(r); - - r = new TegakiBinReader(data.buffer); - - this.readMeta(r); - - this.readToolMap(r); - - this.readEventStack(r); - - this.loaded = true; - - return true; - } -} -var TegakiUI = { - draggedNode: null, - - draggedLabelLastX: 0, - draggedLabelFn: null, - - statusTimeout: 0, - - layerPreviewCtxCache: new WeakMap(), - - getLayerPreviewSize: function() { - return $T.calcThumbSize(Tegaki.baseWidth, Tegaki.baseHeight, 24); - }, - - setupDragLabel: function(e, moveFn) { - TegakiUI.draggedLabelFn = moveFn; - TegakiUI.draggedLabelLastX = e.clientX; - $T.on(document, 'pointermove', TegakiUI.processDragLabel); - $T.on(document, 'pointerup', TegakiUI.clearDragLabel); - }, - - processDragLabel: function(e) { - TegakiUI.draggedLabelFn.call(Tegaki, e.clientX - TegakiUI.draggedLabelLastX); - TegakiUI.draggedLabelLastX = e.clientX; - }, - - clearDragLabel: function(e) { - $T.off(document, 'pointermove', TegakiUI.processDragLabel); - $T.off(document, 'pointerup', TegakiUI.clearDragLabel); - }, - - printMsg: function(str, timeout = 5000) { - TegakiUI.clearMsg(); - - $T.id('tegaki-status-output').textContent = str; - - if (timeout > 0) { - TegakiUI.statusTimeout = setTimeout(TegakiUI.clearMsg, 5000); - } - }, - - clearMsg: function() { - if (TegakiUI.statusTimeout) { - clearTimeout(TegakiUI.statusTimeout); - TegakiUI.statusTimeout = 0; - } - - $T.id('tegaki-status-output').textContent = ''; - }, - - buildUI: function() { - var bg, cnt, el, ctrl, layersCnt, canvasCnt; - - // - // Grid container - // - bg = $T.el('div'); - bg.id = 'tegaki'; - - // - // Menu area - // - el = $T.el('div'); - el.id = 'tegaki-menu-cnt'; - - if (!Tegaki.replayMode) { - el.appendChild(TegakiUI.buildMenuBar()); - } - else { - el.appendChild(TegakiUI.buildViewerMenuBar()); - el.appendChild(TegakiUI.buildReplayControls()); - } - - el.appendChild(TegakiUI.buildToolModeBar()); - - bg.appendChild(el); - - bg.appendChild(TegakiUI.buildDummyFilePicker()); - - // - // Tools area - // - cnt = $T.el('div'); - cnt.id = 'tegaki-tools-cnt'; - - cnt.appendChild(TegakiUI.buildToolsMenu()); - - bg.appendChild(cnt); - - // - // Canvas area - // - [canvasCnt, layersCnt] = TegakiUI.buildCanvasCnt(); - - bg.appendChild(canvasCnt); - - // - // Controls area - // - ctrl = $T.el('div'); - ctrl.id = 'tegaki-ctrl-cnt'; - - // Zoom control - ctrl.appendChild(TegakiUI.buildZoomCtrlGroup()); - - // Colorpicker - ctrl.appendChild( - TegakiUI.buildColorCtrlGroup(Tegaki.toolColor) - ); - - // Size control - ctrl.appendChild(TegakiUI.buildSizeCtrlGroup()); - - // Alpha control - ctrl.appendChild(TegakiUI.buildAlphaCtrlGroup()); - - // Flow control - ctrl.appendChild(TegakiUI.buildFlowCtrlGroup()); - - // Layers control - ctrl.appendChild(TegakiUI.buildLayersCtrlGroup()); - - // --- - - bg.appendChild(ctrl); - - // - // Status area - // - bg.appendChild(TegakiUI.buildStatusCnt()); - - return [bg, canvasCnt, layersCnt]; - }, - - buildDummyFilePicker: function() { - var el = $T.el('input'); - - el.type = 'file'; - el.id = 'tegaki-filepicker'; - el.className = 'tegaki-hidden'; - el.accept = 'image/png, image/jpeg'; - $T.on(el, 'change', Tegaki.onOpenFileSelected); - - return el; - }, - - buildMenuBar: function() { - var frag, btn; - - frag = $T.el('div'); - frag.id = 'tegaki-menu-bar'; - - btn = $T.el('span'); - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.newCanvas; - $T.on(btn, 'click', Tegaki.onNewClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.open; - $T.on(btn, 'click', Tegaki.onOpenClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.export; - $T.on(btn, 'click', Tegaki.onExportClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-undo-btn'; - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.undo; - btn.title = TegakiKeybinds.getCaption('undo'); - $T.on(btn, 'click', Tegaki.onUndoClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-redo-btn'; - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.redo; - btn.title = TegakiKeybinds.getCaption('redo'); - $T.on(btn, 'click', Tegaki.onRedoClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.close; - $T.on(btn, 'click', Tegaki.onCancelClick); - frag.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-finish-btn'; - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.finish; - $T.on(btn, 'click', Tegaki.onDoneClick); - frag.appendChild(btn); - - return frag; - }, - - buildViewerMenuBar: function() { - var frag, btn; - - frag = $T.el('div'); - frag.id = 'tegaki-menu-bar'; - - btn = $T.el('span'); - btn.id = 'tegaki-finish-btn'; - btn.className = 'tegaki-mb-btn'; - btn.textContent = TegakiStrings.close; - $T.on(btn, 'click', Tegaki.onCloseViewerClick); - frag.appendChild(btn); - - return frag; - }, - - buildToolModeBar: function() { - var cnt, grp, el, btn; - - cnt = $T.el('div'); - cnt.id = 'tegaki-toolmode-bar'; - - if (!Tegaki.tool) { - cnt.classList.add('tegaki-hidden'); - } - - // Dynamics - grp = $T.el('span'); - grp.id = 'tegaki-tool-mode-dynamics'; - grp.className = 'tegaki-toolmode-grp'; - - el = $T.el('span'); - el.className = 'tegaki-toolmode-lbl'; - el.textContent = TegakiStrings.pressure; - grp.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-tool-mode-dynamics-ctrl'; - el.className = 'tegaki-toolmode-ctrl'; - - btn = $T.el('span'); - btn.id = 'tegaki-tool-mode-dynamics-size'; - btn.className = 'tegaki-sw-btn'; - btn.textContent = TegakiStrings.size; - $T.on(btn, 'mousedown', Tegaki.onToolPressureSizeClick); - el.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-tool-mode-dynamics-alpha'; - btn.className = 'tegaki-sw-btn'; - btn.textContent = TegakiStrings.alpha; - $T.on(btn, 'mousedown', Tegaki.onToolPressureAlphaClick); - el.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-tool-mode-dynamics-flow'; - btn.className = 'tegaki-sw-btn'; - btn.textContent = TegakiStrings.flow; - $T.on(btn, 'mousedown', Tegaki.onToolPressureFlowClick); - el.appendChild(btn); - - grp.appendChild(el); - - cnt.appendChild(grp); - - // Preserve Alpha - grp = $T.el('span'); - grp.id = 'tegaki-tool-mode-mask'; - grp.className = 'tegaki-toolmode-grp'; - - el = $T.el('span'); - el.id = 'tegaki-toolmode-ctrl-tip'; - el.className = 'tegaki-toolmode-ctrl'; - - btn = $T.el('span'); - btn.id = 'tegaki-tool-mode-mask-alpha'; - btn.className = 'tegaki-sw-btn'; - btn.textContent = TegakiStrings.preserveAlpha; - $T.on(btn, 'mousedown', Tegaki.onToolPreserveAlphaClick); - el.appendChild(btn); - - grp.appendChild(el); - - cnt.appendChild(grp); - - // Tip - grp = $T.el('span'); - grp.id = 'tegaki-tool-mode-tip'; - grp.className = 'tegaki-toolmode-grp'; - - el = $T.el('span'); - el.className = 'tegaki-toolmode-lbl'; - el.textContent = TegakiStrings.tip; - grp.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-tool-mode-tip-ctrl'; - el.className = 'tegaki-toolmode-ctrl'; - grp.appendChild(el); - - cnt.appendChild(grp); - - return cnt; - }, - - buildToolsMenu: function() { - var grp, el, lbl, name; - - grp = $T.el('div'); - grp.id = 'tegaki-tools-grid'; - - for (name in Tegaki.tools) { - el = $T.el('span'); - el.setAttribute('data-tool', name); - - lbl = TegakiStrings[name]; - - if (Tegaki.tools[name].keybind) { - lbl += ' (' + Tegaki.tools[name].keybind.toUpperCase() + ')'; - } - - el.setAttribute('title', lbl); - el.id = 'tegaki-tool-btn-' + name; - el.className = 'tegaki-tool-btn tegaki-icon tegaki-' + name; - - $T.on(el, 'click', Tegaki.onToolClick); - - grp.appendChild(el); - } - - return grp; - }, - - buildCanvasCnt: function() { - var canvasCnt, wrap, layersCnt; - - canvasCnt = $T.el('div'); - canvasCnt.id = 'tegaki-canvas-cnt'; - - wrap = $T.el('div'); - wrap.id = 'tegaki-layers-wrap'; - - layersCnt = $T.el('div'); - layersCnt.id = 'tegaki-layers'; - - wrap.appendChild(layersCnt); - - canvasCnt.appendChild(wrap); - - return [canvasCnt, layersCnt]; - }, - - buildCtrlGroup: function(id, title) { - var cnt, el; - - cnt = $T.el('div'); - cnt.className = 'tegaki-ctrlgrp'; - - if (id) { - cnt.id = 'tegaki-ctrlgrp-' + id; - } - - if (title !== undefined) { - el = $T.el('div'); - el.className = 'tegaki-ctrlgrp-title'; - el.textContent = title; - cnt.appendChild(el); - } - - return cnt; - }, - - buildLayersCtrlGroup: function() { - var el, ctrl, row, cnt; - - ctrl = this.buildCtrlGroup('layers', TegakiStrings.layers); - - // Layer options row - row = $T.el('div'); - row.id = 'tegaki-layers-opts'; - - // Alpha - cnt = $T.el('div'); - cnt.id = 'tegaki-layer-alpha-cell'; - - el = $T.el('span'); - el.className = 'tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl'; - el.textContent = TegakiStrings.alpha; - $T.on(el, 'pointerdown', Tegaki.onLayerAlphaDragStart); - cnt.appendChild(el); - - el = $T.el('input'); - el.id = 'tegaki-layer-alpha-opt'; - el.className = 'tegaki-stealth-input tegaki-range-lbl-xs'; - el.setAttribute('maxlength', 3); - $T.on(el, 'input', Tegaki.onLayerAlphaChange); - cnt.appendChild(el); - - row.appendChild(cnt); - - ctrl.appendChild(row); - - el = $T.el('div'); - el.id = 'tegaki-layers-grid'; - ctrl.appendChild(el); - - row = $T.el('div'); - row.id = 'tegaki-layers-ctrl'; - - el = $T.el('span'); - el.title = TegakiStrings.addLayer; - el.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; - $T.on(el, 'click', Tegaki.onLayerAddClick); - row.appendChild(el); - - el = $T.el('span'); - el.title = TegakiStrings.delLayers; - el.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; - $T.on(el, 'click', Tegaki.onLayerDeleteClick); - row.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-layer-merge'; - el.title = TegakiStrings.mergeLayers; - el.className = 'tegaki-ui-btn tegaki-icon tegaki-level-down'; - $T.on(el, 'click', Tegaki.onMergeLayersClick); - row.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-layer-up'; - el.title = TegakiStrings.moveLayerUp; - el.setAttribute('data-up', '1'); - el.className = 'tegaki-ui-btn tegaki-icon tegaki-up-open'; - $T.on(el, 'click', Tegaki.onMoveLayerClick); - row.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-layer-down'; - el.title = TegakiStrings.moveLayerDown; - el.className = 'tegaki-ui-btn tegaki-icon tegaki-down-open'; - $T.on(el, 'click', Tegaki.onMoveLayerClick); - row.appendChild(el); - - ctrl.appendChild(row); - - return ctrl; - }, - - buildSizeCtrlGroup: function() { - var el, ctrl, row; - - ctrl = this.buildCtrlGroup('size', TegakiStrings.size); - - row = $T.el('div'); - row.className = 'tegaki-ctrlrow'; - - el = $T.el('input'); - el.id = 'tegaki-size'; - el.className = 'tegaki-ctrl-range'; - el.min = 1; - el.max = Tegaki.maxSize; - el.type = 'range'; - el.title = TegakiKeybinds.getCaption('toolSize'); - $T.on(el, 'input', Tegaki.onToolSizeChange); - row.appendChild(el); - - el = $T.el('input'); - el.id = 'tegaki-size-lbl'; - el.setAttribute('maxlength', 3); - el.className = 'tegaki-stealth-input tegaki-range-lbl'; - $T.on(el, 'input', Tegaki.onToolSizeChange); - row.appendChild(el); - - ctrl.appendChild(row); - - return ctrl; - }, - - buildAlphaCtrlGroup: function() { - var el, ctrl, row; - - ctrl = this.buildCtrlGroup('alpha', TegakiStrings.alpha); - - row = $T.el('div'); - row.className = 'tegaki-ctrlrow'; - - el = $T.el('input'); - el.id = 'tegaki-alpha'; - el.className = 'tegaki-ctrl-range'; - el.min = 0; - el.max = 100; - el.step = 1; - el.type = 'range'; - $T.on(el, 'input', Tegaki.onToolAlphaChange); - row.appendChild(el); - - el = $T.el('input'); - el.id = 'tegaki-alpha-lbl'; - el.setAttribute('maxlength', 3); - el.className = 'tegaki-stealth-input tegaki-range-lbl'; - $T.on(el, 'input', Tegaki.onToolAlphaChange); - row.appendChild(el); - - ctrl.appendChild(row); - - return ctrl; - }, - - buildFlowCtrlGroup: function() { - var el, ctrl, row; - - ctrl = this.buildCtrlGroup('flow', TegakiStrings.flow); - - row = $T.el('div'); - row.className = 'tegaki-ctrlrow'; - - el = $T.el('input'); - el.id = 'tegaki-flow'; - el.className = 'tegaki-ctrl-range'; - el.min = 0; - el.max = 100; - el.step = 1; - el.type = 'range'; - $T.on(el, 'input', Tegaki.onToolFlowChange); - row.appendChild(el); - - el = $T.el('input'); - el.id = 'tegaki-flow-lbl'; - el.setAttribute('maxlength', 3); - el.className = 'tegaki-stealth-input tegaki-range-lbl'; - $T.on(el, 'input', Tegaki.onToolFlowChange); - row.appendChild(el); - - ctrl.appendChild(row); - - return ctrl; - }, - - buildZoomCtrlGroup: function() { - var el, btn, ctrl; - - ctrl = this.buildCtrlGroup('zoom', TegakiStrings.zoom); - - btn = $T.el('div'); - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; - btn.id = 'tegaki-zoomin-btn'; - btn.setAttribute('data-in', 1); - $T.on(btn, 'click', Tegaki.onZoomChange); - ctrl.appendChild(btn); - - btn = $T.el('div'); - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; - btn.id = 'tegaki-zoomout-btn'; - btn.setAttribute('data-out', 1); - $T.on(btn, 'click', Tegaki.onZoomChange); - ctrl.appendChild(btn); - - el = $T.el('div'); - el.id = 'tegaki-zoom-lbl'; - ctrl.appendChild(el); - - return ctrl; - }, - - buildColorCtrlGroup: function(mainColor) { - var el, cnt, btn, ctrl, color, edge, i, palette, cls; - - edge = / Edge\//i.test(window.navigator.userAgent); - - ctrl = this.buildCtrlGroup('color', TegakiStrings.color); - - cnt = $T.el('div'); - cnt.id = 'tegaki-color-ctrl'; - - el = $T.el('div'); - el.id = 'tegaki-color'; - edge && el.classList.add('tegaki-hidden'); - el.style.backgroundColor = mainColor; - $T.on(el, 'mousedown', Tegaki.onMainColorClick); - cnt.appendChild(el); - - el = $T.el('div'); - el.id = 'tegaki-palette-switcher'; - - btn = $T.el('span'); - btn.id = 'tegaki-palette-prev-btn'; - btn.title = TegakiStrings.switchPalette; - btn.setAttribute('data-prev', '1'); - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled'; - $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); - el.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-palette-next-btn'; - btn.title = TegakiStrings.switchPalette; - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-right-open'; - $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); - el.appendChild(btn); - - cnt.appendChild(el); - - ctrl.appendChild(cnt); - - cnt = $T.el('div'); - cnt.id = 'tegaki-color-grids'; - - for (i = 0; i < TegakiColorPalettes.length; ++i) { - el = $T.el('div'); - - el.setAttribute('data-id', i); - - cls = 'tegaki-color-grid'; - - palette = TegakiColorPalettes[i]; - - if (palette.length <= 18) { - cls += ' tegaki-color-grid-20'; - } - else { - cls += ' tegaki-color-grid-15'; - } - - if (i > 0) { - cls += ' tegaki-hidden'; - } - - el.className = cls; - - for (color of palette) { - btn = $T.el('div'); - btn.title = TegakiStrings.paletteSlotReplace; - btn.className = 'tegaki-color-btn'; - btn.setAttribute('data-color', color); - btn.style.backgroundColor = color; - $T.on(btn, 'mousedown', Tegaki.onPaletteColorClick); - el.appendChild(btn); - } - - cnt.appendChild(el); - } - - ctrl.appendChild(cnt); - - el = $T.el('input'); - el.id = 'tegaki-colorpicker'; - !edge && el.classList.add('tegaki-invis'); - el.value = color; - el.type = 'color'; - $T.on(el, 'change', Tegaki.onColorPicked); - - ctrl.appendChild(el); - - return ctrl; - }, - - buildStatusCnt: function() { - var cnt, el; - - cnt = $T.el('div'); - cnt.id = 'tegaki-status-cnt'; - - if (Tegaki.saveReplay) { - el = $T.el('div'); - el.id = 'tegaki-status-replay'; - el.textContent = '⬤'; - el.setAttribute('title', TegakiStrings.recordingEnabled); - cnt.appendChild(el); - } - - el = $T.el('div'); - el.id = 'tegaki-status-output'; - cnt.appendChild(el); - - el = $T.el('div'); - el.id = 'tegaki-status-version'; - el.textContent = 'tegaki.js v' + Tegaki.VERSION; - cnt.appendChild(el); - - return cnt; - }, - - buildReplayControls: function() { - var cnt, btn, el; - - cnt = $T.el('div'); - cnt.id = 'tegaki-replay-controls'; - cnt.className = 'tegaki-hidden'; - - btn = $T.el('span'); - btn.id = 'tegaki-replay-gapless-btn'; - btn.className = 'tegaki-ui-cb-w'; - $T.on(btn, 'click', Tegaki.onReplayGaplessClick); - - el = $T.el('span'); - el.id = 'tegaki-replay-gapless-cb'; - el.className = 'tegaki-ui-cb'; - btn.appendChild(el); - - el = $T.el('span'); - el.className = 'tegaki-menu-lbl'; - el.textContent = TegakiStrings.gapless; - btn.appendChild(el); - - cnt.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-replay-play-btn'; - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-play'; - btn.setAttribute('title', TegakiStrings.play); - $T.on(btn, 'click', Tegaki.onReplayPlayPauseClick); - cnt.appendChild(btn); - - btn = $T.el('span'); - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-to-start'; - btn.setAttribute('title', TegakiStrings.rewind); - $T.on(btn, 'click', Tegaki.onReplayRewindClick); - cnt.appendChild(btn); - - btn = $T.el('span'); - btn.id = 'tegaki-replay-slower-btn'; - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-bw'; - btn.setAttribute('title', TegakiStrings.slower); - $T.on(btn, 'click', Tegaki.onReplaySlowDownClick); - cnt.appendChild(btn); - - el = $T.el('span'); - el.id = 'tegaki-replay-speed-lbl'; - el.className = 'tegaki-menu-lbl'; - el.textContent = '1.0'; - cnt.appendChild(el); - - btn = $T.el('span'); - btn.id = 'tegaki-replay-faster-btn'; - btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-fw'; - btn.setAttribute('title', TegakiStrings.faster); - $T.on(btn, 'click', Tegaki.onReplaySpeedUpClick); - cnt.appendChild(btn); - - el = $T.el('span'); - el.id = 'tegaki-replay-now-lbl'; - el.className = 'tegaki-menu-lbl'; - el.textContent = '00:00'; - cnt.appendChild(el); - - el = $T.el('span'); - el.id = 'tegaki-replay-end-lbl'; - el.className = 'tegaki-menu-lbl'; - el.textContent = '00:00'; - cnt.appendChild(el); - - return cnt; - }, - - buildLayerGridCell: function(layer) { - var cnt, el, cell; - - cnt = $T.el('div'); - cnt.id = 'tegaki-layers-cell-' + layer.id; - cnt.className = 'tegaki-layers-cell'; - cnt.setAttribute('data-id', layer.id); - cnt.draggable = true; - cnt.setAttribute('data-id', layer.id); - - $T.on(cnt, 'pointerdown', TegakiUI.onLayerSelectorPtrDown); - $T.on(cnt, 'pointerup', Tegaki.onLayerSelectorClick); - - $T.on(cnt, 'dragstart', TegakiUI.onLayerDragStart); - $T.on(cnt, 'dragover', TegakiUI.onLayerDragOver); - $T.on(cnt, 'drop', TegakiUI.onLayerDragDrop); - $T.on(cnt, 'dragend', TegakiUI.onLayerDragEnd); - $T.on(cnt, 'dragleave', TegakiUI.onLayerDragLeave); - $T.on(cnt, 'dragexit', TegakiUI.onLayerDragLeave); - - // visibility toggle - cell = $T.el('div'); - cell.className = 'tegaki-layers-cell-v'; - - el = $T.el('span'); - el.id = 'tegaki-layers-cb-v-' + layer.id; - el.className = 'tegaki-ui-cb'; - el.setAttribute('data-id', layer.id); - el.title = TegakiStrings.toggleVisibility; - $T.on(el, 'click', Tegaki.onLayerToggleVisibilityClick); - - if (layer.visible) { - el.className += ' tegaki-ui-cb-a'; - } - - cell.appendChild(el); - cnt.appendChild(cell); - - // preview - cell = $T.el('div'); - cell.className = 'tegaki-layers-cell-p'; - - el = $T.el('canvas'); - el.id = 'tegaki-layers-p-canvas-' + layer.id; - el.className = 'tegaki-alpha-bg-xs'; - [el.width, el.height] = TegakiUI.getLayerPreviewSize(); - - cell.appendChild(el); - cnt.appendChild(cell); - - // name - cell = $T.el('div'); - cell.className = 'tegaki-layers-cell-n'; - - el = $T.el('div'); - el.id = 'tegaki-layer-name-' + layer.id; - el.className = 'tegaki-ellipsis'; - el.setAttribute('data-id', layer.id); - el.textContent = layer.name; - $T.on(el, 'dblclick', Tegaki.onLayerNameChangeClick); - - cell.appendChild(el); - cnt.appendChild(cell); - - return cnt; - }, - - // --- - - onLayerSelectorPtrDown: function(e) { - if (e.pointerType === 'mouse') { - if (this.hasAttribute('data-nodrag')) { - this.removeAttribute('data-nodrag'); - $T.on(this, 'dragstart', TegakiUI.onLayerDragStart); - } - } - else if (!this.hasAttribute('data-nodrag')) { - this.setAttribute('data-nodrag', 1); - $T.off(this, 'dragstart', TegakiUI.onLayerDragStart); - } - }, - - onLayerDragStart: function(e) { - var el, id; - - if (e.ctrlKey) { - return; - } - - TegakiUI.draggedNode = null; - - if (!$T.id('tegaki-layers-grid').children[1]) { - e.preventDefault(); - return; - } - - id = +e.target.getAttribute('data-id'); - - el = $T.el('div'); - el.className = 'tegaki-invis'; - e.dataTransfer.setDragImage(el, 0, 0); - e.dataTransfer.setData('text/plain', id); - e.dataTransfer.effectAllowed = 'move'; - - TegakiUI.draggedNode = e.target; - - TegakiUI.updateLayersGridDragExt(true); - }, - - onLayerDragOver: function(e) { - e.preventDefault(); - - e.dataTransfer.dropEffect = 'move'; - - TegakiUI.updateLayersGridDragEffect( - e.target, - +TegakiUI.draggedNode.getAttribute('data-id') - ); - }, - - onLayerDragLeave: function(e) { - TegakiUI.updateLayersGridDragEffect(); - }, - - onLayerDragEnd: function(e) { - TegakiUI.draggedNode = null; - TegakiUI.updateLayersGridDragExt(false); - TegakiUI.updateLayersGridDragEffect(); - }, - - onLayerDragDrop: function(e) { - var tgtId, srcId, belowPos; - - e.preventDefault(); - - TegakiUI.draggedNode = null; - - [tgtId] = TegakiUI.layersGridFindDropTgt(e.target); - srcId = +e.dataTransfer.getData('text/plain'); - - TegakiUI.updateLayersGridDragEffect(e.target.parentNode); - TegakiUI.updateLayersGridDragExt(false); - - if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { - return; - } - - if (!tgtId) { - belowPos = Tegaki.layers.length; - } - else { - belowPos = TegakiLayers.getLayerPosById(tgtId); - } - - if (!TegakiLayers.selectedLayersHas(srcId)) { - Tegaki.setActiveLayer(srcId); - } - - Tegaki.moveSelectedLayers(belowPos); - }, - - updateLayersGridDragExt: function(flag) { - var cnt, el; - - cnt = $T.id('tegaki-layers-grid'); - - if (!cnt.children[1]) { - return; - } - - if (flag) { - el = $T.el('div'); - el.id = 'tegaki-layers-cell-dx'; - el.draggable = true; - $T.on(el, 'dragover', TegakiUI.onLayerDragOver); - $T.on(el, 'drop', TegakiUI.onLayerDragDrop); - cnt.parentNode.insertBefore(el, cnt); - } - else { - if (el = $T.id('tegaki-layers-cell-dx')) { - el.parentNode.removeChild(el); - } - } - }, - - updateLayersGridDragEffect: function(tgt, srcId) { - var el, nodes, tgtId; - - nodes = $T.cls('tegaki-layers-cell-d', $T.id('tegaki-ctrlgrp-layers')); - - for (el of nodes) { - el.classList.remove('tegaki-layers-cell-d'); - } - - if (!tgt || !srcId) { - return; - } - - [tgtId, tgt] = TegakiUI.layersGridFindDropTgt(tgt); - - if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { - return; - } - - if (!tgt) { - tgt = $T.id('tegaki-layers-grid'); - } - - tgt.classList.add('tegaki-layers-cell-d'); - }, - - layersGridFindDropTgt: function(tgt) { - var tgtId, cnt; - - tgtId = +tgt.getAttribute('data-id'); - - cnt = $T.id('tegaki-ctrlgrp-layers'); - - while (!tgt.draggable && tgt !== cnt) { - tgt = tgt.parentNode; - tgtId = +tgt.getAttribute('data-id'); - } - - if (tgt === cnt || !tgt.draggable) { - return [0, null]; - } - - return [tgtId, tgt]; - }, - - layersGridCanDrop: function(tgtId, srcId) { - var srcEl; - - if (tgtId === srcId) { - return false; - } - - srcEl = $T.id('tegaki-layers-cell-' + srcId); - - if (!srcEl.previousElementSibling) { - if (!tgtId) { - return false; - } - } - else if (+srcEl.previousElementSibling.getAttribute('data-id') === tgtId) { - return false; - } - - return true; - }, - - // --- - - setReplayMode: function(flag) { - Tegaki.bg.classList[flag ? 'add' : 'remove']('tegaki-replay-mode'); - }, - - // --- - - onToolChanged: function() { - $T.id('tegaki-toolmode-bar').classList.remove('tegaki-hidden'); - TegakiUI.updateToolSize(); - TegakiUI.updateToolAlpha(); - TegakiUI.updateToolFlow(); - TegakiUI.updateToolModes(); - }, - - // --- - - updateLayerAlphaOpt: function() { - var el = $T.id('tegaki-layer-alpha-opt'); - el.value = Math.round(Tegaki.activeLayer.alpha * 100); - }, - - updateLayerName: function(layer) { - var el; - - if (el = $T.id('tegaki-layer-name-' + layer.id)) { - el.textContent = layer.name; - } - }, - - updateLayerPreview: function(layer) { - var canvas, ctx; - - canvas = $T.id('tegaki-layers-p-canvas-' + layer.id); - - if (!canvas) { - return; - } - - ctx = TegakiUI.getLayerPreviewCtx(layer); - - if (!ctx) { - ctx = canvas.getContext('2d'); - ctx.imageSmoothingEnabled = false; - TegakiUI.setLayerPreviewCtx(layer, ctx); - } - - $T.clearCtx(ctx); - ctx.drawImage(layer.canvas, 0, 0, canvas.width, canvas.height); - }, - - updateLayerPreviewSize: function(regen) { - var el, layer, size; - - size = TegakiUI.getLayerPreviewSize(); - - for (layer of Tegaki.layers) { - if (el = $T.id('tegaki-layers-p-canvas-' + layer.id)) { - [el.width, el.height] = size; - - if (regen) { - TegakiUI.updateLayerPreview(layer); - } - } - } - }, - - getLayerPreviewCtx: function(layer) { - TegakiUI.layerPreviewCtxCache.get(layer); - }, - - setLayerPreviewCtx: function(layer, ctx) { - TegakiUI.layerPreviewCtxCache.set(layer, ctx); - }, - - deleteLayerPreviewCtx: function(layer) { - TegakiUI.layerPreviewCtxCache.delete(layer); - }, - - updateLayersGridClear: function() { - $T.id('tegaki-layers-grid').innerHTML = ''; - }, - - updateLayersGrid: function() { - var layer, el, frag, cnt; - - frag = $T.frag(); - - for (layer of Tegaki.layers) { - el = TegakiUI.buildLayerGridCell(layer); - frag.insertBefore(el, frag.firstElementChild); - } - - TegakiUI.updateLayersGridClear(); - - cnt.appendChild(frag); - }, - - updateLayersGridActive: function(layerId) { - var el; - - el = $T.cls('tegaki-layers-cell-a', $T.id('tegaki-layers-grid'))[0]; - - if (el) { - el.classList.remove('tegaki-layers-cell-a'); - } - - el = $T.id('tegaki-layers-cell-' + layerId); - - if (el) { - el.classList.add('tegaki-layers-cell-a'); - } - - TegakiUI.updateLayerAlphaOpt(); - }, - - updateLayersGridAdd: function(layer, aboveId) { - var el, cnt, ref; - - el = TegakiUI.buildLayerGridCell(layer); - - cnt = $T.id('tegaki-layers-grid'); - - if (aboveId) { - ref = $T.id('tegaki-layers-cell-' + aboveId); - } - else { - ref = null; - } - - cnt.insertBefore(el, ref); - }, - - updateLayersGridRemove: function(id) { - var el; - - if (el = $T.id('tegaki-layers-cell-' + id)) { - el.parentNode.removeChild(el); - } - }, - - updayeLayersGridOrder: function() { - var layer, cnt, el; - - cnt = $T.id('tegaki-layers-grid'); - - for (layer of Tegaki.layers) { - el = $T.id('tegaki-layers-cell-' + layer.id); - cnt.insertBefore(el, cnt.firstElementChild); - } - }, - - updateLayersGridVisibility: function(id, flag) { - var el; - - el = $T.id('tegaki-layers-cb-v-' + id); - - if (!el) { - return; - } - - if (flag) { - el.classList.add('tegaki-ui-cb-a'); - } - else { - el.classList.remove('tegaki-ui-cb-a'); - } - }, - - updateLayersGridSelectedClear: function() { - var layer, el; - - for (layer of Tegaki.layers) { - if (el = $T.id('tegaki-layers-cell-' + layer.id)) { - el.classList.remove('tegaki-layers-cell-s'); - } - } - }, - - updateLayersGridSelectedSet: function(id, flag) { - var el; - - if (el = $T.id('tegaki-layers-cell-' + id)) { - if (flag) { - el.classList.add('tegaki-layers-cell-s'); - } - else { - el.classList.remove('tegaki-layers-cell-s'); - } - } - }, - - updateToolSize: function() { - var el = $T.id('tegaki-ctrlgrp-size'); - - if (Tegaki.tool.useSize) { - el.classList.remove('tegaki-hidden'); - - $T.id('tegaki-size-lbl').value = Tegaki.tool.size; - $T.id('tegaki-size').value = Tegaki.tool.size; - } - else { - el.classList.add('tegaki-hidden'); - } - }, - - updateToolAlpha: function() { - var val, el = $T.id('tegaki-ctrlgrp-alpha'); - - if (Tegaki.tool.useAlpha) { - el.classList.remove('tegaki-hidden'); - - val = Math.round(Tegaki.tool.alpha * 100); - $T.id('tegaki-alpha-lbl').value = val; - $T.id('tegaki-alpha').value = val; - } - else { - el.classList.add('tegaki-hidden'); - } - }, - - updateToolFlow: function() { - var val, el = $T.id('tegaki-ctrlgrp-flow'); - - if (Tegaki.tool.useFlow) { - el.classList.remove('tegaki-hidden'); - - val = Math.round(Tegaki.tool.flow * 100); - $T.id('tegaki-flow-lbl').value = val; - $T.id('tegaki-flow').value = val; - } - else { - el.classList.add('tegaki-hidden'); - } - }, - - updateToolDynamics: function() { - var ctrl, cb; - - ctrl = $T.id('tegaki-tool-mode-dynamics'); - - if (!Tegaki.tool.usesDynamics()) { - ctrl.classList.add('tegaki-hidden'); - } - else { - cb = $T.id('tegaki-tool-mode-dynamics-size'); - - if (Tegaki.tool.useSizeDynamics) { - if (Tegaki.tool.sizeDynamicsEnabled) { - cb.classList.add('tegaki-sw-btn-a'); - } - else { - cb.classList.remove('tegaki-sw-btn-a'); - } - - cb.classList.remove('tegaki-hidden'); - } - else { - cb.classList.add('tegaki-hidden'); - } - - cb = $T.id('tegaki-tool-mode-dynamics-alpha'); - - if (Tegaki.tool.useAlphaDynamics) { - if (Tegaki.tool.alphaDynamicsEnabled) { - cb.classList.add('tegaki-sw-btn-a'); - } - else { - cb.classList.remove('tegaki-sw-btn-a'); - } - - cb.classList.remove('tegaki-hidden'); - } - else { - cb.classList.add('tegaki-hidden'); - } - - cb = $T.id('tegaki-tool-mode-dynamics-flow'); - - if (Tegaki.tool.useFlowDynamics) { - if (Tegaki.tool.flowDynamicsEnabled) { - cb.classList.add('tegaki-sw-btn-a'); - } - else { - cb.classList.remove('tegaki-sw-btn-a'); - } - - cb.classList.remove('tegaki-hidden'); - } - else { - cb.classList.add('tegaki-hidden'); - } - - ctrl.classList.remove('tegaki-hidden'); - } - }, - - updateToolShape: function() { - var tipId, ctrl, cnt, btn, tipList; - - ctrl = $T.id('tegaki-tool-mode-tip'); - - if (!Tegaki.tool.tipList) { - ctrl.classList.add('tegaki-hidden'); - } - else { - tipList = Tegaki.tool.tipList; - - cnt = $T.id('tegaki-tool-mode-tip-ctrl'); - - cnt.innerHTML = ''; - - for (tipId = 0; tipId < tipList.length; ++tipId) { - btn = $T.el('span'); - btn.id = 'tegaki-tool-mode-tip-' + tipId; - btn.className = 'tegaki-sw-btn'; - btn.setAttribute('data-id', tipId); - btn.textContent = TegakiStrings[tipList[tipId]]; - - $T.on(btn, 'mousedown', Tegaki.onToolTipClick); - - cnt.appendChild(btn); - - if (Tegaki.tool.tipId === tipId) { - btn.classList.add('tegaki-sw-btn-a'); - } - } - - ctrl.classList.remove('tegaki-hidden'); - } - }, - - updateToolPreserveAlpha: function() { - var cb, ctrl; - - ctrl = $T.id('tegaki-tool-mode-mask'); - - if (!Tegaki.tool.usePreserveAlpha) { - ctrl.classList.add('tegaki-hidden'); - } - else { - cb = $T.id('tegaki-tool-mode-mask-alpha'); - - if (Tegaki.tool.preserveAlphaEnabled) { - cb.classList.add('tegaki-sw-btn-a'); - } - else { - cb.classList.remove('tegaki-sw-btn-a'); - } - - ctrl.classList.remove('tegaki-hidden'); - } - }, - - updateToolModes: function() { - var el, flag; - - TegakiUI.updateToolShape(); - TegakiUI.updateToolDynamics(); - TegakiUI.updateToolPreserveAlpha(); - - flag = false; - - for (el of $T.id('tegaki-toolmode-bar').children) { - if (!flag && !el.classList.contains('tegaki-hidden')) { - el.classList.add('tegaki-ui-borderless'); - flag = true; - } - else { - el.classList.remove('tegaki-ui-borderless'); - } - } - }, - - updateUndoRedo: function(undoSize, redoSize) { - var u, r; - - if (Tegaki.replayMode) { - return; - } - - u = $T.id('tegaki-undo-btn').classList; - r = $T.id('tegaki-redo-btn').classList; - - if (undoSize) { - if (u.contains('tegaki-disabled')) { - u.remove('tegaki-disabled'); - } - } - else { - if (!u.contains('tegaki-disabled')) { - u.add('tegaki-disabled'); - } - } - - if (redoSize) { - if (r.contains('tegaki-disabled')) { - r.remove('tegaki-disabled'); - } - } - else { - if (!r.contains('tegaki-disabled')) { - r.add('tegaki-disabled'); - } - } - }, - - updateZoomLevel: function() { - $T.id('tegaki-zoom-lbl').textContent = (Tegaki.zoomFactor * 100) + '%'; - - if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel >= Tegaki.zoomFactorList.length) { - $T.id('tegaki-zoomin-btn').classList.add('tegaki-disabled'); - } - else { - $T.id('tegaki-zoomin-btn').classList.remove('tegaki-disabled'); - } - - if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel <= 0) { - $T.id('tegaki-zoomout-btn').classList.add('tegaki-disabled'); - } - else { - $T.id('tegaki-zoomout-btn').classList.remove('tegaki-disabled'); - } - }, - - updateColorPalette: function() { - var el, nodes, id; - - id = Tegaki.colorPaletteId; - - nodes = $T.cls('tegaki-color-grid', $T.id('tegaki-color-grids')); - - for (el of nodes) { - if (+el.getAttribute('data-id') === id) { - el.classList.remove('tegaki-hidden'); - } - else { - el.classList.add('tegaki-hidden'); - } - } - - el = $T.id('tegaki-palette-prev-btn'); - - if (id === 0) { - el.classList.add('tegaki-disabled'); - } - else { - el.classList.remove('tegaki-disabled'); - } - - el = $T.id('tegaki-palette-next-btn'); - - if (id === TegakiColorPalettes.length - 1) { - el.classList.add('tegaki-disabled'); - } - else { - el.classList.remove('tegaki-disabled'); - } - }, - - updateReplayTime: function(full) { - var now, end, r = Tegaki.replayViewer; - - now = r.getCurrentPos(); - - end = r.getDuration(); - - if (now > end) { - now = end; - } - - $T.id('tegaki-replay-now-lbl').textContent = $T.msToHms(now); - - if (full) { - $T.id('tegaki-replay-end-lbl').textContent = $T.msToHms(end); - } - }, - - updateReplayControls: function() { - TegakiUI.updateReplayGapless(); - TegakiUI.updateReplayPlayPause(); - TegakiUI.updateReplaySpeed(); - }, - - updateReplayGapless: function() { - var el, r = Tegaki.replayViewer; - - el = $T.id('tegaki-replay-gapless-cb'); - - if (r.gapless) { - el.classList.add('tegaki-ui-cb-a'); - } - else { - el.classList.remove('tegaki-ui-cb-a'); - } - }, - - updateReplayPlayPause: function() { - var el, r = Tegaki.replayViewer; - - el = $T.id('tegaki-replay-play-btn'); - - if (r.playing) { - el.classList.remove('tegaki-play'); - el.classList.add('tegaki-pause'); - el.setAttribute('title', TegakiStrings.pause); - } - else { - el.classList.add('tegaki-play'); - el.classList.remove('tegaki-pause'); - el.setAttribute('title', TegakiStrings.play); - - if (r.getCurrentPos() < r.getDuration()) { - el.classList.remove('tegaki-disabled'); - } - else { - el.classList.add('tegaki-disabled'); - } - } - }, - - updateReplaySpeed: function() { - var el, r = Tegaki.replayViewer; - - $T.id('tegaki-replay-speed-lbl').textContent = r.speed.toFixed(1); - - el = $T.id('tegaki-replay-slower-btn'); - - if (r.speedIndex === 0) { - el.classList.add('tegaki-disabled'); - } - else { - el.classList.remove('tegaki-disabled'); - } - - el = $T.id('tegaki-replay-faster-btn'); - - if (r.speedIndex === r.speedList.length - 1) { - el.classList.add('tegaki-disabled'); - } - else { - el.classList.remove('tegaki-disabled'); - } - }, - - enableReplayControls: function(flag) { - if (flag) { - $T.id('tegaki-replay-controls').classList.remove('tegaki-hidden'); - } - else { - $T.id('tegaki-replay-controls').classList.add('tegaki-hidden'); - } - }, - - setRecordingStatus: function(flag) { - var el = $T.id('tegaki-status-replay'); - - if (flag) { - el.classList.remove('tegaki-hidden'); - } - else { - el.classList.add('tegaki-hidden'); - } - } -}; -/*! UZIP.js, © 2018 Photopea, MIT License */ - -var UZIP = {}; -if(typeof module == "object") module.exports = UZIP; - -UZIP.inflateRaw = function(file, buf) { return UZIP.F.inflate(file, buf); } - -UZIP.deflateRaw = function(data, opts) { - if(opts==null) opts={level:6}; - var buf=new Uint8Array(50+Math.floor(data.length*1.1)); - var off = UZIP.F.deflateRaw(data, buf, off, opts.level); - return new Uint8Array(buf.buffer, 0, off); -} - -UZIP.bin = { - readUshort : function(buff,p) { return (buff[p]) | (buff[p+1]<<8); }, - writeUshort: function(buff,p,n){ buff[p] = (n)&255; buff[p+1] = (n>>8)&255; }, - readUint : function(buff,p) { return (buff[p+3]*(256*256*256)) + ((buff[p+2]<<16) | (buff[p+1]<< 8) | buff[p]); }, - 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; }, - readASCII : function(buff,p,l){ var s = ""; for(var i=0; i> 6)); buff[p+i+1] = (128|((code>> 0)&63)); i+=2; } - 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; } - 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; } - else throw "e"; - } - return i; - }, - sizeUTF8 : function(str) { - var strl = str.length, i=0; - for(var ci=0; ci>>3; - } - - 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 - if(dlen>2) { nc=UZIP.F._hash(data,0); strt[nc]=0; } - var nmch=0,nmci=0; - - for(i=0; i14000 || lc>26697) && (dlen-i)>100) { - if(cvrd>>16)>>16)>(mch>>>16)) mch=0; - }//*/ - var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; - if(mch!=0) { - var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; - var lgi = goodIndex(len, U.of0); U.lhst[257+lgi]++; - var dgi = goodIndex(dst, U.df0); U.dhst[ dgi]++; ebits += U.exb[lgi] + U.dxb[dgi]; - lits[li] = (len<<23)|(i-cvrd); lits[li+1] = (dst<<16)|(lgi<<8)|dgi; li+=2; - cvrd = i + len; - } - else { U.lhst[data[i]]++; } - lc++; - } - } - if(bs!=i || data.length==0) { - if(cvrd>>3; -} -UZIP.F._bestMatch = function(data, i, prev, c, nice, chain) { - var ci = (i&0x7fff), pi=prev[ci]; - //console.log("----", i); - var dif = ((ci-pi + (1<<15)) & 0x7fff); if(pi==ci || c!=UZIP.F._hash(data,i-dif)) return 0; - var tl=0, td=0; // top length, top distance - var dlim = Math.min(0x7fff, i); - while(dif<=dlim && --chain!=0 && pi!=ci /*&& c==UZIP.F._hash(data,i-dif)*/) { - if(tl==0 || (data[i+tl]==data[i+tl-dif])) { - var cl = UZIP.F._howLong(data, i, dif); - if(cl>tl) { - tl=cl; td=dif; if(tl>=nice) break; //* - if(dif+2maxd) { maxd=curd; pi = ei; } - } //*/ - } - } - - ci=pi; pi = prev[ci]; - dif += ((ci-pi + (1<<15)) & 0x7fff); - } - return (tl<<16)|td; -} -UZIP.F._howLong = function(data, i, dif) { - if(data[i]!=data[i-dif] || data[i+1]!=data[i+1-dif] || data[i+2]!=data[i+2-dif]) return 0; - var oi=i, l = Math.min(data.length, i+258); i+=3; - //while(i+4>>23), end = off+(qb&((1<<23)-1)); - while(off>16), lgi=(qc>>8)&255, dgi=(qc&255); - pos = UZIP.F._writeLit(257+lgi, ltree, out, pos); - putsE(out, pos, len-U.of0[lgi]); pos+=U.exb[lgi]; - - pos = UZIP.F._writeLit(dgi, dtree, out, pos); - putsF(out, pos, dst-U.df0[dgi]); pos+=U.dxb[dgi]; off+=len; - } - } - pos = UZIP.F._writeLit(256, ltree, out, pos); - } - //console.log(pos-opos, fxdSize, dynSize, cstSize); - return pos; -} -UZIP.F._copyExact = function(data,off,len,out,pos) { - var p8 = (pos>>>3); - out[p8]=(len); out[p8+1]=(len>>>8); out[p8+2]=255-out[p8]; out[p8+3]=255-out[p8+1]; p8+=4; - out.set(new Uint8Array(data.buffer, off, len), p8); - //for(var i=0; i4 && U.itree[(U.ordr[numh-1]<<1)+1]==0) numh--; - return [ML, MD, MH, numl, numd, numh, lset, dset]; -} -UZIP.F.getSecond= function(a) { var b=[]; for(var i=0; i>1)+","; return b; } -UZIP.F.contSize = function(tree, hst) { var s=0; for(var i=0; i15) { UZIP.F._putsE(out, pos, rst, rsl); pos+=rsl; } - } - return pos; -} -UZIP.F._lenCodes = function(tree, set) { - var len=tree.length; while(len!=2 && tree[len-1]==0) len-=2; // when no distances, keep one code with length 0 - for(var i=0; i>>1, 138); - if(zc<11) set.push(17, zc-3); - else set.push(18, zc-11); - i += zc*2-2; - } - else if(l==prv && nxt==l && nnxt==l) { - var lz = i+5; - while(lz+2>>1, 6); - set.push(16, zc-3); - i += zc*2-2; - } - else set.push(l, 0); - } - return len>>>1; -} -UZIP.F._hufTree = function(hst, tree, MAXL) { - var list=[], hl = hst.length, tl=tree.length, i=0; - for(i=0; iMAXL) { UZIP.F.restrictDepth(l2, MAXL, maxl); maxl = MAXL; } - for(i=0; iMD) { var od=dps[i].d; dps[i].d=MD; dbt+=bCost-(1<<(maxl-od)); } else break; - dbt = dbt>>>(maxl-MD); - while(dbt>0) { var od=dps[i].d; if(od=0; i--) if(dps[i].d==MD && dbt<0) { dps[i].d--; dbt++; } if(dbt!=0) console.log("debt left"); -} - -UZIP.F._goodIndex = function(v, arr) { - 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; -} -UZIP.F._writeLit = function(ch, ltree, out, pos) { - UZIP.F._putsF(out, pos, ltree[ch<<1]); - return pos+ltree[(ch<<1)+1]; -} - - - - - - - - -UZIP.F.inflate = function(data, buf) { - var u8=Uint8Array; - if(data[0]==3 && data[1]==0) return (buf ? buf : new u8(0)); - var F=UZIP.F, bitsF = F._bitsF, bitsE = F._bitsE, decodeTiny = F._decodeTiny, makeCodes = F.makeCodes, codes2map=F.codes2map, get17 = F._get17; - var U = F.U; - - var noBuf = (buf==null); - if(noBuf) buf = new u8((data.length>>>2)<<3); - - var BFINAL=0, BTYPE=0, HLIT=0, HDIST=0, HCLEN=0, ML=0, MD=0; - var off = 0, pos = 0; - var lmap, dmap; - - while(BFINAL==0) { - BFINAL = bitsF(data, pos , 1); - BTYPE = bitsF(data, pos+1, 2); pos+=3; - //console.log(BFINAL, BTYPE); - - if(BTYPE==0) { - if((pos&7)!=0) pos+=8-(pos&7); - var p8 = (pos>>>3)+4, len = data[p8-4]|(data[p8-3]<<8); //console.log(len);//bitsF(data, pos, 16), - if(noBuf) buf=UZIP.F._check(buf, off+len); - buf.set(new u8(data.buffer, data.byteOffset+p8, len), off); - //for(var i=0; itl)tl=l; } pos+=3*HCLEN; //console.log(itree); - makeCodes(U.itree, tl); - codes2map(U.itree, tl, U.imap); - - lmap = U.lmap; dmap = U.dmap; - - pos = decodeTiny(U.imap, (1<>>24))-1; pos+=(ml&0xffffff); - makeCodes(U.ltree, mx0); - codes2map(U.ltree, mx0, lmap); - - //var md = decodeTiny(U.imap, (1<>>24))-1; pos+=(md&0xffffff); - makeCodes(U.dtree, mx1); - codes2map(U.dtree, mx1, dmap); - } - //var ooff=off, opos=pos; - while(true) { - var code = lmap[get17(data, pos) & ML]; pos += code&15; - var lit = code>>>4; //U.lhst[lit]++; - if((lit>>>8)==0) { buf[off++] = lit; } - else if(lit==256) { break; } - else { - var end = off+lit-254; - if(lit>264) { var ebs = U.ldef[lit-257]; end = off + (ebs>>>3) + bitsE(data, pos, ebs&7); pos += ebs&7; } - //UZIP.F.dst[end-off]++; - - var dcode = dmap[get17(data, pos) & MD]; pos += dcode&15; - var dlit = dcode>>>4; - var dbs = U.ddef[dlit], dst = (dbs>>>4) + bitsF(data, pos, dbs&15); pos += dbs&15; - - //var o0 = off-dst, stp = Math.min(end-off, dst); - //if(stp>20) while(off>>3); - } - //console.log(UZIP.F.dst); - //console.log(tlen, dlen, off-tlen+tcnt); - return buf.length==off ? buf : buf.slice(0,off); -} -UZIP.F._check=function(buf, len) { - var bl=buf.length; if(len<=bl) return buf; - var nbuf = new Uint8Array(Math.max(bl<<1,len)); nbuf.set(buf,0); - //for(var i=0; i>>4; - if(lit<=15) { tree[i]=lit; i++; } - else { - var ll = 0, n = 0; - if(lit==16) { - n = (3 + bitsE(data, pos, 2)); pos += 2; ll = tree[i-1]; - } - else if(lit==17) { - n = (3 + bitsE(data, pos, 3)); pos += 3; - } - else if(lit==18) { - n = (11 + bitsE(data, pos, 7)); pos += 7; - } - var ni = i+n; - while(i>>1; - while(imx)mx=v; i++; } - while(i>1; - var cl = tree[i+1], val = (lit<<4)|cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl); - var rest = (MAX_BITS-cl), i0 = tree[i]<>>(15-MAX_BITS); - while(i0!=i1) { - var p0 = r15[i0]>>>(15-MAX_BITS); - map[p0]=val; i0++; - } - } -} -UZIP.F.revCodes = function(tree, MAX_BITS) { - var r15 = UZIP.F.U.rev15, imb = 15-MAX_BITS; - for(var i=0; i>>imb; } -} - -// used only in deflate -UZIP.F._putsE= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); } -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); } - -UZIP.F._bitsE= function(dt, pos, length) { return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) )>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16))>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8))>>>(pos&7))&511; -} */ -UZIP.F._get17= function(dt, pos) { // return at least 17 meaningful bytes - return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) )>>>(pos&7); -} -UZIP.F._get25= function(dt, pos) { // return at least 17 meaningful bytes - return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) | (dt[(pos>>>3)+3]<<24) )>>>(pos&7); -} -UZIP.F.U = function(){ - var u16=Uint16Array, u32=Uint32Array; - return { - next_code : new u16(16), - bl_count : new u16(16), - ordr : [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ], - 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], - 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], - ldef : new u16(32), - 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], - 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], - ddef : new u32(32), - flmap: new u16( 512), fltree: [], - fdmap: new u16( 32), fdtree: [], - lmap : new u16(32768), ltree : [], ttree:[], - dmap : new u16(32768), dtree : [], - imap : new u16( 512), itree : [], - //rev9 : new u16( 512) - rev15: new u16(1<<15), - lhst : new u32(286), dhst : new u32( 30), ihst : new u32(19), - lits : new u32(15000), - strt : new u16(1<<16), - prev : new u16(1<<15) - }; -} (); - -(function(){ - var U = UZIP.F.U; - var len = 1<<15; - for(var i=0; i>> 1) | ((x & 0x55555555) << 1)); - x = (((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2)); - x = (((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4)); - x = (((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8)); - U.rev15[i] = (((x >>> 16) | (x << 16)))>>>17; - } - - function pushV(tgt, n, sv) { while(n--!=0) tgt.push(0,sv); } - - 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]; } - - pushV(U.fltree, 144, 8); pushV(U.fltree, 255-143, 9); pushV(U.fltree, 279-255, 7); pushV(U.fltree,287-279,8); - /* - var i = 0; - for(; i<=143; i++) U.fltree.push(0,8); - for(; i<=255; i++) U.fltree.push(0,9); - for(; i<=279; i++) U.fltree.push(0,7); - for(; i<=287; i++) U.fltree.push(0,8); - */ - UZIP.F.makeCodes(U.fltree, 9); - UZIP.F.codes2map(U.fltree, 9, U.flmap); - UZIP.F.revCodes (U.fltree, 9) - - pushV(U.fdtree,32,5); - //for(i=0;i<32; i++) U.fdtree.push(0,5); - UZIP.F.makeCodes(U.fdtree, 5); - UZIP.F.codes2map(U.fdtree, 5, U.fdmap); - UZIP.F.revCodes (U.fdtree, 5) - - pushV(U.itree,19,0); pushV(U.ltree,286,0); pushV(U.dtree,30,0); pushV(U.ttree,320,0); - /* - for(var i=0; i< 19; i++) U.itree.push(0,0); - for(var i=0; i<286; i++) U.ltree.push(0,0); - for(var i=0; i< 30; i++) U.dtree.push(0,0); - for(var i=0; i<320; i++) U.ttree.push(0,0); - */ -})() - - - - - - - - - - - - - - - - +/*! tegaki.js, MIT License */'use strict';var TegakiStrings = { + // Messages + badDimensions: __('Invalid dimensions.'), + promptWidth: __('Canvas width in pixels'), + promptHeight: __('Canvas height in pixels'), + confirmDelLayers: __('Delete selected layers?'), + confirmMergeLayers: __('Merge selected layers?'), + tooManyLayers: __('Layer limit reached.'), + errorLoadImage: __('Could not load the image.'), + noActiveLayer: __('No active layer.'), + hiddenActiveLayer: __('The active layer is not visible.'), + confirmCancel: __('Are you sure? Your work will be lost.'), + confirmChangeCanvas: __('Are you sure? Changing the canvas will clear all layers and history and disable replay recording.'), + + // Controls + color: __('Color'), + size: __('Size'), + alpha: __('Opacity'), + flow: __('Flow'), + zoom: __('Zoom'), + layers: __('Layers'), + switchPalette: __('Switch color palette'), + paletteSlotReplace: __('Right click to replace with the current color'), + + // Layers + layer: __('Layer'), + addLayer: __('Add layer'), + delLayers: __('Delete layers'), + mergeLayers: __('Merge layers'), + moveLayerUp: __('Move up'), + moveLayerDown: __('Move down'), + toggleVisibility: __('Toggle visibility'), + + // Menu bar + newCanvas: __('New'), + open: __('Open'), + save: __('Save'), + saveAs: __('Save As'), + export: __('Export'), + undo: __('Undo'), + redo: __('Redo'), + close: __('Close'), + finish: __('Finish'), + + // Tool modes + tip: __('Tip'), + pressure: __('Pressure'), + preserveAlpha: __('Preserve Alpha'), + + // Tools + pen: __('Pen'), + pencil: __('Pencil'), + airbrush: __('Airbrush'), + pipette: __('Pipette'), + blur: __('Blur'), + eraser: __('Eraser'), + bucket: __('Bucket'), + tone: __('Tone'), + + // Replay + gapless: __('Gapless'), + play: __('Play'), + pause: __('Pause'), + rewind: __('Rewind'), + slower: __('Slower'), + faster: __('Faster'), + recordingEnabled: __('Recording replay'), + errorLoadReplay: __('Could not load the replay: '), + loadingReplay: __('Loading replay…'), +}; + +class TegakiTool { + constructor() { + this.id = 0; + + this.name = null; + + this.keybind = null; + + this.useFlow = false; + + this.useSizeDynamics = false; + this.useAlphaDynamics = false; + this.useFlowDynamics = false; + + this.usePreserveAlpha = false; + + this.step = 0.0; + + this.size = 1; + this.alpha = 1.0; + this.flow = 1.0; + + this.useSize = true; + this.useAlpha = true; + this.useFlow = true; + + this.noCursor = false; + + this.color = '#000000'; + this.rgb = [0, 0, 0]; + + this.brushSize = 0; + this.brushAlpha = 0.0; + this.brushFlow = 0.0; + this.stepSize = 0.0; + this.center = 0.0; + + this.sizeDynamicsEnabled = false; + this.alphaDynamicsEnabled = false; + this.flowDynamicsEnabled = false; + this.preserveAlphaEnabled = false; + + this.tip = -1; + this.tipList = null; + + this.stepAcc = 0; + + this.shapeCache = null; + + this.kernel = null; + } + + brushFn(x, y, offsetX, offsetY) {} + + start(posX, posY) {} + + commit() {} + + draw(posX, posY) {} + + usesDynamics() { + return this.useSizeDynamics || this.useAlphaDynamics || this.useFlowDynamics; + } + + enabledDynamics() { + return this.sizeDynamicsEnabled || this.alphaDynamicsEnabled || this.flowDynamicsEnabled; + } + + setSize(size) { + this.size = size; + } + + setAlpha(alpha) { + this.alpha = alpha; + this.brushAlpha = alpha; + } + + setFlow(flow) { + this.flow = flow; + this.brushFlow = this.easeFlow(flow); + } + + easeFlow(flow) { + return flow; + } + + setColor(hex) { + this.rgb = $T.hexToRgb(hex); + } + + setSizeDynamics(flag) { + if (!this.useSizeDynamics) { + return; + } + + if (!flag) { + this.setSize(this.size); + } + + this.sizeDynamicsEnabled = flag; + } + + setAlphaDynamics(flag) { + if (!this.useAlphaDynamics) { + return; + } + + if (!flag) { + this.setAlpha(this.alpha); + } + + this.alphaDynamicsEnabled = flag; + } + + setFlowDynamics(flag) { + if (!this.useFlowDynamics) { + return; + } + + if (!flag) { + this.setFlow(this.flow); + } + + this.flowDynamicsEnabled = flag; + } + + setPreserveAlpha(flag) { + this.preserveAlphaEnabled = flag; + } + + set() { + this.setAlpha(this.alpha); + this.setFlow(this.flow); + this.setSize(this.size); + this.setColor(Tegaki.toolColor); + + Tegaki.onToolChanged(this); + } +} +class TegakiBrush extends TegakiTool { + constructor() { + super(); + } + + generateShape(size) {} + + brushFn(x, y, offsetX, offsetY) { + var aData, gData, bData, aWidth, canvasWidth, canvasHeight, + kernel, xx, yy, ix, iy, + pa, ka, a, sa, + kr, kg, kb, + r, g, b, + pr, pg, pb, + px, ba, + brushSize, brushAlpha, brushFlow, preserveAlpha; + + preserveAlpha = this.preserveAlphaEnabled; + + kernel = this.kernel; + + brushAlpha = this.brushAlpha; + brushFlow = this.brushFlow; + brushSize = this.brushSize; + + aData = Tegaki.activeLayer.imageData.data; + gData = Tegaki.ghostBuffer.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + aWidth = canvasWidth; + + kr = this.rgb[0]; + kg = this.rgb[1]; + kb = this.rgb[2]; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; + + if (ka <= 0.0) { + continue; + } + + px = (iy * canvasWidth + ix) * 4; + + sa = bData[px + 3] / 255; + sa = sa + ka * brushFlow * (brushAlpha - sa); + + ba = Math.ceil(sa * 255); + + if (ba > bData[px + 3]) { + if (bData[px] === 0) { + gData[px] = aData[px]; + gData[px + 1] = aData[px + 1]; + gData[px + 2] = aData[px + 2]; + gData[px + 3] = aData[px + 3]; + } + + bData[px] = 1; + bData[px + 3] = ba; + + pr = gData[px]; + pg = gData[px + 1]; + pb = gData[px + 2]; + pa = gData[px + 3] / 255; + + a = pa + sa - pa * sa; + + r = ((kr * sa) + (pr * pa) * (1 - sa)) / a; + g = ((kg * sa) + (pg * pa) * (1 - sa)) / a; + b = ((kb * sa) + (pb * pa) * (1 - sa)) / a; + + aData[px] = (kr > pr) ? Math.ceil(r) : Math.floor(r); + aData[px + 1] = (kg > pg) ? Math.ceil(g) : Math.floor(g); + aData[px + 2] = (kb > pb) ? Math.ceil(b) : Math.floor(b); + + if (!preserveAlpha) { + aData[px + 3] = Math.ceil(a * 255); + } + } + } + } + } + + generateShapeCache(force) { + var i, shape; + + if (!this.shapeCache) { + this.shapeCache = new Array(Tegaki.maxSize); + } + + if (this.shapeCache[0] && !force) { + return; + } + + for (i = 0; i < Tegaki.maxSize; ++i) { + shape = this.generateShape(i + 1); + this.shapeCache[i] = shape; + this.setShape(shape); + } + } + + updateDynamics(t) { + var pressure, shape, val; + + pressure = TegakiPressure.lerp(t); + + if (this.sizeDynamicsEnabled) { + val = Math.ceil(pressure * this.size); + + if (val === 0) { + return false; + } + + shape = this.shapeCache[val - 1]; + + this.setShape(shape); + } + + if (this.alphaDynamicsEnabled) { + val = this.alpha * pressure; + + if (val <= 0) { + return false; + } + + this.brushAlpha = val; + } + + if (this.flowDynamicsEnabled) { + val = this.flow * pressure; + + if (val <= 0) { + return false; + } + + this.brushFlow = this.easeFlow(val); + } + + return true; + } + + start(posX, posY) { + var sampleX, sampleY; + + this.stepAcc = 0; + this.posX = posX; + this.posY = posY; + + if (this.enabledDynamics()) { + if (!this.updateDynamics(1.0)) { + return; + } + } + + sampleX = posX - this.center; + sampleY = posY - this.center; + + this.readImageData(sampleX, sampleY, this.brushSize, this.brushSize); + + this.brushFn(0, 0, sampleX, sampleY); + + this.writeImageData(sampleX, sampleY, this.brushSize, this.brushSize); + } + + commit() { + Tegaki.clearBuffers(); + } + + draw(posX, posY) { + var mx, my, fromX, fromY, sampleX, sampleY, dx, dy, err, derr, stepAcc, + lastX, lastY, distBase, shape, center, brushSize, t, tainted, w, h; + + stepAcc = this.stepAcc; + + fromX = this.posX; + fromY = this.posY; + + if (fromX < posX) { dx = posX - fromX; sampleX = fromX; mx = 1; } + else { dx = fromX - posX; sampleX = posX; mx = -1; } + + if (fromY < posY) { dy = posY - fromY; sampleY = fromY; my = 1; } + else { dy = fromY - posY; sampleY = posY; my = -1; } + + if (this.enabledDynamics()) { + distBase = Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)); + } + + if (this.sizeDynamicsEnabled) { + shape = this.shapeCache[this.size - 1]; + center = shape.center; + brushSize = shape.brushSize; + } + else { + center = this.center; + brushSize = this.brushSize; + } + + sampleX -= center; + sampleY -= center; + + w = dx + brushSize; + h = dy + brushSize; + + this.readImageData(sampleX, sampleY, w, h); + + err = (dx > dy ? dx : (dy !== 0 ? -dy : 0)) / 2; + + if (dx !== 0) { + dx = -dx; + } + + tainted = false; + + lastX = fromX; + lastY = fromY; + + while (true) { + stepAcc += Math.max(Math.abs(lastX - fromX), Math.abs(lastY - fromY)); + + lastX = fromX; + lastY = fromY; + + if (stepAcc >= this.stepSize) { + if (this.enabledDynamics()) { + if (distBase > 0) { + t = 1.0 - (Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)) / distBase); + } + else { + t = 0.0; + } + + if (this.updateDynamics(t)) { + this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); + tainted = true; + } + } + else { + this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); + tainted = true; + } + + stepAcc = 0; + } + + if (fromX === posX && fromY === posY) { + break; + } + + derr = err; + + if (derr > dx) { err -= dy; fromX += mx; } + if (derr < dy) { err -= dx; fromY += my; } + } + + this.stepAcc = stepAcc; + this.posX = posX; + this.posY = posY; + + if (tainted) { + this.writeImageData(sampleX, sampleY, w, h); + } + } + + writeImageData(x, y, w, h) { + Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0, x, y, w, h); + } + + readImageData(x, y, w, h) {} + + setShape(shape) { + this.center = shape.center; + this.stepSize = shape.stepSize; + this.brushSize = shape.brushSize; + this.kernel = shape.kernel; + } + + setSize(size) { + this.size = size; + + if (this.sizeDynamicsEnabled) { + this.generateShapeCache(); + } + else { + this.setShape(this.generateShape(size)); + } + } + + setSizeDynamics(flag) { + if (!this.useSizeDynamics) { + return; + } + + if (this.sizeDynamicsEnabled === flag) { + return; + } + + if (flag) { + this.generateShapeCache(); + } + else { + this.setShape(this.generateShape(this.size)); + } + + this.sizeDynamicsEnabled = flag; + } + + setTip(tipId) { + this.tipId = tipId; + + if (this.sizeDynamicsEnabled) { + this.generateShapeCache(true); + } + else { + this.setShape(this.generateShape(this.size)); + } + } +} +class TegakiPencil extends TegakiBrush { + constructor() { + super(); + + this.id = 1; + + this.name = 'pencil'; + + this.keybind = 'b'; + + this.step = 0.01; + + this.useFlow = false; + + this.size = 1; + this.alpha = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = true; + } + + generateShape(size) { + var e, x, y, imageData, data, c, color, r, rr; + + r = 0 | ((size) / 2); + + rr = 0 | ((size + 1) % 2); + + imageData = new ImageData(size, size); + + data = new Uint32Array(imageData.data.buffer); + + color = 0xFF000000; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + data[c + x - rr + (c + y - rr) * size] = color; + data[c + y - rr + (c + x - rr) * size] = color; + + data[c - y + (c + x - rr) * size] = color; + data[c - x + (c + y - rr) * size] = color; + + data[c - y + (c - x) * size] = color; + data[c - x + (c - y) * size] = color; + + data[c + y - rr + (c - x) * size] = color; + data[c + x - rr + (c - y) * size] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + if (r > 0) { + Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); + } + + return { + center: r, + stepSize: Math.ceil(size * this.step), + brushSize: size, + kernel: imageData.data, + }; + } +} +class TegakiAirbrush extends TegakiBrush { + constructor() { + super(); + + this.id = 3; + + this.name = 'airbrush'; + + this.keybind = 'a'; + + this.step = 0.1; + + this.size = 32; + this.alpha = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.useFlowDynamics = true; + + this.usePreserveAlpha = true; + } + + easeFlow(flow) { + return 1 - Math.sqrt(1 - flow); + } + + generateShape(size) { + var i, r, data, len, sqd, sqlen, hs, col, row, + ecol, erow, a; + + r = size; + size = size * 2; + + data = new ImageData(size, size).data; + + len = size * size * 4; + sqlen = Math.sqrt(r * r); + hs = Math.round(r); + col = row = -hs; + + i = 0; + while (i < len) { + if (col >= hs) { + col = -hs; + ++row; + continue; + } + + ecol = col; + erow = row; + + if (ecol < 0) { ecol = -ecol; } + if (erow < 0) { erow = -erow; } + + sqd = Math.sqrt(ecol * ecol + erow * erow); + + if (sqd >= sqlen) { + a = 0; + } + else if (sqd === 0) { + a = 255; + } + else { + a = (sqd / sqlen) + 0.1; + + if (a > 1.0) { + a = 1.0; + } + + a = (1 - (Math.exp(1 - 1 / a) / a)) * 255; + } + + data[i + 3] = a; + + i += 4; + + ++col; + } + + return { + center: r, + stepSize: Math.ceil(size * this.step), + brushSize: size, + kernel: data, + }; + } +} +class TegakiPen extends TegakiBrush { + constructor() { + super(); + + this.id = 2; + + this.name = 'pen'; + + this.keybind = 'p'; + + this.step = 0.05; + + this.size = 8; + this.alpha = 1.0; + this.flow = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.useFlowDynamics = true; + + this.usePreserveAlpha = true; + } + + easeFlow(flow) { + return 1 - Math.sqrt(1 - Math.pow(flow, 3)); + } + + generateShape(size) { + var e, x, y, imageData, data, c, color, r, rr, + f, ff, bSize, bData, i, ii, xx, yy, center, brushSize; + + center = Math.floor(size / 2) + 1; + + brushSize = size + 2; + + f = 4; + + ff = f * f; + + bSize = brushSize * f; + + r = Math.floor(bSize / 2); + + rr = Math.floor((bSize + 1) % 2); + + imageData = new ImageData(bSize, bSize); + bData = new Uint32Array(imageData.data.buffer); + + color = 0x55000000; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + bData[c + x - rr + (c + y - rr) * bSize] = color; + bData[c + y - rr + (c + x - rr) * bSize] = color; + + bData[c - y + (c + x - rr) * bSize] = color; + bData[c - x + (c + y - rr) * bSize] = color; + + bData[c - y + (c - x) * bSize] = color; + bData[c - x + (c - y) * bSize] = color; + + bData[c + y - rr + (c - x) * bSize] = color; + bData[c + x - rr + (c - y) * bSize] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + color = 0xFF000000; + + x = r - 3; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + bData[c + x - rr + (c + y - rr) * bSize] = color; + bData[c + y - rr + (c + x - rr) * bSize] = color; + + bData[c - y + (c + x - rr) * bSize] = color; + bData[c - x + (c + y - rr) * bSize] = color; + + bData[c - y + (c - x) * bSize] = color; + bData[c - x + (c - y) * bSize] = color; + + bData[c + y - rr + (c - x) * bSize] = color; + bData[c + x - rr + (c - y) * bSize] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + if (r > 0) { + Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); + } + + bData = imageData.data; + data = new ImageData(brushSize, brushSize).data; + + for (x = 0; x < brushSize; ++x) { + for (y = 0; y < brushSize; ++y) { + i = (y * brushSize + x) * 4 + 3; + + color = 0; + + for (xx = 0; xx < f; ++xx) { + for (yy = 0; yy < f; ++yy) { + ii = ((yy + y * f) * bSize + (xx + x * f)) * 4 + 3; + color += bData[ii]; + } + } + + data[i] = color / ff; + } + } + + return { + center: center, + stepSize: Math.ceil(size * this.step), + brushSize: brushSize, + kernel: data, + }; + } +} +class TegakiBucket extends TegakiTool { + constructor() { + super(); + + this.id = 4; + + this.name = 'bucket'; + + this.keybind = 'g'; + + this.step = 100.0; + + this.useSize = false; + this.useFlow = false; + + this.noCursor = true; + } + + fill(imageData, x, y, color, alpha) { + var r, g, b, px, tr, tg, tb, ta, q, pxMap, yy, xx, yn, ys, + yyy, yyn, yys, xd, data, w, h; + + w = imageData.width; + h = imageData.height; + + r = color[0]; + g = color[1]; + b = color[2]; + + px = (y * w + x) * 4; + + data = imageData.data; + + tr = data[px]; + tg = data[px + 1]; + tb = data[px + 2]; + ta = data[px + 3]; + + pxMap = new Uint8Array(w * h * 4); + + q = []; + + q[0] = x; + q[1] = y; + + while (q.length) { + yy = q.pop(); + xx = q.pop(); + + yn = (yy - 1); + ys = (yy + 1); + + yyy = yy * w; + yyn = yn * w; + yys = ys * w; + + xd = xx; + + while (xd >= 0) { + px = (yyy + xd) * 4; + + if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + break; + } + + this.blendPixel(data, px, r, g, b, alpha); + + pxMap[px] = 1; + + if (yn >= 0) { + px = (yyn + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(yn); + } + } + + if (ys < h) { + px = (yys + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(ys); + } + } + + xd--; + } + + xd = xx + 1; + + while (xd < w) { + px = (yyy + xd) * 4; + + if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + break; + } + + this.blendPixel(data, px, r, g, b, alpha); + + pxMap[px] = 1; + + if (yn >= 0) { + px = (yyn + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(yn); + } + } + + if (ys < h) { + px = (yys + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(ys); + } + } + + ++xd; + } + } + } + + brushFn(x, y) { + if (x < 0 || y < 0 || x >= Tegaki.baseWidth || y >= Tegaki.baseHeight) { + return; + } + + this.fill(Tegaki.activeLayer.imageData, x, y, this.rgb, this.alpha); + + // TODO: write back only the tainted rect + Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0); + } + + blendPixel(data, px, r, g, b, a) { + var sr, sg, sb, sa, dr, dg, db, da; + + sr = data[px]; + sg = data[px + 1]; + sb = data[px + 2]; + sa = data[px + 3] / 255; + + da = sa + a - sa * a; + + dr = ((r * a) + (sr * sa) * (1 - a)) / da; + dg = ((g * a) + (sg * sa) * (1 - a)) / da; + db = ((b * a) + (sb * sa) * (1 - a)) / da; + + data[px] = (r > sr) ? Math.ceil(dr) : Math.floor(dr); + data[px + 1] = (g > sg) ? Math.ceil(dg) : Math.floor(dg); + data[px + 2] = (b > sb) ? Math.ceil(db) : Math.floor(db); + data[px + 3] = Math.ceil(da * 255); + } + + testPixel(data, px, pxMap, tr, tg, tb, ta) { + return !pxMap[px] && (data[px] == tr + && data[++px] == tg + && data[++px] == tb + && data[++px] == ta) + ; + } + + start(x, y) { + this.brushFn(x, y); + } + + draw(x, y) { + this.brushFn(x, y); + } + + setSize(size) {} +} +class TegakiTone extends TegakiPencil { + constructor() { + super(); + + this.id = 5; + + this.name = 'tone'; + + this.keybind = 't'; + + this.step = 0.01; + + this.useFlow = false; + + this.size = 8; + this.alpha = 0.5; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = true; + + this.matrix = [ + [0, 8, 2, 10], + [12, 4, 14, 6], + [3, 11, 1 ,9], + [15, 7, 13, 5] + ]; + + this.mapCache = null; + this.mapWidth = 0; + this.mapHeight = 0; + } + + start(x, y) { + if (this.mapWidth !== Tegaki.baseWidth || this.mapHeight !== Tegaki.baseHeight) { + this.generateMapCache(true); + } + + super.start(x, y); + } + + brushFn(x, y, offsetX, offsetY) { + var data, kernel, brushSize, map, idx, preserveAlpha, + px, mx, mapWidth, xx, yy, ix, iy, canvasWidth, canvasHeight; + + data = Tegaki.activeLayer.imageData.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + kernel = this.kernel; + + brushSize = this.brushSize; + + mapWidth = this.mapWidth; + + preserveAlpha = this.preserveAlphaEnabled; + + idx = Math.round(this.brushAlpha * 16) - 1; + + if (idx < 0) { + return; + } + + map = this.mapCache[idx]; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + if (kernel[(yy * brushSize + xx) * 4 + 3] === 0) { + continue; + } + + mx = iy * canvasWidth + ix; + px = mx * 4; + + if (map[mx] === 0) { + data[px] = this.rgb[0]; + data[px + 1] = this.rgb[1]; + data[px + 2] = this.rgb[2]; + + if (!preserveAlpha) { + data[px + 3] = 255; + } + } + } + } + } + + generateMap(w, h, idx) { + var data, x, y; + + data = new Uint8Array(w * h); + + for (y = 0; y < h; ++y) { + for (x = 0; x < w; ++x) { + if (idx < this.matrix[y % 4][x % 4]) { + data[w * y + x] = 1; + } + } + } + + return data; + } + + generateMapCache(force) { + var i, cacheSize; + + cacheSize = this.matrix.length * this.matrix[0].length; + + if (!this.mapCache) { + this.mapCache = new Array(cacheSize); + } + + if (!force && this.mapCache[0] + && this.mapWidth === Tegaki.baseWidth + && this.mapHeight === Tegaki.baseHeight) { + return; + } + + this.mapWidth = Tegaki.baseWidth; + this.mapHeight = Tegaki.baseHeight; + + for (i = 0; i < cacheSize; ++i) { + this.mapCache[i] = this.generateMap(this.mapWidth, this.mapHeight, i); + } + } + + setAlpha(alpha) { + super.setAlpha(alpha); + this.generateMapCache(); + } +} +class TegakiPipette extends TegakiTool { + constructor() { + super(); + + this.id = 6; + + this.name = 'pipette'; + + this.keybind = 'i'; + + this.step = 100.0; + + this.useSize = false; + this.useAlpha = false; + this.useFlow = false; + + this.noCursor = true; + } + + start(posX, posY) { + this.draw(posX, posY); + } + + draw(posX, posY) { + var c, ctx; + + if (true) { + ctx = Tegaki.flatten().getContext('2d'); + } + else { + ctx = Tegaki.activeLayer.ctx; + } + + c = $T.getColorAt(ctx, posX, posY); + + Tegaki.setToolColor(c); + } + + set() { + Tegaki.onToolChanged(this); + } + + commit() {} + + setSize() {} + + setAlpha() {} +} +class TegakiBlur extends TegakiBrush { + constructor() { + super(); + + this.id = 7; + + this.name = 'blur'; + + this.step = 0.25; + + this.useFlow = false; + + this.size = 32; + this.alpha = 0.5; + + this.useAlphaDynamics = true; + this.usePreserveAlpha = false; + } + + writeImageData(x, y, w, h) { + var xx, yy, ix, iy, px, canvasWidth, aData, bData; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + + for (xx = 0; xx < w; ++xx) { + ix = x + xx; + + for (yy = 0; yy < h; ++yy) { + iy = y + yy; + + px = (iy * canvasWidth + ix) * 4; + + aData[px] = bData[px]; + aData[px + 1] = bData[px + 1]; + aData[px + 2] = bData[px + 2]; + aData[px + 3] = bData[px + 3]; + } + } + + super.writeImageData(x, y, w, h); + } + + readImageData(x, y, w, h) { + var xx, yy, ix, iy, px, canvasWidth, aData, bData; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + + for (xx = 0; xx < w; ++xx) { + ix = x + xx; + + for (yy = 0; yy < h; ++yy) { + iy = y + yy; + + px = (iy * canvasWidth + ix) * 4; + + bData[px] = aData[px]; + bData[px + 1] = aData[px + 1]; + bData[px + 2] = aData[px + 2]; + bData[px + 3] = aData[px + 3]; + } + } + } + + brushFn(x, y, offsetX, offsetY) { + var i, j, size, aData, bData, limX, limY, + kernel, alpha, alpha0, ix, iy, canvasWidth, canvasHeight, + sx, sy, r, g, b, a, kx, ky, px, pa, acc, aa; + + alpha0 = this.brushAlpha; + alpha = alpha0 * alpha0 * alpha0; + + if (alpha <= 0.0) { + return; + } + + size = this.brushSize; + + kernel = this.kernel; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + limX = canvasWidth - 1; + limY = canvasHeight - 1; + + for (sx = 0; sx < size; ++sx) { + ix = x + sx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + for (sy = 0; sy < size; ++sy) { + iy = y + sy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + i = (sy * size + sx) * 4; + + px = (iy * canvasWidth + ix) * 4; + + if (kernel[i + 3] === 0 || ix <= 0 || iy <= 0 || ix >= limX || iy >= limY) { + continue; + } + + r = g = b = a = acc = 0; + + for (kx = -1; kx < 2; ++kx) { + for (ky = -1; ky < 2; ++ky) { + j = ((iy - ky) * canvasWidth + (ix - kx)) * 4; + + pa = aData[j + 3]; + + if (kx === 0 && ky === 0) { + aa = pa * alpha0; + acc += alpha0; + } + else { + aa = pa * alpha; + acc += alpha; + } + + r = r + aData[j] * aa; ++j; + g = g + aData[j] * aa; ++j; + b = b + aData[j] * aa; + a = a + aa; + } + } + + a = a / acc; + + if (a <= 0.0) { + continue; + } + + bData[px] = Math.round((r / acc) / a); + bData[px + 1] = Math.round((g / acc) / a); + bData[px + 2] = Math.round((b / acc) / a); + bData[px + 3] = Math.round(a); + } + } + } +} + +TegakiBlur.prototype.generateShape = TegakiPencil.prototype.generateShape; +class TegakiEraser extends TegakiBrush { + constructor() { + super(); + + this.id = 8; + + this.name = 'eraser'; + + this.keybind = 'e'; + + this.step = 0.1; + + this.size = 8; + this.alpha = 1.0; + + this.useFlow = false; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = false; + + this.tipId = 0; + this.tipList = [ 'pencil', 'pen', 'airbrush' ]; + } + + brushFn(x, y, offsetX, offsetY) { + var aData, bData, gData, kernel, canvasWidth, canvasHeight, + ka, ba, px, xx, yy, ix, iy, + brushSize, brushAlpha; + + brushAlpha = this.brushAlpha; + brushSize = this.brushSize; + + kernel = this.kernel; + + aData = Tegaki.activeLayer.imageData.data; + gData = Tegaki.ghostBuffer.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; + + px = (iy * canvasWidth + ix) * 4 + 3; + + if (gData[px] === 0) { + gData[px] = aData[px]; + } + + ba = bData[px] / 255; + ba = ba + ka * (brushAlpha - ba); + + bData[px] = Math.floor(ba * 255); + aData[px] = Math.floor(gData[px] * (1 - ba)); + } + } + } + + generateShape(size) { + if (this.tipId === 0) { + return this.generateShapePencil(size); + } + else if (this.tipId === 1) { + return this.generateShapePen(size); + } + else { + return this.generateShapeAirbrush(size); + } + } +} + +TegakiEraser.prototype.generateShapePencil = TegakiPencil.prototype.generateShape; +TegakiEraser.prototype.generateShapePen = TegakiPen.prototype.generateShape; +TegakiEraser.prototype.generateShapeAirbrush = TegakiAirbrush.prototype.generateShape; +var $T = { + docEl: document.documentElement, + + id: function(id) { + return document.getElementById(id); + }, + + cls: function(klass, root) { + return (root || document).getElementsByClassName(klass); + }, + + on: function(o, e, h) { + o.addEventListener(e, h, false); + }, + + off: function(o, e, h) { + o.removeEventListener(e, h, false); + }, + + el: function(name) { + return document.createElement(name); + }, + + frag: function() { + return document.createDocumentFragment(); + }, + + copyImageData(imageData) { + return new ImageData( + new Uint8ClampedArray(imageData.data), + imageData.width + ); + }, + + copyCanvas: function(source, clone) { + var canvas; + + if (!clone) { + canvas = $T.el('canvas'); + canvas.width = source.width; + canvas.height = source.height; + } + else { + canvas = source.cloneNode(false); + } + + canvas.getContext('2d').drawImage(source, 0, 0); + + return canvas; + }, + + clearCtx: function(ctx) { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + }, + + hexToRgb: function(hex) { + var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i); + + if (c) { + return [ + parseInt(c[1], 16), + parseInt(c[2], 16), + parseInt(c[3], 16) + ]; + } + + return null; + }, + + RgbToHex: function(r, g, b) { + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + }, + + getColorAt: function(ctx, posX, posY) { + var rgba = ctx.getImageData(posX, posY, 1, 1).data; + + return '#' + + ('0' + rgba[0].toString(16)).slice(-2) + + ('0' + rgba[1].toString(16)).slice(-2) + + ('0' + rgba[2].toString(16)).slice(-2); + }, + + generateFilename: function() { + return 'tegaki_' + (new Date()).toISOString().split('.')[0].replace(/[^0-9]/g, '_'); + }, + + sortAscCb: function(a, b) { + if (a > b) { return 1; } + if (a < b) { return -1; } + return 0; + }, + + sortDescCb: function(a, b) { + if (a > b) { return -1; } + if (a < b) { return 1; } + return 0; + }, + + msToHms: function(ms) { + var h, m, s, ary; + + s = 0 | (ms / 1000); + h = 0 | (s / 3600); + m = 0 | ((s - h * 3600) / 60); + s = s - h * 3600 - m * 60; + + ary = []; + + if (h) { + ary.push(h < 10 ? ('0' + h) : h); + } + + if (m) { + ary.push(m < 10 ? ('0' + m) : m); + } + else { + ary.push('00'); + } + + if (s) { + ary.push(s < 10 ? ('0' + s) : s); + } + else { + ary.push('00'); + } + + return ary.join(':'); + }, + + calcThumbSize(w, h, maxSide) { + var r; + + if (w > maxSide) { + r = maxSide / w; + w = maxSide; + h = h * r; + } + + if (h > maxSide) { + r = maxSide / h; + h = maxSide; + w = w * r; + } + + return [Math.ceil(w), Math.ceil(h)]; + } +}; +class TegakiBinReader { + constructor(buf) { + this.pos = 0; + this.view = new DataView(buf); + this.buf = buf; + } + + readInt8() { + var data = this.view.getInt8(this.pos); + this.pos += 1; + return data; + } + + readUint8() { + var data = this.view.getUint8(this.pos); + this.pos += 1; + return data; + } + + readInt16() { + var data = this.view.getInt16(this.pos); + this.pos += 2; + return data; + } + + readUint16() { + var data = this.view.getUint16(this.pos); + this.pos += 2; + return data; + } + + readUint32() { + var data = this.view.getUint32(this.pos); + this.pos += 4; + return data; + } + + readFloat32() { + var data = this.view.getFloat32(this.pos); + this.pos += 4; + return data; + } +} + +class TegakiBinWriter { + constructor(buf) { + this.pos = 0; + this.view = new DataView(buf); + this.buf = buf; + } + + writeInt8(val) { + this.view.setInt8(this.pos, val); + this.pos += 1; + } + + writeUint8(val) { + this.view.setUint8(this.pos, val); + this.pos += 1; + } + + writeInt16(val) { + this.view.setInt16(this.pos, val); + this.pos += 2; + } + + writeUint16(val) { + this.view.setUint16(this.pos, val); + this.pos += 2; + } + + writeUint32(val) { + this.view.setUint32(this.pos, val); + this.pos += 4; + } + + writeFloat32(val) { + this.view.setFloat32(this.pos, val); + this.pos += 4; + } +} +var TegakiCursor = { + size: 0, + radius: 0, + + points: null, + + cursorCtx: null, + + offsetX: 0, + offsetY: 0, + + lastX: 0, + lastY: 0, + lastSize: 0, + + init: function(w, h) { + var el; + + el = $T.el('canvas'); + el.id = 'tegaki-cursor-layer'; + [ el.width, el.height ] = TegakiCursor.getMaxCanvasSize() + + Tegaki.canvasCnt.appendChild(el); + + this.offsetX = el.offsetLeft; + this.offsetY = el.offsetTop; + + this.cursorCtx = el.getContext('2d'); + }, + + getCanvas: function() { + if (this.cursorCtx) { + return this.cursorCtx.canvas; + } + else { + return null; + } + }, + + getMaxCanvasSize: function() { + let w = Tegaki.canvasCnt.offsetWidth; + let h = Tegaki.canvasCnt.offsetHeight; + let [ sbwh, scbv ] = Tegaki.getScrollbarSizes(); + return [ w - sbwh, h - scbv ]; + }, + + updateCanvasSize: function() { + let canvas = this.cursorCtx.canvas; + + let [w, h] = this.getMaxCanvasSize(); + + if (w !== canvas.width || h !== canvas.height) { + canvas.width = w; + canvas.height = h; + + this.offsetX = canvas.offsetLeft; + this.offsetY = canvas.offsetTop; + } + }, + + render: function(rawX, rawY) { + var x, y, i, destImg, destData; + + x = rawX - this.offsetX - this.radius; + y = rawY - this.offsetY - this.radius; + + this.clear(); + + this.lastX = x; + this.lastY = y; + this.lastSize = this.size; + + destImg = this.cursorCtx.createImageData(this.size, this.size); + destData = new Uint32Array(destImg.data.buffer); + + for (i = 0; i < this.points.length; ++i) { + destData[this.points[i]] = 0xFFFFFF7F; + } + + this.cursorCtx.putImageData(destImg, x, y); + }, + + clear: function() { + this.cursorCtx.clearRect(this.lastX, this.lastY, this.lastSize, this.lastSize); + }, + + clearAll: function() { + var canvas = this.cursorCtx.canvas; + this.cursorCtx.clearRect(0, 0, canvas.width, canvas.height); + }, + + destroy() { + this.size = 0; + this.radius = 0; + this.offsetX = 0; + this.offsetY = 0; + this.lastX = 0; + this.lastY = 0; + this.lastSize = 0; + this.points = null; + this.cursorCtx = null; + }, + + generate: function(size) { + var e, x, y, c, r, rr, points; + + size = 0 | (size * Tegaki.zoomFactor); + + if (size < 2) { + return false; + } + + r = 0 | ((size) / 2); + + rr = 0 | ((size + 1) % 2); + + points = []; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + points.push(c + x - rr + (c + y - rr) * size); + points.push(c + y - rr + (c + x - rr) * size); + + points.push(c - y + (c + x - rr) * size); + points.push(c - x + (c + y - rr) * size); + + points.push(c - y + (c - x) * size); + points.push(c - x + (c - y) * size); + + points.push(c + y - rr + (c - x) * size); + points.push(c + x - rr + (c - y) * size); + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + this.size = size; + this.radius = r; + this.points = points; + + return true; + } +}; +var TegakiHistory = { + maxSize: 50, + + undoStack: [], + redoStack: [], + + pendingAction: null, + + clear: function() { + this.undoStack = []; + this.redoStack = []; + this.pendingAction = null; + + this.onChange(0); + }, + + push: function(action) { + if (!action) { + return; + } + + if (action.coalesce) { + if (this.undoStack[this.undoStack.length - 1] instanceof action.constructor) { + if (this.undoStack[this.undoStack.length - 1].coalesce(action)) { + return; + } + } + } + + this.undoStack.push(action); + + if (this.undoStack.length > this.maxSize) { + this.undoStack.shift(); + } + + if (this.redoStack.length > 0) { + this.redoStack = []; + } + + this.onChange(0); + }, + + undo: function() { + var action; + + if (!this.undoStack.length) { + return; + } + + action = this.undoStack.pop(); + + action.undo(); + + this.redoStack.push(action); + + this.onChange(-1); + }, + + redo: function() { + var action; + + if (!this.redoStack.length) { + return; + } + + action = this.redoStack.pop(); + + action.redo(); + + this.undoStack.push(action); + + this.onChange(1); + }, + + onChange: function(type) { + Tegaki.onHistoryChange(this.undoStack.length, this.redoStack.length, type); + }, + + sortPosLayerCallback: function(a, b) { + if (a[0] > b[0]) { return 1; } + if (a[0] < b[0]) { return -1; } + return 0; + } +}; + +var TegakiHistoryActions = { + Dummy: function() { + this.undo = function() {}; + this.redo = function() {}; + }, + + Draw: function(layerId) { + this.coalesce = false; + + this.imageDataBefore = null; + this.imageDataAfter = null; + this.layerId = layerId; + }, + + DeleteLayers: function(layerPosMap, params) { + var item; + + this.coalesce = false; + + this.layerPosMap = []; + + for (item of layerPosMap.sort(TegakiHistory.sortPosLayerCallback)) { + item[1] = TegakiLayers.cloneLayer(item[1]); + this.layerPosMap.push(item); + } + + this.tgtLayerId = null; + + this.aLayerIdBefore = null; + this.aLayerIdAfter = null; + + this.imageDataBefore = null; + this.imageDataAfter = null; + + this.mergeDown = false; + + if (params) { + for (let k in params) { + this[k] = params[k]; + } + } + }, + + AddLayer: function(params, aLayerIdBefore, aLayerIdAfter) { + this.coalesce = false; + + this.layer = params; + this.layerId = params.id; + this.aLayerIdBefore = aLayerIdBefore; + this.aLayerIdAfter = aLayerIdAfter; + }, + + MoveLayers: function(layers, belowPos, activeLayerId) { + this.coalesce = false; + + this.layers = layers; + this.belowPos = belowPos; + this.aLayerId = activeLayerId; + }, + + SetLayersAlpha: function(layerAlphas, newAlpha) { + this.layerAlphas = layerAlphas; + this.newAlpha = newAlpha; + }, + + SetLayerName: function(id, oldName, newName) { + this.layerId = id; + this.oldName = oldName; + this.newName = newName; + } +}; + +// --- + +TegakiHistoryActions.Draw.prototype.addCanvasState = function(imageData, type) { + if (type) { + this.imageDataAfter = $T.copyImageData(imageData); + } + else { + this.imageDataBefore = $T.copyImageData(imageData); + } +}; + +TegakiHistoryActions.Draw.prototype.exec = function(type) { + var layer = TegakiLayers.getLayerById(this.layerId); + + if (type) { + layer.ctx.putImageData(this.imageDataAfter, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataAfter); + } + else { + layer.ctx.putImageData(this.imageDataBefore, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); + } + + TegakiUI.updateLayerPreview(layer); + TegakiLayers.setActiveLayer(this.layerId); +}; + +TegakiHistoryActions.Draw.prototype.undo = function() { + this.exec(0); +}; + +TegakiHistoryActions.Draw.prototype.redo = function() { + this.exec(1); +}; + +TegakiHistoryActions.DeleteLayers.prototype.undo = function() { + var i, lim, refLayer, layer, pos, refId; + + for (i = 0, lim = this.layerPosMap.length; i < lim; ++i) { + [pos, layer] = this.layerPosMap[i]; + + layer = TegakiLayers.cloneLayer(layer); + + refLayer = Tegaki.layers[pos]; + + if (refLayer) { + if (refId = TegakiLayers.getLayerBelowId(refLayer.id)) { + refId = refId.id; + } + + TegakiUI.updateLayersGridAdd(layer, refId); + TegakiUI.updateLayerPreview(layer); + Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.canvas); + Tegaki.layers.splice(pos, 0, layer); + } + else { + + if (!Tegaki.layers[0]) { + refLayer = Tegaki.layersCnt.children[0]; + } + else { + refLayer = Tegaki.layers[Tegaki.layers.length - 1].canvas; + } + + TegakiUI.updateLayersGridAdd(layer, TegakiLayers.getTopLayerId()); + TegakiUI.updateLayerPreview(layer); + Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.nextElementSibling); + Tegaki.layers.push(layer); + } + } + + if (this.tgtLayerId) { + layer = TegakiLayers.getLayerById(this.tgtLayerId); + layer.ctx.putImageData(this.imageDataBefore, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); + TegakiLayers.setLayerAlpha(layer, this.tgtLayerAlpha); + TegakiUI.updateLayerPreview(layer); + } + + TegakiLayers.setActiveLayer(this.aLayerIdBefore); +}; + +TegakiHistoryActions.DeleteLayers.prototype.redo = function() { + var layer, ids = []; + + for (layer of this.layerPosMap) { + ids.unshift(layer[1].id); + } + + if (this.tgtLayerId) { + if (!this.mergeDown) { + ids.unshift(this.tgtLayerId); + } + TegakiLayers.mergeLayers(new Set(ids)); + } + else { + TegakiLayers.deleteLayers(ids); + } + + TegakiLayers.setActiveLayer(this.aLayerIdAfter); +}; + +TegakiHistoryActions.MoveLayers.prototype.undo = function() { + var i, layer, stack, ref, posMap, len; + + stack = new Array(Tegaki.layers.length); + + posMap = {}; + + for (layer of this.layers) { + posMap[layer[1].id] = layer[0]; + } + + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + layer = Tegaki.layers[i]; + + if (posMap[layer.id] !== undefined) { + Tegaki.layers.splice(i, 1); + Tegaki.layers.splice(posMap[layer.id], 0, layer); + } + } + + TegakiUI.updayeLayersGridOrder(); + + ref = Tegaki.layersCnt.children[0]; + + for (i = Tegaki.layers.length - 1; i >= 0; i--) { + layer = Tegaki.layers[i]; + Tegaki.layersCnt.insertBefore(layer.canvas, ref.nextElementSibling); + } + + TegakiLayers.setActiveLayer(this.aLayerId); +}; + +TegakiHistoryActions.MoveLayers.prototype.redo = function() { + var layer, layers = new Set(); + + for (layer of this.layers.slice().reverse()) { + layers.add(layer[1].id); + } + + TegakiLayers.setActiveLayer(this.aLayerId); + TegakiLayers.moveLayers(layers, this.belowPos); +}; + +TegakiHistoryActions.AddLayer.prototype.undo = function() { + TegakiLayers.deleteLayers([this.layer.id]); + TegakiLayers.setActiveLayer(this.aLayerIdBefore); + Tegaki.layerCounter--; +}; + +TegakiHistoryActions.AddLayer.prototype.redo = function() { + TegakiLayers.setActiveLayer(this.aLayerIdBefore); + TegakiLayers.addLayer(this.layer); + TegakiLayers.setActiveLayer(this.aLayerIdAfter); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.undo = function() { + var id, layerAlpha, layer; + + for (layerAlpha of this.layerAlphas) { + [id, layerAlpha] = layerAlpha; + + if (layer = TegakiLayers.getLayerById(id)) { + TegakiLayers.setLayerAlpha(layer, layerAlpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.redo = function() { + var id, layerAlpha, layer; + + for (layerAlpha of this.layerAlphas) { + [id, layerAlpha] = layerAlpha; + + if (layer = TegakiLayers.getLayerById(id)) { + TegakiLayers.setLayerAlpha(layer, this.newAlpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.coalesce = function(action) { + var i; + + if (this.layerAlphas.length !== action.layerAlphas.length) { + return false; + } + + for (i = 0; i < this.layerAlphas.length; ++i) { + if (this.layerAlphas[i][0] !== action.layerAlphas[i][0]) { + return false; + } + } + + this.newAlpha = action.newAlpha; + + return true; +}; + +TegakiHistoryActions.SetLayerName.prototype.exec = function(type) { + var layer = TegakiLayers.getLayerById(this.layerId); + + if (layer) { + layer.name = type ? this.newName : this.oldName; + TegakiUI.updateLayerName(layer); + } +}; + +TegakiHistoryActions.SetLayerName.prototype.undo = function() { + this.exec(0); +}; + +TegakiHistoryActions.SetLayerName.prototype.redo = function() { + this.exec(1); +}; +var TegakiKeybinds = { + keyMap: {}, + + captionMap: {}, + + clear: function() { + this.keyMap = {}; + this.captionMap = {}; + }, + + bind: function(keys, klass, fn, id, caption) { + this.keyMap[keys] = [klass, fn]; + + if (id) { + this.captionMap[id] = caption; + } + }, + + getCaption(id) { + return this.captionMap[id]; + }, + + resolve: function(e) { + var fn, mods, keys, el; + + el = e.target; + + if (el.nodeName == 'INPUT' && (el.type === 'text' || el.type === 'number')) { + return; + } + + mods = []; + + if (e.ctrlKey) { + mods.push('ctrl'); + } + + if (e.shiftKey) { + mods.push('shift'); + } + + keys = e.key.toLowerCase(); + + if (mods[0]) { + keys = mods.join('+') + '+' + keys; + } + + fn = TegakiKeybinds.keyMap[keys]; + + if (fn && !e.altKey && !e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + fn[0][fn[1]](); + } + }, +}; +var TegakiLayers = { + cloneLayer: function(layer) { + var newLayer = Object.assign({}, layer); + + newLayer.canvas = $T.copyCanvas(layer.canvas, true); + newLayer.ctx = newLayer.canvas.getContext('2d'); + newLayer.imageData = $T.copyImageData(layer.imageData); + + return newLayer; + }, + + getCanvasById: function(id) { + return $T.id('tegaki-canvas-' + id); + }, + + getActiveLayer: function() { + return Tegaki.activeLayer; + }, + + getLayerPosById: function(id) { + var i, layers = Tegaki.layers; + + for (i = 0; i < layers.length; ++i) { + if (layers[i].id === id) { + return i; + } + } + + return -1; + }, + + getTopFencedLayerId: function() { + var i, id, layer, layers = Tegaki.layers; + + for (i = layers.length - 1; i >= 0; i--) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + for (i = i - 1; i >= 0; i--) { + if (!TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + if (layer = layers[i]) { + id = layer.id; + } + else { + id = 0; + } + + return id; + }, + + getSelectedEdgeLayerPos: function(top) { + var i, layers = Tegaki.layers; + + if (top) { + for (i = Tegaki.layers.length - 1; i >= 0; i--) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + } + else { + for (i = 0; i < layers.length; ++i) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + if (i >= layers.length) { + i = -1; + } + } + + return i; + }, + + getTopLayer: function() { + return Tegaki.layers[Tegaki.layers.length - 1]; + }, + + getTopLayerId: function() { + var layer = TegakiLayers.getTopLayer(); + + if (layer) { + return layer.id; + } + else { + return 0; + } + }, + + getLayerBelowId: function(belowId) { + var idx; + + idx = TegakiLayers.getLayerPosById(belowId); + + if (idx < 1) { + return null; + } + + return Tegaki.layers[idx - 1]; + }, + + getLayerById: function(id) { + return Tegaki.layers[TegakiLayers.getLayerPosById(id)]; + }, + + isSameLayerOrder: function(a, b) { + var i, al; + + if (a.length !== b.length) { + return false; + } + + for (i = 0; al = a[i]; ++i) { + if (al.id !== b[i].id) { + return false; + } + } + + return true; + }, + + addLayer: function(baseLayer = {}) { + var id, canvas, k, params, layer, afterNode, afterPos, + aLayerIdBefore, ctx; + + if (Tegaki.activeLayer) { + aLayerIdBefore = Tegaki.activeLayer.id; + afterPos = TegakiLayers.getLayerPosById(Tegaki.activeLayer.id); + afterNode = $T.cls('tegaki-layer', Tegaki.layersCnt)[afterPos]; + } + else { + afterPos = -1; + afterNode = null; + } + + if (!afterNode) { + afterNode = Tegaki.layersCnt.firstElementChild; + } + + canvas = $T.el('canvas'); + canvas.className = 'tegaki-layer'; + canvas.width = Tegaki.baseWidth; + canvas.height = Tegaki.baseHeight; + + id = ++Tegaki.layerCounter; + + canvas.id = 'tegaki-canvas-' + id; + canvas.setAttribute('data-id', id); + + params = { + name: TegakiStrings.layer + ' ' + id, + visible: true, + alpha: 1.0, + }; + + ctx = canvas.getContext('2d'); + + layer = { + id: id, + canvas: canvas, + ctx: ctx, + imageData: ctx.getImageData(0, 0, canvas.width, canvas.height) + }; + + for (k in params) { + if (baseLayer[k] !== undefined) { + params[k] = baseLayer[k]; + } + + layer[k] = params[k]; + } + + Tegaki.layers.splice(afterPos + 1, 0, layer); + + TegakiUI.updateLayersGridAdd(layer, aLayerIdBefore); + + Tegaki.layersCnt.insertBefore(canvas, afterNode.nextElementSibling); + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.AddLayer(layer, aLayerIdBefore, id); + }, + + deleteLayers: function(ids, extraParams) { + var id, idx, layer, layers, delIndexes, params; + + params = { + aLayerIdBefore: Tegaki.activeLayer ? Tegaki.activeLayer.id : -1, + aLayerIdAfter: TegakiLayers.getTopFencedLayerId() + }; + + layers = []; + + delIndexes = []; + + for (id of ids) { + idx = TegakiLayers.getLayerPosById(id); + layer = Tegaki.layers[idx]; + + layers.push([idx, layer]); + + Tegaki.layersCnt.removeChild(layer.canvas); + + delIndexes.push(idx); + + TegakiUI.updateLayersGridRemove(id); + + TegakiUI.deleteLayerPreviewCtx(layer); + } + + delIndexes = delIndexes.sort($T.sortDescCb); + + for (idx of delIndexes) { + Tegaki.layers.splice(idx, 1); + } + + if (extraParams) { + Object.assign(params, extraParams); + } + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.DeleteLayers(layers, params); + }, + + mergeLayers: function(idSet) { + var canvas, ctx, imageDataAfter, imageDataBefore, + targetLayer, action, layer, layers, delIds, mergeDown; + + layers = []; + + for (layer of Tegaki.layers) { + if (idSet.has(layer.id)) { + layers.push(layer); + } + } + + if (layers.length < 1) { + return; + } + + if (layers.length === 1) { + targetLayer = TegakiLayers.getLayerBelowId(layers[0].id); + + if (!targetLayer) { + return; + } + + layers.unshift(targetLayer); + + mergeDown = true; + } + else { + targetLayer = layers[layers.length - 1]; + + mergeDown = false; + } + + canvas = $T.el('canvas'); + canvas.width = Tegaki.baseWidth; + canvas.height = Tegaki.baseHeight; + + ctx = canvas.getContext('2d'); + + imageDataBefore = $T.copyImageData(targetLayer.imageData); + + delIds = []; + + for (layer of layers) { + if (layer.id !== targetLayer.id) { + delIds.push(layer.id); + } + + ctx.globalAlpha = layer.alpha; + ctx.drawImage(layer.canvas, 0, 0); + } + + $T.clearCtx(targetLayer.ctx); + + targetLayer.ctx.drawImage(canvas, 0, 0); + + TegakiLayers.syncLayerImageData(targetLayer); + + imageDataAfter = $T.copyImageData(targetLayer.imageData); + + action = TegakiLayers.deleteLayers(delIds, { + tgtLayerId: targetLayer.id, + tgtLayerAlpha: targetLayer.alpha, + aLayerIdAfter: targetLayer.id, + imageDataBefore: imageDataBefore, + imageDataAfter: imageDataAfter, + mergeDown: mergeDown + }); + + TegakiLayers.setLayerAlpha(targetLayer, 1.0); + + TegakiUI.updateLayerAlphaOpt(); + + TegakiUI.updateLayerPreview(targetLayer); + + Tegaki.onLayerStackChanged(); + + return action; + }, + + moveLayers: function(idSet, belowPos) { + var idx, layer, + historyLayers, updLayers, movedLayers, + tgtCanvas, updTgtPos; + + if (!idSet.size || !Tegaki.layers.length) { + return; + } + + if (belowPos >= Tegaki.layers.length) { + tgtCanvas = TegakiLayers.getTopLayer().canvas.nextElementSibling; + } + else { + layer = Tegaki.layers[belowPos]; + tgtCanvas = layer.canvas; + } + + historyLayers = []; + updLayers = []; + movedLayers = []; + + updTgtPos = belowPos; + + idx = 0; + + for (layer of Tegaki.layers) { + if (idSet.has(layer.id)) { + if (belowPos > 0 && idx <= belowPos) { + updTgtPos--; + } + + historyLayers.push([idx, layer]); + movedLayers.push(layer); + } + else { + updLayers.push(layer); + } + + ++idx; + } + + updLayers.splice(updTgtPos, 0, ...movedLayers); + + if (TegakiLayers.isSameLayerOrder(updLayers, Tegaki.layers)) { + return; + } + + Tegaki.layers = updLayers; + + for (layer of historyLayers) { + Tegaki.layersCnt.insertBefore(layer[1].canvas, tgtCanvas); + } + + TegakiUI.updayeLayersGridOrder(); + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.MoveLayers( + historyLayers, belowPos, + Tegaki.activeLayer ? Tegaki.activeLayer.id : -1 + ); + }, + + setLayerVisibility: function(layer, flag) { + layer.visible = flag; + + if (flag) { + layer.canvas.classList.remove('tegaki-hidden'); + } + else { + layer.canvas.classList.add('tegaki-hidden'); + } + + Tegaki.onLayerStackChanged(); + + TegakiUI.updateLayersGridVisibility(layer.id, flag); + }, + + setLayerAlpha: function(layer, alpha) { + layer.alpha = alpha; + layer.canvas.style.opacity = alpha; + }, + + setActiveLayer: function(id) { + var idx, layer; + + if (!id) { + id = TegakiLayers.getTopLayerId(); + + if (!id) { + Tegaki.activeLayer = null; + return; + } + } + + idx = TegakiLayers.getLayerPosById(id); + + if (idx < 0) { + return; + } + + layer = Tegaki.layers[idx]; + + if (Tegaki.activeLayer) { + Tegaki.copyContextState(Tegaki.activeLayer.ctx, layer.ctx); + } + + Tegaki.activeLayer = layer; + + TegakiLayers.selectedLayersClear(); + TegakiLayers.selectedLayersAdd(id); + + TegakiUI.updateLayersGridActive(id); + TegakiUI.updateLayerAlphaOpt(); + + Tegaki.onLayerStackChanged(); + }, + + syncLayerImageData(layer, imageData = null) { + if (imageData) { + layer.imageData = $T.copyImageData(imageData); + } + else { + layer.imageData = layer.ctx.getImageData( + 0, 0, Tegaki.baseWidth, Tegaki.baseHeight + ); + } + }, + + selectedLayersHas: function(id) { + return Tegaki.selectedLayers.has(+id); + }, + + selectedLayersClear: function() { + Tegaki.selectedLayers.clear(); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedClear(); + }, + + selectedLayersAdd: function(id) { + Tegaki.selectedLayers.add(+id); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedSet(id, true); + }, + + selectedLayersRemove: function(id) { + Tegaki.selectedLayers.delete(+id); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedSet(id, false); + }, + + selectedLayersToggle: function(id) { + if (TegakiLayers.selectedLayersHas(id)) { + TegakiLayers.selectedLayersRemove(id); + } + else { + TegakiLayers.selectedLayersAdd(id); + } + } +}; +var Tegaki = { + VERSION: '0.9.2', + + startTimeStamp: 0, + + bg: null, + canvas: null, + ctx: null, + layers: [], + + layersCnt: null, + canvasCnt: null, + + ghostBuffer: null, + blendBuffer: null, + ghostBuffer32: null, + blendBuffer32: null, + + activeLayer: null, + + layerCounter: 0, + selectedLayers: new Set(), + + activePointerId: 0, + activePointerIsPen: false, + + isPainting: false, + + offsetX: 0, + offsetY: 0, + + zoomLevel: 0, + zoomFactor: 1.0, + zoomFactorList: [0.5, 1.0, 2.0, 4.0, 8.0, 16.0], + zoomBaseLevel: 1, + + hasCustomCanvas: false, + + TWOPI: 2 * Math.PI, + + toolList: [ + TegakiPencil, + TegakiPen, + TegakiAirbrush, + TegakiBucket, + TegakiTone, + TegakiPipette, + TegakiBlur, + TegakiEraser + ], + + tools: {}, + + tool: null, + + colorPaletteId: 0, + + toolColor: '#000000', + defaultTool: 'pencil', + + bgColor: '#ffffff', + maxSize: 64, + maxLayers: 25, + baseWidth: 0, + baseHeight: 0, + + replayRecorder: null, + replayViewer: null, + + onDoneCb: null, + onCancelCb: null, + + replayMode: false, + + saveReplay: false, + + open: function(opts = {}) { + var self = Tegaki; + + if (self.bg) { + if (self.replayMode !== (opts.replayMode ? true : false)) { + self.destroy(); + } + else { + self.resume(); + return; + } + } + + self.startTimeStamp = Date.now(); + + if (opts.bgColor) { + self.bgColor = opts.bgColor; + } + + self.hasCustomCanvas = false; + + self.saveReplay = !!opts.saveReplay; + self.replayMode = !!opts.replayMode; + + self.onDoneCb = opts.onDone; + self.onCancelCb = opts.onCancel; + + self.baseWidth = opts.width || 0; + self.baseHeight = opts.height || 0; + + self.createTools(); + + self.initKeybinds(); + + [self.bg, self.canvasCnt, self.layersCnt] = TegakiUI.buildUI(); + + document.body.appendChild(self.bg); + document.body.classList.add('tegaki-backdrop'); + + if (!self.replayMode) { + self.init(); + + self.setTool(self.defaultTool); + + if (self.saveReplay) { + self.replayRecorder = new TegakiReplayRecorder(); + self.replayRecorder.start(); + } + } + else { + TegakiUI.setReplayMode(true); + + self.replayViewer = new TegakiReplayViewer(); + + if (opts.replayURL) { + self.loadReplayFromURL(opts.replayURL); + } + } + }, + + init: function() { + var self = Tegaki; + + self.createCanvas(); + + self.updateLayersCntSize(); + + self.createBuffers(); + + self.updatePosOffset(); + + self.resetLayers(); + + TegakiCursor.init(self.baseWidth, self.baseHeight); + + self.bindGlobalEvents(); + + TegakiUI.updateUndoRedo(0, 0); + TegakiUI.updateZoomLevel(); + }, + + initFromReplay: function() { + var self, r; + + self = Tegaki; + r = self.replayViewer; + + self.initToolsFromReplay(); + + self.baseWidth = r.canvasWidth; + self.baseHeight = r.canvasHeight; + self.bgColor = $T.RgbToHex(...r.bgColor); + + self.toolColor = $T.RgbToHex(...r.toolColor); + }, + + initToolsFromReplay: function() { + var self, r, name, tool, rTool, prop, props; + + self = Tegaki; + r = self.replayViewer; + + for (name in self.tools) { + tool = self.tools[name]; + + if (tool.id === r.toolId) { + self.defaultTool = name; + } + + rTool = r.toolMap[tool.id]; + + props = ['step', 'size', 'alpha', 'flow', 'tipId']; + + for (prop of props) { + if (rTool[prop] !== undefined) { + tool[prop] = rTool[prop]; + } + } + + props = [ + 'sizeDynamicsEnabled', 'alphaDynamicsEnabled', 'flowDynamicsEnabled', + 'usePreserveAlpha' + ]; + + for (prop of props) { + if (rTool[prop] !== undefined) { + tool[prop] = !!rTool[prop]; + } + } + } + }, + + resetLayers: function() { + var i, len; + + if (Tegaki.layers.length) { + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + Tegaki.layersCnt.removeChild(Tegaki.layers[i].canvas); + } + + Tegaki.layers = []; + Tegaki.layerCounter = 0; + + TegakiUI.updateLayersGridClear(); + } + + TegakiLayers.addLayer(); + TegakiLayers.setActiveLayer(0); + }, + + createCanvas: function() { + var canvas, self = Tegaki; + + canvas = $T.el('canvas'); + canvas.id = 'tegaki-canvas'; + canvas.width = self.baseWidth; + canvas.height = self.baseHeight; + + self.canvas = canvas; + + self.ctx = canvas.getContext('2d'); + self.ctx.fillStyle = self.bgColor; + self.ctx.fillRect(0, 0, self.baseWidth, self.baseHeight); + + self.layersCnt.appendChild(canvas); + }, + + createTools: function() { + var klass, tool; + + for (klass of Tegaki.toolList) { + tool = new klass(); + Tegaki.tools[tool.name] = tool; + } + }, + + bindGlobalEvents: function() { + var self = Tegaki; + + if (!self.replayMode) { + let cursorNode = TegakiCursor.getCanvas(); + $T.on(cursorNode, 'pointermove', self.onPointerMove); + $T.on(cursorNode, 'pointerdown', self.onPointerDown); + $T.on(cursorNode, 'pointerout', self.onPointerOut); + + $T.on(document, 'pointerup', self.onPointerUp); + $T.on(document, 'pointercancel', self.onPointerUp); + + $T.on(document, 'keydown', TegakiKeybinds.resolve); + + $T.on(window, 'beforeunload', Tegaki.onTabClose); + } + else { + $T.on(document, 'visibilitychange', Tegaki.onVisibilityChange); + } + + $T.on(self.bg, 'contextmenu', self.onDummy); + $T.on(window, 'resize', self.onWindowResized); + $T.on(window, 'scroll', self.updatePosOffset); + }, + + unBindGlobalEvents: function() { + var self = Tegaki; + + if (!self.replayMode) { + let cursorNode = TegakiCursor.getCanvas(); + $T.off(cursorNode, 'pointermove', self.onPointerMove); + $T.off(cursorNode, 'pointerdown', self.onPointerDown); + $T.off(cursorNode, 'pointerout', self.onPointerOut); + + $T.off(document, 'pointerup', self.onPointerUp); + $T.off(document, 'pointercancel', self.onPointerUp); + + $T.off(document, 'keydown', TegakiKeybinds.resolve); + + $T.off(window, 'beforeunload', Tegaki.onTabClose); + } + else { + $T.off(document, 'visibilitychange', Tegaki.onVisibilityChange); + } + + $T.off(self.bg, 'contextmenu', self.onDummy); + $T.off(window, 'resize', self.onWindowResized); + $T.off(window, 'scroll', self.updatePosOffset); + }, + + createBuffers() { + Tegaki.ghostBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); + Tegaki.blendBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); + Tegaki.ghostBuffer32 = new Uint32Array(Tegaki.ghostBuffer.data.buffer); + Tegaki.blendBuffer32 = new Uint32Array(Tegaki.blendBuffer.data.buffer); + }, + + clearBuffers() { + Tegaki.ghostBuffer32.fill(0); + Tegaki.blendBuffer32.fill(0); + }, + + destroyBuffers() { + Tegaki.ghostBuffer = null; + Tegaki.blendBuffer = null; + Tegaki.ghostBuffer32 = null; + Tegaki.blendBuffer32 = null; + }, + + disableSmoothing: function(ctx) { + ctx.mozImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + ctx.imageSmoothingEnabled = false; + }, + + updateLayersCntSize: function() { + var style = Tegaki.layersCnt.style; + + style.width = Tegaki.baseWidth + 'px'; + style.height = Tegaki.baseHeight + 'px'; + }, + + onTabClose: function(e) { + e.preventDefault(); + e.returnValue = ''; + }, + + onVisibilityChange: function(e) { + if (!Tegaki.replayMode) { + return; + } + + if (document.visibilityState === 'visible') { + if (Tegaki.replayViewer.autoPaused) { + Tegaki.replayViewer.play(); + } + } + else { + if (Tegaki.replayViewer.playing) { + Tegaki.replayViewer.autoPause(); + } + } + }, + + onWindowResized: function() { + Tegaki.updatePosOffset(); + TegakiCursor.updateCanvasSize(); + }, + + initKeybinds: function() { + var cls, tool; + + if (Tegaki.replayMode) { + return; + } + + TegakiKeybinds.bind('ctrl+z', TegakiHistory, 'undo', 'undo', 'Ctrl+Z'); + TegakiKeybinds.bind('ctrl+y', TegakiHistory, 'redo', 'redo', 'Ctrl+Y'); + + TegakiKeybinds.bind('+', Tegaki, 'setToolSizeUp', 'toolSize', 'Numpad +/-'); + TegakiKeybinds.bind('-', Tegaki, 'setToolSizeDown'); + + for (tool in Tegaki.tools) { + cls = Tegaki.tools[tool]; + + if (cls.keybind) { + TegakiKeybinds.bind(cls.keybind, cls, 'set'); + } + } + }, + + getPointerPos: function(e, axis) { + if (axis === 0) { + return 0 | (( + e.clientX + + window.pageXOffset + + Tegaki.canvasCnt.scrollLeft + - Tegaki.offsetX + ) / Tegaki.zoomFactor); + } + else { + return 0 | (( + e.clientY + + window.pageYOffset + + Tegaki.canvasCnt.scrollTop + - Tegaki.offsetY + ) / Tegaki.zoomFactor); + } + }, + + resume: function() { + if (Tegaki.saveReplay) { + Tegaki.replayRecorder.start(); + } + + Tegaki.bg.classList.remove('tegaki-hidden'); + document.body.classList.add('tegaki-backdrop'); + Tegaki.setZoom(0); + Tegaki.updateLayersCntSize(); + Tegaki.updatePosOffset(); + TegakiCursor.updateCanvasSize(); + Tegaki.bindGlobalEvents(); + }, + + hide: function() { + if (Tegaki.saveReplay) { + Tegaki.replayRecorder.stop(); + } + + Tegaki.bg.classList.add('tegaki-hidden'); + document.body.classList.remove('tegaki-backdrop'); + Tegaki.unBindGlobalEvents(); + }, + + destroy: function() { + Tegaki.unBindGlobalEvents(); + + TegakiKeybinds.clear(); + + TegakiHistory.clear(); + + Tegaki.bg.parentNode.removeChild(Tegaki.bg); + + document.body.classList.remove('tegaki-backdrop'); + + Tegaki.startTimeStamp = 0; + + Tegaki.bg = null; + Tegaki.canvasCnt = null; + Tegaki.layersCnt = null; + Tegaki.canvas = null; + Tegaki.ctx = null; + Tegaki.layers = []; + Tegaki.layerCounter = 0; + Tegaki.zoomLevel = 0; + Tegaki.zoomFactor = 1.0; + Tegaki.activeLayer = null; + + Tegaki.tool = null; + + TegakiCursor.destroy(); + + Tegaki.replayRecorder = null; + Tegaki.replayViewer = null; + + Tegaki.destroyBuffers(); + }, + + flatten: function(ctx) { + var i, layer, canvas, len; + + if (!ctx) { + canvas = $T.el('canvas'); + ctx = canvas.getContext('2d'); + } + else { + canvas = ctx.canvas; + } + + canvas.width = Tegaki.canvas.width; + canvas.height = Tegaki.canvas.height; + + ctx.drawImage(Tegaki.canvas, 0, 0); + + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + layer = Tegaki.layers[i]; + + if (!layer.visible) { + continue; + } + + ctx.globalAlpha = layer.alpha; + ctx.drawImage(layer.canvas, 0, 0); + } + + return canvas; + }, + + onReplayLoaded: function() { + TegakiUI.clearMsg(); + Tegaki.initFromReplay(); + Tegaki.init(); + Tegaki.setTool(Tegaki.defaultTool); + TegakiUI.updateReplayControls(); + TegakiUI.updateReplayTime(true); + TegakiUI.enableReplayControls(true); + Tegaki.replayViewer.play(); + }, + + onReplayGaplessClick: function() { + Tegaki.replayViewer.toggleGapless(); + TegakiUI.updateReplayGapless(); + }, + + onReplayPlayPauseClick: function() { + Tegaki.replayViewer.togglePlayPause(); + }, + + onReplayRewindClick: function() { + Tegaki.replayViewer.rewind(); + }, + + onReplaySlowDownClick: function() { + Tegaki.replayViewer.slowDown(); + TegakiUI.updateReplaySpeed(); + }, + + onReplaySpeedUpClick: function() { + Tegaki.replayViewer.speedUp(); + TegakiUI.updateReplaySpeed(); + }, + + onReplayTimeChanged: function() { + TegakiUI.updateReplayTime(); + }, + + onReplayPlayPauseChanged: function() { + TegakiUI.updateReplayPlayPause(); + }, + + onReplayReset: function() { + Tegaki.initFromReplay(); + Tegaki.setTool(Tegaki.defaultTool); + Tegaki.resizeCanvas(Tegaki.baseWidth, Tegaki.baseHeight); + TegakiUI.updateReplayControls(); + TegakiUI.updateReplayTime(); + }, + + onMainColorClick: function(e) { + var el; + e.preventDefault(); + el = $T.id('tegaki-colorpicker'); + el.click(); + }, + + onPaletteColorClick: function(e) { + if (e.button === 2) { + this.style.backgroundColor = Tegaki.toolColor; + this.setAttribute('data-color', Tegaki.toolColor); + } + else if (e.button === 0) { + Tegaki.setToolColor(this.getAttribute('data-color')); + } + }, + + onColorPicked: function(e) { + $T.id('tegaki-color').style.backgroundColor = this.value; + Tegaki.setToolColor(this.value); + }, + + onSwitchPaletteClick: function(e) { + var id; + + if (e.target.hasAttribute('data-prev')) { + id = Tegaki.colorPaletteId - 1; + } + else { + id = Tegaki.colorPaletteId + 1; + } + + Tegaki.setColorPalette(id); + }, + + setColorPalette: function(id) { + if (id < 0 || id >= TegakiColorPalettes.length) { + return; + } + + Tegaki.colorPaletteId = id; + TegakiUI.updateColorPalette(); + }, + + setToolSizeUp: function() { + Tegaki.setToolSize(Tegaki.tool.size + 1); + }, + + setToolSizeDown: function() { + Tegaki.setToolSize(Tegaki.tool.size - 1); + }, + + setToolSize: function(size) { + if (size > 0 && size <= Tegaki.maxSize) { + Tegaki.tool.setSize(size); + Tegaki.updateCursorStatus(); + Tegaki.recordEvent(TegakiEventSetToolSize, performance.now(), size); + TegakiUI.updateToolSize(); + } + }, + + setToolAlpha: function(alpha) { + alpha = Math.fround(alpha); + + if (alpha >= 0.0 && alpha <= 1.0) { + Tegaki.tool.setAlpha(alpha); + Tegaki.recordEvent(TegakiEventSetToolAlpha, performance.now(), alpha); + TegakiUI.updateToolAlpha(); + } + }, + + setToolFlow: function(flow) { + flow = Math.fround(flow); + + if (flow >= 0.0 && flow <= 1.0) { + Tegaki.tool.setFlow(flow); + Tegaki.recordEvent(TegakiEventSetToolFlow, performance.now(), flow); + TegakiUI.updateToolFlow(); + } + }, + + setToolColor: function(color) { + Tegaki.toolColor = color; + $T.id('tegaki-color').style.backgroundColor = color; + $T.id('tegaki-colorpicker').value = color; + Tegaki.tool.setColor(color); + Tegaki.recordEvent(TegakiEventSetColor, performance.now(), Tegaki.tool.rgb); + }, + + setToolColorRGB: function(r, g, b) { + Tegaki.setToolColor($T.RgbToHex(r, g, b)); + }, + + setTool: function(tool) { + Tegaki.tools[tool].set(); + }, + + setToolById: function(id) { + var tool; + + for (tool in Tegaki.tools) { + if (Tegaki.tools[tool].id === id) { + Tegaki.setTool(tool); + return; + } + } + }, + + setZoom: function(level) { + var idx; + + idx = level + Tegaki.zoomBaseLevel; + + if (idx >= Tegaki.zoomFactorList.length || idx < 0 || !Tegaki.canvas) { + return; + } + + Tegaki.zoomLevel = level; + Tegaki.zoomFactor = Tegaki.zoomFactorList[idx]; + + TegakiUI.updateZoomLevel(); + + Tegaki.layersCnt.style.width = Math.ceil(Tegaki.baseWidth * Tegaki.zoomFactor) + 'px'; + Tegaki.layersCnt.style.height = Math.ceil(Tegaki.baseHeight * Tegaki.zoomFactor) + 'px'; + + Tegaki.updateCursorStatus(); + Tegaki.updatePosOffset(); + }, + + onZoomChange: function() { + if (this.hasAttribute('data-in')) { + Tegaki.setZoom(Tegaki.zoomLevel + 1); + } + else { + Tegaki.setZoom(Tegaki.zoomLevel - 1); + } + + TegakiCursor.updateCanvasSize(); + }, + + onNewClick: function() { + var width, height, tmp, self = Tegaki; + + width = prompt(TegakiStrings.promptWidth, self.canvas.width); + + if (!width) { return; } + + height = prompt(TegakiStrings.promptHeight, self.canvas.height); + + if (!height) { return; } + + width = +width; + height = +height; + + if (width < 1 || height < 1) { + TegakiUI.printMsg(TegakiStrings.badDimensions); + return; + } + + tmp = {}; + self.copyContextState(self.activeLayer.ctx, tmp); + self.resizeCanvas(width, height); + self.copyContextState(tmp, self.activeLayer.ctx); + + self.setZoom(0); + TegakiHistory.clear(); + + TegakiUI.updateLayerPreviewSize(); + + self.startTimeStamp = Date.now(); + + if (self.saveReplay) { + self.createTools(); + self.setTool(self.defaultTool); + self.replayRecorder = new TegakiReplayRecorder(); + self.replayRecorder.start(); + } + }, + + onOpenClick: function() { + var el, tainted; + + tainted = TegakiHistory.undoStack[0] || TegakiHistory.redoStack[0]; + + if (tainted || Tegaki.saveReplay) { + if (!confirm(TegakiStrings.confirmChangeCanvas)) { + return; + } + } + + el = $T.id('tegaki-filepicker'); + el.click(); + }, + + loadReplayFromFile: function() { + Tegaki.replayViewer.debugLoadLocal(); + }, + + loadReplayFromURL: function(url) { + TegakiUI.printMsg(TegakiStrings.loadingReplay, 0); + Tegaki.replayViewer.loadFromURL(url); + }, + + onExportClick: function() { + Tegaki.flatten().toBlob(function(b) { + var el = $T.el('a'); + el.className = 'tegaki-hidden'; + el.download = $T.generateFilename() + '.png'; + el.href = URL.createObjectURL(b); + Tegaki.bg.appendChild(el); + el.click(); + Tegaki.bg.removeChild(el); + }, 'image/png'); + }, + + onUndoClick: function() { + TegakiHistory.undo(); + }, + + onRedoClick: function() { + TegakiHistory.redo(); + }, + + onHistoryChange: function(undoSize, redoSize, type = 0) { + TegakiUI.updateUndoRedo(undoSize, redoSize); + + if (type === -1) { + Tegaki.recordEvent(TegakiEventUndo, performance.now()); + } + else if (type === 1) { + Tegaki.recordEvent(TegakiEventRedo, performance.now()); + } + }, + + onDoneClick: function() { + Tegaki.hide(); + Tegaki.onDoneCb(); + }, + + onCancelClick: function() { + if (!confirm(TegakiStrings.confirmCancel)) { + return; + } + + Tegaki.destroy(); + Tegaki.onCancelCb(); + }, + + onCloseViewerClick: function() { + Tegaki.replayViewer.destroy(); + Tegaki.destroy(); + }, + + onToolSizeChange: function() { + var val = +this.value; + + if (val < 1) { + val = 1; + } + else if (val > Tegaki.maxSize) { + val = Tegaki.maxSize; + } + + Tegaki.setToolSize(val); + }, + + onToolAlphaChange: function(e) { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setToolAlpha(val); + }, + + onToolFlowChange: function(e) { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setToolFlow(val); + }, + + onToolPressureSizeClick: function(e) { + if (!Tegaki.tool.useSizeDynamics) { + return; + } + + Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled); + }, + + setToolSizeDynamics: function(flag) { + Tegaki.tool.setSizeDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolSizeDynamics, performance.now(), +flag); + }, + + onToolPressureAlphaClick: function(e) { + if (!Tegaki.tool.useAlphaDynamics) { + return; + } + + Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled); + }, + + setToolAlphaDynamics: function(flag) { + Tegaki.tool.setAlphaDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics, performance.now(), +flag); + }, + + onToolPressureFlowClick: function(e) { + if (!Tegaki.tool.useFlowDynamics) { + return; + } + + Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled); + }, + + setToolFlowDynamics: function(flag) { + Tegaki.tool.setFlowDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolFlowDynamics, performance.now(), +flag); + }, + + onToolPreserveAlphaClick: function(e) { + if (!Tegaki.tool.usePreserveAlpha) { + return; + } + + Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled); + }, + + setToolPreserveAlpha: function(flag) { + Tegaki.tool.setPreserveAlpha(flag); + TegakiUI.updateToolPreserveAlpha(); + Tegaki.recordEvent(TegakiEventPreserveAlpha, performance.now(), +flag); + }, + + onToolTipClick: function(e) { + var tipId = +e.target.getAttribute('data-id'); + + if (tipId !== Tegaki.tool.tipId) { + Tegaki.setToolTip(tipId); + } + }, + + setToolTip: function(id) { + Tegaki.tool.setTip(id); + TegakiUI.updateToolShape(); + Tegaki.recordEvent(TegakiEventSetToolTip, performance.now(), id); + }, + + onLayerSelectorClick: function(e) { + var id = +this.getAttribute('data-id'); + + if (!id || e.target.classList.contains('tegaki-ui-cb')) { + return; + } + + if (e.ctrlKey) { + Tegaki.toggleSelectedLayer(id); + } + else { + Tegaki.setActiveLayer(id); + } + }, + + toggleSelectedLayer: function(id) { + TegakiLayers.selectedLayersToggle(id); + Tegaki.recordEvent(TegakiEventToggleLayerSelection, performance.now(), id); + }, + + setActiveLayer: function(id) { + TegakiLayers.setActiveLayer(id); + Tegaki.recordEvent(TegakiEventSetActiveLayer, performance.now(), id); + }, + + onLayerAlphaDragStart: function(e) { + TegakiUI.setupDragLabel(e, Tegaki.onLayerAlphaDragMove); + }, + + onLayerAlphaDragMove: function(delta) { + var val; + + if (!delta) { + return; + } + + val = Tegaki.activeLayer.alpha + delta / 100 ; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setSelectedLayersAlpha(val); + }, + + onLayerAlphaChange: function() { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setSelectedLayersAlpha(val); + }, + + setSelectedLayersAlpha: function(alpha) { + var layer, id, layerAlphas; + + alpha = Math.fround(alpha); + + if (alpha >= 0.0 && alpha <= 1.0 && Tegaki.selectedLayers.size > 0) { + layerAlphas = []; + + for (id of Tegaki.selectedLayers) { + if (layer = TegakiLayers.getLayerById(id)) { + layerAlphas.push([layer.id, layer.alpha]); + TegakiLayers.setLayerAlpha(layer, alpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); + + TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(layerAlphas, alpha)); + + Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha, performance.now(), alpha); + } + }, + + onLayerNameChangeClick: function(e) { + var id, name, layer; + + id = +this.getAttribute('data-id'); + + layer = TegakiLayers.getLayerById(id); + + if (!layer) { + return; + } + + if (name = prompt(undefined, layer.name)) { + Tegaki.setLayerName(id, name); + } + }, + + setLayerName: function(id, name) { + var oldName, layer; + + name = name.trim().slice(0, 25); + + layer = TegakiLayers.getLayerById(id); + + if (!layer || !name || name === layer.name) { + return; + } + + oldName = layer.name; + + layer.name = name; + + TegakiUI.updateLayerName(layer); + + TegakiHistory.push(new TegakiHistoryActions.SetLayerName(id, oldName, name)); + + Tegaki.recordEvent(TegakiEventHistoryDummy, performance.now()); + }, + + onLayerAddClick: function() { + Tegaki.addLayer(); + }, + + addLayer: function() { + var action; + + if (Tegaki.layers.length >= Tegaki.maxLayers) { + TegakiUI.printMsg(TegakiStrings.tooManyLayers); + return; + } + + TegakiHistory.push(action = TegakiLayers.addLayer()); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventAddLayer, performance.now()); + }, + + onLayerDeleteClick: function() { + Tegaki.deleteSelectedLayers(); + }, + + deleteSelectedLayers: function() { + var action, layerSet; + + layerSet = Tegaki.selectedLayers; + + if (layerSet.size === Tegaki.layers.length) { + return; + } + + if (!layerSet.size || Tegaki.layers.length < 2) { + return; + } + + TegakiHistory.push(action = TegakiLayers.deleteLayers(layerSet)); + TegakiLayers.selectedLayersClear(); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventDeleteLayers, performance.now()); + }, + + onLayerToggleVisibilityClick: function() { + Tegaki.toggleLayerVisibility(+this.getAttribute('data-id')); + }, + + toggleLayerVisibility: function(id) { + var layer = TegakiLayers.getLayerById(id); + TegakiLayers.setLayerVisibility(layer, !layer.visible); + Tegaki.recordEvent(TegakiEventToggleLayerVisibility, performance.now(), id); + }, + + onMergeLayersClick: function() { + Tegaki.mergeSelectedLayers(); + }, + + mergeSelectedLayers: function() { + var action; + + if (Tegaki.selectedLayers.size) { + if (action = TegakiLayers.mergeLayers(Tegaki.selectedLayers)) { + TegakiHistory.push(action); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventMergeLayers, performance.now()); + } + } + }, + + onMoveLayerClick: function(e) { + var belowPos, up; + + if (!Tegaki.selectedLayers.size) { + return; + } + + up = e.target.hasAttribute('data-up'); + + belowPos = TegakiLayers.getSelectedEdgeLayerPos(up); + + if (belowPos < 0) { + return; + } + + if (up) { + belowPos += 2; + } + else if (belowPos >= 1) { + belowPos--; + } + + Tegaki.moveSelectedLayers(belowPos); + }, + + moveSelectedLayers: function(belowPos) { + TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers, belowPos)); + Tegaki.recordEvent(TegakiEventMoveLayers, performance.now(), belowPos); + }, + + onToolClick: function() { + Tegaki.setTool(this.getAttribute('data-tool')); + }, + + onToolChanged: function(tool) { + var el; + + Tegaki.tool = tool; + + if (el = $T.cls('tegaki-tool-active')[0]) { + el.classList.remove('tegaki-tool-active'); + } + + $T.id('tegaki-tool-btn-' + tool.name).classList.add('tegaki-tool-active'); + + Tegaki.recordEvent(TegakiEventSetTool, performance.now(), Tegaki.tool.id); + + TegakiUI.onToolChanged(); + Tegaki.updateCursorStatus(); + }, + + onLayerStackChanged: function() {}, + + onOpenFileSelected: function() { + var img; + + if (this.files && this.files[0]) { + img = new Image(); + img.onload = Tegaki.onOpenImageLoaded; + img.onerror = Tegaki.onOpenImageError; + + img.src = URL.createObjectURL(this.files[0]); + } + }, + + onOpenImageLoaded: function() { + var tmp = {}, self = Tegaki; + + self.hasCustomCanvas = true; + + self.copyContextState(self.activeLayer.ctx, tmp); + self.resizeCanvas(this.naturalWidth, this.naturalHeight); + self.activeLayer.ctx.drawImage(this, 0, 0); + TegakiLayers.syncLayerImageData(self.activeLayer); + self.copyContextState(tmp, self.activeLayer.ctx); + + self.setZoom(0); + + TegakiHistory.clear(); + + TegakiUI.updateLayerPreviewSize(true); + + self.startTimeStamp = Date.now(); + + if (self.saveReplay) { + self.replayRecorder.stop(); + self.replayRecorder = null; + self.saveReplay = false; + TegakiUI.setRecordingStatus(false); + } + }, + + onOpenImageError: function() { + TegakiUI.printMsg(TegakiStrings.errorLoadImage); + }, + + resizeCanvas: function(width, height) { + Tegaki.baseWidth = width; + Tegaki.baseHeight = height; + + Tegaki.createBuffers(); + + Tegaki.canvas.width = width; + Tegaki.canvas.height = height; + + Tegaki.ctx.fillStyle = Tegaki.bgColor; + Tegaki.ctx.fillRect(0, 0, width, height); + + Tegaki.activeLayer = null; + + Tegaki.resetLayers(); + + Tegaki.updateLayersCntSize(); + Tegaki.updatePosOffset(); + TegakiCursor.updateCanvasSize(); + }, + + copyContextState: function(src, dest) { + var i, p, props = [ + 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha', + 'lineWidth', 'globalCompositeOperation' + ]; + + for (i = 0; p = props[i]; ++i) { + dest[p] = src[p]; + } + }, + + updateCursorStatus: function() { + if (!Tegaki.tool.noCursor && Tegaki.tool.size > 1) { + Tegaki.cursor = TegakiCursor.generate(Tegaki.tool.size); + } + else { + Tegaki.cursor = false; + TegakiCursor.clear(); + } + }, + + updatePosOffset: function() { + var aabb = Tegaki.canvas.getBoundingClientRect(); + + Tegaki.offsetX = aabb.left + window.pageXOffset + + Tegaki.canvasCnt.scrollLeft + Tegaki.layersCnt.scrollLeft; + Tegaki.offsetY = aabb.top + window.pageYOffset + + Tegaki.canvasCnt.scrollTop + Tegaki.layersCnt.scrollTop; + }, + + getScrollbarSizes: function() { + return [ + Tegaki.canvasCnt.offsetWidth - Tegaki.canvasCnt.clientWidth, + Tegaki.canvasCnt.offsetHeight - Tegaki.canvasCnt.clientHeight + ]; + }, + + isScrollbarClick: function(e) { + var [ sbwh, scbv ] = Tegaki.getScrollbarSizes(); + + if (sbwh > 0 + && e.clientX >= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth + && e.clientX <= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth + + sbwh) { + return true; + } + + if (scbv > 0 + && e.clientY >= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight + && e.clientY <= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight + + sbwh) { + return true; + } + + return false; + }, + + onPointerMove: function(e) { + var events, x, y, tool, ts, p; + + if (e.mozInputSource !== undefined) { + // Firefox thing where mouse events fire for no reason when the pointer is a pen + if (Tegaki.activePointerIsPen && e.pointerType === 'mouse') { + return; + } + } + else { + // Webkit thing where a pointermove event is fired at pointerdown location after a pointerup + if (Tegaki.activePointerId !== e.pointerId) { + Tegaki.activePointerId = e.pointerId; + return; + } + } + + if (Tegaki.isPainting) { + tool = Tegaki.tool; + + if (Tegaki.activePointerIsPen && e.getCoalescedEvents) { + events = e.getCoalescedEvents(); + + ts = e.timeStamp; + + for (e of events) { + x = Tegaki.getPointerPos(e, 0); + y = Tegaki.getPointerPos(e, 1); + + if (!tool.enabledDynamics()) { + Tegaki.recordEvent(TegakiEventDrawNoP, ts, x, y); + } + else { + p = TegakiPressure.toShort(e.pressure); + TegakiPressure.push(p); + Tegaki.recordEvent(TegakiEventDraw, ts, x, y, p); + } + + tool.draw(x, y); + } + } + else { + x = Tegaki.getPointerPos(e, 0); + y = Tegaki.getPointerPos(e, 1); + p = TegakiPressure.toShort(e.pressure); + Tegaki.recordEvent(TegakiEventDraw, e.timeStamp, x, y, p); + TegakiPressure.push(p); + tool.draw(x, y); + } + } + else { + x = Tegaki.getPointerPos(e, 0); + y = Tegaki.getPointerPos(e, 1); + } + + if (Tegaki.cursor) { + TegakiCursor.render(e.clientX, e.clientY); + } + }, + + onPointerDown: function(e) { + var x, y, tool, p; + + if (Tegaki.isScrollbarClick(e)) { + return; + } + + Tegaki.activePointerId = e.pointerId; + + Tegaki.activePointerIsPen = e.pointerType === 'pen'; + + if (Tegaki.activeLayer === null) { + if (e.target.parentNode === Tegaki.layersCnt) { + TegakiUI.printMsg(TegakiStrings.noActiveLayer); + } + + return; + } + + if (!TegakiLayers.getActiveLayer().visible) { + if (e.target.parentNode === Tegaki.layersCnt) { + TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer); + } + + return; + } + + x = Tegaki.getPointerPos(e, 0); + y = Tegaki.getPointerPos(e, 1); + + if (e.button === 2 || e.altKey) { + e.preventDefault(); + + Tegaki.isPainting = false; + + Tegaki.tools.pipette.draw(x, y); + } + else if (e.button === 0) { + e.preventDefault(); + + tool = Tegaki.tool; + + if (!tool.enabledDynamics()) { + Tegaki.recordEvent(TegakiEventDrawStartNoP, e.timeStamp, x, y); + } + else { + p = TegakiPressure.toShort(e.pressure); + TegakiPressure.push(p); + Tegaki.recordEvent(TegakiEventDrawStart, e.timeStamp, x, y, p); + } + + Tegaki.isPainting = true; + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + tool.start(x, y); + } + + if (Tegaki.cursor) { + TegakiCursor.render(e.clientX, e.clientY); + } + }, + + onPointerUp: function(e) { + Tegaki.activePointerId = e.pointerId; + + Tegaki.activePointerIsPen = false; + + if (Tegaki.isPainting) { + Tegaki.recordEvent(TegakiEventDrawCommit, e.timeStamp); + Tegaki.tool.commit(); + TegakiUI.updateLayerPreview(Tegaki.activeLayer); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); + TegakiHistory.push(TegakiHistory.pendingAction); + Tegaki.isPainting = false; + } + }, + + onPointerOut: function(e) { + TegakiCursor.clearAll(); + }, + + onDummy: function(e) { + e.preventDefault(); + e.stopPropagation(); + }, + + recordEvent(klass, ...args) { + if (Tegaki.replayRecorder) { + Tegaki.replayRecorder.push(new klass(...args)); + } + } +}; +var TegakiColorPalettes = [ + [ + '#ffffff', '#000000', '#888888', '#b47575', '#c096c0', + '#fa9696', '#8080ff', '#ffb6ff', '#e7e58d', '#25c7c9', + '#99cb7b', '#e7962d', '#f9ddcf', '#fcece2' + ], + + [ + '#000000', '#ffffff', '#7f7f7f', '#c3c3c3', '#880015', '#b97a57', '#ed1c24', + '#ffaec9', '#ff7f27', '#ffc90e', '#fff200', '#efe4b0', '#22b14c', '#b5e61d', + '#00a2e8', '#99d9ea', '#3f48cc', '#7092be', '#a349a4', '#c8bfe7' + ], + + [ + '#000000', '#ffffff', '#8a8a8a', '#cacaca', '#fcece2', '#f9ddcf', '#e0a899', '#a05b53', + '#7a444a', '#960018', '#c41e3a', '#de4537', '#ff3300', '#ff9800', '#ffc107', + '#ffd700', '#ffeb3b', '#ffffcc', '#f3e5ab', '#cddc39', '#8bc34a', '#4caf50', '#3e8948', + '#355e3b', '#3eb489', '#f0f8ff', '#87ceeb', '#6699cc', '#007fff', '#2d68c4', '#364478', + '#352c4a', '#9c27b0', '#da70d6', '#ff0090', '#fa8072', '#f19cbb', '#c78b95' + ] +]; +var TegakiPressure = { + pressureNow: 0.0, + pressureThen: 0.0, + + toShort: function(pressure) { + return 0 | (pressure * 65535); + }, + + get: function() { + return this.pressureNow; + }, + + lerp: function(t) { + return this.pressureThen * (1.0 - t) + this.pressureNow * t; + }, + + push: function(p) { + this.pressureThen = this.pressureNow; + this.pressureNow = p / 65535; + }, + + set: function(p) { + this.pressureThen = this.pressureNow = p / 65535; + } +}; +class TegakiEvent_void { + constructor() { + this.size = 5; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + } + + static unpack(r) { + return new this(r.readUint32()); + } +} + +class TegakiEvent_c { + constructor() { + this.size = 6; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeUint8(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readUint8()); + } +} + +// --- + +class TegakiEventPrelude extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() {} +} + +class TegakiEventConclusion extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() {} +} + +class TegakiEventHistoryDummy extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + TegakiHistory.push(new TegakiHistoryActions.Dummy()); + } +} + +class TegakiEventDrawStart { + constructor(timeStamp, x, y, pressure) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.pressure = pressure; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 11; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + w.writeUint16(this.pressure); + } + + static unpack(r) { + var timeStamp, x, y, pressure; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + pressure = r.readUint16(); + + return new TegakiEventDrawStart(timeStamp, x, y, pressure); + } + + dispatch() { + TegakiPressure.set(this.pressure); + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + Tegaki.tool.start(this.x, this.y); + } +} + +class TegakiEventDrawStartNoP { + constructor(timeStamp, x, y) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + } + + static unpack(r) { + var timeStamp, x, y; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + + return new TegakiEventDrawStartNoP(timeStamp, x, y); + } + + dispatch() { + TegakiPressure.set(0.5); + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + Tegaki.tool.start(this.x, this.y); + } +} + +class TegakiEventDraw { + constructor(timeStamp, x, y, pressure) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.pressure = pressure; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 11; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + w.writeUint16(this.pressure); + } + + static unpack(r) { + var timeStamp, x, y, pressure; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + pressure = r.readUint16(); + + return new TegakiEventDraw(timeStamp, x, y, pressure); + } + + dispatch() { + TegakiPressure.push(this.pressure); + Tegaki.tool.draw(this.x, this.y); + } +} + +class TegakiEventDrawNoP { + constructor(timeStamp, x, y) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + } + + static unpack(r) { + var timeStamp, x, y; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + + return new TegakiEventDraw(timeStamp, x, y); + } + + dispatch() { + TegakiPressure.push(0.5); + Tegaki.tool.draw(this.x, this.y); + } +} + +class TegakiEventDrawCommit extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + Tegaki.tool.commit(); + TegakiUI.updateLayerPreview(Tegaki.activeLayer); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); + TegakiHistory.push(TegakiHistory.pendingAction); + Tegaki.isPainting = false; + } +} + +class TegakiEventUndo extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + TegakiHistory.undo(); + } +} + +class TegakiEventRedo extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + TegakiHistory.redo(); + } +} + +class TegakiEventSetColor { + constructor(timeStamp, rgb) { + this.timeStamp = timeStamp; + [this.r, this.g, this.b] = rgb; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 8; + this.coalesce = true; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeUint8(this.r); + w.writeUint8(this.g); + w.writeUint8(this.b); + } + + static unpack(r) { + var timeStamp, rgb; + + timeStamp = r.readUint32(); + + rgb = [r.readUint8(), r.readUint8(), r.readUint8()]; + + return new TegakiEventSetColor(timeStamp, rgb); + } + + dispatch() { + Tegaki.setToolColorRGB(this.r, this.g, this.b); + } +} + +class TegakiEventSetTool extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolById(this.value); + } +} + +class TegakiEventSetToolSize extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolSize(this.value); + } +} + +class TegakiEventSetToolAlpha { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setToolAlpha(this.value); + } +} + +class TegakiEventSetToolFlow { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setToolFlow(this.value); + } +} + +class TegakiEventPreserveAlpha extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolPreserveAlpha(!!this.value); + } +} + +class TegakiEventSetToolSizeDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolSizeDynamics(!!this.value); + } +} + +class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolAlphaDynamics(!!this.value); + } +} + +class TegakiEventSetToolFlowDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolFlowDynamics(!!this.value); + } +} + +class TegakiEventSetToolTip extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolTip(this.value); + } +} + +class TegakiEventAddLayer extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + Tegaki.addLayer(); + } +} + +class TegakiEventDeleteLayers extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + Tegaki.deleteSelectedLayers(); + } +} + +class TegakiEventMoveLayers extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.moveSelectedLayers(this.value); + } +} + +class TegakiEventMergeLayers extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + static unpack(r) { return super.unpack(r); } // FF bug 1628719 + + dispatch() { + Tegaki.mergeSelectedLayers(); + } +} + +class TegakiEventToggleLayerVisibility extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.toggleLayerVisibility(this.value); + } +} + +class TegakiEventSetActiveLayer extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.setActiveLayer(this.value); + } +} + +class TegakiEventToggleLayerSelection extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.toggleSelectedLayer(this.value); + } +} + +class TegakiEventSetSelectedLayersAlpha { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setSelectedLayersAlpha(this.value); + } +} + +const TegakiEvents = Object.freeze({ + TegakiEventPrelude: [0, TegakiEventPrelude], + + TegakiEventDrawStart: [1, TegakiEventDrawStart], + TegakiEventDraw: [2, TegakiEventDraw], + TegakiEventDrawCommit: [3, TegakiEventDrawCommit], + TegakiEventUndo: [4, TegakiEventUndo], + TegakiEventRedo: [5, TegakiEventRedo], + TegakiEventSetColor: [6, TegakiEventSetColor], + TegakiEventDrawStartNoP: [7, TegakiEventDrawStartNoP], + TegakiEventDrawNoP: [8, TegakiEventDrawNoP], + + TegakiEventSetTool: [10, TegakiEventSetTool], + TegakiEventSetToolSize: [11, TegakiEventSetToolSize], + TegakiEventSetToolAlpha: [12, TegakiEventSetToolAlpha], + TegakiEventSetToolSizeDynamics: [13, TegakiEventSetToolSizeDynamics], + TegakiEventSetToolAlphaDynamics: [14, TegakiEventSetToolAlphaDynamics], + TegakiEventSetToolTip: [15, TegakiEventSetToolTip], + TegakiEventPreserveAlpha: [16, TegakiEventPreserveAlpha], + TegakiEventSetToolFlowDynamics: [17, TegakiEventSetToolFlowDynamics], + TegakiEventSetToolFlow: [18, TegakiEventSetToolFlow], + + TegakiEventAddLayer: [20, TegakiEventAddLayer], + TegakiEventDeleteLayers: [21, TegakiEventDeleteLayers], + TegakiEventMoveLayers: [22, TegakiEventMoveLayers], + TegakiEventMergeLayers: [23, TegakiEventMergeLayers], + TegakiEventToggleLayerVisibility: [24, TegakiEventToggleLayerVisibility], + TegakiEventSetActiveLayer: [25, TegakiEventSetActiveLayer], + TegakiEventToggleLayerSelection: [26, TegakiEventToggleLayerSelection], + TegakiEventSetSelectedLayersAlpha: [27, TegakiEventSetSelectedLayersAlpha], + + TegakiEventHistoryDummy: [254, TegakiEventHistoryDummy], + + TegakiEventConclusion: [255, TegakiEventConclusion] +}); +class TegakiReplayRecorder { + constructor() { + this.formatVersion = 1; + + this.compressed = true; + + this.tegakiVersion = Tegaki.VERSION.split('.').map((v) => +v); + + this.canvasWidth = Tegaki.baseWidth; + this.canvasHeight = Tegaki.baseHeight; + + this.bgColor = $T.hexToRgb(Tegaki.bgColor); + this.toolColor = $T.hexToRgb(Tegaki.toolColor); + + this.toolId = Tegaki.tools[Tegaki.defaultTool].id; + + this.toolList = this.buildToolList(Tegaki.tools); + + this.startTimeStamp = 0; + this.endTimeStamp = 0; + + this.recording = false; + + this.events = []; + + this.mimeType = 'application/octet-stream'; + } + + buildToolList(tools) { + var k, tool, toolMap; + + toolMap = []; + + for (k in tools) { + tool = tools[k]; + + toolMap.push({ + id: tool.id, + size: tool.size, + alpha: tool.alpha, + flow: tool.flow, + step: tool.step, + sizeDynamicsEnabled: +tool.sizeDynamicsEnabled, + alphaDynamicsEnabled: +tool.alphaDynamicsEnabled, + flowDynamicsEnabled: +tool.flowDynamicsEnabled, + usePreserveAlpha: +tool.usePreserveAlpha, + tipId: tool.tipId + }); + } + + return toolMap; + } + + start() { + if (this.recording) { + return; + } + + if (this.endTimeStamp > 0) { + this.events.pop(); + this.endTimeStamp = 0; + } + + if (this.startTimeStamp === 0) { + this.events.push(new TegakiEventPrelude(performance.now())); + this.startTimeStamp = Date.now(); + } + + this.recording = true; + } + + stop() { + if (this.startTimeStamp === 0 || !this.recording) { + return; + } + + this.events.push(new TegakiEventConclusion(performance.now())); + this.endTimeStamp = Date.now(); + this.recording = false; + } + + push(e) { + if (this.recording) { + if (e.coalesce && this.events[this.events.length - 1].type === e.type) { + this.events[this.events.length - 1] = e; + } + else { + this.events.push(e); + } + } + } + + getEventStackSize() { + var e, size; + + size = 4; + + for (e of this.events) { + size += e.size; + } + + return size; + } + + getHeaderSize() { + return 12; + } + + getMetaSize() { + return 21; + } + + getToolSize() { + return 19; + } + + getToolListSize() { + return 2 + (this.toolList.length * this.getToolSize()); + } + + writeToolList(w) { + var tool, field, fields; + + fields = [ + ['id', 'Uint8'], + ['size', 'Uint8'], + ['alpha', 'Float32'], + ['step', 'Float32'], + ['sizeDynamicsEnabled', 'Uint8'], + ['alphaDynamicsEnabled', 'Uint8'], + ['usePreserveAlpha', 'Uint8'], + ['tipId', 'Int8'], + ['flow', 'Float32'], + ['flowDynamicsEnabled', 'Uint8'], + ]; + + w.writeUint8(this.toolList.length); + + w.writeUint8(this.getToolSize()); + + for (tool of this.toolList) { + for (field of fields) { + w['write' + field[1]](tool[field[0]]); + } + } + } + + writeMeta(w) { + w.writeUint16(this.getMetaSize()); + + w.writeUint32(Math.ceil(this.startTimeStamp / 1000)); + w.writeUint32(Math.ceil(this.endTimeStamp / 1000)); + + w.writeUint16(this.canvasWidth); + w.writeUint16(this.canvasHeight); + + w.writeUint8(this.bgColor[0]); + w.writeUint8(this.bgColor[1]); + w.writeUint8(this.bgColor[2]); + + w.writeUint8(this.toolColor[0]); + w.writeUint8(this.toolColor[1]); + w.writeUint8(this.toolColor[2]); + + w.writeUint8(this.toolId); + } + + writeEventStack(w) { + var event; + + w.writeUint32(this.events.length); + + for (event of this.events) { + event.pack(w); + } + } + + writeHeader(w, dataSize) { + w.writeUint8(0x54); + w.writeUint8(0x47); + w.writeUint8(0x4B); + + w.writeUint8(+this.compressed); + + w.writeUint32(dataSize); + + w.writeUint8(this.tegakiVersion[0]); + w.writeUint8(this.tegakiVersion[1]); + w.writeUint8(this.tegakiVersion[2]); + w.writeUint8(this.formatVersion); + } + + compressData(w) { + return UZIP.deflateRaw(new Uint8Array(w.buf), { level: 9 }); + } + + toUint8Array() { + var headerSize, dataSize, data, w, compData, bytes; + + if (!this.startTimeStamp || !this.endTimeStamp) { + return null; + } + + headerSize = this.getHeaderSize(); + dataSize = this.getMetaSize() + this.getToolListSize() + this.getEventStackSize(); + + data = new ArrayBuffer(dataSize); + + w = new TegakiBinWriter(data); + + this.writeMeta(w); + + this.writeToolList(w); + + this.writeEventStack(w); + + compData = this.compressData(w); + //compData = new Uint8Array(data.slice(0)); + + w = new TegakiBinWriter(new ArrayBuffer(headerSize + compData.length)); + + this.writeHeader(w, dataSize); + + bytes = new Uint8Array(w.buf); + + bytes.set(compData, headerSize); + + return bytes; + } + + toBlob() { + var ary = this.toUint8Array(); + + if (!ary) { + return null; + } + + return new Blob([ary.buffer], { type: this.mimeType }); + } +} +class TegakiReplayViewer { + constructor() { + this.formatVersion = 1; + + this.compressed = true; + + this.tegakiVersion = [0, 0, 0]; + + this.dataSize = 0; + + this.canvasWidth = 0; + this.canvasHeight = 0; + + this.bgColor = [0, 0, 0]; + this.toolColor = [0, 0, 0]; + + this.toolId = 1; + + this.toolMap = {}; + + this.startTimeStamp = 0; + this.endTimeStamp = 0; + + this.loaded = false; + this.playing = false; + this.gapless = true; + + this.autoPaused = false; + + this.destroyed = false; + + this.speedIndex = 1; + this.speedList = [0.5, 1.0, 2.0, 5.0, 10.0, 25.0]; + this.speed = this.speedList[this.speedIndex]; + + this.maxEventsPerFrame = 50; + + this.maxEventCount = 8640000; + + this.events = []; + + this.preludePos = 0.0; + this.currentPos = 0.0; + this.conclusionPos = 0.0; + this.duration = 0.0; + + this.playTimeStart = 0.0; + this.playTimeCurrent = 0.0; + + this.eventIndex = 0; + + this.maxCanvasWH = 8192; + + this.maxGapTime = 3000; + + this.uiAccTime = 0; + + this.onFrameThis = this.onFrame.bind(this); + } + + destroy() { + this.destroyed = true; + this.pause(); + this.events = null; + } + + speedUp() { + if (this.speedIndex + 1 < this.speedList.length) { + this.speed = this.speedList[++this.speedIndex]; + } + } + + slowDown() { + if (this.speedIndex - 1 >= 0) { + this.speed = this.speedList[--this.speedIndex]; + } + } + + toggleGapless() { + this.gapless = !this.gapless; + } + + getCurrentPos() { + return this.currentPos; + } + + getDuration() { + return this.duration; + } + + loadFromURL(url) { + fetch(url) + .then((resp) => this.onResponseReady(resp)) + .catch((err) => this.onLoadError(err)); + } + + onResponseReady(resp) { + if (resp.ok) { + resp.arrayBuffer() + .then((buf) => this.onResponseBodyReady(buf)) + .catch((err) => this.onLoadError(err)); + } + else { + this.onLoadError(resp.statusText); + } + } + + onResponseBodyReady(data) { + this.loadFromBuffer(data); + Tegaki.onReplayLoaded(); + } + + onLoadError(err) { + TegakiUI.printMsg(TegakiStrings.errorLoadReplay + err, 0); + } + + autoPause() { + this.autoPaused = true; + this.pause(); + } + + pause(rewind) { + window.cancelAnimationFrame(this.onFrameThis); + + this.playing = false; + + if (rewind) { + this.currentPos = 0; + this.eventIndex = 0; + } + + Tegaki.onReplayTimeChanged(); + Tegaki.onReplayPlayPauseChanged(); + } + + rewind() { + this.autoPaused = false; + this.pause(true); + Tegaki.onReplayReset(); + } + + play() { + this.playTimeStart = performance.now(); + this.playTimeCurrent = this.playTimeStart; + + this.playing = true; + this.autoPaused = false; + + this.uiAccTime = 0; + + Tegaki.onReplayPlayPauseChanged(); + + window.requestAnimationFrame(this.onFrameThis); + } + + togglePlayPause() { + if (this.playing) { + this.pause(); + } + else { + this.play(); + } + } + + onFrame(ts) { + var delta = ts - this.playTimeCurrent; + + if (!this.playing) { + return; + } + + this.playTimeCurrent = ts; + + this.step(delta); + + this.uiAccTime += delta; + + if (this.uiAccTime > 1000) { + Tegaki.onReplayTimeChanged(); + this.uiAccTime = 0; + } + + if (this.currentPos < this.duration) { + window.requestAnimationFrame(this.onFrameThis); + } + else { + this.pause(); + } + } + + step(delta) { + var event, currentEventTime, i; + + this.currentPos += (delta * this.speed); + + currentEventTime = this.currentPos + this.preludePos; + + if (this.gapless && this.eventIndex < this.events.length) { + event = this.events[this.eventIndex]; + + if (event.timeStamp - currentEventTime > this.maxGapTime) { + this.currentPos = event.timeStamp - this.preludePos; + currentEventTime = event.timeStamp; + } + } + + i = 0; + + while (this.eventIndex < this.events.length) { + event = this.events[this.eventIndex]; + + if (event.timeStamp <= currentEventTime) { + if (i >= this.maxEventsPerFrame) { + this.currentPos = event.timeStamp - this.preludePos; + break; + } + + event.dispatch(); + + ++this.eventIndex; + ++i; + } + else { + break; + } + } + } + + getEventIdMap() { + var map, key, val; + + map = {}; + + for (key in TegakiEvents) { + val = TegakiEvents[key]; + map[val[0]] = val[1]; + } + + return map; + } + + readToolMap(r) { + var i, len, size, tool, field, fields, pos; + + this.toolMap = {}; + + fields = [ + ['id', 'Uint8'], + ['size', 'Uint8'], + ['alpha', 'Float32'], + ['step', 'Float32'], + ['sizeDynamicsEnabled', 'Uint8'], + ['alphaDynamicsEnabled', 'Uint8'], + ['usePreserveAlpha', 'Uint8'], + ['tipId', 'Int8'], + ['flow', 'Float32'], + ['flowDynamicsEnabled', 'Uint8'], + ]; + + len = r.readUint8(); + + size = r.readUint8(); + + for (i = 0; i < len; ++i) { + pos = r.pos + size; + + tool = {}; + + for (field of fields) { + if (r.pos >= pos) { + break; + } + + tool[field[0]] = r['read' + field[1]](); + } + + this.toolMap[tool.id] = tool; + + r.pos = pos; + } + } + + readHeader(r) { + var tgk; + + tgk = String.fromCharCode(r.readUint8(), r.readUint8(), r.readUint8()); + + if (tgk !== 'TGK') { + throw 'invalid header'; + } + + this.compressed = r.readUint8() === 1; + + this.dataSize = r.readUint32(); + + this.tegakiVersion[0] = r.readUint8(); + this.tegakiVersion[1] = r.readUint8(); + this.tegakiVersion[2] = r.readUint8(); + + this.formatVersion = r.readUint8(); + } + + decompressData(r) { + return UZIP.inflateRaw( + new Uint8Array(r.buf, r.pos), + new Uint8Array(this.dataSize) + ); + } + + readMeta(r) { + var pos, size; + + size = r.readUint16(); + + pos = r.pos + size - 2; + + this.startTimeStamp = r.readUint32() * 1000; + this.endTimeStamp = r.readUint32() * 1000; + + this.canvasWidth = r.readUint16(); + this.canvasHeight = r.readUint16(); + + if (this.canvasWidth > this.maxCanvasWH + || this.canvasHeight > this.maxCanvasWH) { + throw 'canvas too large'; + } + + this.bgColor[0] = r.readUint8(); + this.bgColor[1] = r.readUint8(); + this.bgColor[2] = r.readUint8(); + + this.toolColor[0] = r.readUint8(); + this.toolColor[1] = r.readUint8(); + this.toolColor[2] = r.readUint8(); + + this.toolId = r.readUint8(); + + r.pos = pos; + } + + readEventStack(r) { + var i, len, type, klass, event, eventMap; + + eventMap = this.getEventIdMap(); + + len = r.readUint32(); + + if (len < 1 || len > this.maxEventCount) { + throw 'invalid event count'; + } + + for (i = 0; i < len; ++i) { + type = r.readUint8(); + + klass = eventMap[type]; + + if (!klass) { + throw 'invalid event id'; + } + + event = klass.unpack(r); + + this.events.push(event); + } + + if (this.events[0].type !== TegakiEvents.TegakiEventPrelude[0]) { + throw 'invalid prelude'; + } + + if (this.events[len - 1].type !== TegakiEvents.TegakiEventConclusion[0]) { + throw 'invalid conclusion'; + } + + this.preludePos = this.events[0].timeStamp; + this.conclusionPos = this.events[len - 1].timeStamp; + + this.duration = this.conclusionPos - this.preludePos; + + if (this.duration <= 0) { + throw 'invalid duration'; + } + } + + loadFromBuffer(buffer) { + var r, data; + + if (this.destroyed || this.loaded) { + return false; + } + + r = new TegakiBinReader(buffer); + + this.readHeader(r); + + data = this.decompressData(r); + + r = new TegakiBinReader(data.buffer); + + this.readMeta(r); + + this.readToolMap(r); + + this.readEventStack(r); + + this.loaded = true; + + return true; + } +} +var TegakiUI = { + draggedNode: null, + + draggedLabelLastX: 0, + draggedLabelFn: null, + + statusTimeout: 0, + + layerPreviewCtxCache: new WeakMap(), + + getLayerPreviewSize: function() { + return $T.calcThumbSize(Tegaki.baseWidth, Tegaki.baseHeight, 24); + }, + + setupDragLabel: function(e, moveFn) { + TegakiUI.draggedLabelFn = moveFn; + TegakiUI.draggedLabelLastX = e.clientX; + $T.on(document, 'pointermove', TegakiUI.processDragLabel); + $T.on(document, 'pointerup', TegakiUI.clearDragLabel); + }, + + processDragLabel: function(e) { + TegakiUI.draggedLabelFn.call(Tegaki, e.clientX - TegakiUI.draggedLabelLastX); + TegakiUI.draggedLabelLastX = e.clientX; + }, + + clearDragLabel: function(e) { + $T.off(document, 'pointermove', TegakiUI.processDragLabel); + $T.off(document, 'pointerup', TegakiUI.clearDragLabel); + }, + + printMsg: function(str, timeout = 5000) { + TegakiUI.clearMsg(); + + $T.id('tegaki-status-output').textContent = str; + + if (timeout > 0) { + TegakiUI.statusTimeout = setTimeout(TegakiUI.clearMsg, 5000); + } + }, + + clearMsg: function() { + if (TegakiUI.statusTimeout) { + clearTimeout(TegakiUI.statusTimeout); + TegakiUI.statusTimeout = 0; + } + + $T.id('tegaki-status-output').textContent = ''; + }, + + buildUI: function() { + var bg, cnt, el, ctrl, layersCnt, canvasCnt; + + // + // Grid container + // + bg = $T.el('div'); + bg.id = 'tegaki'; + + // + // Menu area + // + el = $T.el('div'); + el.id = 'tegaki-menu-cnt'; + + if (!Tegaki.replayMode) { + el.appendChild(TegakiUI.buildMenuBar()); + } + else { + el.appendChild(TegakiUI.buildViewerMenuBar()); + el.appendChild(TegakiUI.buildReplayControls()); + } + + el.appendChild(TegakiUI.buildToolModeBar()); + + bg.appendChild(el); + + bg.appendChild(TegakiUI.buildDummyFilePicker()); + + // + // Tools area + // + cnt = $T.el('div'); + cnt.id = 'tegaki-tools-cnt'; + + cnt.appendChild(TegakiUI.buildToolsMenu()); + + bg.appendChild(cnt); + + // + // Canvas area + // + [canvasCnt, layersCnt] = TegakiUI.buildCanvasCnt(); + + bg.appendChild(canvasCnt); + + // + // Controls area + // + ctrl = $T.el('div'); + ctrl.id = 'tegaki-ctrl-cnt'; + + // Zoom control + ctrl.appendChild(TegakiUI.buildZoomCtrlGroup()); + + // Colorpicker + ctrl.appendChild( + TegakiUI.buildColorCtrlGroup(Tegaki.toolColor) + ); + + // Size control + ctrl.appendChild(TegakiUI.buildSizeCtrlGroup()); + + // Alpha control + ctrl.appendChild(TegakiUI.buildAlphaCtrlGroup()); + + // Flow control + ctrl.appendChild(TegakiUI.buildFlowCtrlGroup()); + + // Layers control + ctrl.appendChild(TegakiUI.buildLayersCtrlGroup()); + + // --- + + bg.appendChild(ctrl); + + // + // Status area + // + bg.appendChild(TegakiUI.buildStatusCnt()); + + return [bg, canvasCnt, layersCnt]; + }, + + buildDummyFilePicker: function() { + var el = $T.el('input'); + + el.type = 'file'; + el.id = 'tegaki-filepicker'; + el.className = 'tegaki-hidden'; + el.accept = 'image/png, image/jpeg'; + $T.on(el, 'change', Tegaki.onOpenFileSelected); + + return el; + }, + + buildMenuBar: function() { + var frag, btn; + + frag = $T.el('div'); + frag.id = 'tegaki-menu-bar'; + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.newCanvas; + $T.on(btn, 'click', Tegaki.onNewClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.open; + $T.on(btn, 'click', Tegaki.onOpenClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.export; + $T.on(btn, 'click', Tegaki.onExportClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-undo-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.undo; + btn.title = TegakiKeybinds.getCaption('undo'); + $T.on(btn, 'click', Tegaki.onUndoClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-redo-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.redo; + btn.title = TegakiKeybinds.getCaption('redo'); + $T.on(btn, 'click', Tegaki.onRedoClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.close; + $T.on(btn, 'click', Tegaki.onCancelClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-finish-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.finish; + $T.on(btn, 'click', Tegaki.onDoneClick); + frag.appendChild(btn); + + return frag; + }, + + buildViewerMenuBar: function() { + var frag, btn; + + frag = $T.el('div'); + frag.id = 'tegaki-menu-bar'; + + btn = $T.el('span'); + btn.id = 'tegaki-finish-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.close; + $T.on(btn, 'click', Tegaki.onCloseViewerClick); + frag.appendChild(btn); + + return frag; + }, + + buildToolModeBar: function() { + var cnt, grp, el, btn; + + cnt = $T.el('div'); + cnt.id = 'tegaki-toolmode-bar'; + + if (!Tegaki.tool) { + cnt.classList.add('tegaki-hidden'); + } + + // Dynamics + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-dynamics'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.className = 'tegaki-toolmode-lbl'; + el.textContent = TegakiStrings.pressure; + grp.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-tool-mode-dynamics-ctrl'; + el.className = 'tegaki-toolmode-ctrl'; + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-size'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.size; + $T.on(btn, 'mousedown', Tegaki.onToolPressureSizeClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-alpha'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.alpha; + $T.on(btn, 'mousedown', Tegaki.onToolPressureAlphaClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-flow'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.flow; + $T.on(btn, 'mousedown', Tegaki.onToolPressureFlowClick); + el.appendChild(btn); + + grp.appendChild(el); + + cnt.appendChild(grp); + + // Preserve Alpha + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-mask'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.id = 'tegaki-toolmode-ctrl-tip'; + el.className = 'tegaki-toolmode-ctrl'; + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-mask-alpha'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.preserveAlpha; + $T.on(btn, 'mousedown', Tegaki.onToolPreserveAlphaClick); + el.appendChild(btn); + + grp.appendChild(el); + + cnt.appendChild(grp); + + // Tip + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-tip'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.className = 'tegaki-toolmode-lbl'; + el.textContent = TegakiStrings.tip; + grp.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-tool-mode-tip-ctrl'; + el.className = 'tegaki-toolmode-ctrl'; + grp.appendChild(el); + + cnt.appendChild(grp); + + return cnt; + }, + + buildToolsMenu: function() { + var grp, el, lbl, name; + + grp = $T.el('div'); + grp.id = 'tegaki-tools-grid'; + + for (name in Tegaki.tools) { + el = $T.el('span'); + el.setAttribute('data-tool', name); + + lbl = TegakiStrings[name]; + + if (Tegaki.tools[name].keybind) { + lbl += ' (' + Tegaki.tools[name].keybind.toUpperCase() + ')'; + } + + el.setAttribute('title', lbl); + el.id = 'tegaki-tool-btn-' + name; + el.className = 'tegaki-tool-btn tegaki-icon tegaki-' + name; + + $T.on(el, 'click', Tegaki.onToolClick); + + grp.appendChild(el); + } + + return grp; + }, + + buildCanvasCnt: function() { + var canvasCnt, wrap, layersCnt; + + canvasCnt = $T.el('div'); + canvasCnt.id = 'tegaki-canvas-cnt'; + + wrap = $T.el('div'); + wrap.id = 'tegaki-layers-wrap'; + + layersCnt = $T.el('div'); + layersCnt.id = 'tegaki-layers'; + + wrap.appendChild(layersCnt); + + canvasCnt.appendChild(wrap); + + return [canvasCnt, layersCnt]; + }, + + buildCtrlGroup: function(id, title) { + var cnt, el; + + cnt = $T.el('div'); + cnt.className = 'tegaki-ctrlgrp'; + + if (id) { + cnt.id = 'tegaki-ctrlgrp-' + id; + } + + if (title !== undefined) { + el = $T.el('div'); + el.className = 'tegaki-ctrlgrp-title'; + el.textContent = title; + cnt.appendChild(el); + } + + return cnt; + }, + + buildLayersCtrlGroup: function() { + var el, ctrl, row, cnt; + + ctrl = this.buildCtrlGroup('layers', TegakiStrings.layers); + + // Layer options row + row = $T.el('div'); + row.id = 'tegaki-layers-opts'; + + // Alpha + cnt = $T.el('div'); + cnt.id = 'tegaki-layer-alpha-cell'; + + el = $T.el('span'); + el.className = 'tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl'; + el.textContent = TegakiStrings.alpha; + $T.on(el, 'pointerdown', Tegaki.onLayerAlphaDragStart); + cnt.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-layer-alpha-opt'; + el.className = 'tegaki-stealth-input tegaki-range-lbl-xs'; + el.setAttribute('maxlength', 3); + $T.on(el, 'input', Tegaki.onLayerAlphaChange); + cnt.appendChild(el); + + row.appendChild(cnt); + + ctrl.appendChild(row); + + el = $T.el('div'); + el.id = 'tegaki-layers-grid'; + ctrl.appendChild(el); + + row = $T.el('div'); + row.id = 'tegaki-layers-ctrl'; + + el = $T.el('span'); + el.title = TegakiStrings.addLayer; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; + $T.on(el, 'click', Tegaki.onLayerAddClick); + row.appendChild(el); + + el = $T.el('span'); + el.title = TegakiStrings.delLayers; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; + $T.on(el, 'click', Tegaki.onLayerDeleteClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-merge'; + el.title = TegakiStrings.mergeLayers; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-level-down'; + $T.on(el, 'click', Tegaki.onMergeLayersClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-up'; + el.title = TegakiStrings.moveLayerUp; + el.setAttribute('data-up', '1'); + el.className = 'tegaki-ui-btn tegaki-icon tegaki-up-open'; + $T.on(el, 'click', Tegaki.onMoveLayerClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-down'; + el.title = TegakiStrings.moveLayerDown; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-down-open'; + $T.on(el, 'click', Tegaki.onMoveLayerClick); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildSizeCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('size', TegakiStrings.size); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-size'; + el.className = 'tegaki-ctrl-range'; + el.min = 1; + el.max = Tegaki.maxSize; + el.type = 'range'; + el.title = TegakiKeybinds.getCaption('toolSize'); + $T.on(el, 'input', Tegaki.onToolSizeChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-size-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolSizeChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildAlphaCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('alpha', TegakiStrings.alpha); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-alpha'; + el.className = 'tegaki-ctrl-range'; + el.min = 0; + el.max = 100; + el.step = 1; + el.type = 'range'; + $T.on(el, 'input', Tegaki.onToolAlphaChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-alpha-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolAlphaChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildFlowCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('flow', TegakiStrings.flow); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-flow'; + el.className = 'tegaki-ctrl-range'; + el.min = 0; + el.max = 100; + el.step = 1; + el.type = 'range'; + $T.on(el, 'input', Tegaki.onToolFlowChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-flow-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolFlowChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildZoomCtrlGroup: function() { + var el, btn, ctrl; + + ctrl = this.buildCtrlGroup('zoom', TegakiStrings.zoom); + + btn = $T.el('div'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; + btn.id = 'tegaki-zoomin-btn'; + btn.setAttribute('data-in', 1); + $T.on(btn, 'click', Tegaki.onZoomChange); + ctrl.appendChild(btn); + + btn = $T.el('div'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; + btn.id = 'tegaki-zoomout-btn'; + btn.setAttribute('data-out', 1); + $T.on(btn, 'click', Tegaki.onZoomChange); + ctrl.appendChild(btn); + + el = $T.el('div'); + el.id = 'tegaki-zoom-lbl'; + ctrl.appendChild(el); + + return ctrl; + }, + + buildColorCtrlGroup: function(mainColor) { + var el, cnt, btn, ctrl, color, edge, i, palette, cls; + + edge = / Edge\//i.test(window.navigator.userAgent); + + ctrl = this.buildCtrlGroup('color', TegakiStrings.color); + + cnt = $T.el('div'); + cnt.id = 'tegaki-color-ctrl'; + + el = $T.el('div'); + el.id = 'tegaki-color'; + edge && el.classList.add('tegaki-hidden'); + el.style.backgroundColor = mainColor; + $T.on(el, 'mousedown', Tegaki.onMainColorClick); + cnt.appendChild(el); + + el = $T.el('div'); + el.id = 'tegaki-palette-switcher'; + + btn = $T.el('span'); + btn.id = 'tegaki-palette-prev-btn'; + btn.title = TegakiStrings.switchPalette; + btn.setAttribute('data-prev', '1'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled'; + $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-palette-next-btn'; + btn.title = TegakiStrings.switchPalette; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-right-open'; + $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); + el.appendChild(btn); + + cnt.appendChild(el); + + ctrl.appendChild(cnt); + + cnt = $T.el('div'); + cnt.id = 'tegaki-color-grids'; + + for (i = 0; i < TegakiColorPalettes.length; ++i) { + el = $T.el('div'); + + el.setAttribute('data-id', i); + + cls = 'tegaki-color-grid'; + + palette = TegakiColorPalettes[i]; + + if (palette.length <= 18) { + cls += ' tegaki-color-grid-20'; + } + else { + cls += ' tegaki-color-grid-15'; + } + + if (i > 0) { + cls += ' tegaki-hidden'; + } + + el.className = cls; + + for (color of palette) { + btn = $T.el('div'); + btn.title = TegakiStrings.paletteSlotReplace; + btn.className = 'tegaki-color-btn'; + btn.setAttribute('data-color', color); + btn.style.backgroundColor = color; + $T.on(btn, 'mousedown', Tegaki.onPaletteColorClick); + el.appendChild(btn); + } + + cnt.appendChild(el); + } + + ctrl.appendChild(cnt); + + el = $T.el('input'); + el.id = 'tegaki-colorpicker'; + !edge && el.classList.add('tegaki-invis'); + el.value = color; + el.type = 'color'; + $T.on(el, 'change', Tegaki.onColorPicked); + + ctrl.appendChild(el); + + return ctrl; + }, + + buildStatusCnt: function() { + var cnt, el; + + cnt = $T.el('div'); + cnt.id = 'tegaki-status-cnt'; + + if (Tegaki.saveReplay) { + el = $T.el('div'); + el.id = 'tegaki-status-replay'; + el.textContent = '⬤'; + el.setAttribute('title', TegakiStrings.recordingEnabled); + cnt.appendChild(el); + } + + el = $T.el('div'); + el.id = 'tegaki-status-output'; + cnt.appendChild(el); + + el = $T.el('div'); + el.id = 'tegaki-status-version'; + el.textContent = 'tegaki.js v' + Tegaki.VERSION; + cnt.appendChild(el); + + return cnt; + }, + + buildReplayControls: function() { + var cnt, btn, el; + + cnt = $T.el('div'); + cnt.id = 'tegaki-replay-controls'; + cnt.className = 'tegaki-hidden'; + + btn = $T.el('span'); + btn.id = 'tegaki-replay-gapless-btn'; + btn.className = 'tegaki-ui-cb-w'; + $T.on(btn, 'click', Tegaki.onReplayGaplessClick); + + el = $T.el('span'); + el.id = 'tegaki-replay-gapless-cb'; + el.className = 'tegaki-ui-cb'; + btn.appendChild(el); + + el = $T.el('span'); + el.className = 'tegaki-menu-lbl'; + el.textContent = TegakiStrings.gapless; + btn.appendChild(el); + + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-play-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-play'; + btn.setAttribute('title', TegakiStrings.play); + $T.on(btn, 'click', Tegaki.onReplayPlayPauseClick); + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-to-start'; + btn.setAttribute('title', TegakiStrings.rewind); + $T.on(btn, 'click', Tegaki.onReplayRewindClick); + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-slower-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-bw'; + btn.setAttribute('title', TegakiStrings.slower); + $T.on(btn, 'click', Tegaki.onReplaySlowDownClick); + cnt.appendChild(btn); + + el = $T.el('span'); + el.id = 'tegaki-replay-speed-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '1.0'; + cnt.appendChild(el); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-faster-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-fw'; + btn.setAttribute('title', TegakiStrings.faster); + $T.on(btn, 'click', Tegaki.onReplaySpeedUpClick); + cnt.appendChild(btn); + + el = $T.el('span'); + el.id = 'tegaki-replay-now-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '00:00'; + cnt.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-replay-end-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '00:00'; + cnt.appendChild(el); + + return cnt; + }, + + buildLayerGridCell: function(layer) { + var cnt, el, cell; + + cnt = $T.el('div'); + cnt.id = 'tegaki-layers-cell-' + layer.id; + cnt.className = 'tegaki-layers-cell'; + cnt.setAttribute('data-id', layer.id); + cnt.draggable = true; + cnt.setAttribute('data-id', layer.id); + + $T.on(cnt, 'pointerdown', TegakiUI.onLayerSelectorPtrDown); + $T.on(cnt, 'pointerup', Tegaki.onLayerSelectorClick); + + $T.on(cnt, 'dragstart', TegakiUI.onLayerDragStart); + $T.on(cnt, 'dragover', TegakiUI.onLayerDragOver); + $T.on(cnt, 'drop', TegakiUI.onLayerDragDrop); + $T.on(cnt, 'dragend', TegakiUI.onLayerDragEnd); + $T.on(cnt, 'dragleave', TegakiUI.onLayerDragLeave); + $T.on(cnt, 'dragexit', TegakiUI.onLayerDragLeave); + + // visibility toggle + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-v'; + + el = $T.el('span'); + el.id = 'tegaki-layers-cb-v-' + layer.id; + el.className = 'tegaki-ui-cb'; + el.setAttribute('data-id', layer.id); + el.title = TegakiStrings.toggleVisibility; + $T.on(el, 'click', Tegaki.onLayerToggleVisibilityClick); + + if (layer.visible) { + el.className += ' tegaki-ui-cb-a'; + } + + cell.appendChild(el); + cnt.appendChild(cell); + + // preview + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-p'; + + el = $T.el('canvas'); + el.id = 'tegaki-layers-p-canvas-' + layer.id; + el.className = 'tegaki-alpha-bg-xs'; + [el.width, el.height] = TegakiUI.getLayerPreviewSize(); + + cell.appendChild(el); + cnt.appendChild(cell); + + // name + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-n'; + + el = $T.el('div'); + el.id = 'tegaki-layer-name-' + layer.id; + el.className = 'tegaki-ellipsis'; + el.setAttribute('data-id', layer.id); + el.textContent = layer.name; + $T.on(el, 'dblclick', Tegaki.onLayerNameChangeClick); + + cell.appendChild(el); + cnt.appendChild(cell); + + return cnt; + }, + + // --- + + onLayerSelectorPtrDown: function(e) { + if (e.pointerType === 'mouse') { + if (this.hasAttribute('data-nodrag')) { + this.removeAttribute('data-nodrag'); + $T.on(this, 'dragstart', TegakiUI.onLayerDragStart); + } + } + else if (!this.hasAttribute('data-nodrag')) { + this.setAttribute('data-nodrag', 1); + $T.off(this, 'dragstart', TegakiUI.onLayerDragStart); + } + }, + + onLayerDragStart: function(e) { + var el, id; + + if (e.ctrlKey) { + return; + } + + TegakiUI.draggedNode = null; + + if (!$T.id('tegaki-layers-grid').children[1]) { + e.preventDefault(); + return; + } + + id = +e.target.getAttribute('data-id'); + + el = $T.el('div'); + el.className = 'tegaki-invis'; + e.dataTransfer.setDragImage(el, 0, 0); + e.dataTransfer.setData('text/plain', id); + e.dataTransfer.effectAllowed = 'move'; + + TegakiUI.draggedNode = e.target; + + TegakiUI.updateLayersGridDragExt(true); + }, + + onLayerDragOver: function(e) { + e.preventDefault(); + + e.dataTransfer.dropEffect = 'move'; + + TegakiUI.updateLayersGridDragEffect( + e.target, + +TegakiUI.draggedNode.getAttribute('data-id') + ); + }, + + onLayerDragLeave: function(e) { + TegakiUI.updateLayersGridDragEffect(); + }, + + onLayerDragEnd: function(e) { + TegakiUI.draggedNode = null; + TegakiUI.updateLayersGridDragExt(false); + TegakiUI.updateLayersGridDragEffect(); + }, + + onLayerDragDrop: function(e) { + var tgtId, srcId, belowPos; + + e.preventDefault(); + + TegakiUI.draggedNode = null; + + [tgtId] = TegakiUI.layersGridFindDropTgt(e.target); + srcId = +e.dataTransfer.getData('text/plain'); + + TegakiUI.updateLayersGridDragEffect(e.target.parentNode); + TegakiUI.updateLayersGridDragExt(false); + + if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { + return; + } + + if (!tgtId) { + belowPos = Tegaki.layers.length; + } + else { + belowPos = TegakiLayers.getLayerPosById(tgtId); + } + + if (!TegakiLayers.selectedLayersHas(srcId)) { + Tegaki.setActiveLayer(srcId); + } + + Tegaki.moveSelectedLayers(belowPos); + }, + + updateLayersGridDragExt: function(flag) { + var cnt, el; + + cnt = $T.id('tegaki-layers-grid'); + + if (!cnt.children[1]) { + return; + } + + if (flag) { + el = $T.el('div'); + el.id = 'tegaki-layers-cell-dx'; + el.draggable = true; + $T.on(el, 'dragover', TegakiUI.onLayerDragOver); + $T.on(el, 'drop', TegakiUI.onLayerDragDrop); + cnt.parentNode.insertBefore(el, cnt); + } + else { + if (el = $T.id('tegaki-layers-cell-dx')) { + el.parentNode.removeChild(el); + } + } + }, + + updateLayersGridDragEffect: function(tgt, srcId) { + var el, nodes, tgtId; + + nodes = $T.cls('tegaki-layers-cell-d', $T.id('tegaki-ctrlgrp-layers')); + + for (el of nodes) { + el.classList.remove('tegaki-layers-cell-d'); + } + + if (!tgt || !srcId) { + return; + } + + [tgtId, tgt] = TegakiUI.layersGridFindDropTgt(tgt); + + if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { + return; + } + + if (!tgt) { + tgt = $T.id('tegaki-layers-grid'); + } + + tgt.classList.add('tegaki-layers-cell-d'); + }, + + layersGridFindDropTgt: function(tgt) { + var tgtId, cnt; + + tgtId = +tgt.getAttribute('data-id'); + + cnt = $T.id('tegaki-ctrlgrp-layers'); + + while (!tgt.draggable && tgt !== cnt) { + tgt = tgt.parentNode; + tgtId = +tgt.getAttribute('data-id'); + } + + if (tgt === cnt || !tgt.draggable) { + return [0, null]; + } + + return [tgtId, tgt]; + }, + + layersGridCanDrop: function(tgtId, srcId) { + var srcEl; + + if (tgtId === srcId) { + return false; + } + + srcEl = $T.id('tegaki-layers-cell-' + srcId); + + if (!srcEl.previousElementSibling) { + if (!tgtId) { + return false; + } + } + else if (+srcEl.previousElementSibling.getAttribute('data-id') === tgtId) { + return false; + } + + return true; + }, + + // --- + + setReplayMode: function(flag) { + Tegaki.bg.classList[flag ? 'add' : 'remove']('tegaki-replay-mode'); + }, + + // --- + + onToolChanged: function() { + $T.id('tegaki-toolmode-bar').classList.remove('tegaki-hidden'); + TegakiUI.updateToolSize(); + TegakiUI.updateToolAlpha(); + TegakiUI.updateToolFlow(); + TegakiUI.updateToolModes(); + }, + + // --- + + updateLayerAlphaOpt: function() { + var el = $T.id('tegaki-layer-alpha-opt'); + el.value = Math.round(Tegaki.activeLayer.alpha * 100); + }, + + updateLayerName: function(layer) { + var el; + + if (el = $T.id('tegaki-layer-name-' + layer.id)) { + el.textContent = layer.name; + } + }, + + updateLayerPreview: function(layer) { + var canvas, ctx; + + canvas = $T.id('tegaki-layers-p-canvas-' + layer.id); + + if (!canvas) { + return; + } + + ctx = TegakiUI.getLayerPreviewCtx(layer); + + if (!ctx) { + ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; + TegakiUI.setLayerPreviewCtx(layer, ctx); + } + + $T.clearCtx(ctx); + ctx.drawImage(layer.canvas, 0, 0, canvas.width, canvas.height); + }, + + updateLayerPreviewSize: function(regen) { + var el, layer, size; + + size = TegakiUI.getLayerPreviewSize(); + + for (layer of Tegaki.layers) { + if (el = $T.id('tegaki-layers-p-canvas-' + layer.id)) { + [el.width, el.height] = size; + + if (regen) { + TegakiUI.updateLayerPreview(layer); + } + } + } + }, + + getLayerPreviewCtx: function(layer) { + TegakiUI.layerPreviewCtxCache.get(layer); + }, + + setLayerPreviewCtx: function(layer, ctx) { + TegakiUI.layerPreviewCtxCache.set(layer, ctx); + }, + + deleteLayerPreviewCtx: function(layer) { + TegakiUI.layerPreviewCtxCache.delete(layer); + }, + + updateLayersGridClear: function() { + $T.id('tegaki-layers-grid').innerHTML = ''; + }, + + updateLayersGrid: function() { + var layer, el, frag, cnt; + + frag = $T.frag(); + + for (layer of Tegaki.layers) { + el = TegakiUI.buildLayerGridCell(layer); + frag.insertBefore(el, frag.firstElementChild); + } + + TegakiUI.updateLayersGridClear(); + + cnt.appendChild(frag); + }, + + updateLayersGridActive: function(layerId) { + var el; + + el = $T.cls('tegaki-layers-cell-a', $T.id('tegaki-layers-grid'))[0]; + + if (el) { + el.classList.remove('tegaki-layers-cell-a'); + } + + el = $T.id('tegaki-layers-cell-' + layerId); + + if (el) { + el.classList.add('tegaki-layers-cell-a'); + } + + TegakiUI.updateLayerAlphaOpt(); + }, + + updateLayersGridAdd: function(layer, aboveId) { + var el, cnt, ref; + + el = TegakiUI.buildLayerGridCell(layer); + + cnt = $T.id('tegaki-layers-grid'); + + if (aboveId) { + ref = $T.id('tegaki-layers-cell-' + aboveId); + } + else { + ref = null; + } + + cnt.insertBefore(el, ref); + }, + + updateLayersGridRemove: function(id) { + var el; + + if (el = $T.id('tegaki-layers-cell-' + id)) { + el.parentNode.removeChild(el); + } + }, + + updayeLayersGridOrder: function() { + var layer, cnt, el; + + cnt = $T.id('tegaki-layers-grid'); + + for (layer of Tegaki.layers) { + el = $T.id('tegaki-layers-cell-' + layer.id); + cnt.insertBefore(el, cnt.firstElementChild); + } + }, + + updateLayersGridVisibility: function(id, flag) { + var el; + + el = $T.id('tegaki-layers-cb-v-' + id); + + if (!el) { + return; + } + + if (flag) { + el.classList.add('tegaki-ui-cb-a'); + } + else { + el.classList.remove('tegaki-ui-cb-a'); + } + }, + + updateLayersGridSelectedClear: function() { + var layer, el; + + for (layer of Tegaki.layers) { + if (el = $T.id('tegaki-layers-cell-' + layer.id)) { + el.classList.remove('tegaki-layers-cell-s'); + } + } + }, + + updateLayersGridSelectedSet: function(id, flag) { + var el; + + if (el = $T.id('tegaki-layers-cell-' + id)) { + if (flag) { + el.classList.add('tegaki-layers-cell-s'); + } + else { + el.classList.remove('tegaki-layers-cell-s'); + } + } + }, + + updateToolSize: function() { + var el = $T.id('tegaki-ctrlgrp-size'); + + if (Tegaki.tool.useSize) { + el.classList.remove('tegaki-hidden'); + + $T.id('tegaki-size-lbl').value = Tegaki.tool.size; + $T.id('tegaki-size').value = Tegaki.tool.size; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolAlpha: function() { + var val, el = $T.id('tegaki-ctrlgrp-alpha'); + + if (Tegaki.tool.useAlpha) { + el.classList.remove('tegaki-hidden'); + + val = Math.round(Tegaki.tool.alpha * 100); + $T.id('tegaki-alpha-lbl').value = val; + $T.id('tegaki-alpha').value = val; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolFlow: function() { + var val, el = $T.id('tegaki-ctrlgrp-flow'); + + if (Tegaki.tool.useFlow) { + el.classList.remove('tegaki-hidden'); + + val = Math.round(Tegaki.tool.flow * 100); + $T.id('tegaki-flow-lbl').value = val; + $T.id('tegaki-flow').value = val; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolDynamics: function() { + var ctrl, cb; + + ctrl = $T.id('tegaki-tool-mode-dynamics'); + + if (!Tegaki.tool.usesDynamics()) { + ctrl.classList.add('tegaki-hidden'); + } + else { + cb = $T.id('tegaki-tool-mode-dynamics-size'); + + if (Tegaki.tool.useSizeDynamics) { + if (Tegaki.tool.sizeDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + cb = $T.id('tegaki-tool-mode-dynamics-alpha'); + + if (Tegaki.tool.useAlphaDynamics) { + if (Tegaki.tool.alphaDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + cb = $T.id('tegaki-tool-mode-dynamics-flow'); + + if (Tegaki.tool.useFlowDynamics) { + if (Tegaki.tool.flowDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolShape: function() { + var tipId, ctrl, cnt, btn, tipList; + + ctrl = $T.id('tegaki-tool-mode-tip'); + + if (!Tegaki.tool.tipList) { + ctrl.classList.add('tegaki-hidden'); + } + else { + tipList = Tegaki.tool.tipList; + + cnt = $T.id('tegaki-tool-mode-tip-ctrl'); + + cnt.innerHTML = ''; + + for (tipId = 0; tipId < tipList.length; ++tipId) { + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-tip-' + tipId; + btn.className = 'tegaki-sw-btn'; + btn.setAttribute('data-id', tipId); + btn.textContent = TegakiStrings[tipList[tipId]]; + + $T.on(btn, 'mousedown', Tegaki.onToolTipClick); + + cnt.appendChild(btn); + + if (Tegaki.tool.tipId === tipId) { + btn.classList.add('tegaki-sw-btn-a'); + } + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolPreserveAlpha: function() { + var cb, ctrl; + + ctrl = $T.id('tegaki-tool-mode-mask'); + + if (!Tegaki.tool.usePreserveAlpha) { + ctrl.classList.add('tegaki-hidden'); + } + else { + cb = $T.id('tegaki-tool-mode-mask-alpha'); + + if (Tegaki.tool.preserveAlphaEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolModes: function() { + var el, flag; + + TegakiUI.updateToolShape(); + TegakiUI.updateToolDynamics(); + TegakiUI.updateToolPreserveAlpha(); + + flag = false; + + for (el of $T.id('tegaki-toolmode-bar').children) { + if (!flag && !el.classList.contains('tegaki-hidden')) { + el.classList.add('tegaki-ui-borderless'); + flag = true; + } + else { + el.classList.remove('tegaki-ui-borderless'); + } + } + }, + + updateUndoRedo: function(undoSize, redoSize) { + var u, r; + + if (Tegaki.replayMode) { + return; + } + + u = $T.id('tegaki-undo-btn').classList; + r = $T.id('tegaki-redo-btn').classList; + + if (undoSize) { + if (u.contains('tegaki-disabled')) { + u.remove('tegaki-disabled'); + } + } + else { + if (!u.contains('tegaki-disabled')) { + u.add('tegaki-disabled'); + } + } + + if (redoSize) { + if (r.contains('tegaki-disabled')) { + r.remove('tegaki-disabled'); + } + } + else { + if (!r.contains('tegaki-disabled')) { + r.add('tegaki-disabled'); + } + } + }, + + updateZoomLevel: function() { + $T.id('tegaki-zoom-lbl').textContent = (Tegaki.zoomFactor * 100) + '%'; + + if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel >= Tegaki.zoomFactorList.length) { + $T.id('tegaki-zoomin-btn').classList.add('tegaki-disabled'); + } + else { + $T.id('tegaki-zoomin-btn').classList.remove('tegaki-disabled'); + } + + if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel <= 0) { + $T.id('tegaki-zoomout-btn').classList.add('tegaki-disabled'); + } + else { + $T.id('tegaki-zoomout-btn').classList.remove('tegaki-disabled'); + } + }, + + updateColorPalette: function() { + var el, nodes, id; + + id = Tegaki.colorPaletteId; + + nodes = $T.cls('tegaki-color-grid', $T.id('tegaki-color-grids')); + + for (el of nodes) { + if (+el.getAttribute('data-id') === id) { + el.classList.remove('tegaki-hidden'); + } + else { + el.classList.add('tegaki-hidden'); + } + } + + el = $T.id('tegaki-palette-prev-btn'); + + if (id === 0) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + + el = $T.id('tegaki-palette-next-btn'); + + if (id === TegakiColorPalettes.length - 1) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + }, + + updateReplayTime: function(full) { + var now, end, r = Tegaki.replayViewer; + + now = r.getCurrentPos(); + + end = r.getDuration(); + + if (now > end) { + now = end; + } + + $T.id('tegaki-replay-now-lbl').textContent = $T.msToHms(now); + + if (full) { + $T.id('tegaki-replay-end-lbl').textContent = $T.msToHms(end); + } + }, + + updateReplayControls: function() { + TegakiUI.updateReplayGapless(); + TegakiUI.updateReplayPlayPause(); + TegakiUI.updateReplaySpeed(); + }, + + updateReplayGapless: function() { + var el, r = Tegaki.replayViewer; + + el = $T.id('tegaki-replay-gapless-cb'); + + if (r.gapless) { + el.classList.add('tegaki-ui-cb-a'); + } + else { + el.classList.remove('tegaki-ui-cb-a'); + } + }, + + updateReplayPlayPause: function() { + var el, r = Tegaki.replayViewer; + + el = $T.id('tegaki-replay-play-btn'); + + if (r.playing) { + el.classList.remove('tegaki-play'); + el.classList.add('tegaki-pause'); + el.setAttribute('title', TegakiStrings.pause); + } + else { + el.classList.add('tegaki-play'); + el.classList.remove('tegaki-pause'); + el.setAttribute('title', TegakiStrings.play); + + if (r.getCurrentPos() < r.getDuration()) { + el.classList.remove('tegaki-disabled'); + } + else { + el.classList.add('tegaki-disabled'); + } + } + }, + + updateReplaySpeed: function() { + var el, r = Tegaki.replayViewer; + + $T.id('tegaki-replay-speed-lbl').textContent = r.speed.toFixed(1); + + el = $T.id('tegaki-replay-slower-btn'); + + if (r.speedIndex === 0) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + + el = $T.id('tegaki-replay-faster-btn'); + + if (r.speedIndex === r.speedList.length - 1) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + }, + + enableReplayControls: function(flag) { + if (flag) { + $T.id('tegaki-replay-controls').classList.remove('tegaki-hidden'); + } + else { + $T.id('tegaki-replay-controls').classList.add('tegaki-hidden'); + } + }, + + setRecordingStatus: function(flag) { + var el = $T.id('tegaki-status-replay'); + + if (flag) { + el.classList.remove('tegaki-hidden'); + } + else { + el.classList.add('tegaki-hidden'); + } + } +}; +/*! UZIP.js, © 2018 Photopea, MIT License */ + +var UZIP = {}; +if(typeof module == "object") module.exports = UZIP; + +UZIP.inflateRaw = function(file, buf) { return UZIP.F.inflate(file, buf); } + +UZIP.deflateRaw = function(data, opts) { + if(opts==null) opts={level:6}; + var buf=new Uint8Array(50+Math.floor(data.length*1.1)); + var off = UZIP.F.deflateRaw(data, buf, off, opts.level); + return new Uint8Array(buf.buffer, 0, off); +} + +UZIP.bin = { + readUshort : function(buff,p) { return (buff[p]) | (buff[p+1]<<8); }, + writeUshort: function(buff,p,n){ buff[p] = (n)&255; buff[p+1] = (n>>8)&255; }, + readUint : function(buff,p) { return (buff[p+3]*(256*256*256)) + ((buff[p+2]<<16) | (buff[p+1]<< 8) | buff[p]); }, + 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; }, + readASCII : function(buff,p,l){ var s = ""; for(var i=0; i> 6)); buff[p+i+1] = (128|((code>> 0)&63)); i+=2; } + 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; } + 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; } + else throw "e"; + } + return i; + }, + sizeUTF8 : function(str) { + var strl = str.length, i=0; + for(var ci=0; ci>>3; + } + + 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 + if(dlen>2) { nc=UZIP.F._hash(data,0); strt[nc]=0; } + var nmch=0,nmci=0; + + for(i=0; i14000 || lc>26697) && (dlen-i)>100) { + if(cvrd>>16)>>16)>(mch>>>16)) mch=0; + }//*/ + var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; + if(mch!=0) { + var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; + var lgi = goodIndex(len, U.of0); U.lhst[257+lgi]++; + var dgi = goodIndex(dst, U.df0); U.dhst[ dgi]++; ebits += U.exb[lgi] + U.dxb[dgi]; + lits[li] = (len<<23)|(i-cvrd); lits[li+1] = (dst<<16)|(lgi<<8)|dgi; li+=2; + cvrd = i + len; + } + else { U.lhst[data[i]]++; } + lc++; + } + } + if(bs!=i || data.length==0) { + if(cvrd>>3; +} +UZIP.F._bestMatch = function(data, i, prev, c, nice, chain) { + var ci = (i&0x7fff), pi=prev[ci]; + //console.log("----", i); + var dif = ((ci-pi + (1<<15)) & 0x7fff); if(pi==ci || c!=UZIP.F._hash(data,i-dif)) return 0; + var tl=0, td=0; // top length, top distance + var dlim = Math.min(0x7fff, i); + while(dif<=dlim && --chain!=0 && pi!=ci /*&& c==UZIP.F._hash(data,i-dif)*/) { + if(tl==0 || (data[i+tl]==data[i+tl-dif])) { + var cl = UZIP.F._howLong(data, i, dif); + if(cl>tl) { + tl=cl; td=dif; if(tl>=nice) break; //* + if(dif+2maxd) { maxd=curd; pi = ei; } + } //*/ + } + } + + ci=pi; pi = prev[ci]; + dif += ((ci-pi + (1<<15)) & 0x7fff); + } + return (tl<<16)|td; +} +UZIP.F._howLong = function(data, i, dif) { + if(data[i]!=data[i-dif] || data[i+1]!=data[i+1-dif] || data[i+2]!=data[i+2-dif]) return 0; + var oi=i, l = Math.min(data.length, i+258); i+=3; + //while(i+4>>23), end = off+(qb&((1<<23)-1)); + while(off>16), lgi=(qc>>8)&255, dgi=(qc&255); + pos = UZIP.F._writeLit(257+lgi, ltree, out, pos); + putsE(out, pos, len-U.of0[lgi]); pos+=U.exb[lgi]; + + pos = UZIP.F._writeLit(dgi, dtree, out, pos); + putsF(out, pos, dst-U.df0[dgi]); pos+=U.dxb[dgi]; off+=len; + } + } + pos = UZIP.F._writeLit(256, ltree, out, pos); + } + //console.log(pos-opos, fxdSize, dynSize, cstSize); + return pos; +} +UZIP.F._copyExact = function(data,off,len,out,pos) { + var p8 = (pos>>>3); + out[p8]=(len); out[p8+1]=(len>>>8); out[p8+2]=255-out[p8]; out[p8+3]=255-out[p8+1]; p8+=4; + out.set(new Uint8Array(data.buffer, off, len), p8); + //for(var i=0; i4 && U.itree[(U.ordr[numh-1]<<1)+1]==0) numh--; + return [ML, MD, MH, numl, numd, numh, lset, dset]; +} +UZIP.F.getSecond= function(a) { var b=[]; for(var i=0; i>1)+","; return b; } +UZIP.F.contSize = function(tree, hst) { var s=0; for(var i=0; i15) { UZIP.F._putsE(out, pos, rst, rsl); pos+=rsl; } + } + return pos; +} +UZIP.F._lenCodes = function(tree, set) { + var len=tree.length; while(len!=2 && tree[len-1]==0) len-=2; // when no distances, keep one code with length 0 + for(var i=0; i>>1, 138); + if(zc<11) set.push(17, zc-3); + else set.push(18, zc-11); + i += zc*2-2; + } + else if(l==prv && nxt==l && nnxt==l) { + var lz = i+5; + while(lz+2>>1, 6); + set.push(16, zc-3); + i += zc*2-2; + } + else set.push(l, 0); + } + return len>>>1; +} +UZIP.F._hufTree = function(hst, tree, MAXL) { + var list=[], hl = hst.length, tl=tree.length, i=0; + for(i=0; iMAXL) { UZIP.F.restrictDepth(l2, MAXL, maxl); maxl = MAXL; } + for(i=0; iMD) { var od=dps[i].d; dps[i].d=MD; dbt+=bCost-(1<<(maxl-od)); } else break; + dbt = dbt>>>(maxl-MD); + while(dbt>0) { var od=dps[i].d; if(od=0; i--) if(dps[i].d==MD && dbt<0) { dps[i].d--; dbt++; } if(dbt!=0) console.log("debt left"); +} + +UZIP.F._goodIndex = function(v, arr) { + 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; +} +UZIP.F._writeLit = function(ch, ltree, out, pos) { + UZIP.F._putsF(out, pos, ltree[ch<<1]); + return pos+ltree[(ch<<1)+1]; +} + + + + + + + + +UZIP.F.inflate = function(data, buf) { + var u8=Uint8Array; + if(data[0]==3 && data[1]==0) return (buf ? buf : new u8(0)); + var F=UZIP.F, bitsF = F._bitsF, bitsE = F._bitsE, decodeTiny = F._decodeTiny, makeCodes = F.makeCodes, codes2map=F.codes2map, get17 = F._get17; + var U = F.U; + + var noBuf = (buf==null); + if(noBuf) buf = new u8((data.length>>>2)<<3); + + var BFINAL=0, BTYPE=0, HLIT=0, HDIST=0, HCLEN=0, ML=0, MD=0; + var off = 0, pos = 0; + var lmap, dmap; + + while(BFINAL==0) { + BFINAL = bitsF(data, pos , 1); + BTYPE = bitsF(data, pos+1, 2); pos+=3; + //console.log(BFINAL, BTYPE); + + if(BTYPE==0) { + if((pos&7)!=0) pos+=8-(pos&7); + var p8 = (pos>>>3)+4, len = data[p8-4]|(data[p8-3]<<8); //console.log(len);//bitsF(data, pos, 16), + if(noBuf) buf=UZIP.F._check(buf, off+len); + buf.set(new u8(data.buffer, data.byteOffset+p8, len), off); + //for(var i=0; itl)tl=l; } pos+=3*HCLEN; //console.log(itree); + makeCodes(U.itree, tl); + codes2map(U.itree, tl, U.imap); + + lmap = U.lmap; dmap = U.dmap; + + pos = decodeTiny(U.imap, (1<>>24))-1; pos+=(ml&0xffffff); + makeCodes(U.ltree, mx0); + codes2map(U.ltree, mx0, lmap); + + //var md = decodeTiny(U.imap, (1<>>24))-1; pos+=(md&0xffffff); + makeCodes(U.dtree, mx1); + codes2map(U.dtree, mx1, dmap); + } + //var ooff=off, opos=pos; + while(true) { + var code = lmap[get17(data, pos) & ML]; pos += code&15; + var lit = code>>>4; //U.lhst[lit]++; + if((lit>>>8)==0) { buf[off++] = lit; } + else if(lit==256) { break; } + else { + var end = off+lit-254; + if(lit>264) { var ebs = U.ldef[lit-257]; end = off + (ebs>>>3) + bitsE(data, pos, ebs&7); pos += ebs&7; } + //UZIP.F.dst[end-off]++; + + var dcode = dmap[get17(data, pos) & MD]; pos += dcode&15; + var dlit = dcode>>>4; + var dbs = U.ddef[dlit], dst = (dbs>>>4) + bitsF(data, pos, dbs&15); pos += dbs&15; + + //var o0 = off-dst, stp = Math.min(end-off, dst); + //if(stp>20) while(off>>3); + } + //console.log(UZIP.F.dst); + //console.log(tlen, dlen, off-tlen+tcnt); + return buf.length==off ? buf : buf.slice(0,off); +} +UZIP.F._check=function(buf, len) { + var bl=buf.length; if(len<=bl) return buf; + var nbuf = new Uint8Array(Math.max(bl<<1,len)); nbuf.set(buf,0); + //for(var i=0; i>>4; + if(lit<=15) { tree[i]=lit; i++; } + else { + var ll = 0, n = 0; + if(lit==16) { + n = (3 + bitsE(data, pos, 2)); pos += 2; ll = tree[i-1]; + } + else if(lit==17) { + n = (3 + bitsE(data, pos, 3)); pos += 3; + } + else if(lit==18) { + n = (11 + bitsE(data, pos, 7)); pos += 7; + } + var ni = i+n; + while(i>>1; + while(imx)mx=v; i++; } + while(i>1; + var cl = tree[i+1], val = (lit<<4)|cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl); + var rest = (MAX_BITS-cl), i0 = tree[i]<>>(15-MAX_BITS); + while(i0!=i1) { + var p0 = r15[i0]>>>(15-MAX_BITS); + map[p0]=val; i0++; + } + } +} +UZIP.F.revCodes = function(tree, MAX_BITS) { + var r15 = UZIP.F.U.rev15, imb = 15-MAX_BITS; + for(var i=0; i>>imb; } +} + +// used only in deflate +UZIP.F._putsE= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); } +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); } + +UZIP.F._bitsE= function(dt, pos, length) { return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) )>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16))>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8))>>>(pos&7))&511; +} */ +UZIP.F._get17= function(dt, pos) { // return at least 17 meaningful bytes + return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) )>>>(pos&7); +} +UZIP.F._get25= function(dt, pos) { // return at least 17 meaningful bytes + return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) | (dt[(pos>>>3)+3]<<24) )>>>(pos&7); +} +UZIP.F.U = function(){ + var u16=Uint16Array, u32=Uint32Array; + return { + next_code : new u16(16), + bl_count : new u16(16), + ordr : [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ], + 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], + 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], + ldef : new u16(32), + 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], + 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], + ddef : new u32(32), + flmap: new u16( 512), fltree: [], + fdmap: new u16( 32), fdtree: [], + lmap : new u16(32768), ltree : [], ttree:[], + dmap : new u16(32768), dtree : [], + imap : new u16( 512), itree : [], + //rev9 : new u16( 512) + rev15: new u16(1<<15), + lhst : new u32(286), dhst : new u32( 30), ihst : new u32(19), + lits : new u32(15000), + strt : new u16(1<<16), + prev : new u16(1<<15) + }; +} (); + +(function(){ + var U = UZIP.F.U; + var len = 1<<15; + for(var i=0; i>> 1) | ((x & 0x55555555) << 1)); + x = (((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2)); + x = (((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4)); + x = (((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8)); + U.rev15[i] = (((x >>> 16) | (x << 16)))>>>17; + } + + function pushV(tgt, n, sv) { while(n--!=0) tgt.push(0,sv); } + + 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]; } + + pushV(U.fltree, 144, 8); pushV(U.fltree, 255-143, 9); pushV(U.fltree, 279-255, 7); pushV(U.fltree,287-279,8); + /* + var i = 0; + for(; i<=143; i++) U.fltree.push(0,8); + for(; i<=255; i++) U.fltree.push(0,9); + for(; i<=279; i++) U.fltree.push(0,7); + for(; i<=287; i++) U.fltree.push(0,8); + */ + UZIP.F.makeCodes(U.fltree, 9); + UZIP.F.codes2map(U.fltree, 9, U.flmap); + UZIP.F.revCodes (U.fltree, 9) + + pushV(U.fdtree,32,5); + //for(i=0;i<32; i++) U.fdtree.push(0,5); + UZIP.F.makeCodes(U.fdtree, 5); + UZIP.F.codes2map(U.fdtree, 5, U.fdmap); + UZIP.F.revCodes (U.fdtree, 5) + + pushV(U.itree,19,0); pushV(U.ltree,286,0); pushV(U.dtree,30,0); pushV(U.ttree,320,0); + /* + for(var i=0; i< 19; i++) U.itree.push(0,0); + for(var i=0; i<286; i++) U.ltree.push(0,0); + for(var i=0; i< 30; i++) U.dtree.push(0,0); + for(var i=0; i<320; i++) U.ttree.push(0,0); + */ +})() + + + + + + + + + + + + + + + +