// NAXUS — Floating particle constellation background // Canvas-based: dots float, lines connect nearby pairs. // Sits fixed behind all content, respects bold/conservative modes. const { useEffect, useRef } = React; function ParticleField() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); let raf = 0; let particles = []; let mouse = { x: -9999, y: -9999, active: false }; let dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + "px"; canvas.style.height = window.innerHeight + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); seedParticles(); } function seedParticles() { const area = window.innerWidth * window.innerHeight; // ~1 particle per 14000px² — gives ~140 on a 1920×1080 viewport const count = Math.min(180, Math.max(40, Math.round(area / 14000))); particles = []; for (let i = 0; i < count; i++) { particles.push({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, vx: (Math.random() - 0.5) * 0.25, vy: (Math.random() - 0.5) * 0.25, r: 0.8 + Math.random() * 1.6, // hue bias: cyan or magenta, with a few violet ones tone: Math.random() < 0.55 ? "cyan" : (Math.random() < 0.8 ? "magenta" : "violet"), twinkle: Math.random() * Math.PI * 2, }); } } const COLORS = { cyan: { r: 34, g: 230, b: 255 }, magenta: { r: 255, g: 94, b: 201 }, violet: { r: 138, g: 107, b: 255 }, }; function step(now) { const W = window.innerWidth; const H = window.innerHeight; const isBold = document.documentElement.getAttribute("data-mode") !== "conservative"; const opMul = isBold ? 1 : 0.55; const linkDist = isBold ? 130 : 110; const linkDistSq = linkDist * linkDist; ctx.clearRect(0, 0, W, H); // Update positions for (const p of particles) { p.x += p.vx; p.y += p.vy; p.twinkle += 0.02; // Wrap around edges if (p.x < -10) p.x = W + 10; if (p.x > W + 10) p.x = -10; if (p.y < -10) p.y = H + 10; if (p.y > H + 10) p.y = -10; // Mouse repulsion (gentle) if (mouse.active) { const dx = p.x - mouse.x; const dy = p.y - mouse.y; const d2 = dx * dx + dy * dy; if (d2 < 18000) { const f = (1 - d2 / 18000) * 0.6; const d = Math.sqrt(d2) || 1; p.vx += (dx / d) * f * 0.15; p.vy += (dy / d) * f * 0.15; } } // Velocity damping so particles don't accelerate forever p.vx *= 0.985; p.vy *= 0.985; // Keep some minimum drift if (Math.abs(p.vx) < 0.05) p.vx += (Math.random() - 0.5) * 0.04; if (Math.abs(p.vy) < 0.05) p.vy += (Math.random() - 0.5) * 0.04; } // Connection lines (constellation) ctx.lineWidth = 0.6; for (let i = 0; i < particles.length; i++) { const a = particles[i]; for (let j = i + 1; j < particles.length; j++) { const b = particles[j]; const dx = a.x - b.x; const dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < linkDistSq) { const alpha = (1 - d2 / linkDistSq) * 0.28 * opMul; // Mix the two endpoint colors const ca = COLORS[a.tone]; const cb = COLORS[b.tone]; const r = (ca.r + cb.r) / 2 | 0; const g = (ca.g + cb.g) / 2 | 0; const bl = (ca.b + cb.b) / 2 | 0; ctx.strokeStyle = `rgba(${r},${g},${bl},${alpha})`; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } // Mouse-to-particle lines if (mouse.active) { for (const p of particles) { const dx = p.x - mouse.x; const dy = p.y - mouse.y; const d2 = dx * dx + dy * dy; if (d2 < 22000) { const alpha = (1 - d2 / 22000) * 0.5 * opMul; ctx.strokeStyle = `rgba(34, 230, 255, ${alpha})`; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(mouse.x, mouse.y); ctx.stroke(); } } } // Draw particles for (const p of particles) { const c = COLORS[p.tone]; const tw = 0.6 + 0.4 * Math.sin(p.twinkle); const a = 0.85 * tw * opMul; // Glow halo const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 6); grad.addColorStop(0, `rgba(${c.r},${c.g},${c.b},${a})`); grad.addColorStop(1, `rgba(${c.r},${c.g},${c.b},0)`); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 6, 0, Math.PI * 2); ctx.fill(); // Solid core ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${Math.min(1, a + 0.2)})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } raf = requestAnimationFrame(step); } function onMove(e) { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; } function onLeave() { mouse.active = false; } function onTouch(e) { if (e.touches && e.touches[0]) { mouse.x = e.touches[0].clientX; mouse.y = e.touches[0].clientY; mouse.active = true; } } resize(); raf = requestAnimationFrame(step); window.addEventListener("resize", resize); window.addEventListener("mousemove", onMove); window.addEventListener("mouseleave", onLeave); window.addEventListener("touchmove", onTouch, { passive: true }); window.addEventListener("touchend", onLeave); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseleave", onLeave); window.removeEventListener("touchmove", onTouch); window.removeEventListener("touchend", onLeave); }; }, []); return ; } window.ParticleField = ParticleField;