// MotionGraphics.jsx — animated cybersecurity motion graphics // Three reusable canvases: NetworkGraph, GlobeThreatMap, AttackChainFlow // Uses requestAnimationFrame; cleans up on unmount. const NetworkGraph = ({ height = 320 }) => { const ref = React.useRef(null); React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const resize = () => { const r = canvas.getBoundingClientRect(); canvas.width = r.width * dpr; canvas.height = r.height * dpr; ctx.scale(dpr, dpr); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); // Build a small graph: 1 center, 6 mids, ~14 leaves const W = () => canvas.width / dpr; const H = () => canvas.height / dpr; const nodes = []; const edges = []; // center nodes.push({ id: 0, x: 0.5, y: 0.5, r: 6, kind: 'core' }); const midCount = 6; for (let i = 0; i < midCount; i++) { const a = (i / midCount) * Math.PI * 2; nodes.push({ id: nodes.length, x: 0.5 + Math.cos(a) * 0.22, y: 0.5 + Math.sin(a) * 0.32, r: 4, kind: 'mid', a, ringR: 0.22 }); edges.push({ from: 0, to: nodes.length - 1 }); } // leaves for (let i = 1; i <= midCount; i++) { const parent = nodes[i]; const leafCount = 2 + Math.floor(Math.random() * 2); for (let j = 0; j < leafCount; j++) { const offset = (j - (leafCount - 1) / 2) * 0.18; const lx = parent.x + Math.cos(parent.a) * 0.18 + Math.cos(parent.a + Math.PI / 2) * offset * 0.5; const ly = parent.y + Math.sin(parent.a) * 0.22 + Math.sin(parent.a + Math.PI / 2) * offset * 0.5; nodes.push({ id: nodes.length, x: lx, y: ly, r: 2.5, kind: 'leaf' }); edges.push({ from: i, to: nodes.length - 1 }); } } // Active packets traveling along edges const packets = []; const spawnPacket = () => { const e = edges[Math.floor(Math.random() * edges.length)]; const outbound = Math.random() < 0.7; packets.push({ edge: e, t: 0, speed: 0.005 + Math.random() * 0.008, outbound, color: outbound ? '#5e6ad2' : '#27a644', }); }; let raf; let last = 0; const draw = (now) => { const dt = Math.min(50, now - last); last = now; const w = W(), h = H(); ctx.clearRect(0, 0, w, h); // Subtle radial backdrop const g = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.6); g.addColorStop(0, 'rgba(94,105,210,0.05)'); g.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // Edges edges.forEach(e => { const a = nodes[e.from], b = nodes[e.to]; ctx.strokeStyle = 'rgba(94,105,210,0.18)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(a.x * w, a.y * h); ctx.lineTo(b.x * w, b.y * h); ctx.stroke(); }); // Pulsing concentric rings on core const corePulse = (now / 1000) % 2.4; for (let i = 0; i < 3; i++) { const t = (corePulse + i * 0.8) % 2.4; const radius = 8 + t * 36; const alpha = Math.max(0, 0.3 - t * 0.13); ctx.strokeStyle = `rgba(94,105,210,${alpha})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0.5 * w, 0.5 * h, radius, 0, Math.PI * 2); ctx.stroke(); } // Nodes nodes.forEach(n => { const flicker = 0.6 + 0.4 * Math.sin((now + n.id * 700) / 600); const fill = n.kind === 'core' ? '#828fff' : n.kind === 'mid' ? '#5e6ad2' : '#3a44a8'; ctx.fillStyle = fill; ctx.globalAlpha = n.kind === 'leaf' ? 0.6 + flicker * 0.3 : 0.9; ctx.beginPath(); ctx.arc(n.x * w, n.y * h, n.r, 0, Math.PI * 2); ctx.fill(); // halo if (n.kind !== 'leaf') { ctx.globalAlpha = 0.18; ctx.beginPath(); ctx.arc(n.x * w, n.y * h, n.r * 2.4, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; }); // Packets if (Math.random() < 0.06) spawnPacket(); for (let i = packets.length - 1; i >= 0; i--) { const p = packets[i]; p.t += p.speed * (dt / 16); if (p.t >= 1) { packets.splice(i, 1); continue; } const a = nodes[p.edge.from], b = nodes[p.edge.to]; const t = p.outbound ? p.t : 1 - p.t; const x = (a.x + (b.x - a.x) * t) * w; const y = (a.y + (b.y - a.y) * t) * h; ctx.fillStyle = p.color; ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(x, y, 2.2, 0, Math.PI * 2); ctx.fill(); // trail ctx.globalAlpha = 0.3; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } raf = requestAnimationFrame(draw); }; raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, []); return ; }; const ScanlineGrid = ({ height = 240 }) => { // Subtle "monitoring" aesthetic — moving scan line across a dotted grid const ref = React.useRef(null); React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const resize = () => { const r = canvas.getBoundingClientRect(); canvas.width = r.width * dpr; canvas.height = r.height * dpr; ctx.scale(dpr, dpr); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); let raf; const draw = (now) => { const w = canvas.width / dpr, h = canvas.height / dpr; ctx.clearRect(0, 0, w, h); // Dot grid const step = 18; for (let x = 0; x < w; x += step) { for (let y = 0; y < h; y += step) { ctx.fillStyle = 'rgba(94,105,210,0.2)'; ctx.beginPath(); ctx.arc(x, y, 0.8, 0, Math.PI * 2); ctx.fill(); } } // Scan line const t = (now / 3500) % 1; const sx = t * w; const grad = ctx.createLinearGradient(sx - 80, 0, sx + 80, 0); grad.addColorStop(0, 'rgba(94,105,210,0)'); grad.addColorStop(0.5, 'rgba(94,105,210,0.35)'); grad.addColorStop(1, 'rgba(94,105,210,0)'); ctx.fillStyle = grad; ctx.fillRect(sx - 80, 0, 160, h); // Scanned cells light up briefly behind the line for (let x = 0; x < w; x += step) { for (let y = 0; y < h; y += step) { const dist = Math.abs(x - sx); if (dist < 60) { const a = (1 - dist / 60) * 0.6; ctx.fillStyle = `rgba(130,143,255,${a})`; ctx.beginPath(); ctx.arc(x, y, 1.6, 0, Math.PI * 2); ctx.fill(); } } } raf = requestAnimationFrame(draw); }; raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, []); return ; }; const ThreatMap = ({ height = 360 }) => { // Stylized world map (dot pattern) with attack arcs from origins to a target const ref = React.useRef(null); React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const resize = () => { const r = canvas.getBoundingClientRect(); canvas.width = r.width * dpr; canvas.height = r.height * dpr; ctx.scale(dpr, dpr); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); // continent silhouettes via dot density mask (very rough) const isLand = (lon, lat) => { // Lon -180..180, Lat -90..90 — very crude continent boxes const continents = [ [-130, -55, -65, 60], // Americas [-25, -35, 55, 70], // Africa + Europe [55, -10, 145, 70], // Asia [110, -45, 155, -10], // Australia ]; return continents.some(([x1, y1, x2, y2]) => lon >= x1 && lon <= x2 && lat >= y1 && lat <= y2); }; const target = { lon: -100, lat: 38 }; // North America HQ const origins = [ { lon: 30, lat: 50 }, { lon: 100, lat: 35 }, { lon: 50, lat: 25 }, { lon: -45, lat: -15 }, { lon: 130, lat: -25 }, { lon: 20, lat: -10 }, { lon: 75, lat: 20 }, { lon: 120, lat: 50 }, ]; const arcs = []; const spawnArc = () => { const o = origins[Math.floor(Math.random() * origins.length)]; arcs.push({ from: o, t: 0, speed: 0.005 + Math.random() * 0.006, color: Math.random() < 0.3 ? '#e8543a' : Math.random() < 0.6 ? '#d98326' : '#5e6ad2' }); }; let raf; const project = (lon, lat, w, h) => ({ x: (lon + 180) / 360 * w, y: (90 - lat) / 180 * h }); const draw = (now) => { const w = canvas.width / dpr, h = canvas.height / dpr; ctx.clearRect(0, 0, w, h); // dot map const stepLon = 5, stepLat = 5; for (let lon = -180; lon < 180; lon += stepLon) { for (let lat = -60; lat < 80; lat += stepLat) { if (!isLand(lon, lat)) continue; const p = project(lon, lat, w, h); ctx.fillStyle = 'rgba(94,105,210,0.25)'; ctx.beginPath(); ctx.arc(p.x, p.y, 1, 0, Math.PI * 2); ctx.fill(); } } // Target node const tp = project(target.lon, target.lat, w, h); const pulse = (now / 1000) % 2; ctx.strokeStyle = `rgba(130,143,255,${Math.max(0, 0.5 - pulse * 0.25)})`; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(tp.x, tp.y, 6 + pulse * 22, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = '#828fff'; ctx.beginPath(); ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2); ctx.fill(); // Arcs if (Math.random() < 0.04) spawnArc(); for (let i = arcs.length - 1; i >= 0; i--) { const a = arcs[i]; a.t += a.speed; if (a.t >= 1.2) { arcs.splice(i, 1); continue; } const fp = project(a.from.lon, a.from.lat, w, h); // bezier control point for arc lift const mx = (fp.x + tp.x) / 2; const my = (fp.y + tp.y) / 2 - Math.abs(fp.x - tp.x) * 0.25 - 30; // Draw progressive arc up to t (clamped to 1) const tEnd = Math.min(1, a.t); const segments = 32; ctx.strokeStyle = a.color; ctx.lineWidth = 1.4; ctx.globalAlpha = a.t > 1 ? Math.max(0, 1.2 - a.t) * 5 : 1; ctx.beginPath(); for (let s = 0; s <= segments; s++) { const tt = (s / segments) * tEnd; const x = (1-tt)*(1-tt)*fp.x + 2*(1-tt)*tt*mx + tt*tt*tp.x; const y = (1-tt)*(1-tt)*fp.y + 2*(1-tt)*tt*my + tt*tt*tp.y; if (s === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); // origin dot ctx.fillStyle = a.color; ctx.globalAlpha = 1; ctx.beginPath(); ctx.arc(fp.x, fp.y, 2.4, 0, Math.PI * 2); ctx.fill(); // moving head if (a.t <= 1) { const tt = a.t; const x = (1-tt)*(1-tt)*fp.x + 2*(1-tt)*tt*mx + tt*tt*tp.x; const y = (1-tt)*(1-tt)*fp.y + 2*(1-tt)*tt*my + tt*tt*tp.y; ctx.fillStyle = a.color; ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } raf = requestAnimationFrame(draw); }; raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, []); return ; }; // Animated terminal — types out commands and "responses" const TerminalAnim = ({ lines, speed = 22 }) => { const [shown, setShown] = React.useState([]); React.useEffect(() => { let li = 0, ci = 0; let buffer = lines.map(() => ''); const tick = () => { if (li >= lines.length) { setTimeout(() => { setShown([]); buffer = lines.map(() => ''); li = 0; ci = 0; tick(); }, 4000); return; } buffer[li] = lines[li].text.slice(0, ci + 1); setShown([...buffer]); ci++; if (ci >= lines[li].text.length) { li++; ci = 0; setTimeout(tick, lines[li-1].pause || 350); return; } setTimeout(tick, speed); }; tick(); return () => { li = lines.length + 1; }; }, []); return (
{lines.map((l, i) => (
{l.prompt && {l.prompt} } {shown[i] || ''} {i === shown.findIndex((s, j) => s !== lines[j].text) && (   )}
))}
); }; Object.assign(window, { NetworkGraph, ScanlineGrid, ThreatMap, TerminalAnim });