diff --git a/assets/_runroll.py b/assets/_runroll.py index 2d5f693..5f180c0 100644 --- a/assets/_runroll.py +++ b/assets/_runroll.py @@ -176,6 +176,24 @@ def cut(k, install=False, n=0): os.makedirs(outdir, exist_ok=True) outs = [os.path.join(outdir, f'{pre}{i}{suf}.png') for i in range(1, got + 1)] _fine.main(sheet, outs, TARGET_H, [], -1) + # ANCHOR EVERY FRAME ON ITS FEET. drawChar centres a frame by its total width, and the racket's + # reach varies wildly across a cycle (the cut female frames measured -28..+15 px of feet drift + # from frame centre) — so the BODY teleported sideways every frame, which is exactly Oliver's + # "very flashy and unnatural". Pad each frame with transparency until the lower-third centroid + # (the same surface the stride gate reads) sits at the frame's horizontal centre. + import numpy as np + for o in outs: + im = Image.open(o).convert('RGBA') + a = np.array(im) + op = a[..., 3] > 40 + ys, xs = np.nonzero(op[int(op.shape[0] * 0.66):]) + if not len(xs): continue + cx = xs.mean() + off = round(2 * (cx - im.width / 2)) # feet right of centre -> pad RIGHT, and vice versa: + if off == 0: continue # new centre (w+|off|)/2 then lands on the feet + pad = Image.new('RGBA', (im.width + abs(off), im.height), (0, 0, 0, 0)) + pad.paste(im, (0 if off > 0 else abs(off), 0)) + pad.save(o) # THE STRIDE GATE, on what was actually cut: every neighbouring pair (wrapping — it loops) must # differ in the legs, or the cycle holds a duplicated pose. Threshold calibrated against # _famref's (30% = a real stride pair; 12 catches only true near-duplicates). diff --git a/assets/o_run1.png b/assets/o_run1.png index 79001b0..accf023 100644 Binary files a/assets/o_run1.png and b/assets/o_run1.png differ diff --git a/assets/o_run1_f.png b/assets/o_run1_f.png index 5a550bb..647d155 100644 Binary files a/assets/o_run1_f.png and b/assets/o_run1_f.png differ diff --git a/assets/o_run2.png b/assets/o_run2.png index 88388c6..fe792a7 100644 Binary files a/assets/o_run2.png and b/assets/o_run2.png differ diff --git a/assets/o_run2_f.png b/assets/o_run2_f.png index e7b4105..67313c4 100644 Binary files a/assets/o_run2_f.png and b/assets/o_run2_f.png differ diff --git a/assets/o_run3.png b/assets/o_run3.png index b040929..08104f5 100644 Binary files a/assets/o_run3.png and b/assets/o_run3.png differ diff --git a/assets/o_run3_f.png b/assets/o_run3_f.png index 9c1f184..592488e 100644 Binary files a/assets/o_run3_f.png and b/assets/o_run3_f.png differ diff --git a/assets/o_run4.png b/assets/o_run4.png index 87bbcee..1a6ceeb 100644 Binary files a/assets/o_run4.png and b/assets/o_run4.png differ diff --git a/assets/o_run4_f.png b/assets/o_run4_f.png index f97260e..4539062 100644 Binary files a/assets/o_run4_f.png and b/assets/o_run4_f.png differ diff --git a/assets/o_run5.png b/assets/o_run5.png index e45f024..cb55fca 100644 Binary files a/assets/o_run5.png and b/assets/o_run5.png differ diff --git a/assets/o_run5_f.png b/assets/o_run5_f.png index 6f9058a..ecb8c96 100644 Binary files a/assets/o_run5_f.png and b/assets/o_run5_f.png differ diff --git a/assets/o_run6.png b/assets/o_run6.png index 71e3e9b..124bf12 100644 Binary files a/assets/o_run6.png and b/assets/o_run6.png differ diff --git a/assets/o_run6_f.png b/assets/o_run6_f.png index f74fc1d..6d46a1d 100644 Binary files a/assets/o_run6_f.png and b/assets/o_run6_f.png differ diff --git a/assets/o_run7.png b/assets/o_run7.png index 4e92674..d66a01f 100644 Binary files a/assets/o_run7.png and b/assets/o_run7.png differ diff --git a/assets/o_run7_f.png b/assets/o_run7_f.png index 6a59e00..5c75728 100644 Binary files a/assets/o_run7_f.png and b/assets/o_run7_f.png differ diff --git a/assets/o_run8.png b/assets/o_run8.png index 0efda55..be92f24 100644 Binary files a/assets/o_run8.png and b/assets/o_run8.png differ diff --git a/assets/o_run8_f.png b/assets/o_run8_f.png index 1c25777..ec06ed8 100644 Binary files a/assets/o_run8_f.png and b/assets/o_run8_f.png differ diff --git a/assets/p_run1.png b/assets/p_run1.png index e4cda31..d7852d6 100644 Binary files a/assets/p_run1.png and b/assets/p_run1.png differ diff --git a/assets/p_run1_f.png b/assets/p_run1_f.png index d8c37d7..480bf80 100644 Binary files a/assets/p_run1_f.png and b/assets/p_run1_f.png differ diff --git a/assets/p_run2.png b/assets/p_run2.png index 4522cb1..f17ad9e 100644 Binary files a/assets/p_run2.png and b/assets/p_run2.png differ diff --git a/assets/p_run2_f.png b/assets/p_run2_f.png index 21f55b9..0ed39f8 100644 Binary files a/assets/p_run2_f.png and b/assets/p_run2_f.png differ diff --git a/assets/p_run3.png b/assets/p_run3.png index 627652b..fe5efe4 100644 Binary files a/assets/p_run3.png and b/assets/p_run3.png differ diff --git a/assets/p_run3_f.png b/assets/p_run3_f.png index 1d27b5b..5c777b6 100644 Binary files a/assets/p_run3_f.png and b/assets/p_run3_f.png differ diff --git a/assets/p_run4.png b/assets/p_run4.png index 28c0278..c425704 100644 Binary files a/assets/p_run4.png and b/assets/p_run4.png differ diff --git a/assets/p_run4_f.png b/assets/p_run4_f.png index 24d8e91..a3aebbf 100644 Binary files a/assets/p_run4_f.png and b/assets/p_run4_f.png differ diff --git a/assets/p_run5.png b/assets/p_run5.png index 5664c65..79bb710 100644 Binary files a/assets/p_run5.png and b/assets/p_run5.png differ diff --git a/assets/p_run5_f.png b/assets/p_run5_f.png index 0d7e575..f6134cf 100644 Binary files a/assets/p_run5_f.png and b/assets/p_run5_f.png differ diff --git a/assets/p_run6.png b/assets/p_run6.png index d072e6d..f34e79f 100644 Binary files a/assets/p_run6.png and b/assets/p_run6.png differ diff --git a/assets/p_run6_f.png b/assets/p_run6_f.png index 36ce399..7eda2d8 100644 Binary files a/assets/p_run6_f.png and b/assets/p_run6_f.png differ diff --git a/index.html b/index.html index ca3aae1..19b430f 100644 --- a/index.html +++ b/index.html @@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index c7c7f5c..435c3f2 100644 --- a/src/config.js +++ b/src/config.js @@ -41,6 +41,8 @@ export const TUNE = { // the dive is what buys the pace its net clearance serveHardErr: 1.6,// how much a hard serve multiplies the timing scatter: pace is paid for in // faults, which is what makes tap = safe second serve a real decision + runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was + // tuned on 2-4 near-duplicate frames; on eight real phases it strobed serveSpin: 1.3, // extra gravity a full-power serve carries (was a fixed 0.35). Without it the // net-clearance loop ate the whole smash: a flat fast ball can't get over the // tape AND come down on a target, so it got throttled back to walking pace. diff --git a/src/render.js b/src/render.js index 02fa187..e4e96bc 100644 --- a/src/render.js +++ b/src/render.js @@ -221,7 +221,7 @@ function farB(z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 109; +export const SPRV = 110; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". @@ -1445,10 +1445,12 @@ function drawChar(ch) { name = 'p_pull'; // The run cycle at whatever count the character's sheet carries: the _runroll.py cycles are 6 // (one call, one row, one character — see assets/_runroll.py for why the old per-frame art - // flickered); un-rolled characters fall back to 4 or 2. Same CYCLE rate at every count — more - // frames subdivide the stride, they do not speed it up, exactly as ZFRAMES does for the shamblers. + // flickered); un-rolled characters fall back to 4 or 2. const runN = SPR[pre + 'run8'] ? 8 : SPR[pre + 'run6'] ? 6 : SPR[pre + 'run4'] ? 4 : 2; - if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * 4.5 * runN) % runN + 1); + // The rolled 6/8-frame cycles run at their own STRIDE RATE (TUNE.runHz, cycles/s). The old 4.5 + // was tuned on 2-4 near-duplicate frames, where it read as vigour — on eight real phases it was + // a strobe ("very flashy and unnatural"), most of a full stride every display frame. + if (ch.pose === 'run') name = pre + 'run' + (Math.floor(ch.runT * (runN >= 6 ? TUNE.runHz : 4.5) * runN) % runN + 1); // (the receiver used to flip to run1/run2 here as a "split-step" — it read as a man sprinting on // the spot, because the run frames are a full leaning stride, not a ready-hop. He idles now.) if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');