Phase 10: sprite session — runner, brute, shield zombie, weapon tin
Sheets generated with the existing zombie sheet as the style anchor (edit mode); sliced to z_f1/2 (runner sprint), z_g1/2 (brute + net-post club), z_s1/2/3 (shield zombie, chair up / chair dropped stagger), x_tin (black new-balls tin). assets/_slice.py is now committed (chroma-key + de-fringe + segmentation) so the pipeline survives scratchpad wipes; HANDOFF updated. Sprites are NOT wired into render.js yet — Phase 11 does that after Oliver approves the sheets (plan's design checkpoint). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No files matched your search
@@ -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)
|
||||
|
||||
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 897 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 836 KiB |
@@ -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)
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.5 KiB |