The crowd sheet becomes an animation sheet: three frames, and the stand sits back

Oliver: "when clapping I barely see any movement at all now.. also all of the
art could be colored a little more dim as it is perspective wise farther in the
background and should not steal too much attention" — and then, "can't we make
an animation sprite sheet of the new crowd?", which is the answer to the first
half of the first one.

A third roll (_crowdroll.py mid) takes the arms-up frame as its reference and
asks for the same standing people with their hands brought together in front of
their faces. It drifts by 53% of its pixels and that does not matter: the same
filter keeps only what was stand and became person. 100 arm blobs in pose 1, 74
in pose 2, 134 spectators once merged, 28 of them owning BOTH poses — those 28
cycle between two raised poses, which is a clap; the rest alternate with the
seated frame, which is arms going up and down.

The point is the cycle, not the pose. A raiser is ~8x6 logical px on a phone and
one that appears and then holds for a second is not something the eye catches;
arms moving at 3.5 Hz is. Measured: 2-3% of the stand band now changes every
90 ms during a won game, against near zero before.

The sheets are one file per court now — three frames stacked, graded as a single
image so they cannot drift apart in colour — and x_crowd2/_c2/_h2 are deleted.

TUNE.standBack 0.22 dims the crowd and the boards on every court before the
night court's own dim and the splatter rot, in the one function both the cached
backdrop and the live raisers already read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-21 13:59:02 +02:00
1 parent 1ad2898530
commit 0250e9bd20
14 files changed
+333 -198

No files matched your search

