diff --git a/PROJECT.md b/PROJECT.md index 07f412b..81f2801 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -378,6 +378,11 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu (`HUD`, in portrait logical px — landscape has a shorter band, so the rule is conservative there rather than over-eager). Result: 92 of 101 raisers below the board's bottom edge, against 103 of 134 before. +- **A raise must be ARMS OVER STAND, and `_crowdpass.py` tests all three words.** The rolls + repaint clothing as well as posing people, so "was stand, is now person" also catches a + cream shirt that came back a shade brighter (`PALE_MAX`) and a genuine arm drawn over + something already bright, where raising him DELETES a white block (`COVER_MAX`). Both flash + exactly as hard as a real error would. - **A raiser is ~8x6 logical px, so a pose that appears and holds is invisible.** What reads as applause is the CYCLE — arms high, hands together, up, down, at about 3.5 Hz — and how many people are doing it at once (`crowd.pop`). Oliver on the first per-person cut: "when clapping I barely see @@ -401,6 +406,13 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu `assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history. ## Settled feel decisions — do not regress silently +- **ANYTHING DRAWN OVER THE CACHED STAND MUST CARRY THE STAND'S OWN TREATMENTS.** There are two — + `standDim` and `topFade` — and a raiser box drawn with one of them missing lights up as a bright + rectangle every time that spectator stands. That is what the "white flashing in the upper right + corner of the crowd" was (2026-08-21): the top fade was baked into the backdrop and the live + raisers were not repainting it. Both now come from one function each, called from `paintBackdrop` + and from `drawRaisers`. The gradient is built in USER space, so filling any sub-rect with it picks + up the right alpha for free. Two copies of either number is the same bug waiting again. - **The frame is dark at BOTH ends.** The run-off apron already took the bottom of the screen down to near-black below the near baseline; `TUNE.standTop` does the same at the top, so the stand fades UP out of the dark instead of being cut off at y = 0. Oliver, 2026-08-21: "the bottom of the screen diff --git a/assets/_crowdpass.py b/assets/_crowdpass.py index e6dedc0..c4458b4 100644 --- a/assets/_crowdpass.py +++ b/assets/_crowdpass.py @@ -76,6 +76,19 @@ HUD = [(6, 6, 156, 62), # scoreboard + the GAME/SETS panel under it (170, 6, 264, 30)] # the BLOOD panel, clear of the quit chip MIN_AREA, MAX_AREA = 150, 3000 # an arm blob: one pair of raised arms, or one arm +# ...AND IT HAS TO BE ARMS GOING UP OVER STAND. Oliver: "what is this white flashing while applause +# in the upper right corner of the crowd" — and with the photographers switched off entirely it was +# still there, so it was never them. Two different blobs were doing it, and neither is a raise: +# PALE SWAPS — the roll repainted somebody's cream shirt, and `stand()` (which only asks whether a +# pixel matches its row's modal colour) called the old one stand. Nothing rises; a white block +# just changes pattern at 3.5 Hz. +# BRIGHT COVER-UPS — a genuine arm, but drawn over something ALREADY BRIGHT (a white shirt in the +# row behind, lum 239). Raising him deletes a white block and sitting him down brings it back, +# which flashes exactly as hard as adding one would. +# So a blob must be warm-ish, must not be a pale block, and must ADD light to something dark. +PALE_MAX = 0.35 # of the blob may be bright-and-colourless (a cream shirt) +COVER_MAX = 150 # ...and what it covers in the seated frame must be darker than this +LIFT_MIN = 12 # ...by at least this much: arms over wall, never over a white shirt ARM_H, ARM_W = (10, 70), (8, 90) # ...and its shape, in px of the 1408x768 sheet BAND = 8 # rows the background colour is sampled over PAD = 3 # px of margin around a raiser's box @@ -127,6 +140,13 @@ def raisers(A, B): # sheet; arms that rise out of empty stand belong to nobody and read as floating hands. if bgA[sl[0].stop:min(A.shape[0], sl[0].stop + 6), sl[1]].mean() > 0.55: continue + # ...AND IT HAS TO BE A RAISE (see PALE_MAX / COVER_MAX / LIFT_MIN above). + m = lab[sl] == i + 1 + px, was = B[sl][m].astype(int), A[sl][m].astype(int) + if ((abs(px[:, 0] - px[:, 2]) < 26) & (px[:, 0] > 140)).mean() > PALE_MAX: + continue + if was.mean() > COVER_MAX or px.mean() - was.mean() < LIFT_MIN: + continue out.append([sl[0].start, sl[1].start, sl[0].stop, sl[1].stop, i + 1]) return out, lab diff --git a/assets/x_crowd.png b/assets/x_crowd.png index 2f95fd7..f1bea78 100644 Binary files a/assets/x_crowd.png and b/assets/x_crowd.png differ diff --git a/assets/x_crowd_c.png b/assets/x_crowd_c.png index 511926a..66e2d07 100644 Binary files a/assets/x_crowd_c.png and b/assets/x_crowd_c.png differ diff --git a/assets/x_crowd_h.png b/assets/x_crowd_h.png index 3d01876..3c998ac 100644 Binary files a/assets/x_crowd_h.png and b/assets/x_crowd_h.png differ diff --git a/index.html b/index.html index 1c3e71f..165aa6c 100644 --- a/index.html +++ b/index.html @@ -664,6 +664,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/crowd.js b/src/crowd.js index 743d13b..cde864e 100644 --- a/src/crowd.js +++ b/src/crowd.js @@ -12,101 +12,94 @@ export const RAISERS = [ [-0.0014, 0.6458, 0.0327, 0.0560, 2], [-0.0014, 0.7682, 0.0419, 0.0221, 2], [-0.0014, 0.8451, 0.0327, 0.0521, 3], - [0.0028, 0.7878, 0.0369, 0.0286, 2], - [0.0064, 0.3190, 0.0206, 0.0286, 3], + [0.0064, 0.3190, 0.0206, 0.0286, 2], [0.0071, 0.9128, 0.0369, 0.0286, 3], - [0.0234, 0.5521, 0.0163, 0.0456, 1], [0.0234, 0.9440, 0.0206, 0.0286, 2], [0.0355, 0.6419, 0.0753, 0.0677, 3], [0.0568, 0.8503, 0.0412, 0.0273, 1], [0.0689, 0.3190, 0.0334, 0.0286, 2], [0.0859, 0.7669, 0.0249, 0.0247, 2], - [0.0952, 0.5586, 0.0320, 0.0690, 3], - [0.0980, 0.9544, 0.0419, 0.0286, 2], - [0.1065, 0.8086, 0.0206, 0.0273, 3], + [0.0952, 0.5755, 0.0284, 0.0521, 2], [0.1278, 0.6719, 0.0284, 0.0299, 2], - [0.1293, 0.5000, 0.0511, 0.0755, 1], - [0.1357, 0.7930, 0.1037, 0.0547, 3], [0.1392, 0.9362, 0.0213, 0.0234, 2], [0.1399, 0.3190, 0.0249, 0.0286, 2], [0.1562, 0.6732, 0.0206, 0.0286, 2], [0.1598, 0.9518, 0.0213, 0.0234, 2], - [0.1619, 0.6003, 0.0199, 0.0286, 2], [0.1641, 0.8672, 0.0298, 0.0430, 2], - [0.1690, 0.9753, 0.0291, 0.0221, 2], [0.1768, 0.7305, 0.0376, 0.0299, 1], - [0.1854, 0.5052, 0.0170, 0.0469, 1], + [0.1854, 0.7982, 0.0540, 0.0495, 2], [0.1918, 0.5964, 0.0526, 0.0326, 2], - [0.2259, 0.4857, 0.0732, 0.0911, 3], [0.2393, 0.8594, 0.0298, 0.0299, 2], - [0.2401, 0.7669, 0.0241, 0.0247, 3], [0.2528, 0.9336, 0.0284, 0.0391, 3], [0.2557, 0.7305, 0.0213, 0.0299, 1], [0.2599, 0.5924, 0.0384, 0.0365, 2], [0.2734, 0.6458, 0.0199, 0.0221, 2], - [0.2891, 0.8398, 0.2088, 0.1419, 3], + [0.2891, 0.9440, 0.0213, 0.0378, 2], + [0.2898, 0.8503, 0.1371, 0.0859, 3], [0.2976, 0.6732, 0.0170, 0.0326, 2], - [0.2976, 0.8086, 0.0384, 0.0286, 3], [0.3026, 0.7682, 0.0419, 0.0273, 3], - [0.3359, 0.4844, 0.0724, 0.0911, 3], + [0.3359, 0.4922, 0.0575, 0.0833, 2], + [0.3565, 0.9440, 0.0206, 0.0378, 2], [0.3729, 0.6602, 0.0206, 0.0312, 2], - [0.3899, 0.6003, 0.0447, 0.0391, 2], + [0.3899, 0.6003, 0.0241, 0.0286, 2], + [0.3899, 0.9440, 0.0206, 0.0286, 2], [0.3977, 0.6523, 0.0831, 0.0638, 3], - [0.4055, 0.7669, 0.0724, 0.0690, 3], - [0.4062, 0.4857, 0.0376, 0.1133, 3], + [0.4055, 0.8008, 0.0213, 0.0352, 1], + [0.4233, 0.7669, 0.0504, 0.0247, 2], + [0.4354, 0.8438, 0.0291, 0.0534, 3], + [0.4389, 0.9362, 0.0220, 0.0234, 2], + [0.4602, 0.9427, 0.0170, 0.0299, 2], [0.4730, 0.5534, 0.0291, 0.0508, 3], + [0.4730, 0.8672, 0.0249, 0.0221, 2], [0.4901, 0.6458, 0.0533, 0.0560, 2], [0.4936, 0.7044, 0.0298, 0.0247, 2], [0.4936, 0.7669, 0.0497, 0.0247, 2], [0.5021, 0.8503, 0.0291, 0.0312, 3], [0.5064, 0.3190, 0.0206, 0.0286, 2], [0.5064, 0.5755, 0.0206, 0.0234, 1], - [0.5064, 0.9049, 0.0241, 0.0260, 3], - [0.5312, 0.9141, 0.0831, 0.0690, 3], - [0.5355, 0.8086, 0.0249, 0.0286, 3], - [0.5398, 0.4922, 0.1300, 0.1367, 3], [0.5611, 0.6719, 0.0291, 0.0299, 2], - [0.5646, 0.7878, 0.0668, 0.0391, 3], - [0.5682, 0.1562, 0.0334, 0.0247, 1], [0.5682, 0.8776, 0.0298, 0.0221, 2], [0.5689, 0.3190, 0.0334, 0.0286, 2], + [0.5724, 0.9362, 0.0220, 0.0234, 2], [0.5895, 0.6732, 0.0192, 0.0286, 2], [0.5938, 0.1107, 0.0462, 0.0312, 2], + [0.5938, 0.5534, 0.0334, 0.0299, 3], + [0.5959, 0.5846, 0.0149, 0.0443, 3], [0.6023, 0.2643, 0.0249, 0.0208, 2], + [0.6186, 0.5911, 0.0213, 0.0378, 2], [0.6186, 0.9362, 0.0249, 0.0365, 3], [0.6357, 0.7227, 0.0284, 0.0208, 2], [0.6357, 0.8516, 0.0284, 0.0273, 3], [0.6371, 0.6458, 0.1271, 0.0703, 3], [0.6399, 0.3190, 0.0249, 0.0286, 2], [0.6776, 0.6003, 0.0249, 0.0286, 2], - [0.6925, 0.4870, 0.1016, 0.1146, 3], - [0.6974, 0.9271, 0.0497, 0.0456, 3], + [0.7053, 0.9297, 0.0419, 0.0430, 2], [0.7230, 0.7669, 0.0412, 0.0312, 2], [0.7393, 0.8672, 0.0213, 0.0234, 2], [0.7401, 0.5990, 0.0327, 0.0299, 2], [0.7401, 0.8398, 0.0327, 0.0273, 1], - [0.7727, 0.8086, 0.0923, 0.0729, 3], - [0.7940, 0.8828, 0.0788, 0.0586, 3], + [0.7692, 0.5755, 0.0249, 0.0234, 1], + [0.7727, 0.8503, 0.0916, 0.0312, 3], [0.7940, 0.9440, 0.0163, 0.0286, 2], [0.7983, 0.6732, 0.0163, 0.0326, 2], - [0.8026, 0.2253, 0.0241, 0.0247, 2], [0.8068, 0.7695, 0.0419, 0.0260, 3], + [0.8182, 0.9023, 0.0547, 0.0391, 3], [0.8565, 0.9440, 0.0206, 0.0378, 2], - [0.8622, 0.4844, 0.0817, 0.1549, 3], + [0.8622, 0.4922, 0.0312, 0.0742, 2], [0.8686, 0.8503, 0.0291, 0.0326, 3], - [0.8693, 0.2878, 0.0327, 0.0260, 2], - [0.8771, 0.2253, 0.0206, 0.0365, 3], + [0.8899, 0.6003, 0.0241, 0.0286, 2], [0.8899, 0.9440, 0.0206, 0.0378, 2], [0.8977, 0.6732, 0.0249, 0.0286, 2], [0.8984, 0.8503, 0.0284, 0.0234, 1], - [0.9048, 0.2604, 0.0220, 0.0247, 1], - [0.9062, 0.7982, 0.0249, 0.0378, 3], + [0.9062, 0.5755, 0.0206, 0.0234, 1], + [0.9062, 0.8008, 0.0206, 0.0352, 1], [0.9062, 0.8776, 0.0206, 0.0326, 3], [0.9233, 0.6523, 0.0582, 0.0599, 3], [0.9240, 0.7669, 0.0533, 0.0247, 2], - [0.9240, 0.9271, 0.0533, 0.0456, 3], [0.9354, 0.2461, 0.0291, 0.0286, 2], [0.9354, 0.8438, 0.0291, 0.0534, 3], + [0.9389, 0.9362, 0.0220, 0.0234, 2], + [0.9602, 0.9440, 0.0170, 0.0286, 2], [0.9730, 0.5586, 0.0284, 0.0456, 3], [0.9730, 0.8672, 0.0249, 0.0234, 2], ]; diff --git a/src/render.js b/src/render.js index e7932ca..4204157 100644 --- a/src/render.js +++ b/src/render.js @@ -235,7 +235,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 = 147; +export const SPRV = 148; // 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". @@ -443,11 +443,25 @@ const standDim = pt => Math.min(0.88, (court.dim ?? TUNE.standBack) + 0.26 * pt) // looks pretty artificial"). // The sub-rect is drawn at EXACTLY the scale paintBackdrop used for the whole sheet (kx/ky below), // which is what guarantees the raised arms land on the same pixel grid as the body under them. +// THE TOP FADE, in ONE place. It is baked into the cached stand, and the live raisers have to paint +// it over themselves as well — a spectator whose box was redrawn without it lit up as a bright +// rectangle every time he stood, which is what Oliver saw: "what is this white flashing while +// applause in the upper right corner of the crowd". Same class of bug as two copies of standDim. +// A gradient in user space, so filling ANY sub-rect with it picks up the right alpha for free. +function topFade(c, standsBottom) { + if (!(TUNE.standTop > 0)) return null; + const g = c.createLinearGradient(0, -8, 0, standsBottom * 0.42); + g.addColorStop(0, `rgba(6,10,6,${TUNE.standTop})`); + g.addColorStop(1, 'rgba(6,10,6,0)'); + return g; +} + const rnd = i => ((i * 9301 + 49297) % 233280) / 233280; // one stable pseudo-random per spectator function drawRaisers(img, standsBottom, pt, frameOf) { const fh = img.height / FRAMES, crowdH = standsBottom - AD_H + 2; const kx = W / img.width, ky = (crowdH + 2) / fh; const d = standDim(pt), tint = d > 0 ? `rgba(12,8,20,${d})` : null; + const fade = topFade(ctx, standsBottom); let open = false; for (let i = 0; i < RAISERS.length; i++) { const r = RAISERS[i], f = frameOf(i, r[0], r[4]); @@ -458,8 +472,9 @@ function drawRaisers(img, standsBottom, pt, frameOf) { } const sx = r[0] * img.width, sy = r[1] * fh, sw = r[2] * img.width, sh = r[3] * fh; ctx.drawImage(img, sx, sy + f * fh, sw, sh, sx * kx, sy * ky, sw * kx + 0.5, sh * ky + 0.5); - // re-tinted like the cached stand, so a raised pair of arms cannot pop brighter than the rot + // re-tinted AND re-faded exactly like the cached stand underneath, or the box lights up if (tint) { ctx.fillStyle = tint; ctx.fillRect(sx * kx, sy * ky, sw * kx + 0.5, sh * ky + 0.5); } + if (fade) { ctx.fillStyle = fade; ctx.fillRect(sx * kx, sy * ky, sw * kx + 0.5, sh * ky + 0.5); } } if (open) ctx.restore(); } @@ -514,14 +529,8 @@ function paintBackdrop(c, pal, pt) { // function): the stand is the far edge of the frame, and a hard cut at y = 0 reads as a // photograph that has been cropped rather than as a stadium with a roof somewhere above it. // It also happens to be where the scoreboard sits, which is the second reason it earns its keep. - if (TUNE.standTop > 0) { - const deep = standsBottom * 0.42; - const g = c.createLinearGradient(0, -8, 0, deep); - g.addColorStop(0, `rgba(6,10,6,${TUNE.standTop})`); - g.addColorStop(1, 'rgba(6,10,6,0)'); - c.fillStyle = g; - c.fillRect(-8, -8, W + 16, deep + 8); - } + const fade = topFade(c, standsBottom); + if (fade) { c.fillStyle = fade; c.fillRect(-8, -8, W + 16, standsBottom * 0.42 + 8); } // ground + court (Super Tennis lawn); everything outside the doubles court is OUT const dw = COURT.dblW / 2; quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach