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:
1 parent
90aefdce27
commit
c601035eab
3 files changed
+29
-17
No files matched your search
+7
-5
@@ -182,11 +182,13 @@ 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
|
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
|
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.
|
everything in reach, so counting after counts the survivors and the cooldown barely moves.
|
||||||
- **The safe area is handled in ONE place: index.html's canvas rule.** The canvas is `position: fixed`
|
- **The safe area is handled ASYMMETRICALLY, on purpose.** TOP: index.html insets the canvas element
|
||||||
and inset by `env(safe-area-inset-*)`, so the whole game sits inside the safe region — nothing ever
|
below the status bar, because that is opaque UI the scoreboard must clear - `SAFE.top` stays 0, and
|
||||||
renders under the notch, the status bar or the home indicator. `render.js` does NOT offset the HUD as
|
adding a HUD offset there as well subtracts the notch twice. BOTTOM: the canvas deliberately runs to
|
||||||
well (`SAFE` stays at zero); doing both subtracts the notch twice. Full-bleed with an inset HUD was
|
the PHYSICAL bottom edge so the court art fills the screen (the home indicator is a slim translucent
|
||||||
tried first and Oliver rejected it twice.
|
bar, not opaque UI); only the interactive RACK and the draw meter lift clear of it, via
|
||||||
|
`SAFE.bottom`, which `measureSafe` reads from the hidden `#safe` probe (env() is CSS-only).
|
||||||
|
Reserving the bottom inset on the element instead just left a dead band.
|
||||||
- **The canvas is STRETCHED by CSS (100vw x 100dvh minus insets) and `fitViewport` derives the logical
|
- **The canvas is STRETCHED by CSS (100vw x 100dvh minus insets) and `fitViewport` derives the logical
|
||||||
grid from the box it was actually given** — `cv.clientWidth/clientHeight`, never `innerWidth/innerHeight`.
|
grid from the box it was actually given** — `cv.clientWidth/clientHeight`, never `innerWidth/innerHeight`.
|
||||||
Sizing it the other way round (compute a scale, then set explicit px) laid it out against whatever
|
Sizing it the other way round (compute a scale, then set explicit px) laid it out against whatever
|
||||||
|
|||||||
+6
-5
@@ -31,15 +31,16 @@
|
|||||||
explicit px) meant it was laid out against whatever innerHeight reported at load — before iOS
|
explicit px) meant it was laid out against whatever innerHeight reported at load — before iOS
|
||||||
Safari's toolbars had settled — and it stayed a strip short of the bottom forever after.
|
Safari's toolbars had settled — and it stayed a strip short of the bottom forever after.
|
||||||
100dvh is the dynamic viewport height, so it tracks those toolbars. */
|
100dvh is the dynamic viewport height, so it tracks those toolbars. */
|
||||||
/* The canvas lives ENTIRELY INSIDE the safe area — never under the notch, the status bar or the
|
/* TOP inset only. The status bar is opaque UI and the scoreboard must clear it, so the canvas starts
|
||||||
home indicator. Doing the inset here rather than offsetting the HUD in canvas space keeps it in
|
below it — but the home indicator is a slim translucent bar, so the canvas runs all the way to the
|
||||||
ONE place: render.js then just matches its logical grid to whatever box it was given.
|
PHYSICAL bottom edge and the court art fills it. Only the weapon RACK lifts clear of the indicator,
|
||||||
|
via SAFE.bottom in render.js. Reserving the bottom inset here instead just left a dead band.
|
||||||
100dvh is the dynamic viewport height, so it tracks iOS Safari's toolbars appearing/hiding. */
|
100dvh is the dynamic viewport height, so it tracks iOS Safari's toolbars appearing/hiding. */
|
||||||
canvas { image-rendering: pixelated; display: block; position: fixed; touch-action: none;
|
canvas { image-rendering: pixelated; display: block; position: fixed; touch-action: none;
|
||||||
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(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 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; }
|
||||||
@media (pointer: coarse) { #hint, #intro .kb { display: none; } }
|
@media (pointer: coarse) { #hint, #intro .kb { display: none; } }
|
||||||
@@ -79,6 +80,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=46"></script>
|
<script type="module" src="main.js?v=47"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -45,11 +45,18 @@ const IS_TOUCH = matchMedia('(pointer: coarse)').matches; // hoisted: a matchMed
|
|||||||
export const RES = 4;
|
export const RES = 4;
|
||||||
export let W = 480, H = 270;
|
export let W = 480, H = 270;
|
||||||
|
|
||||||
// Safe-area handling lives ENTIRELY in index.html's canvas rule: the element is inset by
|
// Split responsibility, and the split is deliberate:
|
||||||
// env(safe-area-inset-*) so the whole game sits inside the safe region. SAFE stays at zero and is kept
|
// TOP — index.html insets the canvas element below the status bar. SAFE.top stays 0; adding an
|
||||||
// only so nothing has to change if that decision is ever revisited — do NOT re-add offsets here as
|
// offset here as well would subtract the notch twice.
|
||||||
// well, or the notch gets subtracted 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 };
|
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
|
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
|
||||||
const portrait = (cv.clientHeight || innerHeight) > (cv.clientWidth || innerWidth);
|
const portrait = (cv.clientHeight || innerHeight) > (cv.clientWidth || innerWidth);
|
||||||
@@ -88,7 +95,9 @@ export function fitViewport() { // landscape 480x270; portrait 270 wide and as T
|
|||||||
buildCrowd();
|
buildCrowd();
|
||||||
}
|
}
|
||||||
ctx.imageSmoothingEnabled = false; // resets on canvas resize
|
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);
|
addEventListener('resize', fitViewport);
|
||||||
|
|
||||||
@@ -104,7 +113,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 = 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 = {};
|
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',
|
||||||
@@ -807,7 +816,7 @@ export function render() {
|
|||||||
const ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3
|
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')
|
? (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)];
|
: ['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)
|
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
|
if (SPR.scoreboard) ctx.drawImage(SPR.scoreboard, 0, 0, bW, bH); // generated ornate board backdrop
|
||||||
else { // code-drawn fallback until the PNG loads
|
else { // code-drawn fallback until the PNG loads
|
||||||
|
|||||||
Reference in new issue
Block a user