Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; | |
| const ParticlesBg = forwardRef((props, ref) => { | |
| const canvasRef = useRef(null); | |
| const targetColorRef = useRef({ r: 56, g: 189, b: 248 }); | |
| const currentColorRef = useRef({ r: 56, g: 189, b: 248 }); | |
| const hexToRgb = (hex) => { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result ? { | |
| r: parseInt(result[1], 16), | |
| g: parseInt(result[2], 16), | |
| b: parseInt(result[3], 16) | |
| } : { r: 56, g: 189, b: 248 }; | |
| }; | |
| useImperativeHandle(ref, () => ({ | |
| setAccentColor: (hex) => { | |
| if (hex) { | |
| targetColorRef.current = hexToRgb(hex); | |
| } else { | |
| targetColorRef.current = { r: 56, g: 189, b: 248 }; | |
| } | |
| } | |
| })); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let animationFrameId; | |
| let particles = []; | |
| let connectionDist = 120; | |
| const BASELINE_AREA = 1920 * 1080; | |
| const BASELINE_PARTICLES = 70; | |
| const BASELINE_DIAGONAL = Math.sqrt(1920 * 1920 + 1080 * 1080); | |
| const BASELINE_CONN_DIST = 120; | |
| const MIN_PARTICLES = 12; | |
| const resize = () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const area = canvas.width * canvas.height; | |
| const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2); | |
| const areaRatio = area / BASELINE_AREA; | |
| const targetCount = Math.max(MIN_PARTICLES, Math.round(BASELINE_PARTICLES * areaRatio)); | |
| connectionDist = Math.round(BASELINE_CONN_DIST * (diagonal / BASELINE_DIAGONAL)); | |
| if (particles.length > targetCount) { | |
| particles.length = targetCount; | |
| } else { | |
| while (particles.length < targetCount) { | |
| particles.push(new Particle()); | |
| } | |
| } | |
| }; | |
| window.addEventListener('resize', resize); | |
| class Particle { | |
| constructor() { | |
| this.x = Math.random() * canvas.width; | |
| this.y = Math.random() * canvas.height; | |
| this.vx = (Math.random() - 0.5) * 0.8; | |
| this.vy = (Math.random() - 0.5) * 0.8; | |
| this.radius = Math.random() * 2 + 1; | |
| } | |
| update() { | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| if (this.x < 0 || this.x > canvas.width) this.vx *= -1; | |
| if (this.y < 0 || this.y > canvas.height) this.vy *= -1; | |
| } | |
| draw(ctx, color) { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(${Math.floor(color.r)}, ${Math.floor(color.g)}, ${Math.floor(color.b)}, 0.5)`; | |
| ctx.fill(); | |
| } | |
| } | |
| resize(); | |
| const animate = () => { | |
| currentColorRef.current.r += (targetColorRef.current.r - currentColorRef.current.r) * 0.05; | |
| currentColorRef.current.g += (targetColorRef.current.g - currentColorRef.current.g) * 0.05; | |
| currentColorRef.current.b += (targetColorRef.current.b - currentColorRef.current.b) * 0.05; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(ctx, currentColorRef.current); | |
| }); | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| const dx = particles[i].x - particles[j].x; | |
| const dy = particles[i].y - particles[j].y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < connectionDist) { | |
| ctx.beginPath(); | |
| ctx.moveTo(particles[i].x, particles[i].y); | |
| ctx.lineTo(particles[j].x, particles[j].y); | |
| ctx.strokeStyle = `rgba(${Math.floor(currentColorRef.current.r)}, ${Math.floor(currentColorRef.current.g)}, ${Math.floor(currentColorRef.current.b)}, ${(1 - dist / connectionDist) * 0.4})`; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| animationFrameId = requestAnimationFrame(animate); | |
| }; | |
| animate(); | |
| return () => { | |
| window.removeEventListener('resize', resize); | |
| cancelAnimationFrame(animationFrameId); | |
| }; | |
| }, []); | |
| return ( | |
| <canvas | |
| ref={canvasRef} | |
| style={{ | |
| position: 'fixed', | |
| top: 0, | |
| left: 0, | |
| width: '100vw', | |
| height: '100vh', | |
| zIndex: -1, | |
| pointerEvents: 'none' | |
| }} | |
| /> | |
| ); | |
| }); | |
| ParticlesBg.displayName = 'ParticlesBg'; | |
| export default ParticlesBg; | |