+5
View File
@@ -297,6 +297,11 @@ export const TUNE = {
// cheered (render's drawClapColumns). Between points the crowd used to be a photograph. This is
// deliberately slow and single-column: a stand that fidgets constantly is a stand you look AT.
fidgetEvery: 4.5,
// HOW FAR BACK THE STAND SITS. A flat dim over the crowd and the boards, on every court, before
// the night court's own `dim` and the splatter rot are added on top. Oliver: "all of the art could
// be colored a little more dim as it is perspective wise farther in the background and should not
// steal too much attention." The crowd is the only thing on screen that is 30 m away.
standBack: 0.22,
// ---- damage. Bodies with hp (racket boss 'r', brute 'g') take damage in these units, so a big
// boss is just a bigger hp number and every weapon stays proportional. Fractions are legal: they
// bank on zb.chip and only whole points come off hp (the wound SKINS switch on hp thresholds).
+142 -97
View File
@@ -1,100 +1,145 @@
// GENERATED by assets/_crowdpass.py — do not edit by hand, re-run the tool.
// The spectators who can stand up: [x, y, w, h] as fractions of the crowd sheet, sorted
// left to right. Each box holds ONE person whose arms are raised in <crowd>2 and nobody
// else; everything outside these boxes is identical in the two sheets, which is what lets
// render.js raise them one at a time with no seam. See "THE CLAP FRAME" in PROJECT.md.
// The crowd sheet is FRAMES frames stacked vertically: 0 seated (the backdrop), 1 arms
// high, 2 hands together in front of the face. RAISERS lists the spectators who can move:
// [x, y, w, h] as fractions of ONE FRAME, then a bitmask of the raised frames that person
// actually appears in (1 = arms high, 2 = mid-clap, 3 = both, and 3 is a real clap cycle).
// Each box holds ONE person and nobody else, and outside these boxes every frame is
// pixel-identical to frame 0 — which is what lets render.js animate them one at a time
// with no seam. See "THE CROWD SHEET" in PROJECT.md.
export const FRAMES = 3;
export const RAISERS = [
[-0.0014, 0.8451, 0.0327, 0.0521],
[0.0021, 0.7148, 0.0256, 0.0286],
[0.0071, 0.9128, 0.0369, 0.0456],
[0.0234, 0.5560, 0.0163, 0.0417],
[0.0391, 0.6419, 0.0384, 0.0234],
[0.0533, 0.7669, 0.0241, 0.0247],
[0.0611, 0.5586, 0.0661, 0.0404],
[0.0646, 0.7135, 0.0369, 0.0299],
[0.0689, 0.0521, 0.0227, 0.0221],
[0.0859, 0.7669, 0.0249, 0.0247],
[0.1016, 0.2643, 0.0256, 0.0312],
[0.1236, 0.7214, 0.0376, 0.0221],
[0.1399, 0.8516, 0.0249, 0.0273],
[0.1435, 0.1224, 0.0334, 0.0221],
[0.1577, 0.1732, 0.0199, 0.0586],
[0.1896, 0.7669, 0.0213, 0.0247],
[0.1932, 0.1732, 0.0170, 0.0508],
[0.1996, 0.0742, 0.0312, 0.0443],
[0.2017, 0.5990, 0.0298, 0.0299],
[0.2067, 0.7148, 0.0263, 0.0286],
[0.2358, 0.8594, 0.0462, 0.0234],
[0.2401, 0.7669, 0.0241, 0.0247],
[0.2599, 0.5586, 0.0518, 0.0599],
[0.2628, 0.1732, 0.0178, 0.0521],
[0.2898, 0.7135, 0.0376, 0.0299],
[0.2976, 0.8086, 0.0384, 0.0286],
[0.2997, 0.0326, 0.0362, 0.0651],
[0.3026, 0.7669, 0.0419, 0.0286],
[0.3068, 0.8828, 0.1044, 0.0755],
[0.3189, 0.1732, 0.0185, 0.0482],
[0.3643, 0.4883, 0.0170, 0.0781],
[0.3686, 0.7148, 0.0312, 0.0286],
[0.3885, 0.4883, 0.0178, 0.0729],
[0.3899, 0.7669, 0.0192, 0.0247],
[0.3956, 0.1732, 0.0149, 0.0508],
[0.3984, 0.8503, 0.0284, 0.0234],
[0.4062, 0.2604, 0.0206, 0.0247],
[0.4062, 0.5755, 0.0206, 0.0234],
[0.4062, 0.6146, 0.0277, 0.0247],
[0.4233, 0.7148, 0.0376, 0.0286],
[0.4233, 0.9271, 0.0540, 0.0352],
[0.4268, 0.1914, 0.0107, 0.0417],
[0.4276, 0.8438, 0.0369, 0.0534],
[0.4645, 0.0286, 0.0376, 0.0612],
[0.4737, 0.5534, 0.0284, 0.0456],
[0.4858, 0.9232, 0.0206, 0.0286],
[0.4901, 0.6315, 0.0455, 0.0208],
[0.4901, 0.7669, 0.0533, 0.0247],
[0.4979, 0.7148, 0.0355, 0.0286],
[0.5064, 0.5755, 0.0213, 0.0234],
[0.5398, 0.5586, 0.0206, 0.0404],
[0.5568, 0.7148, 0.0376, 0.0286],
[0.5604, 0.9128, 0.0831, 0.0495],
[0.5625, 0.1497, 0.0391, 0.0312],
[0.5973, 0.5586, 0.0298, 0.0247],
[0.6023, 0.2643, 0.0249, 0.0208],
[0.6186, 0.8516, 0.0582, 0.0299],
[0.6271, 0.7669, 0.0163, 0.0247],
[0.6293, 0.7214, 0.0476, 0.0221],
[0.6435, 0.1224, 0.0341, 0.0221],
[0.6562, 0.7526, 0.0249, 0.0391],
[0.6584, 0.1732, 0.0192, 0.0586],
[0.6854, 0.7565, 0.0540, 0.0352],
[0.6925, 0.1732, 0.0178, 0.0508],
[0.6974, 0.9271, 0.0462, 0.0352],
[0.6996, 0.0742, 0.0341, 0.0404],
[0.7060, 0.8529, 0.0256, 0.0260],
[0.7266, 0.5586, 0.0426, 0.0430],
[0.7393, 0.7669, 0.0249, 0.0286],
[0.7401, 0.5977, 0.0249, 0.0208],
[0.7521, 0.9128, 0.1207, 0.0456],
[0.7585, 0.0430, 0.0398, 0.0247],
[0.7621, 0.1732, 0.0156, 0.0521],
[0.7692, 0.5755, 0.0249, 0.0221],
[0.7898, 0.7135, 0.0376, 0.0299],
[0.7997, 0.0326, 0.0362, 0.0651],
[0.8068, 0.7669, 0.0419, 0.0286],
[0.8232, 0.1758, 0.0142, 0.0469],
[0.8523, 0.7669, 0.0213, 0.0247],
[0.8636, 0.7148, 0.0348, 0.0286],
[0.8643, 0.4883, 0.0170, 0.0742],
[0.8885, 0.4883, 0.0178, 0.0729],
[0.8928, 0.1732, 0.0170, 0.0508],
[0.8984, 0.8503, 0.0284, 0.0234],
[0.9062, 0.2604, 0.0206, 0.0247],
[0.9062, 0.5755, 0.0206, 0.0221],
[0.9233, 0.7148, 0.0376, 0.0286],
[0.9268, 0.1901, 0.0149, 0.0430],
[0.9276, 0.8438, 0.0369, 0.0534],
[0.9318, 0.6914, 0.0334, 0.0208],
[0.9318, 0.9271, 0.0455, 0.0352],
[0.9645, 0.0286, 0.0369, 0.0612],
[0.9737, 0.5586, 0.0241, 0.0404],
[-0.0014, 0.8451, 0.0327, 0.0521, 3],
[0.0021, 0.6458, 0.0291, 0.0247, 2],
[0.0021, 0.7148, 0.0256, 0.0286, 1],
[0.0071, 0.9128, 0.0369, 0.0456, 1],
[0.0234, 0.5560, 0.1037, 0.0625, 3],
[0.0355, 0.6419, 0.0419, 0.0286, 3],
[0.0533, 0.7669, 0.0241, 0.0247, 1],
[0.0646, 0.7135, 0.0369, 0.0299, 1],
[0.0689, 0.0521, 0.0227, 0.0221, 1],
[0.0689, 0.0977, 0.0291, 0.0208, 2],
[0.0859, 0.7669, 0.0249, 0.0247, 1],
[0.1016, 0.2630, 0.0256, 0.0326, 3],
[0.1065, 0.8086, 0.0206, 0.0273, 2],
[0.1236, 0.7214, 0.0376, 0.0221, 1],
[0.1321, 0.0911, 0.0277, 0.0312, 2],
[0.1399, 0.8516, 0.0249, 0.0273, 3],
[0.1435, 0.1224, 0.0334, 0.0221, 1],
[0.1577, 0.1732, 0.0199, 0.0586, 1],
[0.1605, 0.5990, 0.0213, 0.0299, 2],
[0.1641, 0.8190, 0.0589, 0.0286, 2],
[0.1690, 0.0898, 0.0284, 0.0286, 2],
[0.1690, 0.3086, 0.0249, 0.0299, 2],
[0.1896, 0.7669, 0.0213, 0.0247, 1],
[0.1932, 0.1732, 0.0170, 0.0508, 1],
[0.1982, 0.5612, 0.0334, 0.0260, 2],
[0.1996, 0.0742, 0.0312, 0.0443, 3],
[0.2017, 0.5990, 0.0298, 0.0299, 3],
[0.2067, 0.3008, 0.0206, 0.0312, 2],
[0.2067, 0.7148, 0.0263, 0.0286, 1],
[0.2358, 0.0820, 0.0334, 0.0365, 2],
[0.2358, 0.6458, 0.0291, 0.0247, 2],
[0.2358, 0.8086, 0.0291, 0.0391, 2],
[0.2358, 0.8594, 0.0462, 0.0365, 3],
[0.2401, 0.7669, 0.0241, 0.0247, 1],
[0.2599, 0.5586, 0.0518, 0.0599, 1],
[0.2628, 0.1732, 0.0178, 0.0521, 1],
[0.2734, 0.6458, 0.0249, 0.0247, 2],
[0.2898, 0.7135, 0.0376, 0.0299, 1],
[0.2976, 0.8086, 0.0384, 0.0286, 3],
[0.2997, 0.0326, 0.0362, 0.0651, 1],
[0.3026, 0.2253, 0.0241, 0.0247, 2],
[0.3026, 0.7669, 0.0419, 0.0286, 1],
[0.3068, 0.6458, 0.0206, 0.0247, 2],
[0.3068, 0.8828, 0.1044, 0.0755, 3],
[0.3189, 0.1732, 0.0185, 0.0482, 1],
[0.3643, 0.4883, 0.0170, 0.0781, 3],
[0.3686, 0.7148, 0.0312, 0.0286, 1],
[0.3693, 0.2878, 0.0320, 0.0260, 2],
[0.3693, 0.8138, 0.0291, 0.0378, 2],
[0.3729, 0.0977, 0.0249, 0.0208, 2],
[0.3771, 0.1732, 0.0334, 0.0885, 3],
[0.3885, 0.4883, 0.0178, 0.0729, 1],
[0.3899, 0.7669, 0.0192, 0.0247, 1],
[0.3984, 0.8503, 0.0284, 0.0234, 3],
[0.4062, 0.2604, 0.0206, 0.0247, 3],
[0.4062, 0.5755, 0.0334, 0.0638, 3],
[0.4070, 0.8086, 0.0199, 0.0273, 2],
[0.4233, 0.7148, 0.0376, 0.0286, 1],
[0.4233, 0.9271, 0.0540, 0.0352, 1],
[0.4268, 0.1914, 0.0107, 0.0417, 1],
[0.4276, 0.8438, 0.0369, 0.0534, 3],
[0.4354, 0.6523, 0.0298, 0.0286, 2],
[0.4645, 0.0286, 0.0376, 0.0612, 1],
[0.4730, 0.8216, 0.0241, 0.0260, 2],
[0.4737, 0.5534, 0.0284, 0.0456, 1],
[0.4858, 0.9232, 0.0206, 0.0286, 1],
[0.4901, 0.6315, 0.0455, 0.0443, 3],
[0.4901, 0.7669, 0.0533, 0.0247, 1],
[0.4979, 0.7148, 0.0355, 0.0286, 1],
[0.5064, 0.5755, 0.0213, 0.0234, 1],
[0.5355, 0.8086, 0.0249, 0.0286, 2],
[0.5398, 0.5586, 0.0206, 0.0404, 1],
[0.5568, 0.7148, 0.0376, 0.0286, 1],
[0.5604, 0.9128, 0.0831, 0.0495, 1],
[0.5625, 0.1497, 0.0391, 0.0312, 3],
[0.5689, 0.0977, 0.0291, 0.0208, 2],
[0.5973, 0.5586, 0.0298, 0.0247, 1],
[0.6023, 0.2630, 0.0249, 0.0221, 3],
[0.6101, 0.1185, 0.0341, 0.0339, 2],
[0.6186, 0.8516, 0.0582, 0.0299, 3],
[0.6271, 0.7669, 0.0163, 0.0247, 1],
[0.6293, 0.7214, 0.0476, 0.0221, 1],
[0.6314, 0.0846, 0.0305, 0.0378, 2],
[0.6435, 0.1224, 0.0341, 0.0221, 1],
[0.6562, 0.7526, 0.0249, 0.0391, 1],
[0.6584, 0.1732, 0.0192, 0.0586, 1],
[0.6648, 0.5612, 0.0291, 0.0260, 2],
[0.6690, 0.0898, 0.0284, 0.0286, 2],
[0.6690, 0.3021, 0.0249, 0.0365, 2],
[0.6854, 0.7565, 0.0540, 0.0352, 1],
[0.6925, 0.1732, 0.0178, 0.0508, 1],
[0.6974, 0.9271, 0.0462, 0.0352, 1],
[0.6996, 0.0742, 0.0341, 0.0443, 3],
[0.7024, 0.6458, 0.0249, 0.0286, 2],
[0.7060, 0.8529, 0.0256, 0.0260, 3],
[0.7266, 0.5586, 0.0426, 0.0430, 1],
[0.7351, 0.0820, 0.0341, 0.0365, 2],
[0.7358, 0.6458, 0.0291, 0.0247, 2],
[0.7393, 0.7669, 0.0249, 0.0286, 1],
[0.7401, 0.5977, 0.0249, 0.0208, 1],
[0.7401, 0.8398, 0.0327, 0.0273, 2],
[0.7521, 0.9128, 0.1207, 0.0456, 1],
[0.7585, 0.0430, 0.0398, 0.0247, 1],
[0.7621, 0.1732, 0.0156, 0.0521, 1],
[0.7692, 0.5755, 0.0249, 0.0221, 1],
[0.7898, 0.7135, 0.0376, 0.0299, 1],
[0.7997, 0.0326, 0.0362, 0.0651, 1],
[0.8026, 0.2253, 0.0241, 0.0247, 2],
[0.8026, 0.8086, 0.0334, 0.0417, 2],
[0.8068, 0.6458, 0.0241, 0.0247, 2],
[0.8068, 0.7669, 0.0419, 0.0286, 1],
[0.8232, 0.1758, 0.0142, 0.0469, 1],
[0.8374, 0.5221, 0.0227, 0.0378, 2],
[0.8438, 0.1185, 0.0291, 0.0260, 2],
[0.8523, 0.7669, 0.0213, 0.0247, 1],
[0.8636, 0.7148, 0.0348, 0.0286, 1],
[0.8643, 0.4883, 0.0419, 0.0781, 3],
[0.8693, 0.2878, 0.0284, 0.0260, 2],
[0.8693, 0.8138, 0.0291, 0.0378, 2],
[0.8729, 0.0977, 0.0249, 0.0208, 2],
[0.8771, 0.1732, 0.0327, 0.0885, 3],
[0.8984, 0.8503, 0.0284, 0.0234, 3],
[0.9062, 0.2604, 0.0206, 0.0247, 3],
[0.9062, 0.5755, 0.0341, 0.0638, 3],
[0.9070, 0.8086, 0.0199, 0.0273, 2],
[0.9233, 0.7148, 0.0376, 0.0286, 1],
[0.9268, 0.1901, 0.0149, 0.0430, 1],
[0.9276, 0.8438, 0.0369, 0.0534, 3],
[0.9318, 0.6914, 0.0334, 0.0208, 1],
[0.9318, 0.9271, 0.0455, 0.0352, 1],
[0.9645, 0.0286, 0.0369, 0.0612, 1],
[0.9695, 0.8594, 0.0320, 0.0234, 2],
[0.9730, 0.8216, 0.0241, 0.0365, 2],
[0.9737, 0.5586, 0.0241, 0.0404, 1],
];
+50 -39
View File
@@ -3,7 +3,7 @@
import { cv, ctx } from './dom.js';
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, CUP_ROUNDS, TINT, SHOT_DEPTH } from './config.js';
import { P, O, ball, score, crowd, G, court, tour, kit, skin } from './state.js';
import { RAISERS } from './crowd.js';
import { RAISERS, FRAMES } from './crowd.js';
import { predictLanding, previewServe, slotId, slotStock, serveApexY, rallyFeel, swayOf } from './gameplay.js';
import { drillQueue, drillLegal, drillMult } from './drill.js';
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.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 <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.
export const SPRV = 144;
export const SPRV = 145;
// 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".
@@ -258,9 +258,11 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
'n_run1','n_run2',
'o_run1','o_run2', // TWO frames, a true leg-swap pair — the whole 2026-08-14 tuning session's verdict: fewer, better-chosen frames at a calm rate beat every 6/8-frame roll ('this looks better than anything before')
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4',
'x_crowd','x_crowd2','x_ads', // CENTRE COURT (grass) — also the fallback set
'x_crowd_c','x_crowd_c2','x_ads_c', // TERRE ROUGE (clay)
'x_crowd_h','x_crowd_h2','x_ads_h', // THE SLAB (hard)
'x_crowd','x_ads', // CENTRE COURT (grass) — also the fallback set
'x_crowd_c','x_ads_c', // TERRE ROUGE (clay)
'x_crowd_h','x_ads_h', // THE SLAB (hard)
// ...each crowd sheet is FRAMES frames stacked vertically (seated / arms high /
// mid-clap). See src/crowd.js, and drawRaisers below.
'x_boymop1','x_boymop2','x_boymop3','x_boymop4', // the push cycle, used only while he is stopped and working
'x_boycarry1','x_boycarry2','x_boycarry3','x_boycarry4','x_howto','x_cross', // walking with the mop shouldered, and the slingshot tutorial panel
'x_hand', // the ghost tutorial hand (drawGhostHand) — generated art, Oliver's reference pose
@@ -418,13 +420,14 @@ function tickFlashes(standsBottom) {
ctx.globalAlpha = 1;
}
// How dark the STAND is: the court's own night (`dim` on its COURTS row — 0 everywhere but the
// night court) plus the rot the splatter palette brings with it. ONE function because it is applied
// How dark the STAND is: `TUNE.standBack` (every court, always — the stand is scenery a long way
// behind the court and should not compete with it) plus the court's own night (`dim` on its COURTS
// row, 0 everywhere but the night court) plus the rot the splatter palette brings with it. ONE function because it is applied
// in two places and they must agree exactly — the cached sheet and the live clap columns drawn over
// it. Two copies of this number is a stand whose clapping columns pop brighter than the rest of it,
// which is the same class of bug as the crowd frames drifting apart. Capped: past ~0.85 the stand
// is a black rectangle and the applause stops reading at all.
const standDim = pt => Math.min(0.85, (court.dim || 0) + 0.26 * pt);
const standDim = pt => Math.min(0.88, TUNE.standBack + (court.dim || 0) + 0.26 * pt);
// THE STAND MOVES IN COLUMNS, and the sheets are cut to match: `assets/_crowdpass.py` guarantees
// the clap frame is pixel-identical to the seated one EXCEPT inside blobs that each sit inside ONE
@@ -440,21 +443,20 @@ const standDim = pt => Math.min(0.85, (court.dim || 0) + 0.26 * pt);
// 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.
const rnd = i => ((i * 9301 + 49297) % 233280) / 233280; // one stable pseudo-random per spectator
function drawRaisers(clapImg, standsBottom, pt, up) {
const crowdH = standsBottom - AD_H + 2;
const kx = W / clapImg.width, ky = (crowdH + 2) / clapImg.height;
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;
let open = false;
for (let i = 0; i < RAISERS.length; i++) {
const r = RAISERS[i];
if (!up(i, r[0])) continue;
const r = RAISERS[i], f = frameOf(i, r[0], r[4]);
if (!f) continue; // frame 0 IS the backdrop: nothing to draw
if (!open) { // most frames raise nobody: no clip, no save, nothing drawn
open = true;
ctx.save(); ctx.beginPath(); ctx.rect(0, 0, W, standsBottom - AD_H); ctx.clip();
}
const sx = r[0] * clapImg.width, sy = r[1] * clapImg.height;
const sw = r[2] * clapImg.width, sh = r[3] * clapImg.height;
ctx.drawImage(clapImg, sx, sy, sw, sh, sx * kx, sy * ky, sw * kx + 0.5, sh * ky + 0.5);
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
if (tint) { ctx.fillStyle = tint; ctx.fillRect(sx * kx, sy * ky, sw * kx + 0.5, sh * ky + 0.5); }
}
@@ -487,7 +489,9 @@ function paintBackdrop(c, pal, pt) {
// per-column split only ever existed for the clap ripple, which drawBackdrop overlays live.
const standsBottom = proj(0, 0, -6).y, crowdH = standsBottom - AD_H + 2;
const crowdImg = SPR[court.crowd] || SPR['x_crowd'];
if (crowdImg) c.drawImage(crowdImg, 0, 0, W, crowdH + 2);
// FRAME 0 of the stacked sheet: the whole stand, seated. The raised frames below it in the same
// image are never drawn whole — drawRaisers picks one spectator's box out of them.
if (crowdImg) c.drawImage(crowdImg, 0, 0, crowdImg.width, crowdImg.height / FRAMES, 0, 0, W, crowdH + 2);
else { // fallback until the image loads: the old tier stripes + dots (static — boot gates play anyway)
c.fillStyle = pal.stands; c.fillRect(0, 0, W, standsBottom);
for (let r = 0; r < 16; r++) { c.fillStyle = r % 2 ? pal.tierA : pal.tierB; c.fillRect(0, 6 + r * 5.5, W, 5.5); }
@@ -564,33 +568,40 @@ function drawBackdrop(pal, pt) {
}
ctx.drawImage(bg, -8, -8, W + 16, H + 16);
const standsBottom = proj(0, 0, -6).y;
// Applause is a real animation: <crowd>2 is the seated stand with 93 pairs of arms raised into
// the wall above the rows, and each of those people is raised on their own clock.
const clapImg = crowdImg && (crowdImg === SPR['x_crowd'] ? SPR['x_crowd2'] : SPR[court.crowd + '2']);
if (crowdImg && clapImg) {
// WHO IS ON THEIR FEET. Three behaviours, one draw:
// applause — HOW MANY of them join scales with the size of the moment, each on their own
// period, so a groan for a lost point is a dozen people and a match point is the whole
// stand. That is what makes the same machinery do "subtle" (Oliver, on the aww sample:
// "the crowd should still clap but more subtle");
// the WAVE — a front travelling across the stand, person by person, for a moment big enough;
// THE CROWD SHEET IS AN ANIMATION SHEET (assets/_crowdpass.py): frame 0 is the seated stand that
// the backdrop above is drawn from, frame 1 is 100 pairs of arms raised over it, frame 2 is those
// arms brought down and in, mid-clap. Only the arms differ, so ONE SPECTATOR can be redrawn over
// the cached stand without touching anybody near him.
if (crowdImg) {
// WHO IS MOVING, AND IN WHICH POSE. Three behaviours, one draw:
// applause — how many join scales with the size of the moment, and each of them CYCLES: the
// 28 spectators who own both raised poses clap between them, the rest go up and down. A
// raiser is ~8x6 logical px on a phone, so a pose that appears once and holds is not
// something the eye catches — Oliver, "when clapping I barely see any movement at all now";
// the WAVE — a front travelling across the stand, person by person, arms high;
// a FIDGET — one person, briefly, between points, so an idle stand is not a photograph.
const pop = crowd.cheer > 0 ? crowd.pop : 0;
// 0.75, not 0.6: a game won calls cheer(2.4, 0.6) and a wave on every held serve is a wave
// nobody looks up for. This leaves it to the set, the match, and the ones the umpire calls
// unbelievable.
const up = pop >= 0.75
? (i, x) => ((G.idleT / 1.4 - x * 0.8 - rnd(i) * 0.03) % 1 + 1) % 1 < 0.18
const joins = 0.2 + 0.8 * pop;
const beat = (i) => Math.floor(G.idleT * 7 + rnd(i + 11) * 6) % 2;
// one pose, or the other, or the seated frame — whichever this spectator actually has
const pose = (i, mask, b) => mask === 3 ? (b ? 2 : 1) : (b ? 0 : mask);
const frameOf = pop >= 0.75
// 0.75, not 0.6: a game won calls cheer(2.4, 0.6), and a wave on every held serve is a wave
// nobody looks up for. This leaves it to the set, the match, and the umpire's "unbelievable".
? (i, x, mask) => ((G.idleT / 1.4 - x * 0.8 - rnd(i) * 0.03) % 1 + 1) % 1 < 0.18
? pose(i, mask, beat(i)) : 0
: pop > 0
// rnd(i) picks WHO (a stable draw per spectator, so the same people join a small moment),
// rnd(i+7) staggers WHEN, and the period varies per person — a stand clapping in unison is
// a machine.
? i => rnd(i) < pop && ((G.idleT / (1.5 + rnd(i + 7) * 1.3) + rnd(i + 3)) % 1) < 0.45
: (i) => {
// rnd(i) picks WHO (a stable draw, so the same people come in for a small moment), rnd(i+3)
// and a period of his own stagger WHEN — a stand clapping in unison is a machine.
? (i, x, mask) => rnd(i) < joins
&& ((G.idleT / (1.5 + rnd(i + 7) * 1.3) + rnd(i + 3)) % 1) < 0.5
? pose(i, mask, beat(i)) : 0
: (i, x, mask) => {
const slot = Math.floor(G.idleT / TUNE.fidgetEvery);
return i === (slot * 37) % RAISERS.length && (G.idleT / TUNE.fidgetEvery) % 1 < 0.34;
return i === (slot * 37) % RAISERS.length && (G.idleT / TUNE.fidgetEvery) % 1 < 0.34
? pose(i, mask, beat(i)) : 0;
};
drawRaisers(clapImg, standsBottom, pt, up);
drawRaisers(crowdImg, standsBottom, pt, frameOf);
}
tickFlashes(standsBottom); // ...and somebody in there is taking pictures
return standsBottom;