Commit Graph
5 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 5f53a686c8 The crowd is drawn again, in detail — and the stand now moves one spectator at a time
Oliver on the first pass: "the new lighter areas read as rather sloppy artwork.
please regenerate the crowd art and make it more detailed. also the animation
also looks pretty artificial. the flashes from the fotographs are too rare and
the whole thing somehow does not translate well to the other courts. also when
the opponent makes a point there is an aww-sample (which has an applaus trail)
so the crowd should still clap but more subtle."

The paint pass is deleted — recolouring blobs of an old drawing is not drawing,
and the reason it looked necessary was a wrong conclusion: the roll scripts read
OPENROUTER_API_KEY from the repo's .env, so the art could be rolled all along.

_crowdroll.py rolls the crowd itself: individually drawn spectators with hats,
sunglasses, drinks, cameras, flags and banners, empty seats, stairways, and
three zombies sitting in the middle of it behaving like furniture. ONE roll now
serves all three courts through a per-channel histogram match onto each court's
old palette, so anything the art gains shows up on clay and at night too.

_crowdpass.py keeps only what was empty stand in the seated roll and became a
person in the clap roll — the raised arms, 1.2% of the sheet — so the clap frame
cannot drift by construction, and writes a box around each pair of arms into
src/crowd.js. render.js raises ONE SPECTATOR AT A TIME out of that table, each
on their own period: the 16-column strip swap moved a seventeenth of the crowd
as a block, which is what read as machinery. crowd.pop is now a head count, so
the same code covers a dozen people and the whole stand, and past 0.75 the wave
travels person by person.

aww() sets a small crowd.pop of its own — the sample is a groan that settles
into applause, and the stand has to match what you can hear. flashEvery 5.0 ->
1.9, because at a five-second mean over a stand this wide nobody ever saw one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:30:34 +02:00
wuwiandClaude Opus 5 74afd5988c The stand stops wobbling: a real clap frame, a wave, and three spectators who have already turned
The wobble was never the animation. x_crowd2 was a RE-ROLL of x_crowd, not a
pose of it — 13-16% of its pixels differed, over faces, torsos, seat backs and
the railing — so swapping 16 columns at 4.5 Hz re-hashed a strip of crowd twice
a second. assets/_crowdpass.py rebuilds the pair: the clap frame is now the
seated frame plus 36 genuine raises, each contained in ONE of render's columns,
so any column swap is seamless by construction and an empty column is invisible
instead of being a wobble. The three courts are one composition in three
palettes, so a raise found in one court's roll is transplanted into the other
two through a colour map learnt from the aligned seated pair.

The same tool paints the crowd, because at ~8 logical px a spectator detail can
only mean chroma and silhouette: 55 shirts and 44 heads of hair in colours the
court does not own, 28 caps, 4 flags above the heads, 8 emptied seats, 8 people
permanently standing — and 3 who are already turned, grey-green, mid-row, who
are also the only three who never applaud.

Movement, one draw path and three behaviours: applause scatters per column, a
WAVE crosses the stand for a moment big enough to deserve one (crowd.pop >=
0.75), and with nobody cheering ONE column stands for a third of every
TUNE.fidgetEvery, so the stand between points is no longer a photograph.

Paparazzi now scale with the moment: cheer() records the size of it (vol was
already exactly that at every call site) and queues shutters proportional to it,
drained at a fifth of the normal gap. pop is held at the loudest while an
applause is live, so a won rally landing inside a match-point ovation cannot
demote the stand to a polite clap — that rule has a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:58:26 +02:00
wuwiandClaude Opus 5 7dde5874ab Brighter stands, a serve hint that shows the gesture, and a boy who walks
- The crowd sat in the dark, worst of all in splatter mode where you spend the
  game. Both crowd frames get an identical gamma lift (means 30/14/39 ->
  42/22/56) and the rot tint drops 0.5 -> 0.26. The curve MUST stay identical
  across the two frames or the clap swap flashes brightness; _sheet_crowd1.png
  keeps the pre-lift original.
- The splatter hint led with the tap when pulling is the whole gesture. It now
  reads PULL BACK TO AIM - RELEASE TO FIRE, and after 1.5s of sitting in the
  serve stance untouched a ghost thumb slides back while chevrons grow up-court
  on a loop. Pull back, shoot forward is an inverse relationship that a line of
  text cannot carry on its own.
