diff --git a/assets/_fine.py b/assets/_fine.py new file mode 100644 index 0000000..e1b636f --- /dev/null +++ b/assets/_fine.py @@ -0,0 +1,108 @@ +# _fine.py — turn a generated sheet into FULL-RESOLUTION sprites. +# +# _slice.py resamples with NEAREST, which is right when the generator's art-pixel grid is coarse but +# shreds fine detail at a non-integer ratio. This one instead: +# 1. detects the art-pixel BLOCK SIZE the generator actually drew at, and snaps to that grid +# (one output pixel per art pixel — no half-blocks, no broken outlines), +# 2. common-scales the sheet to the target height with a box filter, +# 3. quantises every pixel back to the sheet's own palette, so the result is flat pixel art +# rather than a blurred photo of pixel art. +# Usage: _fine.py sheet.png out1.png out2.png ... [--h 224] [--cut X,Y,...] +import sys +from PIL import Image +import numpy as np + +TARGET_H = 224 +KEY_D2 = 60 * 60 +MIN_GAP = 12 +MIN_BLOB = 60 + +def key(im): + a = np.array(im.convert('RGBA')) + bg = a[0, 0, :3].astype(int) + d = ((a[..., :3].astype(int) - bg) ** 2).sum(2) + magenta = np.minimum(a[..., 0], a[..., 2]).astype(int) > a[..., 1].astype(int) + 40 + a[(d < KEY_D2) | magenta] = 0 + # De-fringe. The generator does not always give you a pure key colour, and whatever it does give + # bleeds a halo one pixel into the sprite — which survives as a magenta rim once the sprite is on + # a green court. Anything on the EDGE that is still leaning towards the background goes too. + for _ in range(2): + solid = a[..., 3] > 0 + edge = solid & ~(np.roll(solid, 1, 0) & np.roll(solid, -1, 0) & np.roll(solid, 1, 1) & np.roll(solid, -1, 1)) + near = ((a[..., :3].astype(int) - bg) ** 2).sum(2) < KEY_D2 * 9 + a[edge & near] = 0 + return a + +def block_size(a): + ink = a[..., 3] > 0 + ys, xs = np.where(ink) + sub = a[ys.min():ys.max()+1, xs.min():xs.max()+1] + h, w = sub.shape[:2] + best = 1 + for b in range(2, 13): + if h < b * 6: break + ok = tot = 0 + for y in range(0, h - b, b): + for x in range(0, w - b, b): + blk = sub[y:y+b, x:x+b].reshape(-1, 4) + tot += 1 + if (blk == blk[0]).all(): ok += 1 + if tot and ok / tot > 0.82: best = b + return best + +def frames(a, cuts): + ink = (a[..., 3] > 0) + cols = ink.any(0) + out, start, gap = [], None, 0 + for x in range(len(cols)): + if cols[x]: + if start is None: start = x + gap = 0 + elif start is not None: + gap += 1 + if gap >= MIN_GAP: out.append((start, x - gap + 1)); start = None + if start is not None: out.append((start, len(cols))) + for c in sorted(cuts, reverse=True): # manual splits where two figures touch + for i, (x0, x1) in enumerate(out): + if x0 < c < x1: out[i:i+1] = [(x0, c), (c, x1)]; break + return out + +def main(sheet, outs, target_h, cuts, anchor): + a = key(Image.open(sheet)) + b = block_size(a) + crops = [] + for x0, x1 in frames(a, cuts): + sub = a[:, x0:x1] + ys = np.where((sub[..., 3] > 0).any(1))[0]; xs = np.where((sub[..., 3] > 0).any(0))[0] + crops.append(sub[ys.min():ys.max()+1, xs.min():xs.max()+1]) + if len(crops) != len(outs): + sys.exit(f'ERROR: found {len(crops)} frames, expected {len(outs)} ({sheet})') + # palette = the colours the generator actually used, on the largest frame + big = max(crops, key=lambda c: (c[..., 3] > 0).sum()) + px = big[big[..., 3] > 200][:, :3] + pal = np.unique((px // 8) * 8, axis=0) + # ONE scale for the sheet, anchored on a chosen frame — the STANDING pose is the 224 reference, + # so a smash with the arm overhead stays legitimately taller (the game scales off img.height). + anchor_h = crops[anchor].shape[0] if 0 <= anchor < len(crops) else max(c.shape[0] for c in crops) + k = target_h / (anchor_h / b) + for c, out in zip(crops, outs): + h, w = c.shape[:2] + snap = Image.fromarray(c, 'RGBA').resize((max(1, round(w / b)), max(1, round(h / b))), Image.BOX) + fh = max(1, round(snap.height * k)); fw = max(1, round(snap.width * k)) + im = snap.resize((fw, fh), Image.BOX) + arr = np.array(im).astype(int) + solid = arr[..., 3] > 110 + flat = arr[..., :3].reshape(-1, 3) + idx = ((flat[:, None, :] - pal[None, :, :]) ** 2).sum(2).argmin(1) + arr[..., :3] = pal[idx].reshape(arr.shape[0], arr.shape[1], 3) + arr[..., 3] = np.where(solid, 255, 0) + Image.fromarray(arr.astype('uint8'), 'RGBA').save(out) + print(f'{out}: {fw}x{fh} (art block {b} in source)') + +if __name__ == '__main__': + args = sys.argv[1:] + th, cuts, anchor = TARGET_H, [], -1 + if '--h' in args: i = args.index('--h'); th = int(args[i+1]); del args[i:i+2] + if '--cut' in args: i = args.index('--cut'); cuts = [int(v) for v in args[i+1].split(',')]; del args[i:i+2] + if '--anchor' in args: i = args.index('--anchor'); anchor = int(args[i+1]); del args[i:i+2] + main(args[0], args[1:], th, cuts, anchor) diff --git a/assets/_sheet_player_fine.png b/assets/_sheet_player_fine.png new file mode 100644 index 0000000..a27a6d5 Binary files /dev/null and b/assets/_sheet_player_fine.png differ diff --git a/assets/_sheet_zombie_fine.png b/assets/_sheet_zombie_fine.png new file mode 100644 index 0000000..2de1cf1 Binary files /dev/null and b/assets/_sheet_zombie_fine.png differ diff --git a/assets/p_idle1.png b/assets/p_idle1.png index 2e752b0..a797c6b 100644 Binary files a/assets/p_idle1.png and b/assets/p_idle1.png differ diff --git a/assets/p_idle2.png b/assets/p_idle2.png index 24579a7..65f2efa 100644 Binary files a/assets/p_idle2.png and b/assets/p_idle2.png differ diff --git a/assets/p_smash.png b/assets/p_smash.png index da7b4cc..e31cff1 100644 Binary files a/assets/p_smash.png and b/assets/p_smash.png differ diff --git a/assets/p_toss.png b/assets/p_toss.png index c70896a..d27d60d 100644 Binary files a/assets/p_toss.png and b/assets/p_toss.png differ diff --git a/assets/z_aw.png b/assets/z_aw.png index 0ac074a..3cfc998 100644 Binary files a/assets/z_aw.png and b/assets/z_aw.png differ diff --git a/assets/z_bw.png b/assets/z_bw.png index fca4bac..a304854 100644 Binary files a/assets/z_bw.png and b/assets/z_bw.png differ diff --git a/assets/z_f1.png b/assets/z_f1.png index cecf680..7a1fe19 100644 Binary files a/assets/z_f1.png and b/assets/z_f1.png differ diff --git a/assets/z_f2.png b/assets/z_f2.png index b05d404..813bcac 100644 Binary files a/assets/z_f2.png and b/assets/z_f2.png differ diff --git a/assets/z_f3.png b/assets/z_f3.png index 1c57acc..bb2df37 100644 Binary files a/assets/z_f3.png and b/assets/z_f3.png differ diff --git a/index.html b/index.html index 0b87adb..ac18b8a 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,6 @@
- +