Draw the wind-up from the photographed serve, not from prose

Oliver's idea, and it is the fix for the process as much as for the frames: give
the generator the real serve sequence as an IMAGE and let it copy the poses,
instead of describing a pose in prose and re-rolling until it lands. One
generation — a composite of his five photographed frames over two of our sprites
— returned a magenta row of the whole serve in the game's own character and kit.
The four stages are sliced straight out of it, anchored on the standing figure.

_fine.py gets --defringe: its two de-fringe passes each eat a one-pixel line, and
a racket frame IS a one-pixel line — that is what had been leaving the men holding
a grip and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 01:00:56 +02:00
1 parent bcff17812f
commit f6dfc57a06
7 files changed
+12 -7

No files matched your search

+10 -5
View File
@@ -17,7 +17,7 @@ KEY_D2 = 60 * 60
MIN_GAP = 12
MIN_BLOB = 60
def key(im):
def key(im, defringe=2):
a = np.array(im.convert('RGBA'))
bg = a[0, 0, :3].astype(int)
d = ((a[..., :3].astype(int) - bg) ** 2).sum(2)
@@ -26,7 +26,10 @@ def key(im):
# 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):
# ...but each pass also eats a 1-pixel line, and a racket frame IS a 1-pixel line: two passes
# removed the rackets from a whole serve sheet and left men holding a grip. --defringe 1 (or 0)
# is for sheets whose art is that thin.
for _ in range(defringe):
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
@@ -67,8 +70,8 @@ def frames(a, cuts):
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))
def main(sheet, outs, target_h, cuts, anchor, defringe=2):
a = key(Image.open(sheet), defringe)
b = block_size(a)
crops = []
for x0, x1 in frames(a, cuts):
@@ -104,5 +107,7 @@ if __name__ == '__main__':
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]
de = 2
if '--defringe' in args: i = args.index('--defringe'); de = int(args[i+1]); 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)
main(args[0], args[1:], th, cuts, anchor, de)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -97,6 +97,6 @@
<p id="build"></p>
</div>
</div>
<script type="module" src="main.js?v=78"></script>
<script type="module" src="main.js?v=79"></script>
</body>
</html>
+1 -1
View File
@@ -155,7 +155,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 71; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPRV = 72; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPR = {};
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_swing1','p_swing2','p_die1','p_die2','p_die3','p_die4','p_die5',
'p_pull1','p_pull2','p_pull3','p_pull4', // the serve WIND-UP, one frame per quarter of the pull (see drawChar)