- The mop boy has three sheets instead of two: the push cycle now plays ONLY
  while he is stopped and working, a new carry sheet walks him upright with the
  mop shouldered, and the run sheet stays for the tennis-mode ball fetch. He was
  mopping thin air the entire way to the stain.

Sheet note: frames 2 and 3 of the carry sheet merged (a mop handle bridging the
gap), so it is cut at the true column gaps plus the thinnest column inside the
merged pair, then common-scaled — same technique as the brute stride sheet.

26-game soak clean. Serve returns 133/150 = 88.7% +/-2.6%, 30 human-tapped
points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:34:43 +03:00
wuwiandClaude Opus 5 55ddf5932e Fix the serve deadlock, give the mop boy a job, align net and umpire
The freeze first: G.mgT only counted down inside `G.phase === 'rally'`, so
anything that moved the phase mid-burst — a scheduled duel calling setupPoint —
stranded it above zero forever. mgT > 0 is one of keyPressed's early returns, so
no tap could serve again, and the yellow shape over the player's head was
drawMgRacket's muzzle flash still being drawn. Fixed at the root (the clock ticks
in every phase, only the pellets are gated on rally) and semantically: starting a
duel cancels any burst, because the duel is tennis.

The mop boy now does an actual round: walk to the biggest nearby stain, stand on
it and push the mop while the cycle animates in place, scrub it out, move to the
next, then leave. Gore fades ~3.5x faster while he is stopped and working than
when walking past, so the stain dies under him instead of fading as he drifts by.
boyMaxJobs caps the round so a kill streak can't employ him forever.

Applause: the clip is 4.16s and always plays in full, but every cheer() passed a
shorter beat, so the stand sat down while it was still audible. Floored at the
clip length. New, much bolder clap frame — the re-roll came back brighter and
bluer, which would have flashed the whole stand, so it is colour-matched to frame
one per channel on mean and sd before quantising.

Net and umpire were both wrong since forever: netW was the singles-stick width
(10.06) against a court drawn to the doubles lines, so the net stopped 0.46m
short each side; the chair's near legs stood 0.45m inside the sideline. Both now
sit exactly on the doubles line.

26-game soak clean, no stalls. Serve returns 134/150 = 89.3% +/-2.5% with the
wider net (the bar is inside one sigma), 30 human-tapped points = 50 hits /
0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:09:18 +03:00
wuwiandClaude Opus 5 32401366fe Drawn frames, not procedural fakes: brute lurch, boy stride, crowd applause
Every animation in this round gets real frames. The procedural squash-and-sway
on the wounded brute read as fake on sight, and that gait is where the comedy
lives.

- Each brute wound stage now has its own 2-frame lurch (z_gw/z_gwb, z_gw2/z_gw2b,
  ...). The generator gave every B frame a dragged pole that only stage 1
  actually carries, so it would have blinked in and out each step — stripped by
  keeping the largest blob, and where the pole touched the body, by cutting on
  column density (a torso is dense, a dragged pole is a thin diagonal).
- Frames step off DISTANCE COVERED, never elapsed time — boy.dist for ball boys,
  z/bruteStride for brutes. A time-driven cycle moonwalks the moment speed and
  frame rate disagree, which is exactly what "he slides faster than the animation
  suggests" was. Change a speed now and the legs keep up on their own.
- One boy, one slot. sendBoy no longer replaces a boy already on court: that was
  the appearing and disappearing, since every kill teleported him back to the
  wing mid-stride. New mess is appended to his round instead. He now walks for
  10.5s a crossing, and there is a lot of mopping to do.
- The applauding crowd is a real two-frame animation (seated / arms up), swapped
  per column on its own phase. The old "smooth wave" still shifted each column by
  a FRACTIONAL dy, and nearest-neighbour resampling at a fractional y re-snaps a
  column's pixel rows every frame — that boiling beside the HUD is what read as
  the scoreboard flickering. The board art is 99.9% opaque; nothing was ever
  showing through it. With the crowd calm the frame delta is now 0.0%.

Benchmarks: 136/150 serve returns (90.7% ±2.4%, the 11/12 bar is inside one
sigma), 30 human-tapped points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:20:43 +03:00