"use client"; import { useEffect, useRef, useState } from "react"; import styles from "./MemoryGraph.module.css"; interface Node { x: number; y: number; vx: number; vy: number; r: number; hot: boolean; } export function MemoryGraph() { const canvasRef = useRef(null); const [running, setRunning] = useState(true); const railRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches; let localRunning = running && !reduceMotion; let nodes: Node[] = []; let rafId = 0; let pulse = 0; const size = () => { const dpr = Math.max(1, window.devicePixelRatio || 1); const w = canvas.clientWidth; const h = canvas.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; const seed = () => { const w = canvas.clientWidth; const h = canvas.clientHeight; const count = Math.min(52, Math.floor((w * h) / 22000)); nodes = new Array(count).fill(0).map(() => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.18, vy: (Math.random() - 0.5) * 0.18, r: 1.2 + Math.random() * 2.2, hot: Math.random() < 0.25, })); }; const draw = () => { const w = canvas.clientWidth; const h = canvas.clientHeight; ctx.clearRect(0, 0, w, h); for (const n of nodes) { n.x += n.vx; n.y += n.vy; if (n.x < 0 || n.x > w) n.vx *= -1; if (n.y < 0 || n.y > h) n.vy *= -1; } const maxDist = 160; ctx.lineWidth = 1; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const a = nodes[i]; const b = nodes[j]; const dx = a.x - b.x; const dy = a.y - b.y; const d = Math.hypot(dx, dy); if (d > maxDist) continue; const alpha = (1 - d / maxDist) * 0.35; const hot = a.hot && b.hot; ctx.strokeStyle = hot ? `rgba(255, 192, 0, ${alpha.toFixed(3)})` : `rgba(255, 255, 255, ${(alpha * 0.5).toFixed(3)})`; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } for (const n of nodes) { const r = n.r + (n.hot ? Math.sin(pulse + n.x) * 0.8 : 0); ctx.fillStyle = n.hot ? "#FFC000" : "rgba(255,255,255,0.85)"; ctx.beginPath(); ctx.arc(n.x, n.y, Math.max(0.5, r), 0, Math.PI * 2); ctx.fill(); if (n.hot) { ctx.fillStyle = "rgba(255, 192, 0, 0.12)"; ctx.beginPath(); ctx.arc(n.x, n.y, r * 3.5, 0, Math.PI * 2); ctx.fill(); } } pulse += 0.04; }; const tick = () => { if (!localRunning) return; draw(); rafId = requestAnimationFrame(tick); }; const onResize = () => { size(); seed(); draw(); }; size(); seed(); draw(); if (localRunning) rafId = requestAnimationFrame(tick); window.addEventListener("resize", onResize); const updateRail = () => { const h = document.documentElement; const max = h.scrollHeight - h.clientHeight; const pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max); if (railRef.current) railRef.current.style.width = `${pct * 100}%`; }; updateRail(); window.addEventListener("scroll", updateRail, { passive: true }); return () => { cancelAnimationFrame(rafId); window.removeEventListener("resize", onResize); window.removeEventListener("scroll", updateRail); localRunning = false; }; }, [running]); return ( <>
); }