diff --git a/HANDOFF.md b/HANDOFF.md index 26172ca..4dac51b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -102,10 +102,14 @@ gives clean manual stepping. `T.press('Space')` = tap. Dismiss the intro overlay preview_start (a new port = new origin = cold cache). Always sanity-check a just-added field exists (e.g. `T.G.newField !== undefined`) before trusting test results. - Sprite pipeline: generate via generate-image skill (needs OPENROUTER_API_KEY in repo .env, - gitignored) → magenta background → scratchpad slice.py (chroma-key, segment, strip ground - lines/dividers, common-scale per sheet, IDLE_TARGET_H=112). Generator sometimes ignores - magenta (then: edit-mode the image back to magenta) or draws near-identical animation - frames (then: re-prompt with strongly contrasted pose descriptions). + gitignored; run the script with the scratchpad venv python — system python lacks requests) + → magenta background → assets/_slice.py (COMMITTED now, no more scratchpad rebuilds: + chroma-key on the corner color + magenta de-fringe, column-gap segmentation, --largest + keeps only the main blob, common-scale per sheet, IDLE_TARGET_H=112). Generator sometimes + ignores magenta (harmless — the key uses the corner color) or draws near-identical + animation frames (re-prompt with strongly contrasted pose descriptions). If two frames + touch (a dragged weapon bridging the gap), split the sheet at the midpoint and slice each + half with --largest. Passing the existing sheet as --input (edit mode) nails the style. - drawChar mirrors sprites by ch.facing — any stance that must not mirror needs facing reset (setupPoint pins facing=1). - The scratchpad venv/scripts get wiped between sessions — recreate venv (requests+pillow) diff --git a/assets/_sheet_brute.png b/assets/_sheet_brute.png new file mode 100644 index 0000000..349461a Binary files /dev/null and b/assets/_sheet_brute.png differ diff --git a/assets/_sheet_runner.png b/assets/_sheet_runner.png new file mode 100644 index 0000000..2a77411 Binary files /dev/null and b/assets/_sheet_runner.png differ diff --git a/assets/_sheet_shield.png b/assets/_sheet_shield.png new file mode 100644 index 0000000..1c258e9 Binary files /dev/null and b/assets/_sheet_shield.png differ diff --git a/assets/_sheet_tin.png b/assets/_sheet_tin.png new file mode 100644 index 0000000..eafb9c0 Binary files /dev/null and b/assets/_sheet_tin.png differ diff --git a/assets/_slice.py b/assets/_slice.py new file mode 100644 index 0000000..0b3457a --- /dev/null +++ b/assets/_slice.py @@ -0,0 +1,71 @@ +# slice.py — chroma-key a generated sprite sheet on its corner color, split into +# frames on column gaps, common-scale the sheet so the tallest frame = IDLE_TARGET_H, +# save each frame to the given output paths. Usage: slice.py sheet.png out1.png out2.png ... +import sys +from PIL import Image + +IDLE_TARGET_H = 112 +KEY_D2 = 60 * 60 # squared RGB distance to the bg color that still counts as background +MIN_GAP = 12 # empty columns that separate frames +MIN_BLOB = 60 # despeckle: drop opaque islands smaller than this many pixels + +def main(sheet_path, out_paths, largest_only=False): + im = Image.open(sheet_path).convert('RGBA') + w, h = im.size + px = im.load() + # background = corner color (average the four corners that agree with the top-left) + bg = px[0, 0][:3] + for x in range(w): + for y in range(h): + r, g, b, a = px[x, y] + if (r - bg[0]) ** 2 + (g - bg[1]) ** 2 + (b - bg[2]) ** 2 < KEY_D2 \ + or (min(r, b) > g + 40): # de-fringe: magenta-hued halo pixels (sprites have no magenta) + px[x, y] = (0, 0, 0, 0) + # despeckle: flood-fill opaque components; wipe tiny ones (halos, dust). + # largest_only additionally wipes everything but the biggest blob (stray fragments + # from splitting a sheet whose frames touch — e.g. the brute's dragged pole). + seen = [[False] * h for _ in range(w)] + blobs = [] + for x in range(w): + for y in range(h): + if seen[x][y] or px[x, y][3] == 0: continue + stack, blob = [(x, y)], [] + seen[x][y] = True + while stack: + cx, cy = stack.pop() + blob.append((cx, cy)) + for nx, ny in ((cx+1,cy),(cx-1,cy),(cx,cy+1),(cx,cy-1)): + if 0 <= nx < w and 0 <= ny < h and not seen[nx][ny] and px[nx, ny][3] > 0: + seen[nx][ny] = True; stack.append((nx, ny)) + blobs.append(blob) + keep = max(blobs, key=len) if (largest_only and blobs) else None + for blob in blobs: + if blob is keep: continue + if largest_only or len(blob) < MIN_BLOB: + for cx, cy in blob: px[cx, cy] = (0, 0, 0, 0) + # segment on empty column runs + cols = [any(px[x, y][3] for y in range(h)) for x in range(w)] + frames, start, gap = [], None, 0 + for x in range(w): + if cols[x]: + if start is None: start = x + gap = 0 + elif start is not None: + gap += 1 + if gap >= MIN_GAP: + frames.append((start, x - gap + 1)); start = None + if start is not None: frames.append((start, w)) + crops = [] + for x0, x1 in frames: + c = im.crop((x0, 0, x1, h)) + crops.append(c.crop(c.getbbox())) + if len(crops) != len(out_paths): + sys.exit(f'ERROR: found {len(crops)} frames, expected {len(out_paths)} ({sheet_path})') + k = IDLE_TARGET_H / max(c.height for c in crops) # one scale per sheet keeps relative frame sizes + for c, out in zip(crops, out_paths): + c.resize((max(1, round(c.width * k)), max(1, round(c.height * k))), Image.NEAREST).save(out) + print(f'{out}: {round(c.width * k)}x{round(c.height * k)}') + +if __name__ == '__main__': + args = [a for a in sys.argv[1:] if a != '--largest'] + main(args[0], args[1:], largest_only='--largest' in sys.argv) diff --git a/assets/x_tin.png b/assets/x_tin.png new file mode 100644 index 0000000..d8d31f0 Binary files /dev/null and b/assets/x_tin.png differ diff --git a/assets/z_f1.png b/assets/z_f1.png new file mode 100644 index 0000000..164cce5 Binary files /dev/null and b/assets/z_f1.png differ diff --git a/assets/z_f2.png b/assets/z_f2.png new file mode 100644 index 0000000..0e63511 Binary files /dev/null and b/assets/z_f2.png differ diff --git a/assets/z_g1.png b/assets/z_g1.png new file mode 100644 index 0000000..ceff35f Binary files /dev/null and b/assets/z_g1.png differ diff --git a/assets/z_g2.png b/assets/z_g2.png new file mode 100644 index 0000000..1d81e7b Binary files /dev/null and b/assets/z_g2.png differ diff --git a/assets/z_s1.png b/assets/z_s1.png new file mode 100644 index 0000000..2bf3d97 Binary files /dev/null and b/assets/z_s1.png differ diff --git a/assets/z_s2.png b/assets/z_s2.png new file mode 100644 index 0000000..414022e Binary files /dev/null and b/assets/z_s2.png differ diff --git a/assets/z_s3.png b/assets/z_s3.png new file mode 100644 index 0000000..ea080b1 Binary files /dev/null and b/assets/z_s3.png differ