// hero-story.jsx — animated hero engine, v3. // // Architecture: // HeroStory (parent) — owns cycleIdx; mounts ONE HeroStoryCycle at a // time via a key. When a cycle completes, the // old instance unmounts and a new one mounts — // state and CSS animations reset cleanly. // HeroStoryCycle (child) — plays through one conversation: ringing → // active typewriter → callEnd → slide1 → // emailWrite → emailHold → slideOut → gap. // Calls onComplete at the end of gap. // // LiveWaveform now has a fill-in animation: a paper-colored veil covers the // strip, then retreats from left over ~4s, revealing bars from right to left // as the call comes online. (function () { const { PALETTE } = window; // --- Tunables --------------------------------------------------------- const BAR_W = 4; const BAR_GAP = 5; const BARS_PER_LOOP = 90; const SCROLL_DURATION_S = 16; const BUILDUP_DURATION_S = 4; const TYPE_RATE_MS = 32; const PUNCT_EXTRA_MS = 90; const PAUSE_BETWEEN_LINES_MS = 750; const PAUSE_AFTER_LAST_LINE_MS = 1600; const RINGING_MS = 1300; const CALL_END_MS = 1900; const SLIDE_MS = 1000; const POST_SLIDE_SETTLE_MS = 600; // --- Email typewriter (character-by-character across whole email) --- const EMAIL_CHAR_MS = 9; const EMAIL_PUNCT_MS = 30; const EMAIL_HOLD_MS = 1800; // pause after typing completes const FADE_AND_SENT_MS = 900; // email text fades, "Sent!" appears const SENT_HOLD_MS = 1200; // "Sent!" stays visible const SLIDE_OFF_MS = 1100; // both zones slide left const GAP_MS = 700; // empty pause before next cycle function generateBars(count, seed = 1) { const out = []; for (let i = 0; i < count; i++) { const v = 0.5 + 0.35 * Math.sin(i * 0.32 + seed) + 0.18 * Math.sin(i * 0.83 + seed * 2) + 0.12 * Math.sin(i * 1.55 + seed * 0.7); out.push(Math.max(0.16, Math.min(0.96, v))); } return out; } // ---------------------------------------------------------------------- // LiveWaveform — continuous-scroll oscilloscope. New for v3: a "fill veil" // covers the strip initially and retreats from left as the call goes // active, revealing bars right-to-left. // ---------------------------------------------------------------------- function LiveWaveform({ color = PALETTE.ink, height = 140, phase = "ringing", veilColor = PALETTE.bg }) { const bars = React.useMemo(() => generateBars(BARS_PER_LOOP, 1.3), []); const stripWidth = BARS_PER_LOOP * (BAR_W + BAR_GAP); let dim = 1; if (phase === "ringing") dim = 0.5; else if (phase === "callEnd" || phase === "gone") dim = 0; // Veil is fully covering during ringing; retreats once the call is active. const veilOpen = phase === "active" || phase === "callEnd" || phase === "gone"; return (