intro and bg
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 716 KiB After Width: | Height: | Size: 2.0 MiB |
456
public/intro-test.html
Normal file
456
public/intro-test.html
Normal file
@@ -0,0 +1,456 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Intro Reveal Tests — In + Out</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Barlow:wght@300;400;600;700&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--paint: #0a0a0a;
|
||||
--bg: #FAFAFA;
|
||||
--text: #1A1A18;
|
||||
--muted: #5A5A54;
|
||||
--green: #2a3010;
|
||||
--cream: #f4ecd8;
|
||||
--easeOut: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
--easeStrong: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--easeIn: cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
font-family: 'Barlow', sans-serif;
|
||||
color: var(--text);
|
||||
padding: 40px 20px 80px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
max-width: 800px;
|
||||
margin: 0 auto 32px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'DM Serif Display', serif;
|
||||
font-weight: 400;
|
||||
font-size: 40px;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.lede {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.lede small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.controls {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.btn-all {
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
background: var(--green);
|
||||
color: var(--cream);
|
||||
border: none;
|
||||
padding: 12px 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,236,200,0.15);
|
||||
}
|
||||
.btn-all:hover { background: #1a2008; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (min-width: 720px) {
|
||||
.grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.card:hover { border-color: rgba(0,0,0,0.18); transform: translateY(-2px); }
|
||||
.card-stage {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
.card-stage .demo-text {
|
||||
display: inline-block;
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(18px, 3.4vw, 26px);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.22em;
|
||||
line-height: 1.3;
|
||||
color: var(--paint);
|
||||
opacity: 0; /* default hidden until .play-in or .play-out applied */
|
||||
will-change: opacity, transform, filter, clip-path;
|
||||
}
|
||||
.card-stage.is-resting .demo-text {
|
||||
opacity: 1; /* resting state for outro previews */
|
||||
filter: none;
|
||||
transform: none;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
.card-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.card-name small {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
color: var(--muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.card-num {
|
||||
display: inline-block;
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
color: var(--cream);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-actions { display: flex; gap: 6px; }
|
||||
.btn {
|
||||
font-family: 'Barlow', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(0,0,0,0.18);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { background: rgba(0,0,0,0.04); }
|
||||
.btn.btn-out { border-color: rgba(192,48,48,0.4); color: #C03030; }
|
||||
.btn.btn-out:hover { background: rgba(192,48,48,0.06); }
|
||||
|
||||
.tip {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-family: 'Barlow', monospace;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
IN + OUT animations — every style now has a paired exit so the
|
||||
user can preview the full cycle. Click the card or "Cycle" to
|
||||
run In → hold → Out; "In" / "Out" buttons fire one direction.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
/* 1. SMOKE */
|
||||
@keyframes a-smoke-in { 0% { opacity: 0; filter: blur(16px); transform: translateY(14px); } 35% { opacity: 0.6; } 100% { opacity: 1; filter: blur(0); transform: translateY(0); } }
|
||||
@keyframes a-smoke-out { 0% { opacity: 1; filter: blur(0); transform: translateY(0); } 100% { opacity: 0; filter: blur(16px); transform: translateY(-14px); } }
|
||||
.demo-1.play-in { animation: a-smoke-in 1.6s var(--easeOut) forwards; }
|
||||
.demo-1.play-out { animation: a-smoke-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 2. FOCUS PULL */
|
||||
@keyframes a-focus-in { 0% { opacity: 0; filter: blur(28px); } 40% { opacity: 1; filter: blur(28px); } 100% { opacity: 1; filter: blur(0); } }
|
||||
@keyframes a-focus-out { 0% { opacity: 1; filter: blur(0); } 60% { opacity: 1; filter: blur(28px); } 100% { opacity: 0; filter: blur(28px); } }
|
||||
.demo-2.play-in { animation: a-focus-in 1.8s var(--easeStrong) forwards; }
|
||||
.demo-2.play-out { animation: a-focus-out 1.4s var(--easeIn) forwards; }
|
||||
|
||||
/* 3. SLOW ZOOM (entry: 1.08 → 1.00 ; exit: 1.00 → 1.10 outward) */
|
||||
@keyframes a-zoom-in { 0% { opacity: 0; transform: scale(1.08); filter: blur(2px); } 100% { opacity: 1; transform: scale(1); filter: blur(0); } }
|
||||
@keyframes a-zoom-out { 0% { opacity: 1; transform: scale(1); filter: blur(0); } 100% { opacity: 0; transform: scale(1.10); filter: blur(2px); } }
|
||||
.demo-3.play-in { animation: a-zoom-in 2.2s var(--easeStrong) forwards; }
|
||||
.demo-3.play-out { animation: a-zoom-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 4. PUSH-IN (entry: 0.92 → 1.00 ; exit: 1.00 → 0.92 reverse dolly) */
|
||||
@keyframes a-push-in { 0% { opacity: 0; transform: scale(0.92); filter: blur(3px); } 100% { opacity: 1; transform: scale(1); filter: blur(0); } }
|
||||
@keyframes a-push-out { 0% { opacity: 1; transform: scale(1); filter: blur(0); } 100% { opacity: 0; transform: scale(0.92); filter: blur(3px); } }
|
||||
.demo-4.play-in { animation: a-push-in 1.6s var(--easeOut) forwards; }
|
||||
.demo-4.play-out { animation: a-push-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 5. MASK UP (entry: clip bottom → top ; exit: clip top → bottom) */
|
||||
@keyframes a-mask-in { 0% { opacity: 1; clip-path: inset(100% 0 0 0); } 10% { opacity: 1; } 100% { opacity: 1; clip-path: inset(0 0 0 0); } }
|
||||
@keyframes a-mask-out { 0% { opacity: 1; clip-path: inset(0 0 0 0); } 100% { opacity: 1; clip-path: inset(0 0 100% 0); } }
|
||||
.demo-5.play-in { animation: a-mask-in 1.4s var(--easeStrong) forwards; }
|
||||
.demo-5.play-out { animation: a-mask-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 6. IRIS (entry: circle 0% → 75% ; exit: circle 75% → 0%) */
|
||||
@keyframes a-iris-in { 0% { opacity: 1; clip-path: circle(0% at 50% 50%); } 100% { opacity: 1; clip-path: circle(75% at 50% 50%); } }
|
||||
@keyframes a-iris-out { 0% { opacity: 1; clip-path: circle(75% at 50% 50%); } 100% { opacity: 1; clip-path: circle(0% at 50% 50%); } }
|
||||
.demo-6.play-in { animation: a-iris-in 1.4s var(--easeStrong) forwards; }
|
||||
.demo-6.play-out { animation: a-iris-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 7. WORD CASCADE (each word in/out with stagger; JS sets per-word delay) */
|
||||
@keyframes a-word-in { 0% { opacity: 0; transform: translateY(14px); filter: blur(8px); } 100% { opacity: 1; transform: translateY(0); filter: blur(0); } }
|
||||
@keyframes a-word-out { 0% { opacity: 1; transform: translateY(0); filter: blur(0); } 100% { opacity: 0; transform: translateY(-12px); filter: blur(6px); } }
|
||||
.card-stage.style-7 .demo-text { opacity: 1; }
|
||||
.card-stage.style-7 .demo-text > .word {
|
||||
display: inline-block;
|
||||
margin-right: 0.25em;
|
||||
opacity: 0;
|
||||
}
|
||||
.card-stage.style-7.is-resting .demo-text > .word { opacity: 1; }
|
||||
.card-stage.style-7.play-in .demo-text > .word { animation: a-word-in 0.7s var(--easeOut) forwards; }
|
||||
.card-stage.style-7.play-out .demo-text > .word { animation: a-word-out 0.55s var(--easeIn) forwards; }
|
||||
|
||||
/* 8. GLOW HALO */
|
||||
@keyframes a-glow-in { 0% { opacity: 0; text-shadow: 0 0 0 rgba(90,154,120,0); filter: blur(2px); transform: scale(0.97); } 45% { opacity: 1; text-shadow: 0 0 28px rgba(90,154,120,0.65), 0 0 12px rgba(90,154,120,0.45); } 100% { opacity: 1; text-shadow: 0 0 0 rgba(90,154,120,0); filter: blur(0); transform: scale(1); } }
|
||||
@keyframes a-glow-out { 0% { opacity: 1; text-shadow: 0 0 0 rgba(90,154,120,0); filter: blur(0); } 50% { opacity: 1; text-shadow: 0 0 32px rgba(90,154,120,0.55); } 100% { opacity: 0; text-shadow: 0 0 4px rgba(90,154,120,0); filter: blur(2px); transform: scale(1.04); } }
|
||||
.demo-8.play-in { animation: a-glow-in 1.9s var(--easeOut) forwards; }
|
||||
.demo-8.play-out { animation: a-glow-out 1.2s var(--easeIn) forwards; }
|
||||
|
||||
/* 9. CHROMATIC */
|
||||
.card-stage.style-9 .demo-text { position: relative; }
|
||||
.card-stage.style-9 .demo-text::before,
|
||||
.card-stage.style-9 .demo-text::after {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-stage.style-9 .demo-text::before { color: rgba(220,40,40,0.85); }
|
||||
.card-stage.style-9 .demo-text::after { color: rgba(40,120,220,0.85); }
|
||||
@keyframes a-chrom-base-in { 0% { opacity: 0; } 20% { opacity: 1; } 100% { opacity: 1; } }
|
||||
@keyframes a-chrom-base-out { 0% { opacity: 1; } 100% { opacity: 0; } }
|
||||
@keyframes a-chrom-r-in { 0% { opacity: 0; transform: translate3d(-22px,0,0); } 20% { opacity: 0.85; } 100% { opacity: 0; transform: translate3d(0,0,0); } }
|
||||
@keyframes a-chrom-r-out { 0% { opacity: 0; transform: translate3d(0,0,0); } 30% { opacity: 0.85; transform: translate3d(-22px,0,0); } 100% { opacity: 0; transform: translate3d(-32px,0,0); } }
|
||||
@keyframes a-chrom-b-in { 0% { opacity: 0; transform: translate3d(22px,0,0); } 20% { opacity: 0.85; } 100% { opacity: 0; transform: translate3d(0,0,0); } }
|
||||
@keyframes a-chrom-b-out { 0% { opacity: 0; transform: translate3d(0,0,0); } 30% { opacity: 0.85; transform: translate3d(22px,0,0); } 100% { opacity: 0; transform: translate3d(32px,0,0); } }
|
||||
.card-stage.style-9.play-in .demo-text { animation: a-chrom-base-in 1.4s var(--easeStrong) forwards; }
|
||||
.card-stage.style-9.play-in .demo-text::before { animation: a-chrom-r-in 1.4s var(--easeStrong) forwards; }
|
||||
.card-stage.style-9.play-in .demo-text::after { animation: a-chrom-b-in 1.4s var(--easeStrong) forwards; }
|
||||
.card-stage.style-9.play-out .demo-text { animation: a-chrom-base-out 1.0s var(--easeIn) forwards; }
|
||||
.card-stage.style-9.play-out .demo-text::before { animation: a-chrom-r-out 1.0s var(--easeIn) forwards; }
|
||||
.card-stage.style-9.play-out .demo-text::after { animation: a-chrom-b-out 1.0s var(--easeIn) forwards; }
|
||||
|
||||
/* 10. VAPOR RISE (entry: rises into focus ; exit: continues rising into haze) */
|
||||
@keyframes a-vapor-in { 0% { opacity: 0; filter: blur(0) brightness(1.3); transform: translateY(28px); } 20% { opacity: 0.45; filter: blur(8px) brightness(1.2); } 100% { opacity: 1; filter: blur(0) brightness(1); transform: translateY(0); } }
|
||||
@keyframes a-vapor-out { 0% { opacity: 1; filter: blur(0) brightness(1); transform: translateY(0); } 100% { opacity: 0; filter: blur(8px) brightness(1.2); transform: translateY(-28px); } }
|
||||
.demo-10.play-in { animation: a-vapor-in 2.0s var(--easeOut) forwards; }
|
||||
.demo-10.play-out { animation: a-vapor-out 1.0s var(--easeIn) forwards; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>Intro Reveal Tests — In + Out</h1>
|
||||
<p class="lede">
|
||||
Ten cinematic reveal styles, each with a matching exit.
|
||||
<small>Tap the card for a full <em>In → hold → Out</em> cycle. "In" / "Out" preview each direction independently.</small>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-all" id="play-all">▶ Cycle All Together</button>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<div class="card" data-num="1">
|
||||
<div class="card-stage style-1"><span class="demo-text demo-1">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">01</span><div>Smoke<small>blur 16px → focus → blur out</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="2">
|
||||
<div class="card-stage style-2"><span class="demo-text demo-2">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">02</span><div>Focus pull<small>blur snaps to focus, then defocuses</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="3">
|
||||
<div class="card-stage style-3"><span class="demo-text demo-3">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">03</span><div>Slow zoom<small>1.08 → 1.00 → 1.10 (camera pulls back)</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="4">
|
||||
<div class="card-stage style-4"><span class="demo-text demo-4">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">04</span><div>Push-in<small>0.92 → 1.00 → 0.92 (reverse dolly)</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="5">
|
||||
<div class="card-stage style-5"><span class="demo-text demo-5">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">05</span><div>Mask up<small>clip bottom → top; exits top → bottom</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="6">
|
||||
<div class="card-stage style-6"><span class="demo-text demo-6">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">06</span><div>Iris<small>circle opens, then closes</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="7">
|
||||
<div class="card-stage style-7"><span class="demo-text demo-7" data-text="Wisdom is to prepare">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">07</span><div>Word cascade<small>words in & out, staggered up/down</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="8">
|
||||
<div class="card-stage style-8"><span class="demo-text demo-8">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">08</span><div>Glow halo<small>halo in, halo flares & fades out</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="9">
|
||||
<div class="card-stage style-9"><span class="demo-text demo-9" data-text="Wisdom is to prepare">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">09</span><div>Chromatic<small>RGB split converges; splits apart on out</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-num="10">
|
||||
<div class="card-stage style-10"><span class="demo-text demo-10">Wisdom is to prepare</span></div>
|
||||
<div class="card-foot">
|
||||
<div class="card-name"><span class="card-num">10</span><div>Vapor rise<small>rises into focus; continues rising into haze</small></div></div>
|
||||
<div class="card-actions"><button class="btn btn-in">In</button><button class="btn btn-out">Out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="tip">Click a card for the full cycle, or use In / Out buttons to preview each direction.</p>
|
||||
|
||||
<script>
|
||||
// Word cascade — split text into <span class="word"> chunks with stagger.
|
||||
function buildWords() {
|
||||
const t = document.querySelector('.demo-7')
|
||||
if (!t) return
|
||||
const text = t.dataset.text || t.textContent || ''
|
||||
t.innerHTML = ''
|
||||
text.split(' ').forEach((w, i) => {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'word'
|
||||
span.dataset.idx = i
|
||||
span.dataset.total = text.split(' ').length
|
||||
span.style.animationDelay = (i * 0.18) + 's'
|
||||
span.textContent = w
|
||||
t.appendChild(span)
|
||||
})
|
||||
}
|
||||
buildWords()
|
||||
|
||||
// Stages 7 and 9 toggle classes on the .card-stage; the rest toggle on
|
||||
// the .demo-N inner element.
|
||||
function targetFor(num) {
|
||||
const stageLevel = ['7', '9'].includes(String(num))
|
||||
return stageLevel
|
||||
? document.querySelector('.style-' + num)
|
||||
: document.querySelector('.demo-' + num)
|
||||
}
|
||||
|
||||
function setClass(num, cls) {
|
||||
const t = targetFor(num)
|
||||
if (!t) return
|
||||
t.classList.remove('play-in', 'play-out')
|
||||
void t.offsetWidth
|
||||
t.classList.add(cls)
|
||||
}
|
||||
|
||||
function reset(num) {
|
||||
const t = targetFor(num)
|
||||
if (!t) return
|
||||
t.classList.remove('play-in', 'play-out')
|
||||
// Park stage 7 (cascade) and 5/6 (clip) in resting state for outro previews.
|
||||
const stage = (['7','9'].includes(String(num))) ? t : t.closest('.card-stage')
|
||||
if (stage) stage.classList.add('is-resting')
|
||||
}
|
||||
|
||||
function playIn(num) { document.querySelector('.style-' + num)?.classList.remove('is-resting'); setClass(num, 'play-in') }
|
||||
function playOut(num) { document.querySelector('.style-' + num)?.classList.add('is-resting'); setClass(num, 'play-out') }
|
||||
|
||||
function cycle(num) {
|
||||
playIn(num)
|
||||
// Hold for a moment after in completes (~1.6s baseline + 0.7s read)
|
||||
setTimeout(() => playOut(num), 2400)
|
||||
}
|
||||
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('click', () => cycle(card.dataset.num))
|
||||
})
|
||||
document.querySelectorAll('.btn-in').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); playIn(btn.closest('.card').dataset.num) })
|
||||
})
|
||||
document.querySelectorAll('.btn-out').forEach(btn => {
|
||||
btn.addEventListener('click', e => { e.stopPropagation(); playOut(btn.closest('.card').dataset.num) })
|
||||
})
|
||||
document.getElementById('play-all').addEventListener('click', () => {
|
||||
for (let n = 1; n <= 10; n++) cycle(n)
|
||||
})
|
||||
|
||||
// Auto-cycle each card once on load with a stagger.
|
||||
window.addEventListener('load', () => {
|
||||
for (let n = 1; n <= 10; n++) {
|
||||
setTimeout(() => cycle(n), 300 + n * 150)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
99
src/App.vue
99
src/App.vue
@@ -192,6 +192,22 @@
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- INTRO OVERLAY — three-stage typographic intro on first visit
|
||||
(sweep reveal → "Even if crisis" pause "is not here yet" → final
|
||||
pitch), then fades out to expose the hero. Skipped on subsequent
|
||||
loads in the same tab via sessionStorage. -->
|
||||
<div class="intro-overlay" id="intro-overlay" aria-hidden="true">
|
||||
<div class="intro-stage stage-1" data-stage="1">
|
||||
<span class="intro-text" data-i18n="intro_l1">Wisdom is to prepare</span>
|
||||
</div>
|
||||
<div class="intro-stage stage-2" data-stage="2">
|
||||
<span class="intro-text" data-i18n="intro_l2">Even if crisis is not here yet</span>
|
||||
</div>
|
||||
<div class="intro-stage stage-3" data-stage="3">
|
||||
<span class="intro-text" data-i18n="intro_l3">Figure out your Plan B in less than two minutes.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="logo" data-i18n="brand">Deepstock</div>
|
||||
<div class="header-right">
|
||||
@@ -548,6 +564,9 @@ const T = {
|
||||
hero_eyebrow: "Crisis Preparedness Advisor",
|
||||
hero_sub: "Preparedness, refined.",
|
||||
hero_cta: "Begin",
|
||||
intro_l1: "Wisdom is to prepare",
|
||||
intro_l2: "Even if crisis is not here yet",
|
||||
intro_l3: "Figure out your Plan B in less than two minutes.",
|
||||
stat_scenarios: "Scenarios",
|
||||
stat_questions: "Questions",
|
||||
stat_free: "Free Forever",
|
||||
@@ -665,6 +684,9 @@ const T = {
|
||||
hero_eyebrow: "⚡ Krisenvorsorge-Berater",
|
||||
hero_sub: "Vorsorge, verfeinert.",
|
||||
hero_cta: "Beginnen",
|
||||
intro_l1: "Weise ist, wer vorsorgt.",
|
||||
intro_l2: "Auch wenn noch keine Krise da ist.",
|
||||
intro_l3: "Finden Sie Ihren Plan B in weniger als zwei Minuten.",
|
||||
stat_scenarios: "Szenarien",
|
||||
stat_questions: "Fragen",
|
||||
stat_free: "Kostenlos",
|
||||
@@ -1750,6 +1772,77 @@ function revealSections() {
|
||||
})
|
||||
}
|
||||
|
||||
// Three-stage intro overlay played on the user's first home-page visit
|
||||
// in this tab. Skipped on subsequent loads (sessionStorage flag), and
|
||||
// also skipped when restoring a saved quiz/results stage so a refresh
|
||||
// mid-flow doesn't replay the intro. Each stage shows for `enter +
|
||||
// hold` ms, then fades out before the next enters.
|
||||
const INTRO_KEY = 'kammergut.intro.shown.v1'
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
async function playIntro() {
|
||||
const overlay = document.getElementById('intro-overlay')
|
||||
if (!overlay) return
|
||||
// Pacing — entry durations match the introSmoke keyframe (1.8s for
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < stages.length; i++) {
|
||||
const s = stages[i]
|
||||
const el = overlay.querySelector(s.sel)
|
||||
el.classList.add('active')
|
||||
await sleep(s.enter + s.hold)
|
||||
// Skip the smoke-out on the last stage — the overlay's own fade
|
||||
// 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) {
|
||||
el.classList.add('leaving')
|
||||
el.classList.remove('active')
|
||||
await sleep(exit)
|
||||
el.classList.remove('leaving')
|
||||
}
|
||||
}
|
||||
// Fade overlay out and let the hero animations run via body class.
|
||||
// The last stage is still .active here, so its text rides the
|
||||
// overlay fade-out as one motion.
|
||||
overlay.classList.add('done')
|
||||
document.body.classList.add('intro-done')
|
||||
await sleep(900)
|
||||
overlay.classList.add('gone')
|
||||
}
|
||||
function maybePlayIntro({ skip = false } = {}) {
|
||||
if (skip || sessionStorage.getItem(INTRO_KEY)) {
|
||||
document.body.classList.add('intro-done')
|
||||
const ov = document.getElementById('intro-overlay')
|
||||
if (ov) ov.classList.add('gone')
|
||||
return
|
||||
}
|
||||
try { sessionStorage.setItem(INTRO_KEY, '1') } catch {}
|
||||
playIntro()
|
||||
}
|
||||
|
||||
// Scroll-lock the page behind the modal. Saving and restoring scrollY +
|
||||
// position:fixed on body is the iOS-Safari-safe pattern; plain
|
||||
// overflow:hidden alone doesn't stop touch-driven body scroll there.
|
||||
@@ -2018,6 +2111,12 @@ onMounted(() => {
|
||||
setLang(lang)
|
||||
updateRegionIndicator()
|
||||
|
||||
// Intro overlay — only on the very first home-page visit in this tab.
|
||||
// If we're resuming a saved quiz/results stage, or the intro has already
|
||||
// played in this session, jump straight to the hero with no animation.
|
||||
const resumingMidFlow = !!(saved && saved.stage && saved.stage !== 'home')
|
||||
maybePlayIntro({ skip: resumingMidFlow })
|
||||
|
||||
if (saved && saved.stage && saved.stage !== 'home') {
|
||||
answers = saved.answers || {}
|
||||
currentQ = typeof saved.currentQ === 'number' ? saved.currentQ : 0
|
||||
|
||||
123
src/styles.css
123
src/styles.css
@@ -1017,6 +1017,129 @@ input[type=range]::-moz-range-thumb {
|
||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* ── INTRO OVERLAY ─────────────────────────────────────────────────
|
||||
Three-stage cinematic typographic intro: a slow left-to-right sweep
|
||||
on the first line, then two stages that drift in from a soft blur
|
||||
while the letter-spacing tightens. Text is flat #0a0a0a on a FAFAFA
|
||||
panel — no paint gloss filter. Body picks up `.intro-done` once all
|
||||
stages have played, which is what lets the hero/header animations
|
||||
start. */
|
||||
.intro-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: #FAFAFA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 1.1s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
.intro-overlay.done { opacity: 0; }
|
||||
.intro-overlay.gone { display: none; }
|
||||
|
||||
.intro-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 32px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* Intro text — uppercase Barlow at heading scale with wide tracking,
|
||||
matching the "Preparedness. Refined." sub-line typeface. No paint
|
||||
filter / no gradient text-fill (those tanked render performance during
|
||||
the animation, hence the wobble). Solid #0a0a0a on the FAFAFA panel. */
|
||||
.intro-stage .intro-text {
|
||||
display: inline-block;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: clamp(22px, 4.6vw, 42px);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.22em;
|
||||
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);
|
||||
}
|
||||
|
||||
/* Smoke reveal — heavy blur clears as the text emerges, with a small
|
||||
y-drift to suggest weight settling. No clip-path wipe and no letter
|
||||
spacing animation (the resizing effect read as twitchy). Used by all
|
||||
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 {
|
||||
display: inline-block;
|
||||
margin-right: 0.28em;
|
||||
opacity: 0;
|
||||
}
|
||||
.intro-stage.stage-3.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
|
||||
before the next one resolves. */
|
||||
.intro-stage.leaving .intro-text {
|
||||
animation: introOut 0.8s cubic-bezier(0.4, 0, 1, 1) forwards;
|
||||
}
|
||||
@keyframes introOut {
|
||||
0% { opacity: 1; filter: blur(0); transform: translateY(0); }
|
||||
100% { opacity: 0; filter: blur(16px); transform: translateY(-14px); }
|
||||
}
|
||||
/* Hero — hidden until intro is done so the bg image, brand wordmark,
|
||||
tagline and Begin button appear AFTER the intro plays out rather
|
||||
than during. The original animations on these elements stay defined
|
||||
below; we just override animation-name to none until intro-done is
|
||||
on the body, at which point each animation plays from its origin. */
|
||||
body:not(.intro-done) .hero-eyebrow,
|
||||
body:not(.intro-done) .hero h1,
|
||||
body:not(.intro-done) .hero .brand-line,
|
||||
body:not(.intro-done) .hero-sub,
|
||||
body:not(.intro-done) .hero .cta-btn {
|
||||
animation-name: none !important;
|
||||
opacity: 0;
|
||||
}
|
||||
/* Header + lang toggle ride along — hidden until intro is done. The
|
||||
intro-overlay already covers the hero bg during the stages, so the
|
||||
bg image's fade-in is the natural consequence of the overlay
|
||||
fading out. */
|
||||
body:not(.intro-done) .site-header { opacity: 0; }
|
||||
body.intro-done .site-header {
|
||||
opacity: 1;
|
||||
transition: opacity 0.6s ease 0.2s;
|
||||
}
|
||||
|
||||
/* ── SINGLE FOLD: hide non-essential sections on load ──
|
||||
(header stays visible with a transparent background on the homepage) */
|
||||
.site-header { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; border-bottom: none; }
|
||||
|
||||
Reference in New Issue
Block a user