"use client"; import { useEffect, useRef, useCallback } from "react"; interface Node { x: number; y: number; baseX: number; baseY: number; vx: number; vy: number; isHub: boolean; pulsePhase: number; radius: number; } interface Pulse { edgeIdx: number; progress: number; speed: number; direction: 1 | -1; } const NODE_COUNT = 80; const MAX_EDGE_DIST = 180; const MOUSE_RADIUS = 250; const MOUSE_REPEL = 0.3; const DRIFT_SPEED = 0.15; const RETURN_FORCE = 0.005; function createNodes(w: number, h: number): Node[] { const nodes: Node[] = []; for (let i = 0; i < NODE_COUNT; i++) { const x = Math.random() * w; const y = Math.random() * h; nodes.push({ x, y, baseX: x, baseY: y, vx: (Math.random() - 0.5) * DRIFT_SPEED, vy: (Math.random() - 0.5) * DRIFT_SPEED, isHub: i < 12, pulsePhase: Math.random() * Math.PI * 2, radius: i < 12 ? 2.5 : 1.2, }); } return nodes; } function buildEdges(nodes: Node[]): [number, number][] { const edges: [number, number][] = []; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const dx = nodes[i].x - nodes[j].x; const dy = nodes[i].y - nodes[j].y; if (dx * dx + dy * dy < MAX_EDGE_DIST * MAX_EDGE_DIST) { edges.push([i, j]); } } } return edges; } export function Background() { const canvasRef = useRef(null); const nodesRef = useRef([]); const pulsesRef = useRef([]); const mouseRef = useRef({ x: -1000, y: -1000 }); const frameRef = useRef(0); const timeRef = useRef(0); const spawnPulse = useCallback((edges: [number, number][]) => { if (edges.length === 0) return; const idx = Math.floor(Math.random() * edges.length); pulsesRef.current.push({ edgeIdx: idx, progress: 0, speed: 0.008 + Math.random() * 0.012, direction: Math.random() > 0.5 ? 1 : -1, }); }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let w = window.innerWidth; let h = window.innerHeight; canvas.width = w; canvas.height = h; nodesRef.current = createNodes(w, h); pulsesRef.current = []; const handleResize = () => { w = window.innerWidth; h = window.innerHeight; canvas.width = w; canvas.height = h; nodesRef.current = createNodes(w, h); pulsesRef.current = []; }; const handleMouseMove = (e: MouseEvent) => { mouseRef.current = { x: e.clientX, y: e.clientY }; }; const handleMouseLeave = () => { mouseRef.current = { x: -1000, y: -1000 }; }; window.addEventListener("resize", handleResize); window.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseleave", handleMouseLeave); const animate = () => { timeRef.current += 1; const nodes = nodesRef.current; const mx = mouseRef.current.x; const my = mouseRef.current.y; ctx.clearRect(0, 0, w, h); // Update node positions for (const node of nodes) { // Drift node.x += node.vx; node.y += node.vy; // Return to base node.vx += (node.baseX - node.x) * RETURN_FORCE; node.vy += (node.baseY - node.y) * RETURN_FORCE; // Damping node.vx *= 0.99; node.vy *= 0.99; // Mouse interaction — repel const dmx = node.x - mx; const dmy = node.y - my; const distMouse = Math.sqrt(dmx * dmx + dmy * dmy); if (distMouse < MOUSE_RADIUS && distMouse > 0) { const force = (1 - distMouse / MOUSE_RADIUS) * MOUSE_REPEL; node.vx += (dmx / distMouse) * force; node.vy += (dmy / distMouse) * force; } // Boundary wrap if (node.x < -20) node.x = w + 20; if (node.x > w + 20) node.x = -20; if (node.y < -20) node.y = h + 20; if (node.y > h + 20) node.y = -20; } // Build edges dynamically const edges = buildEdges(nodes); // Draw edges for (let i = 0; i < edges.length; i++) { const [a, b] = edges[i]; const dx = nodes[a].x - nodes[b].x; const dy = nodes[a].y - nodes[b].y; const dist = Math.sqrt(dx * dx + dy * dy); const alpha = (1 - dist / MAX_EDGE_DIST) * 0.12; // Brighter edges near mouse const edgeMx = (nodes[a].x + nodes[b].x) / 2; const edgeMy = (nodes[a].y + nodes[b].y) / 2; const edgeMouseDist = Math.sqrt( (edgeMx - mx) ** 2 + (edgeMy - my) ** 2 ); const mouseBoost = edgeMouseDist < MOUSE_RADIUS ? (1 - edgeMouseDist / MOUSE_RADIUS) * 0.2 : 0; ctx.beginPath(); ctx.moveTo(nodes[a].x, nodes[a].y); ctx.lineTo(nodes[b].x, nodes[b].y); ctx.strokeStyle = `rgba(192, 119, 144, ${alpha + mouseBoost})`; ctx.lineWidth = 0.5; ctx.stroke(); } // Spawn pulses if (timeRef.current % 8 === 0) { spawnPulse(edges); } // Draw & update pulses const activePulses: Pulse[] = []; for (const pulse of pulsesRef.current) { pulse.progress += pulse.speed; if (pulse.progress > 1) { // Chain reaction — sometimes spawn a new pulse from the destination node if (Math.random() < 0.4 && pulse.edgeIdx < edges.length) { const destNode = pulse.direction === 1 ? edges[pulse.edgeIdx][1] : edges[pulse.edgeIdx][0]; // Find connected edges from destination const connected = edges .map((e, idx) => ({ e, idx })) .filter( ({ e, idx }) => idx !== pulse.edgeIdx && (e[0] === destNode || e[1] === destNode) ); if (connected.length > 0) { const next = connected[Math.floor(Math.random() * connected.length)]; activePulses.push({ edgeIdx: next.idx, progress: 0, speed: 0.01 + Math.random() * 0.01, direction: next.e[0] === destNode ? 1 : -1, }); } } continue; } if (pulse.edgeIdx >= edges.length) continue; const [a, b] = edges[pulse.edgeIdx]; const na = nodes[a]; const nb = nodes[b]; const t = pulse.direction === 1 ? pulse.progress : 1 - pulse.progress; const px = na.x + (nb.x - na.x) * t; const py = na.y + (nb.y - na.y) * t; // Pulse head glow const gradient = ctx.createRadialGradient(px, py, 0, px, py, 12); gradient.addColorStop(0, "rgba(192, 119, 144, 0.6)"); gradient.addColorStop(0.5, "rgba(192, 119, 144, 0.15)"); gradient.addColorStop(1, "rgba(192, 119, 144, 0)"); ctx.beginPath(); ctx.arc(px, py, 12, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.fill(); // Bright core ctx.beginPath(); ctx.arc(px, py, 2, 0, Math.PI * 2); ctx.fillStyle = "rgba(219, 168, 184, 0.8)"; ctx.fill(); // Trail const trailLen = 0.15; const tStart = Math.max(0, t - trailLen); const sx = na.x + (nb.x - na.x) * tStart; const sy = na.y + (nb.y - na.y) * tStart; const trailGrad = ctx.createLinearGradient(sx, sy, px, py); trailGrad.addColorStop(0, "rgba(192, 119, 144, 0)"); trailGrad.addColorStop(1, "rgba(192, 119, 144, 0.3)"); ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(px, py); ctx.strokeStyle = trailGrad; ctx.lineWidth = 1.5; ctx.stroke(); activePulses.push(pulse); } pulsesRef.current = activePulses; // Draw nodes for (const node of nodes) { const pulse = Math.sin(timeRef.current * 0.03 + node.pulsePhase) * 0.5 + 0.5; // Mouse proximity glow const dMouse = Math.sqrt( (node.x - mx) ** 2 + (node.y - my) ** 2 ); const mouseGlow = dMouse < MOUSE_RADIUS ? (1 - dMouse / MOUSE_RADIUS) : 0; if (node.isHub) { // Hub outer glow const glowR = node.radius * (3 + pulse * 2) + mouseGlow * 8; const glowAlpha = 0.06 + pulse * 0.04 + mouseGlow * 0.1; const gradient = ctx.createRadialGradient( node.x, node.y, 0, node.x, node.y, glowR ); gradient.addColorStop(0, `rgba(192, 119, 144, ${glowAlpha})`); gradient.addColorStop(1, "rgba(192, 119, 144, 0)"); ctx.beginPath(); ctx.arc(node.x, node.y, glowR, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.fill(); // Hub core ctx.beginPath(); ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); ctx.fillStyle = `rgba(192, 119, 144, ${0.4 + pulse * 0.2 + mouseGlow * 0.3})`; ctx.fill(); } else { ctx.beginPath(); ctx.arc(node.x, node.y, node.radius + mouseGlow * 1.5, 0, Math.PI * 2); ctx.fillStyle = `rgba(120, 113, 108, ${0.15 + mouseGlow * 0.4})`; ctx.fill(); } } frameRef.current = requestAnimationFrame(animate); }; frameRef.current = requestAnimationFrame(animate); return () => { cancelAnimationFrame(frameRef.current); window.removeEventListener("resize", handleResize); window.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseleave", handleMouseLeave); }; }, [spawnPulse]); return (
{/* Primary glow — top center */}
{/* Accent glow — bottom right */}
{/* Secondary glow — left */}
); }