/* global React */ /* Bature Digital — Hero Story An animated narrative replacing the static hero panel. Tells the story of compounding progress across 5 marketing channels. */ const { useEffect, useState, useRef } = React; const STAGES = [ { key: 'seo', label: 'SEO', eyebrow: 'Month 1 — SEO', headline: 'Climbing search rankings', sub: 'From page 4 to page 1', metric: { from: 20, to: 407, suffix: ' clicks/mo' }, color: '#F1C518', }, { key: 'ads', label: 'Paid Ads', eyebrow: 'Month 3 — Paid Ads', headline: 'ROAS compounding', sub: 'Spend tightened, returns up', metric: { from: 1.2, to: 4.8, suffix: '× ROAS', decimals: 1 }, color: '#A77FD0', }, { key: 'social', label: 'Social', eyebrow: 'Month 5 — Social', headline: 'Audience growing', sub: 'Real reach, not vanity', metric: { from: 1.2, to: 14.6, suffix: 'K followers', decimals: 1 }, color: '#F5D24A', }, { key: 'web', label: 'Web', eyebrow: 'Month 7 — Web', headline: 'Speed & conversion', sub: 'CWV passing, CVR up', metric: { from: 1.4, to: 3.9, suffix: '% CVR', decimals: 1 }, color: '#7B3DB8', }, { key: 'email', label: 'Email', eyebrow: 'Month 12 — Email', headline: 'Automated revenue', sub: 'Flows running on autopilot', metric: { from: 89, to: 370, prefix: '$', suffix: 'K' }, color: '#F1C518', }, ]; // Easing — match brand const easeOut = (t) => 1 - Math.pow(1 - t, 3); const easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; // Revenue curve data — sampled across all 5 stages, exponential growth feel function curvePoints(progress) { // progress 0..1 across the whole story const pts = []; const N = 60; for (let i = 0; i < N; i++) { const x = i / (N - 1); if (x > progress) break; // exponential-ish curve with a bit of organic wobble const wobble = Math.sin(x * 14) * 0.012 + Math.sin(x * 33) * 0.006; const y = Math.pow(x, 1.7) * 0.92 + wobble; pts.push([x, y]); } return pts; } function pathFromPoints(points, w, h, padX, padY) { if (points.length < 2) return ''; const innerW = w - padX * 2; const innerH = h - padY * 2; const xy = points.map(([x, y]) => [padX + x * innerW, padY + (1 - y) * innerH]); let d = `M ${xy[0][0].toFixed(2)} ${xy[0][1].toFixed(2)}`; for (let i = 1; i < xy.length; i++) { const [px, py] = xy[i - 1]; const [cx, cy] = xy[i]; const mx = (px + cx) / 2; d += ` Q ${px.toFixed(2)} ${py.toFixed(2)} ${mx.toFixed(2)} ${((py + cy) / 2).toFixed(2)}`; d += ` T ${cx.toFixed(2)} ${cy.toFixed(2)}`; } return d; } function fillPathFromPoints(points, w, h, padX, padY) { if (points.length < 2) return ''; const linePath = pathFromPoints(points, w, h, padX, padY); const lastX = padX + points[points.length - 1][0] * (w - padX * 2); const firstX = padX + points[0][0] * (w - padX * 2); return `${linePath} L ${lastX.toFixed(2)} ${h - padY} L ${firstX.toFixed(2)} ${h - padY} Z`; } function format(n, p) { const v = p.decimals ? n.toFixed(p.decimals) : Math.round(n); return `${p.prefix || ''}${v}${p.suffix || ''}`; } // ───────────────────────── Stage scene components ───────────────────────── function SeoScene({ t }) { // t: 0..1 within stage const positions = [4, 3, 2, 1, 1, 1]; const idx = Math.min(positions.length - 1, Math.floor(t * positions.length)); const rank = positions[idx]; return (
{"\"best digital marketing agency\"".slice(0, Math.floor(33 * Math.min(1, t * 1.4)))}
); } function AdsScene({ t }) { // CTR rising, impressions filling const ctr = 1.2 + (4.8 - 1.2) * easeOut(t); const bars = [ [0.15, 0.25, 0.35, 0.55, 0.45, 0.7, 0.65, 0.85, 0.78, 0.95], ][0]; return (
Campaign · Conversions {ctr.toFixed(1)}× ROAS
{bars.map((b, i) => { const localT = Math.max(0, Math.min(1, t * 1.3 - i * 0.05)); const h = b * 100 * easeOut(localT); return ( ); })}
MonTueWedThuFri
); } function SocialScene({ t }) { // Followers ticking up; small profile rows appearing const N = 6; const visible = Math.floor(N * Math.min(1, t * 1.1)); return (
@yourbrand {(1.2 + (14.6 - 1.2) * easeOut(t)).toFixed(1)}K followers
); } function WebScene({ t }) { // Performance gauges ramping up const lcp = 5.2 - (5.2 - 1.8) * easeOut(t); // seconds, lower is better const cvr = 1.4 + (3.9 - 1.4) * easeOut(t); // percent const score = 38 + (96 - 38) * easeOut(t); // perf score return (
yourbrand.com
{Math.round(score)}
Performance
LCP {lcp.toFixed(1)}s
CVR {cvr.toFixed(1)}%
CLS 0.04
); } function EmailScene({ t }) { const nodes = [ { x: 12, y: 18, label: 'Welcome' }, { x: 50, y: 14, label: 'Educate' }, { x: 86, y: 22, label: 'Offer' }, { x: 30, y: 60, label: 'Win-back' }, { x: 70, y: 64, label: 'Loyal' }, ]; const lines = [ [0, 1], [1, 2], [1, 3], [3, 4], [2, 4], ]; return (
Automation flow {lines.map(([a, b], i) => { const A = nodes[a], B = nodes[b]; const localT = Math.max(0, Math.min(1, t * 1.4 - i * 0.08)); return ( ); })} {nodes.map((n, i) => { const localT = Math.max(0, Math.min(1, t * 1.6 - i * 0.12)); return ( ); })} {/* pulse traveling on edge 1->2 */} {(() => { const pulseT = (t * 1.2) % 1; const A = nodes[1], B = nodes[2]; if (t < 0.4) return null; return ( ); })()}
{Math.round(38 + 24 * easeOut(t))}% Open
{(2.4 + 8.6 * easeOut(t)).toFixed(1)}% CTR
${Math.round(1.2 + 11.8 * easeOut(t))}K Revenue
); } const SCENES = { seo: SeoScene, ads: AdsScene, social: SocialScene, web: WebScene, email: EmailScene }; // ───────────────────────── Main component ───────────────────────── function HeroStory({ speed = 1, autoplay = true }) { const [stageIdx, setStageIdx] = useState(0); const [stageT, setStageT] = useState(0); // 0..1 progress within current stage const [overallT, setOverallT] = useState(0); // 0..1 progress across all stages const [paused, setPaused] = useState(!autoplay); const rafRef = useRef(null); const lastTickRef = useRef(0); const STAGE_MS = 3200 / Math.max(0.25, speed); // Reset when autoplay flips back on, or speed changes meaningfully useEffect(() => { setPaused(!autoplay); }, [autoplay]); useEffect(() => { function tick(now) { if (lastTickRef.current === 0) lastTickRef.current = now; const dt = now - lastTickRef.current; lastTickRef.current = now; if (!paused) { setStageT((prev) => { const next = prev + dt / STAGE_MS; if (next >= 1) { setStageIdx((s) => { const ns = (s + 1) % STAGES.length; if (ns === 0) setOverallT(0); return ns; }); return 0; } return next; }); setOverallT((prev) => { const inc = dt / (STAGE_MS * STAGES.length); const n = prev + inc; return n > 1 ? 1 : n; }); } rafRef.current = requestAnimationFrame(tick); } rafRef.current = requestAnimationFrame(tick); return () => { cancelAnimationFrame(rafRef.current); lastTickRef.current = 0; }; }, [paused, STAGE_MS]); const stage = STAGES[stageIdx]; const Scene = SCENES[stage.key]; // Curve uses overallT but also adds per-stage smooth fill so it "draws" live const liveProgress = Math.min(1, overallT + (stageT / STAGES.length) * 0.6); const points = curvePoints(liveProgress); const W = 460, H = 90, PX = 8, PY = 10; const linePath = pathFromPoints(points, W, H, PX, PY); const fillPath = fillPathFromPoints(points, W, H, PX, PY); const headPt = points[points.length - 1]; const headXY = headPt ? [PX + headPt[0] * (W - PX * 2), PY + (1 - headPt[1]) * (H - PY * 2)] : null; // Metric counter for current stage const m = stage.metric; const metricVal = m.from + (m.to - m.from) * easeOut(stageT); return (