|
| 1 | +"use client"; |
| 2 | +import React, { useEffect, useRef, useState } from "react"; |
| 3 | +import { useTheme } from "next-themes"; |
| 4 | + |
| 5 | +const BackgroundDots = ({ numDots = 50, speed = 0.5 }) => { |
| 6 | + const canvasRef = useRef(null); |
| 7 | + const dotsRef = useRef([]); |
| 8 | + const linesRef = useRef([]); |
| 9 | + const requestRef = useRef(null); |
| 10 | + |
| 11 | + // 1. Hook de next-themes |
| 12 | + const { resolvedTheme } = useTheme(); |
| 13 | + |
| 14 | + // 2. Estado para evitar errores de hidratación |
| 15 | + const [mounted, setMounted] = useState(false); |
| 16 | + |
| 17 | + useEffect(() => { |
| 18 | + setMounted(true); |
| 19 | + }, []); |
| 20 | + |
| 21 | + const createDot = (width, height, existingDot = null) => { |
| 22 | + const dot = existingDot || {}; |
| 23 | + dot.x = Math.random() * width; |
| 24 | + dot.y = Math.random() * height; |
| 25 | + dot.vx = (Math.random() - 0.5) * speed; |
| 26 | + dot.vy = (Math.random() - 0.5) * speed; |
| 27 | + dot.life = Math.random() * 300 + 100; |
| 28 | + dot.maxLife = dot.life; |
| 29 | + dot.opacity = 0; |
| 30 | + return dot; |
| 31 | + }; |
| 32 | + |
| 33 | + useEffect(() => { |
| 34 | + // Si no está montado aún, no hacemos nada (evita flash incorrecto) |
| 35 | + if (!mounted) return; |
| 36 | + |
| 37 | + const canvas = canvasRef.current; |
| 38 | + if (!canvas) return; |
| 39 | + |
| 40 | + const ctx = canvas.getContext("2d"); |
| 41 | + let width = window.innerWidth; |
| 42 | + let height = window.innerHeight; |
| 43 | + |
| 44 | + // 3. DEFINIR COLORES RGB SEGÚN EL TEMA |
| 45 | + // Si es dark, usamos Blanco (255,255,255). Si es light, usamos Gris Oscuro (20,20,20) |
| 46 | + const isDark = resolvedTheme === "dark"; |
| 47 | + const r = isDark ? 255 : 20; |
| 48 | + const g = isDark ? 255 : 20; |
| 49 | + const b = isDark ? 255 : 20; |
| 50 | + |
| 51 | + const handleResize = () => { |
| 52 | + width = window.innerWidth; |
| 53 | + height = window.innerHeight; |
| 54 | + canvas.width = width; |
| 55 | + canvas.height = height; |
| 56 | + dotsRef.current = Array.from({ length: numDots }, () => |
| 57 | + createDot(width, height) |
| 58 | + ); |
| 59 | + linesRef.current = []; |
| 60 | + }; |
| 61 | + |
| 62 | + window.addEventListener("resize", handleResize); |
| 63 | + handleResize(); |
| 64 | + |
| 65 | + const animate = () => { |
| 66 | + ctx.clearRect(0, 0, width, height); |
| 67 | + |
| 68 | + // --- DIBUJAR PUNTOS --- |
| 69 | + dotsRef.current.forEach((dot) => { |
| 70 | + dot.x += dot.vx; |
| 71 | + dot.y += dot.vy; |
| 72 | + dot.life--; |
| 73 | + |
| 74 | + if (dot.x < 0 || dot.x > width) dot.vx *= -1; |
| 75 | + if (dot.y < 0 || dot.y > height) dot.vy *= -1; |
| 76 | + |
| 77 | + if (dot.life > dot.maxLife - 50) |
| 78 | + dot.opacity = Math.min(1, dot.opacity + 0.02); |
| 79 | + else if (dot.life < 50) dot.opacity = Math.max(0, dot.opacity - 0.02); |
| 80 | + |
| 81 | + ctx.beginPath(); |
| 82 | + ctx.arc(dot.x, dot.y, 3, 0, Math.PI * 2); |
| 83 | + // Usamos las variables r, g, b dinámicas |
| 84 | + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${dot.opacity * 0.5})`; |
| 85 | + ctx.fill(); |
| 86 | + |
| 87 | + if (dot.life <= 0) createDot(width, height, dot); |
| 88 | + }); |
| 89 | + |
| 90 | + // --- DIBUJAR LÍNEAS --- |
| 91 | + if (Math.random() < 0.03 && dotsRef.current.length > 2) { |
| 92 | + const idx1 = Math.floor(Math.random() * dotsRef.current.length); |
| 93 | + let idx2 = Math.floor(Math.random() * dotsRef.current.length); |
| 94 | + while (idx1 === idx2) { |
| 95 | + idx2 = Math.floor(Math.random() * dotsRef.current.length); |
| 96 | + } |
| 97 | + linesRef.current.push({ |
| 98 | + dot1: dotsRef.current[idx1], |
| 99 | + dot2: dotsRef.current[idx2], |
| 100 | + life: 80, |
| 101 | + maxLife: 90, |
| 102 | + }); |
| 103 | + } |
| 104 | + |
| 105 | + for (let i = linesRef.current.length - 1; i >= 0; i--) { |
| 106 | + const line = linesRef.current[i]; |
| 107 | + const dx = line.dot1.x - line.dot2.x; |
| 108 | + const dy = line.dot1.y - line.dot2.y; |
| 109 | + const dist = Math.sqrt(dx * dx + dy * dy); |
| 110 | + |
| 111 | + if (dist < 300) { |
| 112 | + const opacity = (line.life / line.maxLife) * 0.4; |
| 113 | + ctx.beginPath(); |
| 114 | + ctx.moveTo(line.dot1.x, line.dot1.y); |
| 115 | + ctx.lineTo(line.dot2.x, line.dot2.y); |
| 116 | + // Usamos las mismas variables r, g, b para las líneas |
| 117 | + ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`; |
| 118 | + ctx.lineWidth = 1; |
| 119 | + ctx.stroke(); |
| 120 | + } |
| 121 | + |
| 122 | + line.life--; |
| 123 | + if (line.life <= 0) linesRef.current.splice(i, 1); |
| 124 | + } |
| 125 | + |
| 126 | + requestRef.current = requestAnimationFrame(animate); |
| 127 | + }; |
| 128 | + |
| 129 | + requestRef.current = requestAnimationFrame(animate); |
| 130 | + |
| 131 | + return () => { |
| 132 | + window.removeEventListener("resize", handleResize); |
| 133 | + cancelAnimationFrame(requestRef.current); |
| 134 | + }; |
| 135 | + // 4. AGREGAMOS resolvedTheme A LAS DEPENDENCIAS |
| 136 | + // Esto hace que el canvas se reinicie con nuevos colores cuando cambias el tema |
| 137 | + }, [numDots, speed, resolvedTheme, mounted]); |
| 138 | + |
| 139 | + // Si no está montado, devolvemos null para evitar parpadeos |
| 140 | + if (!mounted) return null; |
| 141 | + |
| 142 | + return ( |
| 143 | + <canvas |
| 144 | + ref={canvasRef} |
| 145 | + style={{ |
| 146 | + position: "fixed", |
| 147 | + top: 0, |
| 148 | + left: 0, |
| 149 | + width: "100%", |
| 150 | + height: "100%", |
| 151 | + zIndex: 0, |
| 152 | + pointerEvents: "none", |
| 153 | + }} |
| 154 | + /> |
| 155 | + ); |
| 156 | +}; |
| 157 | + |
| 158 | +export default BackgroundDots; |
0 commit comments