Every screen deals itself in, every time

The entrance is no longer a one-off on the boot: dealStage re-arms the class at
the funnel every stage change already passes, so the title screen flies its
plaque and buttons in whenever you arrive at it. The sub-menus join in — their
header, content and footer arrive one after the other instead of the panel
dropping in as one slab — and the poster wall deals its three rooms rather than
its list, since a moving child inside a moving parent starts at twice the
offset and reads as a stutter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-31 13:25:26 +02:00
1 parent aab83dacdc
commit 3324fe9cda
3 files changed
+67 -18

No files matched your search

+14 -4
View File
@@ -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
+37 -13
View File
@@ -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; }
+16 -1
View File
@@ -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