From 082d8561d2a84fd88c7218ec49087806ed8f0436 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 25 Sep 2026 12:55:01 -0400 Subject: [PATCH] idk --- index.html | 11 ++++++++ js/app.js | 77 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/index.html b/index.html index c25ba54..31b4e0c 100644 --- a/index.html +++ b/index.html @@ -252,6 +252,12 @@ + @@ -269,6 +275,11 @@ edit click a shape to select · drag a vertex or the shape itself · Shift-click an edge inserts a vertex · Alt-click a vertex removes it · Del deletes the layer
+ opacity dims the drawing over the registered source frame so you can + trace it. It is an editing aid only — never rasterised, never exported, and + the flat render is untouched. At 100% the view is exactly the normal + composite; below it the placeholder oval is dropped, since it covers the + face you turned the slider down to see.
Copy previous takes the last enabled frame that actually has a drawing — skipping empty ones, so it behaves the same before and after you thin the strip out. Frames carrying a drawing are marked ▣.
diff --git a/js/app.js b/js/app.js index 8338a3b..79e9a0e 100644 --- a/js/app.js +++ b/js/app.js @@ -586,23 +586,31 @@ function compositeRender(canvas, f, zoom) { mode === 'photo-dim' ? 0.34 : 1); } - const r = renderFrame(f, mode === 'oval+photo' ? 'oval' : (showPhoto ? 'off' : mode)); - const img2 = r.toImageData(PALETTE.map((p) => p.hex), zoom); - if (showPhoto) { - // Keep the photo visible wherever the indexed layer is background. - const bg = PALETTE[IDX.bg].hex.replace('#', ''); - const br = parseInt(bg.slice(0, 2), 16), bgn = parseInt(bg.slice(2, 4), 16), bb = parseInt(bg.slice(4, 6), 16); - const d = img2.data; - for (let i = 0; i < d.length; i += 4) { - if (d[i] === br && d[i + 1] === bgn && d[i + 2] === bb) d[i + 3] = 0; - } - const tmp = document.createElement('canvas'); - tmp.width = img2.width; tmp.height = img2.height; - tmp.getContext('2d').putImageData(img2, 0, 0); - g.drawImage(tmp, 0, 0); - } else { - g.putImageData(img2, 0, 0); + blitIndexed(g, renderFrame(f, mode === 'oval+photo' ? 'oval' : (showPhoto ? 'off' : mode)), + zoom, showPhoto); +} + +// Put an indexed raster onto a 2D context. With `keyBg`, background pixels go +// transparent instead of opaque, so whatever was painted underneath - a +// registered photograph, usually - stays visible through them. +// +// Split out of compositeRender so the paint canvas can draw the same pixels at +// a reduced globalAlpha. Nothing else is different about that path, which is +// what keeps the editing view honest: you are dimming the render, not looking +// at a second renderer that might disagree with it. +function blitIndexed(g, raster, zoom, keyBg) { + const img = raster.toImageData(PALETTE.map((p) => p.hex), zoom); + if (!keyBg) { g.putImageData(img, 0, 0); return; } + const bg = PALETTE[IDX.bg].hex.replace('#', ''); + const br = parseInt(bg.slice(0, 2), 16), bgn = parseInt(bg.slice(2, 4), 16), bb = parseInt(bg.slice(4, 6), 16); + const d = img.data; + for (let i = 0; i < d.length; i += 4) { + if (d[i] === br && d[i + 1] === bgn && d[i + 2] === bb) d[i + 3] = 0; } + const tmp = document.createElement('canvas'); + tmp.width = img.width; tmp.height = img.height; + tmp.getContext('2d').putImageData(img, 0, 0); + g.drawImage(tmp, 0, 0); } const keptSorted = () => [...state.keep].sort((a, b) => a - b); @@ -1292,7 +1300,11 @@ const paint = new PaintUI({ g.fillStyle = '#0d0f16'; g.fillRect(0, 0, cv.width, cv.height); return; } - compositeRender(cv, state.frame, z); + const op = +el('paintOpacity').value / 100; + // At 100% this is exactly the normal composite, so the slider changes + // nothing at all until you reach for it. + if (op >= 1) { compositeRender(cv, state.frame, z); return; } + paintGhost(cv, z, op); }, // paintLabels, not drawPaint: drawPaint re-renders the canvas, and this runs // from inside commit(), which renders immediately afterwards anyway. @@ -1305,6 +1317,11 @@ el('paintTool').addEventListener('change', () => { paint.render(); }); el('paintColor').addEventListener('change', () => { paint.color = +el('paintColor').value; }); +el('paintOpacity').addEventListener('input', () => { + el('paintOpacityv').textContent = `${el('paintOpacity').value}%`; + paint.render(); +}); +el('paintOpacityv').textContent = `${el('paintOpacity').value}%`; el('btn-celclear').onclick = () => { if (!state.dense) return; state.cels.set(celFrame(), newCel()); @@ -1362,6 +1379,32 @@ function drawPaint() { paint.render(); } +// The drawing dimmed over the source frame, for tracing. +// +// An EDITING AID ONLY - opacity is never written to the raster, never exported, +// and the flat render is untouched. Nothing here may reach the output: a +// translucent fill is the one thing this format cannot express, so if it ever +// leaked into the rasteriser it would have to be flattened against a background +// and would silently become a colour that is not in the ramp. +// +// The plate oval is deliberately not drawn. It is a stand-in for art that does +// not exist yet, and it covers the performer's face in flat skin - which is +// exactly the part of the frame you turned the opacity down to look at. +function paintGhost(cv, z, op) { + cv.width = RW * z; cv.height = RH * z; + const g = cv.getContext('2d'); + g.fillStyle = PALETTE[IDX.bg].hex; + g.fillRect(0, 0, cv.width, cv.height); + + const f = state.frame, pf = plateIndex(f); + const img = state.images[pf]; + if (img) drawRegistered(g, img, state.stab.transforms[pf], state.xform, z, 1); + + g.globalAlpha = op; + blitIndexed(g, renderFrame(f, 'off'), z, !!img); + g.globalAlpha = 1; +} + function paintLabels() { if (!state.dense) return; const kept = keptSorted(), cf = celFrame();