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