diff --git a/assets/_famref.py b/assets/_famref.py
index b922436..eeb3950 100644
--- a/assets/_famref.py
+++ b/assets/_famref.py
@@ -77,6 +77,39 @@ def bed_magenta(im):
return Image.fromarray(a.astype('uint8'), 'RGBA')
+def racket_side(im):
+ """Which side of the body the racket is drawn on, as a fraction of the frame width.
+
+ The string bed is the marker and it locates itself: on the master's frames it is PAINTED (a flat
+ pale blob), on a cut family's it keys out to an enclosed hole. Either way it is the largest such
+ region above the shoes, and its centroid against the body's says which way the figure is drawn.
+ Returns +ve for racket-right, -ve for racket-left, or None when no bed can be found.
+ """
+ a = np.array(im.convert('RGBA')).astype(int)
+ R, G, B, A = a[..., 0], a[..., 1], a[..., 2], a[..., 3]
+ h, w = A.shape
+ body = A > 40
+ if not body.any(): return None
+ lo, hi = np.minimum(np.minimum(R, G), B), np.maximum(np.maximum(R, G), B)
+ upper = np.zeros_like(body); upper[:int(h * 0.72)] = True
+ pale = body & (lo > 140) & (hi - lo < 50) & upper
+ # the keyed-out case: transparent pixels NOT reachable from the border are the bed
+ seen = np.zeros_like(body)
+ stack = [(y, x) for x in range(w) for y in (0, h - 1) if not body[y, x]]
+ stack += [(y, x) for y in range(h) for x in (0, w - 1) if not body[y, x]]
+ for y, x in stack: seen[y, x] = True
+ while stack:
+ cy, cx = stack.pop()
+ for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)):
+ ny, nx = cy + dy, cx + dx
+ if 0 <= ny < h and 0 <= nx < w and not body[ny, nx] and not seen[ny, nx]:
+ seen[ny, nx] = True; stack.append((ny, nx))
+ hole = (~body) & (~seen) & upper
+ bed = hole if hole.sum() >= 100 else pale
+ if bed.sum() < 60: return None
+ return float((np.nonzero(bed)[1].mean() - np.nonzero(body)[1].mean()) / w)
+
+
def legs(im):
"""Feet-anchored, mass-centred silhouette of the bottom third — the stride comparison surface.
@@ -183,6 +216,28 @@ def check():
sys.exit('GATE FAILED: frozen stride pair(s): ' + ', '.join(frozen) +
f' — need >={STRIDE_MIN}%. Draw the twin with _stride.py before rolling a family.')
+ # ---- WHICH WAY EACH WALK FRAME IS DRAWN, pinned. render.js mirrors a body to face its direction
+ # of travel, and it can only do that if it knows which way the art faces. Today the healthy stride
+ # is drawn racket-RIGHT and the wound strides racket-LEFT — the wounded body faces the other way,
+ # and has since the master was made — so drawZombie carries a branch keyed on the frame name.
+ # THAT BRANCH IS ONLY CORRECT WHILE THIS HOLDS. A re-roll that mirrors a row would silently make
+ # every body in it walk backwards, which is exactly the bug Oliver found twice from the bench, so
+ # it fails here instead. If a side legitimately changes, fix drawZombie's `artFacesRight` with it.
+ want = {'z_r2': +1, 'z_r3': +1, 'z_rw1': -1, 'z_rw1b': -1, 'z_rw2': -1, 'z_rw2b': -1}
+ wrong = []
+ for n, expect in want.items():
+ r = racket_side(Image.open(os.path.join(HERE, n + '.png')))
+ if r is None:
+ wrong.append(f'{n}: no racket bed found'); continue
+ got = 'RIGHT' if r > 0.06 else 'LEFT' if r < -0.06 else 'CENTRE'
+ ok = (r > 0.06) if expect > 0 else (r < -0.06)
+ print(f' racket {n:9s} {got:6s} {r:+.3f} {"ok" if ok else "CHANGED SIDE"}')
+ if not ok:
+ wrong.append(f'{n}: {got} ({r:+.3f}), expected {"RIGHT" if expect > 0 else "LEFT"}')
+ if wrong:
+ sys.exit('GATE FAILED: walk art changed facing: ' + '; '.join(wrong) +
+ "\n -> update drawZombie's artFacesRight test to match, then re-run.")
+
if __name__ == '__main__':
if '--check' not in sys.argv: compose()
diff --git a/index.html b/index.html
index 4e8fc37..e8960fa 100644
--- a/index.html
+++ b/index.html
@@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
-
+