idk
This commit is contained in:
parent
b68deb838a
commit
082d8561d2
2 changed files with 71 additions and 17 deletions
11
index.html
11
index.html
|
|
@ -252,6 +252,12 @@
|
||||||
<option value="edit">edit</option>
|
<option value="edit">edit</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="paintColor" title="palette colour — indices only, never an RGB value"></select>
|
<select id="paintColor" title="palette colour — indices only, never an RGB value"></select>
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;color:var(--dim)"
|
||||||
|
title="dim the drawing to trace the frame underneath — editing aid only, never exported">
|
||||||
|
opacity
|
||||||
|
<input type="range" id="paintOpacity" min="10" max="100" value="100" style="width:96px">
|
||||||
|
<output id="paintOpacityv" style="color:var(--accent);min-width:34px"></output>
|
||||||
|
</label>
|
||||||
<button id="btn-celprev">Copy previous</button>
|
<button id="btn-celprev">Copy previous</button>
|
||||||
<button id="btn-celclear">Clear cel</button>
|
<button id="btn-celclear">Clear cel</button>
|
||||||
<span class="legend" id="paintframe" style="margin:0"></span>
|
<span class="legend" id="paintframe" style="margin:0"></span>
|
||||||
|
|
@ -269,6 +275,11 @@
|
||||||
<b>edit</b> click a shape to select · drag a vertex or the shape itself ·
|
<b>edit</b> click a shape to select · drag a vertex or the shape itself ·
|
||||||
<kbd>Shift</kbd>-click an edge inserts a vertex · <kbd>Alt</kbd>-click a vertex
|
<kbd>Shift</kbd>-click an edge inserts a vertex · <kbd>Alt</kbd>-click a vertex
|
||||||
removes it · <kbd>Del</kbd> deletes the layer<br>
|
removes it · <kbd>Del</kbd> deletes the layer<br>
|
||||||
|
<b>opacity</b> 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.<br>
|
||||||
<b>Copy previous</b> takes the last enabled frame that actually has a drawing
|
<b>Copy previous</b> takes the last enabled frame that actually has a drawing
|
||||||
— skipping empty ones, so it behaves the same before and after you thin the
|
— skipping empty ones, so it behaves the same before and after you thin the
|
||||||
strip out. Frames carrying a drawing are marked <b style="color:#c084fc">▣</b>.<br>
|
strip out. Frames carrying a drawing are marked <b style="color:#c084fc">▣</b>.<br>
|
||||||
|
|
|
||||||
65
js/app.js
65
js/app.js
|
|
@ -586,23 +586,31 @@ function compositeRender(canvas, f, zoom) {
|
||||||
mode === 'photo-dim' ? 0.34 : 1);
|
mode === 'photo-dim' ? 0.34 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = renderFrame(f, mode === 'oval+photo' ? 'oval' : (showPhoto ? 'off' : mode));
|
blitIndexed(g, renderFrame(f, mode === 'oval+photo' ? 'oval' : (showPhoto ? 'off' : mode)),
|
||||||
const img2 = r.toImageData(PALETTE.map((p) => p.hex), zoom);
|
zoom, showPhoto);
|
||||||
if (showPhoto) {
|
}
|
||||||
// Keep the photo visible wherever the indexed layer is background.
|
|
||||||
|
// 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 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 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;
|
const d = img.data;
|
||||||
for (let i = 0; i < d.length; i += 4) {
|
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;
|
if (d[i] === br && d[i + 1] === bgn && d[i + 2] === bb) d[i + 3] = 0;
|
||||||
}
|
}
|
||||||
const tmp = document.createElement('canvas');
|
const tmp = document.createElement('canvas');
|
||||||
tmp.width = img2.width; tmp.height = img2.height;
|
tmp.width = img.width; tmp.height = img.height;
|
||||||
tmp.getContext('2d').putImageData(img2, 0, 0);
|
tmp.getContext('2d').putImageData(img, 0, 0);
|
||||||
g.drawImage(tmp, 0, 0);
|
g.drawImage(tmp, 0, 0);
|
||||||
} else {
|
|
||||||
g.putImageData(img2, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keptSorted = () => [...state.keep].sort((a, b) => a - b);
|
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);
|
g.fillStyle = '#0d0f16'; g.fillRect(0, 0, cv.width, cv.height);
|
||||||
return;
|
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
|
// paintLabels, not drawPaint: drawPaint re-renders the canvas, and this runs
|
||||||
// from inside commit(), which renders immediately afterwards anyway.
|
// from inside commit(), which renders immediately afterwards anyway.
|
||||||
|
|
@ -1305,6 +1317,11 @@ el('paintTool').addEventListener('change', () => {
|
||||||
paint.render();
|
paint.render();
|
||||||
});
|
});
|
||||||
el('paintColor').addEventListener('change', () => { paint.color = +el('paintColor').value; });
|
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 = () => {
|
el('btn-celclear').onclick = () => {
|
||||||
if (!state.dense) return;
|
if (!state.dense) return;
|
||||||
state.cels.set(celFrame(), newCel());
|
state.cels.set(celFrame(), newCel());
|
||||||
|
|
@ -1362,6 +1379,32 @@ function drawPaint() {
|
||||||
paint.render();
|
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() {
|
function paintLabels() {
|
||||||
if (!state.dense) return;
|
if (!state.dense) return;
|
||||||
const kept = keptSorted(), cf = celFrame();
|
const kept = keptSorted(), cf = celFrame();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue