Paint: copy previous should skip empty frames
Copy previous looked for the nearest earlier ENABLED frame, and every frame is enabled until you thin the strip out - so it resolved to f-1, which has nothing on it, and the button appeared to only ever copy the immediate sibling while reporting "nothing to copy". It now takes the nearest earlier enabled frame that actually carries a drawing. That behaves identically before and after you curate the strip, which is the point: the rhythm of the drawings should not depend on whether you have got round to deleting frames yet. Cels stay tied to the keep-set. They are plate drawings and they hold until the next enabled frame, as originally specified - an earlier version of this commit gave them their own independent set, which is wrong for what they are. Frames carrying a drawing are now marked in the strip, because "copy previous" reaching back to a frame you cannot see is not much better than it reaching to the wrong one. Also fixes stale paint labels: onChange refreshed the strip and the panes but not the paint header, so the drawn-frames readout lagged a copy behind. Split the text update out of drawPaint so it can run from inside commit() without re-rendering the canvas underneath itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d516a03ac
commit
b68deb838a
3 changed files with 48 additions and 15 deletions
11
README.md
11
README.md
|
|
@ -291,9 +291,14 @@ which one you are editing and how far it holds.
|
||||||
remove it, <kbd>Del</kbd> to delete the layer.
|
remove it, <kbd>Del</kbd> to delete the layer.
|
||||||
- **Layers** stack Photoshop-style, front at the top, with per-layer colour,
|
- **Layers** stack Photoshop-style, front at the top, with per-layer colour,
|
||||||
show/hide and reorder.
|
show/hide and reorder.
|
||||||
- **Drag a frame** from the strip onto the canvas to seed this cel from that
|
- **Copy previous** brings the last drawing forward onto this frame. It means
|
||||||
one, every layer, as a deep copy. **Copy previous** does the same for the
|
the nearest earlier enabled frame that *actually has* a drawing, skipping the
|
||||||
previous kept frame, which is the case you reach for constantly.
|
empty ones — every frame is enabled until you thin the strip out, so the naive
|
||||||
|
rule resolved to `f-1` and it looked like it only ever copied the frame to the
|
||||||
|
left. **Drag a frame** from the strip onto the canvas to seed from any other
|
||||||
|
frame instead. Both deep-copy; the two cels never share point arrays.
|
||||||
|
- Frames carrying a drawing are marked **▣** in the strip, so you can see the
|
||||||
|
rhythm rather than having to remember it.
|
||||||
|
|
||||||
Two rules are enforced rather than left to discipline. Colours are **palette
|
Two rules are enforced rather than left to discipline. Colours are **palette
|
||||||
indices**, so you cannot pick one that is not in the ramp — sampling colour from
|
indices**, so you cannot pick one that is not in the ramp — sampling colour from
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@
|
||||||
.fr.drop { border-color:#2a2f3e; }
|
.fr.drop { border-color:#2a2f3e; }
|
||||||
.fr.drop canvas { opacity:.26; filter:grayscale(1); }
|
.fr.drop canvas { opacity:.26; filter:grayscale(1); }
|
||||||
.fr.cur { border-color:var(--accent); }
|
.fr.cur { border-color:var(--accent); }
|
||||||
|
.fr.cel::before { content:'▣'; position:absolute; left:3px; top:1px; color:#c084fc;
|
||||||
|
font-size:10px; text-shadow:0 0 2px #000; }
|
||||||
.fr.keep::after { content:'●'; position:absolute; right:3px; top:1px; color:var(--ok); font-size:10px; }
|
.fr.keep::after { content:'●'; position:absolute; right:3px; top:1px; color:var(--ok); font-size:10px; }
|
||||||
|
|
||||||
#sheet { display:flex; flex-wrap:wrap; gap:10px; }
|
#sheet { display:flex; flex-wrap:wrap; gap:10px; }
|
||||||
|
|
@ -259,6 +261,7 @@
|
||||||
<div id="paintlayers"></div>
|
<div id="paintlayers"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend" id="paintinfo"></div>
|
<div class="legend" id="paintinfo"></div>
|
||||||
|
<div class="legend" id="paintcels"></div>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<b>pen</b> click to place vertices · click the green box on the first one, or
|
<b>pen</b> click to place vertices · click the green box on the first one, or
|
||||||
<kbd>Enter</kbd> / double-click, to close · <kbd>Esc</kbd> cancels ·
|
<kbd>Enter</kbd> / double-click, to close · <kbd>Esc</kbd> cancels ·
|
||||||
|
|
@ -266,6 +269,9 @@
|
||||||
<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>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
|
||||||
|
strip out. Frames carrying a drawing are marked <b style="color:#c084fc">▣</b>.<br>
|
||||||
<b>drag a frame</b> from the strip onto the canvas to copy its drawing, every
|
<b>drag a frame</b> from the strip onto the canvas to copy its drawing, every
|
||||||
layer. Vertices snap to the 320×200 grid, and colours are palette indices —
|
layer. Vertices snap to the 320×200 grid, and colours are palette indices —
|
||||||
you cannot pick one that is not in the ramp.
|
you cannot pick one that is not in the ramp.
|
||||||
|
|
|
||||||
46
js/app.js
46
js/app.js
|
|
@ -894,6 +894,10 @@ function drawStrip() {
|
||||||
|
|
||||||
const tag = document.createElement('span');
|
const tag = document.createElement('span');
|
||||||
tag.textContent = f;
|
tag.textContent = f;
|
||||||
|
// Mark the frames that actually carry a drawing. Without it the only way to
|
||||||
|
// know where your cels are is to scrub and look, and "copy previous" then
|
||||||
|
// reaches back to somewhere you cannot see.
|
||||||
|
if ((state.cels.get(f) || []).length) cell.classList.add('cel');
|
||||||
cell.append(cv, tag);
|
cell.append(cv, tag);
|
||||||
cell.draggable = true;
|
cell.draggable = true;
|
||||||
cell.ondragstart = (ev) => ev.dataTransfer.setData('text/plain', String(f));
|
cell.ondragstart = (ev) => ev.dataTransfer.setData('text/plain', String(f));
|
||||||
|
|
@ -1290,7 +1294,9 @@ const paint = new PaintUI({
|
||||||
}
|
}
|
||||||
compositeRender(cv, state.frame, z);
|
compositeRender(cv, state.frame, z);
|
||||||
},
|
},
|
||||||
onChange: () => { saveCels(); drawPanes(); drawStrip(); drawWorksheet(); },
|
// paintLabels, not drawPaint: drawPaint re-renders the canvas, and this runs
|
||||||
|
// from inside commit(), which renders immediately afterwards anyway.
|
||||||
|
onChange: () => { saveCels(); paintLabels(); drawPanes(); drawStrip(); drawWorksheet(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
el('paintTool').addEventListener('change', () => {
|
el('paintTool').addEventListener('change', () => {
|
||||||
|
|
@ -1306,18 +1312,25 @@ el('btn-celclear').onclick = () => {
|
||||||
paint.commit();
|
paint.commit();
|
||||||
};
|
};
|
||||||
el('btn-celprev').onclick = () => {
|
el('btn-celprev').onclick = () => {
|
||||||
// Copy the previous kept frame's finished drawing onto this one. The same
|
// Copy the last finished drawing onto this one - the case you reach for
|
||||||
// thing the drag-and-drop does, for the case you reach for constantly.
|
// constantly, stepping forward and carrying the previous cel with you.
|
||||||
|
//
|
||||||
|
// "The last drawing" means the nearest earlier enabled frame that ACTUALLY
|
||||||
|
// HAS one, not simply the nearest earlier enabled frame. Every frame is
|
||||||
|
// enabled until you curate the strip, so the naive rule resolved to f-1,
|
||||||
|
// which is empty, and the button looked like it only ever copied the frame
|
||||||
|
// immediately to the left. Skipping the empties makes it behave the same
|
||||||
|
// before and after you thin the strip out.
|
||||||
if (!state.dense) return;
|
if (!state.dense) return;
|
||||||
const kept = keptSorted(), here = celFrame();
|
const here = celFrame();
|
||||||
const prev = kept.filter((k) => k < here).pop();
|
const prev = keptSorted()
|
||||||
|
.filter((k) => k < here && (state.cels.get(k) || []).length)
|
||||||
|
.pop();
|
||||||
if (prev === undefined) { paint.say('no earlier drawing to copy'); return; }
|
if (prev === undefined) { paint.say('no earlier drawing to copy'); return; }
|
||||||
const src = state.cels.get(prev);
|
state.cels.set(here, cloneCel(state.cels.get(prev)));
|
||||||
if (!src || !src.length) { paint.say(`f${prev} has nothing to copy`); return; }
|
|
||||||
state.cels.set(here, cloneCel(src));
|
|
||||||
paint.sel = -1;
|
paint.sel = -1;
|
||||||
paint.commit();
|
paint.commit();
|
||||||
paint.say(`copied f${prev} — ${src.length} layers`);
|
paint.say(`copied f${prev} → f${here} — ${state.cels.get(here).length} layers`);
|
||||||
};
|
};
|
||||||
|
|
||||||
PALETTE.forEach((p, i) => {
|
PALETTE.forEach((p, i) => {
|
||||||
|
|
@ -1345,13 +1358,21 @@ function loadCels() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPaint() {
|
function drawPaint() {
|
||||||
if (!state.dense) { paint.render(); return; }
|
paintLabels();
|
||||||
|
paint.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintLabels() {
|
||||||
|
if (!state.dense) return;
|
||||||
const kept = keptSorted(), cf = celFrame();
|
const kept = keptSorted(), cf = celFrame();
|
||||||
const until = (kept[kept.indexOf(cf) + 1] ?? state.dense.length) - 1;
|
const until = (kept[kept.indexOf(cf) + 1] ?? state.dense.length) - 1;
|
||||||
|
const drawn = kept.filter((k) => (state.cels.get(k) || []).length);
|
||||||
el('paintframe').textContent =
|
el('paintframe').textContent =
|
||||||
`drawing cel f${cf}` + (until > cf ? ` — holds to f${until}` : '') +
|
`drawing cel f${cf}` + (until > cf ? ` — holds to f${until}` : '') +
|
||||||
(state.frame !== cf ? ` (playhead f${state.frame})` : '');
|
(state.frame !== cf ? ` · playhead f${state.frame}` : '');
|
||||||
paint.render();
|
el('paintcels').textContent = drawn.length
|
||||||
|
? `${drawn.length} drawn: ${drawn.join(' ')}`
|
||||||
|
: 'nothing drawn yet';
|
||||||
}
|
}
|
||||||
|
|
||||||
// #synth / #frames autorun, so the tool can be driven headlessly for smoke tests
|
// #synth / #frames autorun, so the tool can be driven headlessly for smoke tests
|
||||||
|
|
@ -1368,3 +1389,4 @@ else status('ready — Load frames, then step with \u2190 \u2192 and delete with
|
||||||
window.__roto = state; // headless smoke test reads this
|
window.__roto = state; // headless smoke test reads this
|
||||||
window.__render = compositeRender; // ...and renders arbitrary frames off-screen
|
window.__render = compositeRender; // ...and renders arbitrary frames off-screen
|
||||||
window.__lead = leadIndex; // ...and resolves the performance frame
|
window.__lead = leadIndex; // ...and resolves the performance frame
|
||||||
|
window.__drawAll = drawAll; // ...and forces a full redraw
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue