Respect the notch and the home indicator

viewport-fit=cover was already on, so the canvas was full-bleed and the iPhone
status bar landed directly on the scoreboard. Keeping the canvas full-bleed —
the stands running to the screen edges is the look — and moving the HUD
instead: a hidden probe div carries env(safe-area-inset-*), which env() only
exposes to CSS, render.js converts it to logical canvas px at the same scale
fitViewport just applied, and the scoreboard shifts down while the rack and the
draw meter lift up. Body height also goes to 100dvh so iOS's dynamic toolbars
stop leaving a strip unused at the bottom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 17:53:36 +02:00
1 parent ae64daa0fb
commit 89667c4018
3 files changed
+34 -6

No files matched your search

+18 -4
View File
@@ -45,6 +45,19 @@ const IS_TOUCH = matchMedia('(pointer: coarse)').matches; // hoisted: a matchMed
export const RES = 4;
export let W = 480, H = 270;
// Device safe-area insets, in LOGICAL canvas px. The canvas is deliberately full-bleed under the
// notch and the home indicator (viewport-fit=cover) — the crowd looks better running to the edges —
// so the HUD is what has to respect them: the scoreboard shifts down by SAFE.top and the rack lifts
// by SAFE.bottom. Without this an iPhone's clock sat directly on top of the scoreboard.
export const SAFE = { top: 0, bottom: 0 };
function measureSafe(scale) {
const el = document.getElementById('safe');
if (!el) return;
const cs = getComputedStyle(el);
SAFE.top = Math.round((parseFloat(cs.paddingTop) || 0) / scale);
SAFE.bottom = Math.round((parseFloat(cs.paddingBottom) || 0) / scale);
}
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
const portrait = innerHeight > innerWidth;
// Portrait height ADAPTS to the device. A fixed 270x480 is 9:16, but a modern iPhone is taller
@@ -79,6 +92,7 @@ export function fitViewport() { // landscape 480x270; portrait 270 wide and as T
ctx.imageSmoothingEnabled = false; // resets on canvas resize
const sc = Math.min(innerWidth / w, innerHeight / h);
cv.style.width = w * sc + 'px'; cv.style.height = h * sc + 'px';
measureSafe(sc); // CSS px -> logical px, so the HUD insets track however the canvas got scaled
}
addEventListener('resize', fitViewport);
@@ -94,7 +108,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 44; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPRV = 45; // 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',
@@ -421,10 +435,10 @@ function drawMgRacket() { // horde: the player braces the racket forward and up-
// via rackHit so a tap lands on the tile you see.
const RACK_BOT = 8; // px of breathing room below the rack (floats it up off the screen edge)
const RACK_GRAB = 7; // px the TOUCH band extends above the drawn rack (below, it runs to the screen edge)
export const METER_Y = () => H - TUNE.rackH - RACK_BOT - 17; // the draw meter's fixed bottom slot
export const METER_Y = () => H - TUNE.rackH - RACK_BOT - 17 - SAFE.bottom; // the draw meter's fixed bottom slot
function rackGeom() {
const h = TUNE.rackH, n = 5, tw = Math.min(64, (W - 4) / n), rw = tw * n;
return { x0: Math.round((W - rw) / 2), y0: H - h - RACK_BOT, tw, h, n, rw };
return { x0: Math.round((W - rw) / 2), y0: H - h - RACK_BOT - SAFE.bottom, tw, h, n, rw };
}
export function rackHit(nx, ny) { // normalized [0,1] canvas coords -> slot index, or -1
if (G.mode !== 'splatter' || G.interlude) return -1; // no rack during the tennis-rules duel
@@ -797,7 +811,7 @@ export function render() {
const ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3
? (score.pts[i] === score.pts[1 - i] ? '40' : score.pts[i] > score.pts[1 - i] ? 'AD' : '40')
: ['0', '15', '30', '40'][Math.min(score.pts[i], 3)];
ctx.save(); ctx.translate(6, 6);
ctx.save(); ctx.translate(6, 6 + SAFE.top);
const bW = 150, bH = 42; // fixed: keeps the ornate PNG undistorted (art is 300x84 = 150x42)
if (SPR.scoreboard) ctx.drawImage(SPR.scoreboard, 0, 0, bW, bH); // generated ornate board backdrop
else { // code-drawn fallback until the PNG loads