// Variant 2 — Dashboard Pulse // 라이브 대시보드 (트래픽 그래프 + 보안 알림 + 마케팅 KPI) 가 살아 움직이고 // 마지막에 통합 뷰로 줌아웃하며 로고 등장. function StatCard({ label, value, delta, color, children, width = 480, height = 240 }) { return (
{label}
{delta && (
{delta}
)}
{value}
{children}
); } function LiveLine({ time, color, seed = 0, height = 60 }) { // animated line chart that scrolls const points = []; const N = 40; for (let i = 0; i < N; i++) { const phase = time * 1.4 + i * 0.4 + seed; const v = (Math.sin(phase) * 0.3 + Math.sin(phase * 1.7 + seed) * 0.2 + 0.5); points.push([i / (N - 1) * 320, height - v * height * 0.85]); } const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0]} ${p[1]}`).join(' '); const fillD = d + ` L 320 ${height} L 0 ${height} Z`; return ( {/* Trailing dot */} ); } function ThreatLog({ time }) { const allItems = [ { t: '14:02:11', m: 'SQLi 시도 차단', tag: 'BLOCK', c: '#E5484D' }, { t: '14:02:14', m: '정상 트래픽', tag: 'PASS', c: '#12B981' }, { t: '14:02:18', m: '봇 트래픽 차단', tag: 'BLOCK', c: '#E5484D' }, { t: '14:02:22', m: '인증 성공', tag: 'PASS', c: '#12B981' }, { t: '14:02:25', m: 'XSS 시도 차단', tag: 'BLOCK', c: '#E5484D' }, { t: '14:02:28', m: '취약점 스캔', tag: 'SCAN', c: WP.cyan400 }, { t: '14:02:31', m: '정상 트래픽', tag: 'PASS', c: '#12B981' }, { t: '14:02:35', m: 'Path Traversal 차단', tag: 'BLOCK', c: '#E5484D' }, ]; const visibleCount = Math.min(allItems.length, Math.floor(time * 2.2) + 1); const items = allItems.slice(0, visibleCount).slice(-4); return (
{items.map((it, i) => (
{it.t} {it.tag} {it.m}
))}
); } function KeywordRanks({ time }) { const kws = [ { k: '브랜드 키워드 A', from: 12, to: 3 }, { k: '브랜드 키워드 B', from: 8, to: 2 }, { k: '카테고리 검색', from: 23, to: 7 }, ]; const p = clamp((time - 0.5) / 2.5, 0, 1); const e = Easing.easeOutCubic(p); return (
{kws.map((kw, i) => { const v = Math.round(kw.from + (kw.to - kw.from) * e); return (
{kw.k}
#{kw.from} #{v}
); })}
); } function PulseRing({ time, x, y, color }) { const period = 2.5; const t = (time % period) / period; return (
); } function Variant2() { const T = useTime(); // 0–2 intro: dashboard chrome assembles // 2–8 live data: graphs animate, log streams, KPI counts up // 8–11 zoom out + 3 panels merge → unified view // 11–15 logo + slogan hold const introP = clamp(T / 1.8, 0, 1); const introE = Easing.easeOutCubic(introP); const zoomStart = 8.0; const zoomEnd = 10.5; const zoom = clamp((T - zoomStart) / (zoomEnd - zoomStart), 0, 1); const zoomE = Easing.easeInOutCubic(zoom); // Camera scale: dashboard at scale 1, zooms out to 0.55 + bg fades to deep const camScale = 1 - zoomE * 0.5; const camY = zoomE * -120; // Logo phase const logoStart = 10.0; const logoP = clamp((T - logoStart) / 1.5, 0, 1); const logoE = Easing.easeOutCubic(logoP); const sloganP = clamp((T - 11.0) / 1.5, 0, 1); const sloganE = Easing.easeOutCubic(sloganP); // Loop bridge const outroP = clamp((T - 13.5) / 1.5, 0, 1); return (
{/* Deep gradient background */}
{/* Dot grid */} {/* Subtle wave */}
{/* Camera container */}
{/* Header bar */}
LIVE · 24/365
UPTIME 99.99% · RT {(T * 1000 % 100 + 12).toFixed(0)}ms
{/* 3 Panels grid */}
{/* Panel 1 — Web traffic */}
{(() => { const N = 60; const pts = []; for (let i = 0; i < N; i++) { const phase = i * 0.25 + T * 0.4; const v = (Math.sin(phase) * 0.25 + Math.sin(phase * 1.6 + 1) * 0.15 + Math.sin(phase * 0.6) * 0.1 + 0.55) * (1 + i / N * 0.4); pts.push([i / (N - 1) * 510, 380 - clamp(v * 360, 0, 360)]); } const d = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0]} ${p[1]}`).join(' '); const fillD = d + ' L 510 380 L 0 380 Z'; return ( <> ); })()}
{/* Panel 2 — Security */}
{/* radar arc */}
{/* Panel 3 — Marketing */}
{[0, 1, 2, 3, 4, 5, 6].map(i => { const h = 18 + Math.sin(i + T * 0.5) * 8 + i * 6; return ( ); })}
{/* Pulse rings on each panel */} {introE > 0.6 && [ { x: 640, y: 198, c: WP.cyan400 }, { x: 1150, y: 198, c: '#12B981' }, { x: 1660, y: 198, c: WP.orange500 }, ].map((p, i) => ( ))}
{/* Convergence sweep */} {T > 7.8 && T < 10.5 && (
)} {/* Logo */}
WAVEPIX
{/* Slogan */}
AI로 중심을 잡고, 한발 앞서갑니다.
WEB · SECURITY · MARKETING — POWERED BY AI
); } window.Variant2 = Variant2;