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:
1 parent
ae64daa0fb
commit
89667c4018
3 files changed
+34
-6
No files matched your search
@@ -182,6 +182,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 canvas is deliberately FULL-BLEED under the notch and home indicator; the HUD is what respects
|
||||
the safe area.** `viewport-fit=cover` is set, and the stands running to the screen edges look right —
|
||||
but an iPhone's clock was landing directly on the scoreboard. A hidden `#safe` probe div carries
|
||||
`env(safe-area-inset-*)` (env() is only readable from CSS), `measureSafe()` converts it to LOGICAL
|
||||
canvas px using the same scale `fitViewport` just applied, and `SAFE.top`/`SAFE.bottom` shift the
|
||||
scoreboard down and lift the rack + `METER_Y` up. Anything new that anchors to the very top or very
|
||||
bottom of the screen must offset by these too — and the rack's hit test shares `rackGeom`, so taps
|
||||
follow automatically.
|
||||
- **`UI` in config.js is the accent palette** (hot/warm/pick/cream/dim/bad/ball). Every HUD colour
|
||||
goes through it. Gore reds are deliberately NOT in it — the splatter stays loud.
|
||||
- **`F(size, bold)` in render.js builds every canvas font string.** Bare `monospace` resolves to
|
||||
|
||||
+8
-2
@@ -20,7 +20,12 @@
|
||||
src: url('assets/fonts/Silkscreen-Regular.ttf') format('truetype'); }
|
||||
@font-face { font-family: 'Silkscreen'; font-weight: 700; font-display: block;
|
||||
src: url('assets/fonts/Silkscreen-Bold.ttf') format('truetype'); }
|
||||
html, body { margin: 0; height: 100%; background: #10141c; display: grid; place-items: center; }
|
||||
html, body { margin: 0; height: 100%; height: 100dvh; background: #10141c; display: grid; place-items: center; }
|
||||
/* 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) —
|
||||
it is the HUD that has to move, see SAFE in render.js. */
|
||||
#safe { position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; visibility: hidden;
|
||||
padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
|
||||
canvas { image-rendering: pixelated; width: min(96vw, calc(96vh * 16 / 9)); aspect-ratio: 16/9; touch-action: none; }
|
||||
#hint { position: fixed; bottom: 8px; width: 100%; text-align: center;
|
||||
font: 400 10px Silkscreen, monospace; color: #6b7688; }
|
||||
@@ -45,6 +50,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c" width="480" height="270"></canvas>
|
||||
<div id="safe"></div>
|
||||
<div id="hint">move: WASD/arrows · serve/swing: SPACE · aim: hold a direction while swinging · T: tuning panel</div>
|
||||
<div id="intro">
|
||||
<div class="card">
|
||||
@@ -60,6 +66,6 @@
|
||||
<p class="start">TAP TO START</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="main.js?v=44"></script>
|
||||
<script type="module" src="main.js?v=45"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user