diff --git a/PROJECT.md b/PROJECT.md index 6324b65..ded7ebf 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -596,10 +596,20 @@ is in the HTML now and main.js only ever takes it off. The keyboard-hint strip h (`body:has(#splash.busy) #hint`), so the loading screen really is court, title and bar. **THE MENU DEALS ITSELF IN.** The canvas modals have always arrived on an easeOutBack overshoot; -the splash now does the same once, on the boot: plaque, fixture, OTHER GAMES, the pair, the build -line fly in from the left 70ms apart (`@keyframes dealIn`, hung off `#splash:not(.busy)` so the -class removal IS the trigger). It fires on the boot only — coming back from a run must not make -you wait for a flourish you have already seen — and `prefers-reduced-motion` turns it off. +every menu screen does the same now — on EVERY arrival, not just the boot (Oliver, twice: "immer +reinfliegen" and "auch für die untermenüs"). `@keyframes dealIn` flies an element in from the left +on that overshoot, and `main.dealStage()` re-arms the `deal` class on #intro at the one funnel +every stage change passes (remove, force a reflow, add — an animation whose class never left +cannot restart, and CSS has no replay switch). +- THE SPLASH deals its own five: plaque, fixture, OTHER GAMES, the pair, the build line, 70ms apart. +- EVERY OTHER SCREEN deals its BANDS — header, content, footer — through + `#intro.deal > .card > div:not(#splash) > *` with an nth-child stagger. The panel's old + whole-slab `drop` retired: a slab says "a screen appeared", a deal says "the machine is laying + this out for you". +- THE POSTER WALL deals its three ROOMS instead of its list, and the list itself stops animating — + a moving child inside a moving parent starts at twice the offset and reads as a stutter. +- `prefers-reduced-motion` turns all of it off, and the splash holds its deal until the boot gate + lifts (there is nothing to deal while the loading state is up). **EVERY SCREEN OPENS AT ITS TOP.** `#intro` is the one scroller the menus share, so a scroll earned on the stats screen outlived it — go back and the splash came up scrolled, buttons off screen diff --git a/index.html b/index.html index f0884dd..ed3e149 100644 --- a/index.html +++ b/index.html @@ -196,8 +196,32 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat #intro[data-stage="player"] #player, #intro[data-stage="promo"] #promo, #intro[data-stage="card"] #card, - #intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; - animation: drop 0.3s ease-out; } + #intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; } + /* ...and every screen DEALS ITS BANDS IN (2026-08-31, Oliver: "wäre natürlich auch toll für die + untermenüs"). The panel used to drop in as one slab; now its header, its content and its + footer arrive one after the other on the same overshoot the splash and the canvas modals use. + The children of the stage, not the stage itself — a slab that drops says "a screen appeared", + a deal says "the machine is laying this out for you". The splash keeps its own finer stagger + below (its two bands would otherwise fly as two lumps), hence the :not. */ + #intro.deal > .card > div:not(#splash) > * { + animation: dealIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; } + #intro.deal > .card > div:not(#splash) > *:nth-child(1) { animation-delay: 0.02s; } + #intro.deal > .card > div:not(#splash) > *:nth-child(2) { animation-delay: 0.09s; } + #intro.deal > .card > div:not(#splash) > *:nth-child(3) { animation-delay: 0.16s; } + #intro.deal > .card > div:not(#splash) > *:nth-child(4) { animation-delay: 0.23s; } + #intro.deal > .card > div:not(#splash) > *:nth-child(n+5) { animation-delay: 0.30s; } + /* the poster wall deals its ROOMS, not one lump: the list is the whole screen there, so letting + it arrive as a single band wastes the entrance. The list itself stops animating — a moving + child inside a moving parent starts at twice the offset and reads as a stutter. */ + #intro.deal #stmode > #modeList { animation: none; } + #intro.deal #modeList > .mgroup { + animation: dealIn 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; } + #intro.deal #modeList > .mgroup:nth-child(1) { animation-delay: 0.09s; } + #intro.deal #modeList > .mgroup:nth-child(2) { animation-delay: 0.17s; } + #intro.deal #modeList > .mgroup:nth-child(3) { animation-delay: 0.25s; } + @media (prefers-reduced-motion: reduce) { + #intro.deal > .card > div:not(#splash) > *, + #intro.deal #modeList > .mgroup { animation: none; } } /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the display:grid above would otherwise win over the hidden attribute */ #intro[hidden] { display: none; } @@ -522,26 +546,26 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat /* THE DEAL-IN (2026-08-31, Oliver: "es wäre toll, wenn sich die buttons nach dem laden aufbauen würden — arcade style"). The canvas modals have always arrived on an easeOutBack overshoot rather than decelerating politely into place; this is that same snap, in CSS, for the one - moment the menu has an entrance to make. It fires ONCE, on the boot: the rule hangs off - :not(.busy), so it triggers when main.js takes the loading state off and never again — coming - back from a run must not make you wait for a flourish you have already seen. */ + moment the menu has an entrance to make. It plays on EVERY arrival at the title screen, not + just the boot (Oliver) — main.dealSplash re-arms the class, because an animation whose class + never left cannot restart itself. */ @keyframes dealIn { from { opacity: 0; transform: translateX(-56px); } 60% { opacity: 1; } to { opacity: 1; transform: translateX(0); } } - #splash:not(.busy) #plaque, - #splash:not(.busy) .foot.bare > * { + #intro.deal #splash #plaque, + #intro.deal #splash .foot.bare > * { animation: dealIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; } - #splash:not(.busy) #plaque { animation-delay: 0.02s; } - #splash:not(.busy) #mStart { animation-delay: 0.12s; } - #splash:not(.busy) #mOther { animation-delay: 0.19s; } + #intro.deal #splash #plaque { animation-delay: 0.02s; } + #intro.deal #splash #mStart { animation-delay: 0.12s; } + #intro.deal #splash #mOther { animation-delay: 0.19s; } /* the pair of secondary buttons is a CLASS, so it needs the same weight as the shorthand above to override its delay — the id selectors around it get there on their own */ - #splash:not(.busy) .foot.bare > .two { animation-delay: 0.26s; } - #splash:not(.busy) #build { animation-delay: 0.33s; } + #intro.deal #splash .foot.bare > .two { animation-delay: 0.26s; } + #intro.deal #splash #build { animation-delay: 0.33s; } @media (prefers-reduced-motion: reduce) { - #splash:not(.busy) #plaque, #splash:not(.busy) .foot.bare > * { animation: none; } } + #intro.deal #splash #plaque, #intro.deal #splash .foot.bare > * { animation: none; } } /* the boot bar wears the stats screen's kill bar, doing duty as a loading bar. While #splash is .busy it stands in the buttons' place — same band, same height, so nothing jumps when it goes. */ #boot { display: none; margin-bottom: 10px; } diff --git a/src/main.js b/src/main.js index 99dc769..71f62b7 100644 --- a/src/main.js +++ b/src/main.js @@ -93,8 +93,22 @@ const go = stage => { if (stage === 'train') buildTrain(); intro.dataset.stage = stage; intro.scrollTop = 0; + dealStage(stage); menuMusic(); }; +// THE ARCADE ENTRANCE, re-armed. The title screen deals its plaque and buttons in from the left +// every time you arrive at it — off a run, off the stats, off the boot. Removing the class and +// forcing a reflow before adding it back is the whole trick: an animation whose class never left +// cannot restart, and there is no "replay" switch in CSS. +function dealStage(stage) { + // the title screen waits for the boot: while the loading state is up there is nothing to deal + // (the element is looked up here, not read off the const below: go() can fire before that line + // has run, and a temporal-dead-zone throw in the menu funnel would take every screen with it) + if (stage === 'splash' && document.getElementById('splash').classList.contains('busy')) return; + intro.classList.remove('deal'); + void intro.offsetWidth; + intro.classList.add('deal'); +} // --- MODE selection in TWO STEPS (config.MENU + config.MODES): three posters on top, and the // picked one deals its own second step underneath — classic deals the difficulty chips, zombie @@ -1001,7 +1015,8 @@ const tick = setInterval(function watchLoad() { clearInterval(tick); // fonts.ready resolves once nothing is pending — including the three faces warmed above — so the // first canvas frame is never the system serif. - (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => splash.classList.remove('busy')); + (document.fonts ? document.fonts.ready : Promise.resolve()) + .then(() => { splash.classList.remove('busy'); dealStage('splash'); }); }, 100); // THE FIRST TAP ANYWHERE ON THE OVERLAY STARTS THE THEME, and does nothing else — the screens have