From 228071387642bc017faa39151dde5edda058625d Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:01:48 +0100 Subject: [PATCH] Update particles.tsx --- frontend/src/components/ui/particles.tsx | 344 +++++++++++++++-------- 1 file changed, 222 insertions(+), 122 deletions(-) diff --git a/frontend/src/components/ui/particles.tsx b/frontend/src/components/ui/particles.tsx index 7d0987d..3ce2a29 100644 --- a/frontend/src/components/ui/particles.tsx +++ b/frontend/src/components/ui/particles.tsx @@ -1,10 +1,18 @@ "use client"; import { cn } from "@/lib/utils"; -import React, { useEffect, useRef, useState, useCallback } from "react"; +import React, { useEffect, useRef, useState } from "react"; -function useMousePosition() { - const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); +interface MousePosition { + x: number; + y: number; +} + +function MousePosition(): MousePosition { + const [mousePosition, setMousePosition] = useState({ + x: 0, + y: 0, + }); useEffect(() => { const handleMouseMove = (event: MouseEvent) => { @@ -12,37 +20,15 @@ function useMousePosition() { }; window.addEventListener("mousemove", handleMouseMove); - return () => window.removeEventListener("mousemove", handleMouseMove); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; }, []); return mousePosition; } -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), - ]; -} - -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; @@ -54,6 +40,22 @@ 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 = "", @@ -69,112 +71,210 @@ const Particles: React.FC = ({ const canvasRef = useRef(null); const canvasContainerRef = useRef(null); const context = useRef(null); - 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 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; - const rgb = hexToRgb(color); - - 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 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 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 clearCanvas = () => { - context.current?.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h); - }; - - 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 () => { - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current); - } - window.removeEventListener("resize", resizeCanvas); + window.removeEventListener("resize", initCanvas); }; - }, [resizeCanvas, refresh]); + }, [color]); useEffect(() => { - 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; + 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; + } } - }, [mousePosition]); + }; + + 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); + }; return ( -