Fall the light off across the run-off, so it reads as ground and not margin

Riding the court up left a wide band below the near baseline, and `pal.out` is
one flat colour — so the perspective visibly stopped partway down the screen
and the rest read as dead margin rather than as the back of a court.

A gradient from the baseline to the bottom edge fixes it: the band gets depth,
the rack gets a frame, and what darkens is exactly the area the thumb covers
anyway. Starts at alpha 0 on the baseline, so there is no seam.

A procedural "trodden ground" variant (scuffs and drag marks) was built and
cut: random ellipses read as unexplained puddles, not as wear. That idea wants
authored detail and gore that accumulates over a run, not noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-02 15:57:36 +02:00
1 parent 0fe8f37a81
commit 75936d3324
1 file changed
+19
+19
View File
@@ -161,6 +161,24 @@ export function proj(x, y, z) {
return { x: W / 2 + x * s, y: TUNE.horizon + (TUNE.camY + aimCam * TUNE.aimCamY - y) * s, s };
}
// The run-off apron below the near baseline. Raising the court to make room for the pull (see
// fitViewport) left a wide band down there, and `pal.out` is a single flat colour — so the
// perspective visibly stopped partway down the screen and the rest read as dead margin. This falls
// the light off toward you, which gives the band depth, frames the rack, and darkens exactly the
// area the thumb covers anyway. Starts at alpha 0 on the baseline, so there is no seam to see.
// NOT a near backstop: at camZ 48 with the baseline at z 23.77 a real one would sit between the
// camera and the player and draw enormous. You are looking OVER it, which is why a broadcast frame
// never shows one either. Wear and props at the margins are the next layer, not this.
function drawRunOff() {
const top = proj(0, 0, COURT.L).y;
if (top >= H) return; // landscape has no apron at all — the baseline projects off the bottom
const g = ctx.createLinearGradient(0, top, 0, H);
g.addColorStop(0, 'rgba(6,10,6,0)');
g.addColorStop(1, 'rgba(6,10,6,0.72)');
ctx.fillStyle = g;
ctx.fillRect(0, top, W, H - top);
}
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 78; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
@@ -831,6 +849,7 @@ export function render() {
line(-COURT.W / 2, COURT.svcNear, COURT.W / 2, COURT.svcNear, 1);
line(0, COURT.svcFar, 0, COURT.svcNear, 1);
line(0, 0, 0, 0.35, 1); line(0, COURT.L - 0.35, 0, COURT.L, 1); // center marks
drawRunOff(); // light falls off across the apron below the baseline
// gore decals: what remains of the visiting team (flat ground layer, then far-side mounds)
for (const d of gore) {