Carry the court colour into the page background

A phone's rounded display corners clip slivers out of the canvas, and the flat
dark body showed through them as a dark notch where the bottom corners curve in.
The page background is a two-stop gradient now: dark down to the top safe-area
inset, where it sits behind the status bar, then the court's out-area colour for
everything below.

That colour is read live from the palette rather than hardcoded, because it
lerps between the tennis and splatter courts — bright green to dark olive — so
a fixed hex would only match in one mode. Written in resolved pixels rather
than env() so the declaration can't be invalid and drop the background
entirely, and only assigned when the string actually changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 19:15:43 +02:00
1 parent 047d6fb665
commit 7b15e10494
3 files changed
+29 -4

No files matched your search

+8
View File
@@ -186,6 +186,14 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
because the wave grows with `hordeGrow` while the panic button was a flat 1 s; he chose scaling the
melee over shrinking the horde. **Measure the crowd BEFORE calling `meleeSwipe`** — it kills
everything in reach, so counting after counts the survivors and the cooldown barely moves.
- **The page background behind the canvas tracks the live court apron** (`pageBackground` in
render.js). It is visible in exactly two places: the strip above the canvas under the status bar,
and the slivers a phone's ROUNDED DISPLAY CORNERS clip out of the canvas — a flat dark body drew a
visible dark notch where the bottom corners curve in. It is a two-stop gradient: `#10141c` down to
the top inset, then `pal.out` for the rest. **`pal.out` must be read live**, because it lerps
between the tennis and splatter palettes and any fixed hex would only match in one mode. Written in
resolved px rather than `env()` so the declaration can never be invalid and drop the background,
and only assigned when the string actually changes.
- **The safe area is handled ASYMMETRICALLY, on purpose.** TOP: index.html insets the canvas element
below the status bar, because that is opaque UI the scoreboard must clear - `SAFE.top` stays 0, and
adding a HUD offset there as well subtracts the notch twice. BOTTOM: the canvas deliberately runs to
+1 -1
View File
@@ -80,6 +80,6 @@
<p class="start">TAP TO START</p>
</div>
</div>
<script type="module" src="main.js?v=47"></script>
<script type="module" src="main.js?v=48"></script>
</body>
</html>
+20 -3
View File
@@ -51,11 +51,27 @@ export let W = 480, H = 270;
// BOTTOM — the canvas deliberately runs to the PHYSICAL bottom edge so the court art fills the
// screen (the home indicator is a slim bar, not opaque UI). Only the interactive RACK is
// lifted clear of it, by SAFE.bottom below.
export const SAFE = { top: 0, bottom: 0 };
export const SAFE = { top: 0, bottom: 0, topCss: 0 };
function measureSafe(scale) {
const el = document.getElementById('safe');
if (!el) return;
SAFE.bottom = Math.round((parseFloat(getComputedStyle(el).paddingBottom) || 0) / scale);
const cs = getComputedStyle(el);
SAFE.bottom = Math.round((parseFloat(cs.paddingBottom) || 0) / scale);
SAFE.topCss = Math.round(parseFloat(cs.paddingTop) || 0); // CSS px — for the page background below
}
// The page background behind the canvas. It shows in exactly two places: the strip above the canvas
// under the status bar, and the slivers a phone's ROUNDED DISPLAY CORNERS clip out of the canvas
// itself. A flat dark body therefore drew a visible dark notch where the bottom corners curve in.
// So: dark down to the top inset, then the LIVE out-area colour for the rest — live because `pal.out`
// lerps between the tennis and splatter palettes, so any fixed hex would only match in one mode.
// Written in resolved px (not env()) so the declaration can never be invalid and drop the background.
let bgLast = '';
function pageBackground(out) {
const t = SAFE.topCss;
const css = `linear-gradient(#10141c 0px, #10141c ${t}px, ${out} ${t}px, ${out} 100%)`;
if (css === bgLast) return; // only touch the DOM when it actually changed
bgLast = css; document.body.style.background = css;
}
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
@@ -113,7 +129,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 47; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPRV = 48; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPR = {};
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
@@ -595,6 +611,7 @@ export function render() {
}
const pal = palette(), pt = paletteT();
pageBackground(pal.out); // keep the page behind the canvas matching the court's apron
// stands: the generated crowd, drawn in columns so it can BOUNCE when it cheers.
// sit the ad strip flush on the court's back edge (the out-area quad below reaches z=-6);
// pinning to that projected line keeps the boards clear of the lawn in BOTH orientations