Make it full-screen: no scrolling, ever
Lock the viewport (100dvh, overflow:hidden, grid-centered) and scale the active card to fit via a --fit custom property set by fitActiveScreen(). Every screen — setup, game, winner — now always fits the browser at any window size; the card shrinks only when it would otherwise overflow. Refit on screen change, stage switch, resize, orientation, and font load. Verified no vertical/horizontal scroll on all three screens at short-laptop (1366x640) and mobile-portrait (390x844). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bba0a06a71
commit
aa2c3b989d
2 changed files with 42 additions and 7 deletions
29
script.js
29
script.js
|
|
@ -230,6 +230,7 @@ function applyStage(stage, { persist = true } = {}) {
|
||||||
opt.setAttribute('aria-checked', opt.dataset.stage === stage ? 'true' : 'false'));
|
opt.setAttribute('aria-checked', opt.dataset.stage === stage ? 'true' : 'false'));
|
||||||
startLabel.textContent = STAGES[stage].startLabel;
|
startLabel.textContent = STAGES[stage].startLabel;
|
||||||
if (persist) saveState();
|
if (persist) saveState();
|
||||||
|
scheduleFit(); // font swap (e.g. Metal Mania) changes height → refit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Setup interactions ---------- */
|
/* ---------- Setup interactions ---------- */
|
||||||
|
|
@ -424,9 +425,34 @@ function applyPlayerCamps() {
|
||||||
function showScreen(screen) {
|
function showScreen(screen) {
|
||||||
[nameScreen, gameScreen, winnerScreen].forEach((s) => s.classList.remove('active'));
|
[nameScreen, gameScreen, winnerScreen].forEach((s) => s.classList.remove('active'));
|
||||||
screen.classList.add('active');
|
screen.classList.add('active');
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
scheduleFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Fit-to-viewport ----------
|
||||||
|
Scale the active card (via the --fit custom prop) so the whole screen
|
||||||
|
always fits — it's tic-tac-toe, no scrolling, ever. */
|
||||||
|
let fitRAF = 0;
|
||||||
|
function scheduleFit() {
|
||||||
|
if (fitRAF) cancelAnimationFrame(fitRAF);
|
||||||
|
fitRAF = requestAnimationFrame(() => { fitRAF = 0; fitActiveScreen(); });
|
||||||
|
}
|
||||||
|
function fitActiveScreen() {
|
||||||
|
const screen = document.querySelector('.screen.active');
|
||||||
|
if (!screen) return;
|
||||||
|
const card = screen.querySelector('.card');
|
||||||
|
if (!card) return;
|
||||||
|
// offsetWidth/Height ignore transforms, so we always read the natural size.
|
||||||
|
const w = card.offsetWidth;
|
||||||
|
const h = card.offsetHeight;
|
||||||
|
if (!w || !h) return;
|
||||||
|
const margin = 16; // small breathing room around the card
|
||||||
|
const scale = Math.min(1, (window.innerWidth - margin) / w, (window.innerHeight - margin) / h);
|
||||||
|
card.style.setProperty('--fit', String(scale));
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', scheduleFit);
|
||||||
|
window.addEventListener('orientationchange', scheduleFit);
|
||||||
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(scheduleFit);
|
||||||
|
|
||||||
/* ---------- Lifecycle ---------- */
|
/* ---------- Lifecycle ---------- */
|
||||||
function startGame() {
|
function startGame() {
|
||||||
player1Name = player1Input.value.trim() || 'Player 1';
|
player1Name = player1Input.value.trim() || 'Player 1';
|
||||||
|
|
@ -520,5 +546,6 @@ function init() {
|
||||||
applyStage(currentStage, { persist: false });
|
applyStage(currentStage, { persist: false });
|
||||||
applyPlayerCamps();
|
applyPlayerCamps();
|
||||||
updatePlayerHighlight();
|
updatePlayerHighlight();
|
||||||
|
scheduleFit();
|
||||||
}
|
}
|
||||||
init();
|
init();
|
||||||
|
|
|
||||||
20
style.css
20
style.css
|
|
@ -110,18 +110,22 @@
|
||||||
.camp-sparkle { --pc: var(--sparkle-color); --pg: var(--sparkle-glow); --pt: var(--sparkle-tint); }
|
.camp-sparkle { --pc: var(--sparkle-color); --pg: var(--sparkle-glow); --pt: var(--sparkle-tint); }
|
||||||
.camp-shred { --pc: var(--shred-color); --pg: var(--shred-glow); --pt: var(--shred-tint); }
|
.camp-shred { --pc: var(--shred-color); --pg: var(--shred-glow); --pt: var(--shred-tint); }
|
||||||
|
|
||||||
/* ---------- Base layout (scroll-safe centering) ---------- */
|
/* ---------- Base layout (full-screen, never scroll) ----------
|
||||||
|
The viewport is locked to the screen; the active card is scaled by JS
|
||||||
|
(--fit) to always fit. It's tic-tac-toe — everything stays in view. */
|
||||||
|
html { height: 100%; }
|
||||||
body {
|
body {
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
background-size: var(--bg-size);
|
background-size: var(--bg-size);
|
||||||
animation: gradientShift 15s ease infinite;
|
animation: gradientShift 15s ease infinite;
|
||||||
min-height: 100vh;
|
height: 100dvh;
|
||||||
display: flex;
|
min-height: 100dvh;
|
||||||
flex-direction: column;
|
display: grid;
|
||||||
overflow-x: hidden;
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 28px 16px;
|
padding: 8px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
transition: color 0.4s ease;
|
transition: color 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +234,10 @@ body {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border: 3px solid var(--surface-border);
|
border: 3px solid var(--surface-border);
|
||||||
transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
|
||||||
|
/* Scaled to fit the viewport by fitActiveScreen() in script.js */
|
||||||
|
transform: scale(var(--fit, 1));
|
||||||
|
transform-origin: center center;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-card { padding: 30px 30px 34px; }
|
.name-card { padding: 30px 30px 34px; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue