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:
1 parent
aab83dacdc
commit
3324fe9cda
3 files changed
+67
-18
No files matched your search
+14
-4
@@ -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.
|
(`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 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
|
every menu screen does the same now — on EVERY arrival, not just the boot (Oliver, twice: "immer
|
||||||
line fly in from the left 70ms apart (`@keyframes dealIn`, hung off `#splash:not(.busy)` so the
|
reinfliegen" and "auch für die untermenüs"). `@keyframes dealIn` flies an element in from the left
|
||||||
class removal IS the trigger). It fires on the boot only — coming back from a run must not make
|
on that overshoot, and `main.dealStage()` re-arms the `deal` class on #intro at the one funnel
|
||||||
you wait for a flourish you have already seen — and `prefers-reduced-motion` turns it off.
|
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
|
**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
|
on the stats screen outlived it — go back and the splash came up scrolled, buttons off screen
|
||||||
|
|||||||
+37
-13
@@ -196,8 +196,32 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
#intro[data-stage="player"] #player,
|
#intro[data-stage="player"] #player,
|
||||||
#intro[data-stage="promo"] #promo,
|
#intro[data-stage="promo"] #promo,
|
||||||
#intro[data-stage="card"] #card,
|
#intro[data-stage="card"] #card,
|
||||||
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1;
|
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; }
|
||||||
animation: drop 0.3s ease-out; }
|
/* ...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
|
/* 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 */
|
display:grid above would otherwise win over the hidden attribute */
|
||||||
#intro[hidden] { display: none; }
|
#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
|
/* 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
|
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
|
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
|
moment the menu has an entrance to make. It plays on EVERY arrival at the title screen, not
|
||||||
:not(.busy), so it triggers when main.js takes the loading state off and never again — coming
|
just the boot (Oliver) — main.dealSplash re-arms the class, because an animation whose class
|
||||||
back from a run must not make you wait for a flourish you have already seen. */
|
never left cannot restart itself. */
|
||||||
@keyframes dealIn {
|
@keyframes dealIn {
|
||||||
from { opacity: 0; transform: translateX(-56px); }
|
from { opacity: 0; transform: translateX(-56px); }
|
||||||
60% { opacity: 1; }
|
60% { opacity: 1; }
|
||||||
to { opacity: 1; transform: translateX(0); }
|
to { opacity: 1; transform: translateX(0); }
|
||||||
}
|
}
|
||||||
#splash:not(.busy) #plaque,
|
#intro.deal #splash #plaque,
|
||||||
#splash:not(.busy) .foot.bare > * {
|
#intro.deal #splash .foot.bare > * {
|
||||||
animation: dealIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
|
animation: dealIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; }
|
||||||
#splash:not(.busy) #plaque { animation-delay: 0.02s; }
|
#intro.deal #splash #plaque { animation-delay: 0.02s; }
|
||||||
#splash:not(.busy) #mStart { animation-delay: 0.12s; }
|
#intro.deal #splash #mStart { animation-delay: 0.12s; }
|
||||||
#splash:not(.busy) #mOther { animation-delay: 0.19s; }
|
#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
|
/* 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 */
|
to override its delay — the id selectors around it get there on their own */
|
||||||
#splash:not(.busy) .foot.bare > .two { animation-delay: 0.26s; }
|
#intro.deal #splash .foot.bare > .two { animation-delay: 0.26s; }
|
||||||
#splash:not(.busy) #build { animation-delay: 0.33s; }
|
#intro.deal #splash #build { animation-delay: 0.33s; }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@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
|
/* 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. */
|
.busy it stands in the buttons' place — same band, same height, so nothing jumps when it goes. */
|
||||||
#boot { display: none; margin-bottom: 10px; }
|
#boot { display: none; margin-bottom: 10px; }
|
||||||
|
|||||||
+16
-1
@@ -93,8 +93,22 @@ const go = stage => {
|
|||||||
if (stage === 'train') buildTrain();
|
if (stage === 'train') buildTrain();
|
||||||
intro.dataset.stage = stage;
|
intro.dataset.stage = stage;
|
||||||
intro.scrollTop = 0;
|
intro.scrollTop = 0;
|
||||||
|
dealStage(stage);
|
||||||
menuMusic();
|
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
|
// --- 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
|
// 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);
|
clearInterval(tick);
|
||||||
// fonts.ready resolves once nothing is pending — including the three faces warmed above — so the
|
// fonts.ready resolves once nothing is pending — including the three faces warmed above — so the
|
||||||
// first canvas frame is never the system serif.
|
// 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);
|
}, 100);
|
||||||
|
|
||||||
// THE FIRST TAP ANYWHERE ON THE OVERLAY STARTS THE THEME, and does nothing else — the screens have
|
// THE FIRST TAP ANYWHERE ON THE OVERLAY STARTS THE THEME, and does nothing else — the screens have
|
||||||
|
|||||||
Reference in new issue
Block a user