From 67344976fe21b6ec6fe372f6ca1af3631b3d33e9 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 10 May 2026 12:18:48 +0100 Subject: [PATCH] intro and bg --- src/App.vue | 58 ++++++++++++++++++++++++++++++-------------------- src/styles.css | 48 +++++++++++------------------------------ 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/src/App.vue b/src/App.vue index b5bd57f..e840dd7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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. diff --git a/src/styles.css b/src/styles.css index b4b3e17..51e75a3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 + 2 — push-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