diff --git a/README.md b/README.md index 30a3d39..8f13ce7 100644 --- a/README.md +++ b/README.md @@ -291,9 +291,14 @@ which one you are editing and how far it holds. remove it, Del to delete the layer. - **Layers** stack Photoshop-style, front at the top, with per-layer colour, show/hide and reorder. -- **Drag a frame** from the strip onto the canvas to seed this cel from that - one, every layer, as a deep copy. **Copy previous** does the same for the - previous kept frame, which is the case you reach for constantly. +- **Copy previous** brings the last drawing forward onto this frame. It means + the nearest earlier enabled frame that *actually has* a drawing, skipping the + 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 indices**, so you cannot pick one that is not in the ramp — sampling colour from diff --git a/index.html b/index.html index 579e043..c25ba54 100644 --- a/index.html +++ b/index.html @@ -42,6 +42,8 @@ .fr.drop { border-color:#2a2f3e; } .fr.drop canvas { opacity:.26; filter:grayscale(1); } .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; } #sheet { display:flex; flex-wrap:wrap; gap:10px; } @@ -259,6 +261,7 @@
+
pen click to place vertices · click the green box on the first one, or Enter / double-click, to close · Esc cancels · @@ -266,6 +269,9 @@ 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
+ 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 ▣.
drag a frame from the strip onto the canvas to copy its drawing, every layer. Vertices snap to the 320×200 grid, and colours are palette indices — you cannot pick one that is not in the ramp. diff --git a/js/app.js b/js/app.js index ea4eb46..8338a3b 100644 --- a/js/app.js +++ b/js/app.js @@ -894,6 +894,10 @@ function drawStrip() { const tag = document.createElement('span'); 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.draggable = true; cell.ondragstart = (ev) => ev.dataTransfer.setData('text/plain', String(f)); @@ -1290,7 +1294,9 @@ const paint = new PaintUI({ } 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', () => { @@ -1306,18 +1312,25 @@ el('btn-celclear').onclick = () => { paint.commit(); }; el('btn-celprev').onclick = () => { - // Copy the previous kept frame's finished drawing onto this one. The same - // thing the drag-and-drop does, for the case you reach for constantly. + // Copy the last finished drawing onto this one - the case you reach for + // 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; - const kept = keptSorted(), here = celFrame(); - const prev = kept.filter((k) => k < here).pop(); + const here = celFrame(); + const prev = keptSorted() + .filter((k) => k < here && (state.cels.get(k) || []).length) + .pop(); if (prev === undefined) { paint.say('no earlier drawing to copy'); return; } - const src = state.cels.get(prev); - if (!src || !src.length) { paint.say(`f${prev} has nothing to copy`); return; } - state.cels.set(here, cloneCel(src)); + state.cels.set(here, cloneCel(state.cels.get(prev))); paint.sel = -1; 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) => { @@ -1345,13 +1358,21 @@ function loadCels() { } function drawPaint() { - if (!state.dense) { paint.render(); return; } + paintLabels(); + paint.render(); +} + +function paintLabels() { + if (!state.dense) return; const kept = keptSorted(), cf = celFrame(); const until = (kept[kept.indexOf(cf) + 1] ?? state.dense.length) - 1; + const drawn = kept.filter((k) => (state.cels.get(k) || []).length); el('paintframe').textContent = `drawing cel f${cf}` + (until > cf ? ` — holds to f${until}` : '') + - (state.frame !== cf ? ` (playhead f${state.frame})` : ''); - paint.render(); + (state.frame !== cf ? ` · playhead f${state.frame}` : ''); + 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 @@ -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.__render = compositeRender; // ...and renders arbitrary frames off-screen window.__lead = leadIndex; // ...and resolves the performance frame +window.__drawAll = drawAll; // ...and forces a full redraw