intro and bg

This commit is contained in:
Dorian
2026-05-10 12:18:48 +01:00
parent e12faddb5d
commit 67344976fe
2 changed files with 48 additions and 58 deletions

View File

@@ -1786,29 +1786,36 @@ async function playIntro() {
// stages 1 and 3). Stage 2 needs ~2.6s to land both staggered lines
// (line-a 0.1+1.5s, line-b 1.1+1.5s). Holds are tuned so the viewer
// can read each line after it lands.
// Stages 1 + 2 push in (1.6s); stage 3 word-cascades (~2.4s — 10
// words × 0.16s stagger + 0.75s per-word). Stage 3 holds, then the
// overlay fade handles its outro so we don't double-dissolve.
const stages = [
{ sel: '.stage-1', enter: 1600, hold: 800 },
{ sel: '.stage-2', enter: 1600, hold: 1000 },
{ sel: '.stage-3', enter: 2400, hold: 1500 },
]
const exit = 800 // matches the introOut (smoke out) duration
// Split stage 3 into per-word spans for the cascade. Run on each
// play so a language switch between sessions still gets correct words.
const stage3Text = overlay.querySelector('.stage-3 .intro-text')
if (stage3Text) {
const text = stage3Text.textContent.trim()
stage3Text.innerHTML = ''
text.split(/\s+/).forEach((word, i) => {
const span = document.createElement('span')
span.className = 'word'
span.style.animationDelay = (0.05 + i * 0.16) + 's'
span.textContent = word
stage3Text.appendChild(span)
})
// Every stage word-cascades. Entry duration = words × 0.16s stagger
// + 0.75s per-word + 0.05s base delay. JS rebuilds per-word spans
// for every stage so language switches still split correctly, and
// measures the entry length from the live word count.
const STAGGER = 0.16
const PER_WORD = 0.75
const BASE = 0.05
const stages = []
for (let n = 1; n <= 3; n++) {
const stageEl = overlay.querySelector('.stage-' + n)
const textEl = stageEl ? stageEl.querySelector('.intro-text') : null
let words = []
if (textEl) {
const text = textEl.textContent.trim()
textEl.innerHTML = ''
words = text.split(/\s+/)
words.forEach((word, i) => {
const span = document.createElement('span')
span.className = 'word'
span.style.animationDelay = (BASE + i * STAGGER) + 's'
span.textContent = word
textEl.appendChild(span)
})
}
const entryMs = Math.round((BASE + (Math.max(0, words.length - 1)) * STAGGER + PER_WORD) * 1000)
// Stage 3 holds longest because it's the longest line.
const hold = n === 3 ? 1500 : 700
stages.push({ sel: '.stage-' + n, enter: entryMs, hold })
}
const exit = 800 // matches the introOut (smoke out) duration
for (let i = 0; i < stages.length; i++) {
const s = stages[i]
const el = overlay.querySelector(s.sel)
@@ -1818,10 +1825,15 @@ async function playIntro() {
// dissolves the final line, so we don't get two competing outros
// (one on the text, one on the FAFAFA panel).
if (i < stages.length - 1) {
// Keep .active alongside .leaving so the per-word `introWord`
// animation's `forwards` end-state holds during the fade.
// Removing .active here would drop the words back to opacity:0
// instantly and the smoke-out on the parent would dissolve into
// an already-empty wrapper.
el.classList.add('leaving')
el.classList.remove('active')
await sleep(exit)
el.classList.remove('leaving')
el.classList.remove('active')
}
}
// Fade overlay out and let the hero animations run via body class.

View File

@@ -98,7 +98,7 @@ body { background: #FAFAFA; color: var(--text); font-family: var(--font-body); f
and compensates for the scenario-strip below — net effect: visually centered on the viewport */
padding: 100px 16px 24px;
text-align: center;
background: url('/images/bg-1.jpg') center/cover no-repeat;
background: #F0F0F0;
overflow: hidden;
flex: 1;
display: flex;
@@ -132,12 +132,8 @@ body { background: #FAFAFA; color: var(--text); font-family: var(--font-body); f
margin-bottom: 40px;
animation: fadeUp 1s 0.15s ease both;
}
/* Mobile — soften the bg image to 50% by overlaying the body fill */
@media (max-width: 767px) {
.hero {
background: linear-gradient(rgba(250,250,250,0.5), rgba(250,250,250,0.5)), url('/images/bg-1.jpg') center/cover no-repeat;
}
}
/* Mobile uses the same flat #F0F0F0 background as desktop now that
the bg image has been removed. */
/* Tablet — same fill behaviour as mobile (hero padding stays 16px
each side). */
@media (min-width: 768px) {
@@ -1058,14 +1054,12 @@ input[type=range]::-moz-range-thumb {
font-weight: 400;
font-size: clamp(22px, 4.6vw, 42px);
line-height: 1.3;
letter-spacing: 0.22em;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #0a0a0a;
/* Push-in default state — held at slightly back-of-frame, soft blur,
so the .active class can pull the text forward. */
opacity: 0;
filter: blur(3px);
transform: scale(0.92);
/* Word-cascade default — wrapper is visible; per-word spans inside
animate in with a stagger (delays set inline by JS). */
opacity: 1;
}
/* Smoke reveal — heavy blur clears as the text emerges, with a small
@@ -1074,38 +1068,22 @@ input[type=range]::-moz-range-thumb {
three stages so the unveiling is one consistent vocabulary. Stage 3
runs at half-speed (3.6s vs 1.8s) so the closing line lands with
weight instead of rushing past. */
/* Stages 1 + 2push-in entry (scale 0.92 → 1.00, blur 3px → 0). */
.intro-stage.stage-1.active .intro-text,
.intro-stage.stage-2.active .intro-text {
animation: introSmoke 1.6s 0.05s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
/* Stage 3 — word cascade. Wrapper is visible; per-word spans animate
in with a stagger (delays set inline by JS). The overlay's own fade
handles the outro so we don't double-dissolve the closing line. */
.intro-stage.stage-3 .intro-text {
opacity: 1;
filter: none;
transform: none;
letter-spacing: 0.18em;
}
.intro-stage.stage-3 .intro-text > .word {
/* Word cascade — used by every stage. Each word fades / drifts / blurs
in independently with a 0.16s stagger; the parent .intro-text stays
visible the whole time. JS rebuilds the per-word spans on each play
so language switches between sessions still split correctly. */
.intro-stage .intro-text > .word {
display: inline-block;
margin-right: 0.28em;
opacity: 0;
}
.intro-stage.stage-3.active .intro-text > .word {
.intro-stage.active .intro-text > .word {
animation: introWord 0.75s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
@keyframes introWord {
0% { opacity: 0; transform: translateY(14px); filter: blur(8px); }
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}
/* Push-in — camera dollies forward: scale 0.92 → 1.00 with a soft 3px
blur resolving to crisp, fade-up paired. Cinematic without wobble. */
@keyframes introSmoke {
0% { opacity: 0; filter: blur(3px); transform: scale(0.92); }
100% { opacity: 1; filter: blur(0); transform: scale(1); }
}
/* Exit — smoke out: re-blur to 16px, drift upward 14px, fade to 0.
Same effect for every stage so each sentence dissolves into haze