Undo the bad quad, and paint the page background where it shows

The triangle was mine. Extending the out-area quad to z=60 put it past camZ
(48), and proj() divides by (camZ - z) — so the scale explodes at the camera
plane and goes negative beyond it, folding the quad's far edge back above the
screen. The quad goes back to z=30; the full-frame clear is what covers the
screen, so it never needed extending in the first place.

The bottom still not being green was a second bug, in the previous commit. The
gradient was set on body, but the ROOT element's background is what paints the
viewport, and index.html gives html a background too — so body's never
propagated and painted only its own box, which has no height because the canvas
is position:fixed. It goes on documentElement now, and html gets an explicit
height, because a gradient is laid out in the root's own box and that box
collapses for the same reason.

Verified with the canvas deliberately ending 40px short: root box fills the
viewport, gradient's lower stop is rgb(41,92,45), the canvas's bottom row is
the same, and the seam is invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 19:33:12 +02:00
1 parent c9699da6f1
commit 571cd4c399
3 files changed
+25 -4

No files matched your search

+9
View File
@@ -193,6 +193,15 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
perspective floor reaches the bottom on any aspect ratio. If you ever add a new orientation or perspective floor reaches the bottom on any aspect ratio. If you ever add a new orientation or
stretch the canvas further, check the bottom rows are opaque — sample `getImageData` alpha, the stretch the canvas further, check the bottom rows are opaque — sample `getImageData` alpha, the
colour alone will not tell you, because the page behind is deliberately the same green. colour alone will not tell you, because the page behind is deliberately the same green.
**Do NOT try to cover the bottom by extending the out-area quad instead.** `proj()` divides by
`(camZ - z)`, so as z approaches `TUNE.camZ` (48) the scale explodes and past it goes NEGATIVE —
a quad taken to z=60 folded its far edge back above the screen as an inverted triangle. The clear
is what covers the screen; the quad only has to cover the court.
- **The page gradient goes on `document.documentElement`, and `html` needs a real height.** Two
separate CSS traps, both hit: the ROOT element's background is what paints the viewport, so setting
it on `body` while `html` also has a background painted nothing; and a GRADIENT is laid out in the
root's own box, which collapses to zero when the canvas is `position: fixed`, so `html { height:
100dvh }` is load-bearing rather than cosmetic.
- **The page background behind the canvas tracks the live court apron** (`pageBackground` in - **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, 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 and the slivers a phone's ROUNDED DISPLAY CORNERS clip out of the canvas — a flat dark body drew a
+6 -1
View File
@@ -20,6 +20,10 @@
src: url('assets/fonts/Silkscreen-Regular.ttf') format('truetype'); } src: url('assets/fonts/Silkscreen-Regular.ttf') format('truetype'); }
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block; @font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
src: url('assets/fonts/Silkscreen-Bold.ttf') format('truetype'); } src: url('assets/fonts/Silkscreen-Bold.ttf') format('truetype'); }
/* html needs a real HEIGHT: the root element's background paints the viewport, but a GRADIENT is
laid out in the root's own box — and with the canvas position:fixed the root has no content and
collapses, so the gradient render.js sets had nowhere to go. */
html { height: 100%; height: 100dvh; }
html, body { margin: 0; padding: 0; background: #10141c; overflow: hidden; } html, body { margin: 0; padding: 0; background: #10141c; overflow: hidden; }
/* invisible probe: env() is only readable from CSS, so park the insets on an element and let /* invisible probe: env() is only readable from CSS, so park the insets on an element and let
render.js measure them. The canvas stays FULL-BLEED (the stands under the notch look good) — render.js measure them. The canvas stays FULL-BLEED (the stands under the notch look good) —
@@ -40,6 +44,7 @@
top: env(safe-area-inset-top, 0px); top: env(safe-area-inset-top, 0px);
left: env(safe-area-inset-left, 0px); left: env(safe-area-inset-left, 0px);
width: calc(100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px)); width: calc(100vw - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
height: calc(100vh - env(safe-area-inset-top, 0px));
height: calc(100dvh - env(safe-area-inset-top, 0px)); } height: calc(100dvh - env(safe-area-inset-top, 0px)); }
#hint { position: fixed; bottom: 8px; width: 100%; text-align: center; #hint { position: fixed; bottom: 8px; width: 100%; text-align: center;
font: 400 10px Silkscreen, monospace; color: #6b7688; } font: 400 10px Silkscreen, monospace; color: #6b7688; }
@@ -80,6 +85,6 @@
<p class="start">TAP TO START</p> <p class="start">TAP TO START</p>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=49"></script> <script type="module" src="main.js?v=50"></script>
</body> </body>
</html> </html>
+10 -3
View File
@@ -71,7 +71,11 @@ function pageBackground(out) {
const t = SAFE.topCss; const t = SAFE.topCss;
const css = `linear-gradient(#10141c 0px, #10141c ${t}px, ${out} ${t}px, ${out} 100%)`; 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 if (css === bgLast) return; // only touch the DOM when it actually changed
bgLast = css; document.body.style.background = css; bgLast = css;
// documentElement, NOT body: the ROOT element's background is what paints the viewport canvas.
// index.html gives html a background, so body's never propagates — setting it there painted only
// body's own box, which has no height (the canvas is position: fixed) and so painted nothing.
document.documentElement.style.background = css;
} }
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
@@ -129,7 +133,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ---- // ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load. // assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 49; // asset cache-buster — bump with main.js?v=N (see the img.src note below) const SPRV = 50; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPR = {}; 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', 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', 'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
@@ -662,7 +666,10 @@ export function render() {
// 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, 60, pal.out); // out area (past the bottom on any aspect) quadXZ(-14, -6, 14, 30, pal.out); // out area. NEVER let z approach
// TUNE.camZ (48): proj() divides by (camZ - z), so at z >= camZ the scale blows up and then goes
// NEGATIVE, and the quad's far edge folds back above the screen as an inverted triangle. The
// full-frame clear above is what covers the bottom of the screen; this quad does not need to.
quadXZ(-dw - 0.15, -0.15, dw + 0.15, COURT.L + 0.15, '#e8e8dc'); // perimeter border line quadXZ(-dw - 0.15, -0.15, dw + 0.15, COURT.L + 0.15, '#e8e8dc'); // perimeter border line
quadXZ(-dw, 0, dw, COURT.L, pal.lawn); // lawn (doubles court) quadXZ(-dw, 0, dw, COURT.L, pal.lawn); // lawn (doubles court)
for (let z = 0; z < COURT.L; z += 2.97) quadXZ(-dw, z, dw, Math.min(z + 1.49, COURT.L), pal.stripe); // mow stripes for (let z = 0; z < COURT.L; z += 2.97) quadXZ(-dw, z, dw, Math.min(z + 1.49, COURT.L), pal.stripe); // mow stripes