intro and bg
This commit is contained in:
58
src/App.vue
58
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user