Put the whole game inside the safe area
Insetting only the HUD while the canvas stayed full-bleed wasn't what was asked for. The canvas itself is position:fixed and inset by env(safe-area-inset-*) now, so nothing renders under the notch, the status bar or the home indicator, and render.js no longer offsets the HUD as well — doing both would subtract the notch twice. The bottom strip was a second bug: the canvas was sized by computing a scale from innerWidth/innerHeight and setting explicit pixels, so it was laid out against whatever innerHeight reported at load, before Safari's toolbars had settled, and stayed short of the bottom forever after. CSS stretches it now (100vw x 100dvh minus the insets) and fitViewport derives the logical grid from the box it was actually given, so the aspect always matches exactly — no letterbox, no distortion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
89667c4018
commit
90aefdce27
3 files changed
+46
-21
No files matched your search
@@ -45,28 +45,26 @@ 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.
|
||||
// Safe-area handling lives ENTIRELY in index.html's canvas rule: the element is inset by
|
||||
// env(safe-area-inset-*) so the whole game sits inside the safe region. SAFE stays at zero and is kept
|
||||
// only so nothing has to change if that decision is ever revisited — do NOT re-add offsets here as
|
||||
// well, or the notch gets subtracted twice.
|
||||
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;
|
||||
const portrait = (cv.clientHeight || innerHeight) > (cv.clientWidth || innerWidth);
|
||||
// Portrait height ADAPTS to the device. A fixed 270x480 is 9:16, but a modern iPhone is taller
|
||||
// (~390x844 = 0.46), so width bound the fit and ~150 CSS px of screen went unused at the bottom.
|
||||
// The court is positioned by proj() off TUNE.horizon in ABSOLUTE logical px, and the rack/meter hang
|
||||
// off `H - ...`, so extra height simply widens the apron between the court and the rack — nothing
|
||||
// has to be re-laid-out. Clamped so a freak aspect can't stretch the apron absurdly.
|
||||
const tall = Math.round(270 * Math.max(1.6, Math.min(2.3, innerHeight / Math.max(1, innerWidth))));
|
||||
const [w, h] = portrait ? [270, tall] : [480, 270];
|
||||
// Derive the logical size from the box CSS actually gave the canvas, not from innerWidth/innerHeight.
|
||||
// The canvas is stretched to 100vw x 100dvh, so matching its logical aspect to that box exactly is
|
||||
// what guarantees no letterbox and no distortion, however the phone's toolbars behave.
|
||||
const bw = cv.clientWidth || innerWidth, bh = cv.clientHeight || innerHeight;
|
||||
const base = portrait ? 270 : 480;
|
||||
const other = Math.round(base * (portrait ? bh / Math.max(1, bw) : bh / Math.max(1, bw)));
|
||||
const [w, h] = portrait ? [base, Math.max(430, Math.min(680, other))] : [base, Math.max(230, Math.min(340, other))];
|
||||
if (cv.width !== w * RES || cv.height !== h * RES) {
|
||||
W = w; H = h;
|
||||
cv.width = w * RES; cv.height = h * RES;
|
||||
@@ -90,9 +88,7 @@ export function fitViewport() { // landscape 480x270; portrait 270 wide and as T
|
||||
buildCrowd();
|
||||
}
|
||||
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
|
||||
// CSS owns both the canvas's on-screen size AND the safe-area inset; we only match the logical grid.
|
||||
}
|
||||
addEventListener('resize', fitViewport);
|
||||
|
||||
@@ -108,7 +104,7 @@ export function proj(x, y, z) {
|
||||
|
||||
// ---------------------------------------------------------------- sprites ----
|
||||
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
||||
const SPRV = 45; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
||||
const SPRV = 46; // 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',
|
||||
|
||||
Reference in new issue
Block a user