import React, { useRef, useEffect } from 'react' import './BackgroundPattern.css' /** * Full-page particle background — subtle floating dots that react to mouse. * Replaces the old SVG pattern drift animation. */ export default function BackgroundPattern() { const canvasRef = useRef(null) const animRef = useRef(null) const mouseRef = useRef({ x: -1000, y: -1000 }) const particlesRef = useRef([]) useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext('2d') function resize() { const dpr = window.devicePixelRatio || 1 canvas.width = window.innerWidth * dpr canvas.height = window.innerHeight * dpr ctx.scale(dpr, dpr) canvas.style.width = window.innerWidth + 'px' canvas.style.height = window.innerHeight + 'px' initParticles() } function initParticles() { const w = window.innerWidth const h = window.innerHeight const count = Math.floor((w * h) / 4000) // ~500 on 1920x1080 particlesRef.current = Array.from({ length: count }, () => ({ x: Math.random() * w, y: Math.random() * h, baseX: 0, baseY: 0, vx: 0, vy: 0, radius: 0.8 + Math.random() * 1.2, drift: Math.random() * Math.PI * 2, driftSpeed: 0.002 + Math.random() * 0.003, })) particlesRef.current.forEach(p => { p.baseX = p.x p.baseY = p.y }) } function handleMouseMove(e) { mouseRef.current.x = e.clientX mouseRef.current.y = e.clientY } resize() window.addEventListener('resize', resize) window.addEventListener('mousemove', handleMouseMove) function animate() { const w = window.innerWidth const h = window.innerHeight ctx.clearRect(0, 0, w, h) const style = getComputedStyle(document.documentElement) const accentColor = style.getPropertyValue('--accent-color').trim() || '#4a9eff' const mouse = mouseRef.current const repelRadius = 80 particlesRef.current.forEach(p => { // Subtle autonomous drift p.drift += p.driftSpeed const driftX = Math.sin(p.drift) * 0.3 const driftY = Math.cos(p.drift * 0.7) * 0.3 // Mouse repulsion const dx = p.x - mouse.x const dy = p.y - mouse.y const dist = Math.sqrt(dx * dx + dy * dy) if (dist < repelRadius && dist > 0) { const force = (repelRadius - dist) / repelRadius p.vx += (dx / dist) * force * 1.5 p.vy += (dy / dist) * force * 1.5 } // Spring back p.vx += (p.baseX - p.x) * 0.02 + driftX * 0.1 p.vy += (p.baseY - p.y) * 0.02 + driftY * 0.1 p.vx *= 0.92 p.vy *= 0.92 p.x += p.vx p.y += p.vy ctx.beginPath() ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2) ctx.fillStyle = accentColor ctx.globalAlpha = 0.25 ctx.fill() ctx.globalAlpha = 1 }) animRef.current = requestAnimationFrame(animate) } animate() return () => { cancelAnimationFrame(animRef.current) window.removeEventListener('resize', resize) window.removeEventListener('mousemove', handleMouseMove) } }, []) return }