The white flashing was the top fade: the live raisers were not repainting it

Oliver: "what is this white flashing while applause in the upper right corner of
the crowd". With the photographers switched off entirely it was still there, so
it was never them — it was mine, from this morning. The top fade is baked into
the cached stand, and drawRaisers was painting a spectator's box straight over
it with only the dim applied. Every time that spectator stood up his box lost
the fade and lit up; every time he sat down it came back. Measured on the canvas:
a 146-luminance jump, twice a second, in exactly the two places raisers sit near
the top of the picture.

Both stand treatments now come from one function each and are applied in both
places. The gradient is built in user space, so filling a raiser's sub-rect with
it picks up the right alpha for free. Probed again after the fix: zero bright
pops during applause and during the wave, against the four hot spots before.

While chasing it, two real detector faults turned up and are fixed too: a blob
may not be a pale block (a re-rolled cream shirt is "new material" by the
stand-turned-person test but nothing rises), and it may not cover something
already bright (a genuine arm drawn over a white shirt deletes a white block,
which flashes just as hard). 94 raisers survive both rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-21 14:55:13 +02:00
1 parent 93c4a21be5
commit ca3e72d0a9
8 files changed
+81 -47

No files matched your search

+12
View File
@@ -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 (`HUD`, in portrait logical px — landscape has a shorter band, so the rule is conservative
there rather than over-eager). there rather than over-eager).
Result: 92 of 101 raisers below the board's bottom edge, against 103 of 134 before. 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 - **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 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 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. `assets/_sheet_crowd1.png` is the pre-brightening original of the FIRST crowd, kept for history.
## Settled feel decisions — do not regress silently ## 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 - **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 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 UP out of the dark instead of being cut off at y = 0. Oliver, 2026-08-21: "the bottom of the screen
+20
View File
@@ -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 (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 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 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 BAND = 8 # rows the background colour is sampled over
PAD = 3 # px of margin around a raiser's box 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. # 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: if bgA[sl[0].stop:min(A.shape[0], sl[0].stop + 6), sl[1]].mean() > 0.55:
continue 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]) out.append([sl[0].start, sl[1].start, sl[0].stop, sl[1].stop, i + 1])
return out, lab return out, lab
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+1 -1
View File
@@ -664,6 +664,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=303"></script> <script type="module" src="src/main.js?v=304"></script>
</body> </body>
</html> </html>
+29 -36
View File
@@ -12,101 +12,94 @@ export const RAISERS = [
[-0.0014, 0.6458, 0.0327, 0.0560, 2], [-0.0014, 0.6458, 0.0327, 0.0560, 2],
[-0.0014, 0.7682, 0.0419, 0.0221, 2], [-0.0014, 0.7682, 0.0419, 0.0221, 2],
[-0.0014, 0.8451, 0.0327, 0.0521, 3], [-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, 2],
[0.0064, 0.3190, 0.0206, 0.0286, 3],
[0.0071, 0.9128, 0.0369, 0.0286, 3], [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.0234, 0.9440, 0.0206, 0.0286, 2],
[0.0355, 0.6419, 0.0753, 0.0677, 3], [0.0355, 0.6419, 0.0753, 0.0677, 3],
[0.0568, 0.8503, 0.0412, 0.0273, 1], [0.0568, 0.8503, 0.0412, 0.0273, 1],
[0.0689, 0.3190, 0.0334, 0.0286, 2], [0.0689, 0.3190, 0.0334, 0.0286, 2],
[0.0859, 0.7669, 0.0249, 0.0247, 2], [0.0859, 0.7669, 0.0249, 0.0247, 2],
[0.0952, 0.5586, 0.0320, 0.0690, 3], [0.0952, 0.5755, 0.0284, 0.0521, 2],
[0.0980, 0.9544, 0.0419, 0.0286, 2],
[0.1065, 0.8086, 0.0206, 0.0273, 3],
[0.1278, 0.6719, 0.0284, 0.0299, 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.1392, 0.9362, 0.0213, 0.0234, 2],
[0.1399, 0.3190, 0.0249, 0.0286, 2], [0.1399, 0.3190, 0.0249, 0.0286, 2],
[0.1562, 0.6732, 0.0206, 0.0286, 2], [0.1562, 0.6732, 0.0206, 0.0286, 2],
[0.1598, 0.9518, 0.0213, 0.0234, 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.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.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.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.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.2528, 0.9336, 0.0284, 0.0391, 3],
[0.2557, 0.7305, 0.0213, 0.0299, 1], [0.2557, 0.7305, 0.0213, 0.0299, 1],
[0.2599, 0.5924, 0.0384, 0.0365, 2], [0.2599, 0.5924, 0.0384, 0.0365, 2],
[0.2734, 0.6458, 0.0199, 0.0221, 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.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.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.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.3977, 0.6523, 0.0831, 0.0638, 3],
[0.4055, 0.7669, 0.0724, 0.0690, 3], [0.4055, 0.8008, 0.0213, 0.0352, 1],
[0.4062, 0.4857, 0.0376, 0.1133, 3], [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.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.4901, 0.6458, 0.0533, 0.0560, 2],
[0.4936, 0.7044, 0.0298, 0.0247, 2], [0.4936, 0.7044, 0.0298, 0.0247, 2],
[0.4936, 0.7669, 0.0497, 0.0247, 2], [0.4936, 0.7669, 0.0497, 0.0247, 2],
[0.5021, 0.8503, 0.0291, 0.0312, 3], [0.5021, 0.8503, 0.0291, 0.0312, 3],
[0.5064, 0.3190, 0.0206, 0.0286, 2], [0.5064, 0.3190, 0.0206, 0.0286, 2],
[0.5064, 0.5755, 0.0206, 0.0234, 1], [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.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.5682, 0.8776, 0.0298, 0.0221, 2],
[0.5689, 0.3190, 0.0334, 0.0286, 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.5895, 0.6732, 0.0192, 0.0286, 2],
[0.5938, 0.1107, 0.0462, 0.0312, 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.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.6186, 0.9362, 0.0249, 0.0365, 3],
[0.6357, 0.7227, 0.0284, 0.0208, 2], [0.6357, 0.7227, 0.0284, 0.0208, 2],
[0.6357, 0.8516, 0.0284, 0.0273, 3], [0.6357, 0.8516, 0.0284, 0.0273, 3],
[0.6371, 0.6458, 0.1271, 0.0703, 3], [0.6371, 0.6458, 0.1271, 0.0703, 3],
[0.6399, 0.3190, 0.0249, 0.0286, 2], [0.6399, 0.3190, 0.0249, 0.0286, 2],
[0.6776, 0.6003, 0.0249, 0.0286, 2], [0.6776, 0.6003, 0.0249, 0.0286, 2],
[0.6925, 0.4870, 0.1016, 0.1146, 3], [0.7053, 0.9297, 0.0419, 0.0430, 2],
[0.6974, 0.9271, 0.0497, 0.0456, 3],
[0.7230, 0.7669, 0.0412, 0.0312, 2], [0.7230, 0.7669, 0.0412, 0.0312, 2],
[0.7393, 0.8672, 0.0213, 0.0234, 2], [0.7393, 0.8672, 0.0213, 0.0234, 2],
[0.7401, 0.5990, 0.0327, 0.0299, 2], [0.7401, 0.5990, 0.0327, 0.0299, 2],
[0.7401, 0.8398, 0.0327, 0.0273, 1], [0.7401, 0.8398, 0.0327, 0.0273, 1],
[0.7727, 0.8086, 0.0923, 0.0729, 3], [0.7692, 0.5755, 0.0249, 0.0234, 1],
[0.7940, 0.8828, 0.0788, 0.0586, 3], [0.7727, 0.8503, 0.0916, 0.0312, 3],
[0.7940, 0.9440, 0.0163, 0.0286, 2], [0.7940, 0.9440, 0.0163, 0.0286, 2],
[0.7983, 0.6732, 0.0163, 0.0326, 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.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.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.8686, 0.8503, 0.0291, 0.0326, 3],
[0.8693, 0.2878, 0.0327, 0.0260, 2], [0.8899, 0.6003, 0.0241, 0.0286, 2],
[0.8771, 0.2253, 0.0206, 0.0365, 3],
[0.8899, 0.9440, 0.0206, 0.0378, 2], [0.8899, 0.9440, 0.0206, 0.0378, 2],
[0.8977, 0.6732, 0.0249, 0.0286, 2], [0.8977, 0.6732, 0.0249, 0.0286, 2],
[0.8984, 0.8503, 0.0284, 0.0234, 1], [0.8984, 0.8503, 0.0284, 0.0234, 1],
[0.9048, 0.2604, 0.0220, 0.0247, 1], [0.9062, 0.5755, 0.0206, 0.0234, 1],
[0.9062, 0.7982, 0.0249, 0.0378, 3], [0.9062, 0.8008, 0.0206, 0.0352, 1],
[0.9062, 0.8776, 0.0206, 0.0326, 3], [0.9062, 0.8776, 0.0206, 0.0326, 3],
[0.9233, 0.6523, 0.0582, 0.0599, 3], [0.9233, 0.6523, 0.0582, 0.0599, 3],
[0.9240, 0.7669, 0.0533, 0.0247, 2], [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.2461, 0.0291, 0.0286, 2],
[0.9354, 0.8438, 0.0291, 0.0534, 3], [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.5586, 0.0284, 0.0456, 3],
[0.9730, 0.8672, 0.0249, 0.0234, 2], [0.9730, 0.8672, 0.0249, 0.0234, 2],
]; ];
+19 -10
View File
@@ -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 // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too // menu screens hang <img> 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. // 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 // 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 // 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". // 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"). // looks pretty artificial").
// The sub-rect is drawn at EXACTLY the scale paintBackdrop used for the whole sheet (kx/ky below), // 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. // 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 const rnd = i => ((i * 9301 + 49297) % 233280) / 233280; // one stable pseudo-random per spectator
function drawRaisers(img, standsBottom, pt, frameOf) { function drawRaisers(img, standsBottom, pt, frameOf) {
const fh = img.height / FRAMES, crowdH = standsBottom - AD_H + 2; const fh = img.height / FRAMES, crowdH = standsBottom - AD_H + 2;
const kx = W / img.width, ky = (crowdH + 2) / fh; const kx = W / img.width, ky = (crowdH + 2) / fh;
const d = standDim(pt), tint = d > 0 ? `rgba(12,8,20,${d})` : null; const d = standDim(pt), tint = d > 0 ? `rgba(12,8,20,${d})` : null;
const fade = topFade(ctx, standsBottom);
let open = false; let open = false;
for (let i = 0; i < RAISERS.length; i++) { for (let i = 0; i < RAISERS.length; i++) {
const r = RAISERS[i], f = frameOf(i, r[0], r[4]); 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; 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); 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 (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(); 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 // 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. // 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. // It also happens to be where the scoreboard sits, which is the second reason it earns its keep.
if (TUNE.standTop > 0) { const fade = topFade(c, standsBottom);
const deep = standsBottom * 0.42; if (fade) { c.fillStyle = fade; c.fillRect(-8, -8, W + 16, standsBottom * 0.42 + 8); }
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);
}
// ground + court (Super Tennis lawn); everything outside the doubles court is OUT // ground + court (Super Tennis lawn); everything outside the doubles court is OUT
const dw = COURT.dblW / 2; const dw = COURT.dblW / 2;
quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach