Run the art to the bottom edge, lift only the rack

The remaining gap was the home-indicator inset being reserved on the canvas
element. Unlike the status bar that is a slim translucent bar, not opaque UI,
so the canvas now runs to the physical bottom edge and the court art fills it —
only the weapon rack and the draw meter lift clear of it, through SAFE.bottom.
The top keeps its element inset, since the scoreboard genuinely has to clear
the status bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 18:37:09 +02:00
1 parent 90aefdce27
commit c601035eab
3 files changed
+29 -17

No files matched your search

+16 -7
View File
@@ -45,11 +45,18 @@ const IS_TOUCH = matchMedia('(pointer: coarse)').matches; // hoisted: a matchMed
export const RES = 4;
export let W = 480, H = 270;
// 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.
// Split responsibility, and the split is deliberate:
// TOP — index.html insets the canvas element below the status bar. SAFE.top stays 0; adding an
// offset here as well would subtract the notch twice.
// 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 };
function measureSafe(scale) {
const el = document.getElementById('safe');
if (!el) return;
SAFE.bottom = Math.round((parseFloat(getComputedStyle(el).paddingBottom) || 0) / scale);
}
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
const portrait = (cv.clientHeight || innerHeight) > (cv.clientWidth || innerWidth);
@@ -88,7 +95,9 @@ export function fitViewport() { // landscape 480x270; portrait 270 wide and as T
buildCrowd();
}
ctx.imageSmoothingEnabled = false; // resets on canvas resize
// CSS owns both the canvas's on-screen size AND the safe-area inset; we only match the logical grid.
// CSS owns the on-screen size and the TOP inset; we match the logical grid and measure the bottom
// inset in logical px so the rack can lift clear of the home indicator.
measureSafe((cv.clientWidth || innerWidth) / w);
}
addEventListener('resize', fitViewport);
@@ -104,7 +113,7 @@ export function proj(x, y, z) {
// ---------------------------------------------------------------- sprites ----
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
const SPRV = 46; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
const SPRV = 47; // 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',
@@ -807,7 +816,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 + SAFE.top);
ctx.save(); ctx.translate(6, 6);
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