From 5cfbd22cbf6e8f66dd979e0e13a19d8369d32df1 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:04:18 +0100 Subject: [PATCH] New Patricle Option --- frontend/src/components/ui/particles.tsx | 413 +++++++++-------------- 1 file changed, 163 insertions(+), 250 deletions(-) diff --git a/frontend/src/components/ui/particles.tsx b/frontend/src/components/ui/particles.tsx index 8aa7064..35b5aaf 100644 --- a/frontend/src/components/ui/particles.tsx +++ b/frontend/src/components/ui/particles.tsx @@ -3,121 +3,22 @@ import { cn } from "@/lib/utils"; import React, { useEffect, useRef, useState } from "react"; -interface MousePosition { - x: number; - y: number; +function useMousePosition() { + const [mousePosition, setMousePosition] = useState({ x: -9999, y: -9999 }); + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + setMousePosition({ x: event.clientX, y: event.clientY }); + }; + + window.addEventListener("mousemove", handleMouseMove); + return () => window.removeEventListener("mousemove", handleMouseMove); + }, []); + + return mousePosition; } -function MousePosition(): MousePosition { - const [mousePosition, setMousePosition] = useState({ - x: 0, - y: 0, - }); - - useEffect(() => { - const handleMouseMove = (event: MouseEvent) => { - setMousePosition({ x: event.clientX, y: event.clientY }); - }; - - window.addEventListener("mousemove", handleMouseMove); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - }; - }, []); - - return mousePosition; -} - -interface ParticlesProps { - className?: string; - quantity?: number; - staticity?: number; - ease?: number; - size?: number; - refresh?: boolean; - color?: string; - 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 = "", - quantity = 300, - staticity = 50, - ease = 50, - size = 0.4, - refresh = false, - color = `hsl(${Math.random() * 360}, 100%, 70%)`, - vx = 0, - vy = 0, -}) => { - 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 dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; - - useEffect(() => { - if (canvasRef.current) { - context.current = canvasRef.current.getContext("2d"); - } - initCanvas(); - animate(); - window.addEventListener("resize", initCanvas); - - return () => { - window.removeEventListener("resize", initCanvas); - }; - }, [color]); - - 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; - } - } - }; - - type Circle = { +interface Particle { x: number; y: number; translateX: number; @@ -128,156 +29,168 @@ const Particles: React.FC = ({ dx: number; dy: number; magnetism: number; - }; + color: string; +} - 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); - } - }; +interface ParticlesProps { + className?: string; + quantity?: number; + staticity?: number; + ease?: number; + size?: number; + refresh?: boolean; + vx?: number; + vy?: number; +} - 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.random() * 3 + size; - const alpha = 0; - const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); - const dx = (Math.random() - 0.5) * 0.3; - const dy = (Math.random() - 0.5) * 0.3; - const magnetism = 0.1 + Math.random() * 8; - return { - x, - y, - translateX, - translateY, - size: pSize, - alpha, - targetAlpha, - dx, - dy, - magnetism, +const Particles: React.FC = ({ + className = "", + quantity = 400, + staticity = 50, + ease = 25, + size = 0.8, + refresh = false, + vx = 0.1, + vy = 0.1, +}) => { + const canvasRef = useRef(null); + const canvasContainerRef = useRef(null); + const context = useRef(null); + const circles = useRef([]); + 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 getRandomColor = () => { + const colors = ["#ff00ff", "#00ffff", "#ff0099", "#0099ff", "#cc00ff", "#00ffcc"]; + return colors[Math.floor(Math.random() * colors.length)]; }; - }; - 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); + const resizeCanvas = () => { + if (!canvasContainerRef.current || !canvasRef.current) return; - if (!update) { - circles.current.push(circle); - } - } - }; + canvasSize.current.w = canvasContainerRef.current.offsetWidth; + canvasSize.current.h = canvasContainerRef.current.offsetHeight; + const { w, h } = canvasSize.current; - const clearContext = () => { - if (context.current) { - context.current.clearRect( - 0, - 0, - canvasSize.current.w, - canvasSize.current.h, - ); - } - }; + canvasRef.current.width = w * dpr; + canvasRef.current.height = h * dpr; + canvasRef.current.style.width = `${w}px`; + canvasRef.current.style.height = `${h}px`; - 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; + context.current = canvasRef.current.getContext("2d"); + if (context.current) { + context.current.scale(dpr, dpr); } - } 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); + circles.current = Array.from({ length: quantity }, createParticle); + }; - // 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 - } + 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.8 + 0.2, + dx: (Math.random() - 0.5) * Math.random() * 0.4, + dy: (Math.random() - 0.5) * Math.random() * 0.4, + magnetism: 0.5 + Math.random() * 3, + color: getRandomColor(), }); - window.requestAnimationFrame(animate); - }; - return ( - - ); + const clearCanvas = () => { + if (context.current) { + context.current.fillStyle = "rgba(10, 10, 30, 0.2)"; + context.current.fillRect(0, 0, canvasSize.current.w, canvasSize.current.h); + } + }; + + const drawParticle = (particle: Particle) => { + if (!context.current) return; + + context.current.save(); + context.current.translate(particle.translateX, particle.translateY); + context.current.beginPath(); + context.current.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI); + + context.current.fillStyle = particle.color; + context.current.shadowColor = particle.color; + context.current.shadowBlur = 15; + context.current.fill(); + context.current.restore(); + }; + + const connectParticles = () => { + if (!context.current) return; + + const maxDistance = 100; + circles.current.forEach((p1, i) => { + for (let j = i + 1; j < circles.current.length; j++) { + const p2 = circles.current[j]; + const dx = p1.x - p2.x; + const dy = p1.y - p2.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < maxDistance) { + const opacity = 1 - distance / maxDistance; + context.current.strokeStyle = `rgba(255, 0, 255, ${opacity})`; + context.current.lineWidth = 0.3; + + context.current.beginPath(); + context.current.moveTo(p1.x, p1.y); + context.current.lineTo(p2.x, p2.y); + context.current.stroke(); + } + } + }); + }; + + const animateParticles = () => { + clearCanvas(); + circles.current.forEach((particle) => { + const dx = mousePosition.x - particle.x; + const dy = mousePosition.y - particle.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 150) { + particle.dx += dx * 0.0005; + particle.dy += dy * 0.0005; + particle.size = Math.min(particle.size + 0.05, 4); + } else { + particle.size = Math.max(particle.size - 0.02, 0.8); + } + + 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()); + } + }); + + connectParticles(); + requestAnimationFrame(animateParticles); + }; + + useEffect(() => { + if (canvasRef.current) { + resizeCanvas(); + animateParticles(); + window.addEventListener("resize", resizeCanvas); + } + return () => window.removeEventListener("resize", resizeCanvas); + }, [refresh]); + + return
; }; export default Particles;