diff --git a/frontend/src/components/ui/particles.tsx b/frontend/src/components/ui/particles.tsx index 3ce2a29..4672da5 100644 --- a/frontend/src/components/ui/particles.tsx +++ b/frontend/src/components/ui/particles.tsx @@ -1,18 +1,11 @@ "use client"; import { cn } from "@/lib/utils"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState, useCallback } from "react"; -interface MousePosition { - x: number; - y: number; -} - -function MousePosition(): MousePosition { - const [mousePosition, setMousePosition] = useState({ - x: 0, - y: 0, - }); +// Custom Hook für Mausposition +function useMousePosition() { + const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (event: MouseEvent) => { @@ -20,15 +13,39 @@ function MousePosition(): MousePosition { }; window.addEventListener("mousemove", handleMouseMove); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - }; + return () => window.removeEventListener("mousemove", handleMouseMove); }, []); return mousePosition; } +// Umwandlung von HEX in RGB +function hexToRgb(hex: string): number[] { + hex = hex.replace("#", ""); + if (hex.length === 3) { + hex = hex.split("").map((char) => char + char).join(""); + } + return [ + parseInt(hex.substring(0, 2), 16), + parseInt(hex.substring(2, 4), 16), + parseInt(hex.substring(4, 6), 16), + ]; +} + +// Partikel-Interface +interface Particle { + x: number; + y: number; + translateX: number; + translateY: number; + size: number; + alpha: number; + targetAlpha: number; + dx: number; + dy: number; + magnetism: number; +} + interface ParticlesProps { className?: string; quantity?: number; @@ -40,22 +57,6 @@ interface ParticlesProps { vx?: number; vy?: number; } -function hexToRgb(hex: string): number[] { - hex = hex.replace("#", ""); - - if (hex.length === 3) { - hex = hex - .split("") - .map((char) => char + char) - .join(""); - } - - const hexInt = parseInt(hex, 16); - const red = (hexInt >> 16) & 255; - const green = (hexInt >> 8) & 255; - const blue = hexInt & 255; - return [red, green, blue]; -} const Particles: React.FC = ({ className = "", @@ -71,210 +72,112 @@ const Particles: React.FC = ({ const canvasRef = useRef(null); const canvasContainerRef = useRef(null); const context = useRef(null); - const circles = useRef([]); - const mousePosition = MousePosition(); - const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); + const circles = useRef([]); + const animationFrameRef = useRef(null); + const mousePosition = useMousePosition(); + const mouse = useRef({ x: 0, y: 0 }); + const canvasSize = useRef({ w: 0, h: 0 }); const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; + const rgb = hexToRgb(color); + + const resizeCanvas = useCallback(() => { + if (!canvasContainerRef.current || !canvasRef.current) return; + const { offsetWidth: w, offsetHeight: h } = canvasContainerRef.current; + Object.assign(canvasSize.current, { w, h }); + + canvasRef.current.width = w * dpr; + canvasRef.current.height = h * dpr; + canvasRef.current.style.width = `${w}px`; + canvasRef.current.style.height = `${h}px`; + + if (context.current) { + context.current.scale(dpr, dpr); + } + + circles.current = Array.from({ length: quantity }, createParticle); + }, [quantity]); + + const createParticle = (): Particle => ({ + x: Math.random() * canvasSize.current.w, + y: Math.random() * canvasSize.current.h, + translateX: 0, + translateY: 0, + size: Math.random() * 2 + size, + alpha: 0, + targetAlpha: Math.random() * 0.6 + 0.1, + dx: (Math.random() - 0.5) * 0.1, + dy: (Math.random() - 0.5) * 0.1, + magnetism: 0.1 + Math.random() * 4, + }); + + const clearCanvas = () => { + context.current?.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h); + }; + + const drawParticle = (particle: Particle) => { + if (!context.current) return; + const { x, y, translateX, translateY, size, alpha } = particle; + context.current.save(); + context.current.translate(translateX, translateY); + context.current.beginPath(); + context.current.arc(x, y, size, 0, 2 * Math.PI); + context.current.fillStyle = `rgba(${rgb.join(",")}, ${alpha})`; + context.current.fill(); + context.current.restore(); + }; + + const animateParticles = () => { + clearCanvas(); + circles.current.forEach((particle) => { + particle.x += particle.dx + vx; + particle.y += particle.dy + vy; + particle.translateX += (mouse.current.x / (staticity / particle.magnetism) - particle.translateX) / ease; + particle.translateY += (mouse.current.y / (staticity / particle.magnetism) - particle.translateY) / ease; + particle.alpha = Math.min(particle.alpha + 0.02, particle.targetAlpha); + + drawParticle(particle); + + if ( + particle.x < -particle.size || + particle.x > canvasSize.current.w + particle.size || + particle.y < -particle.size || + particle.y > canvasSize.current.h + particle.size + ) { + Object.assign(particle, createParticle()); + } + }); + animationFrameRef.current = requestAnimationFrame(animateParticles); + }; useEffect(() => { if (canvasRef.current) { context.current = canvasRef.current.getContext("2d"); + resizeCanvas(); + animateParticles(); + window.addEventListener("resize", resizeCanvas); } - initCanvas(); - animate(); - window.addEventListener("resize", initCanvas); - return () => { - window.removeEventListener("resize", initCanvas); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + window.removeEventListener("resize", resizeCanvas); }; - }, [color]); + }, [resizeCanvas, refresh]); useEffect(() => { - onMouseMove(); - }, [mousePosition.x, mousePosition.y]); - - useEffect(() => { - initCanvas(); - }, [refresh]); - - const initCanvas = () => { - resizeCanvas(); - drawParticles(); - }; - - const onMouseMove = () => { - if (canvasRef.current) { - const rect = canvasRef.current.getBoundingClientRect(); - const { w, h } = canvasSize.current; - const x = mousePosition.x - rect.left - w / 2; - const y = mousePosition.y - rect.top - h / 2; - const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; - if (inside) { - mouse.current.x = x; - mouse.current.y = y; - } + if (!canvasRef.current) return; + const rect = canvasRef.current.getBoundingClientRect(); + const { w, h } = canvasSize.current; + const x = mousePosition.x - rect.left - w / 2; + const y = mousePosition.y - rect.top - h / 2; + if (x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2) { + mouse.current.x = x; + mouse.current.y = y; } - }; - - type Circle = { - x: number; - y: number; - translateX: number; - translateY: number; - size: number; - alpha: number; - targetAlpha: number; - dx: number; - dy: number; - magnetism: number; - }; - - const resizeCanvas = () => { - if (canvasContainerRef.current && canvasRef.current && context.current) { - circles.current.length = 0; - canvasSize.current.w = canvasContainerRef.current.offsetWidth; - canvasSize.current.h = canvasContainerRef.current.offsetHeight; - canvasRef.current.width = canvasSize.current.w * dpr; - canvasRef.current.height = canvasSize.current.h * dpr; - canvasRef.current.style.width = `${canvasSize.current.w}px`; - canvasRef.current.style.height = `${canvasSize.current.h}px`; - context.current.scale(dpr, dpr); - } - }; - - const circleParams = (): Circle => { - const x = Math.floor(Math.random() * canvasSize.current.w); - const y = Math.floor(Math.random() * canvasSize.current.h); - const translateX = 0; - const translateY = 0; - const pSize = Math.floor(Math.random() * 2) + size; - const alpha = 0; - const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); - const dx = (Math.random() - 0.5) * 0.1; - const dy = (Math.random() - 0.5) * 0.1; - const magnetism = 0.1 + Math.random() * 4; - return { - x, - y, - translateX, - translateY, - size: pSize, - alpha, - targetAlpha, - dx, - dy, - magnetism, - }; - }; - - const rgb = hexToRgb(color); - - const drawCircle = (circle: Circle, update = false) => { - if (context.current) { - const { x, y, translateX, translateY, size, alpha } = circle; - context.current.translate(translateX, translateY); - context.current.beginPath(); - context.current.arc(x, y, size, 0, 2 * Math.PI); - context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; - context.current.fill(); - context.current.setTransform(dpr, 0, 0, dpr, 0, 0); - - if (!update) { - circles.current.push(circle); - } - } - }; - - const clearContext = () => { - if (context.current) { - context.current.clearRect( - 0, - 0, - canvasSize.current.w, - canvasSize.current.h, - ); - } - }; - - const drawParticles = () => { - clearContext(); - const particleCount = quantity; - for (let i = 0; i < particleCount; i++) { - const circle = circleParams(); - drawCircle(circle); - } - }; - - const remapValue = ( - value: number, - start1: number, - end1: number, - start2: number, - end2: number, - ): number => { - const remapped = - ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; - return remapped > 0 ? remapped : 0; - }; - - const animate = () => { - clearContext(); - circles.current.forEach((circle: Circle, i: number) => { - // Handle the alpha value - const edge = [ - circle.x + circle.translateX - circle.size, // distance from left edge - canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge - circle.y + circle.translateY - circle.size, // distance from top edge - canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge - ]; - const closestEdge = edge.reduce((a, b) => Math.min(a, b)); - const remapClosestEdge = parseFloat( - remapValue(closestEdge, 0, 20, 0, 1).toFixed(2), - ); - if (remapClosestEdge > 1) { - circle.alpha += 0.02; - if (circle.alpha > circle.targetAlpha) { - circle.alpha = circle.targetAlpha; - } - } else { - circle.alpha = circle.targetAlpha * remapClosestEdge; - } - circle.x += circle.dx + vx; - circle.y += circle.dy + vy; - circle.translateX += - (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / - ease; - circle.translateY += - (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / - ease; - - drawCircle(circle, true); - - // circle gets out of the canvas - if ( - circle.x < -circle.size || - circle.x > canvasSize.current.w + circle.size || - circle.y < -circle.size || - circle.y > canvasSize.current.h + circle.size - ) { - // remove the circle from the array - circles.current.splice(i, 1); - // create a new circle - const newCircle = circleParams(); - drawCircle(newCircle); - // update the circle position - } - }); - window.requestAnimationFrame(animate); - }; + }, [mousePosition]); return ( -