"use client"; import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; type AgentWorkflowParticleScatterProps = { className?: string; density?: "low" | "medium" | "high"; }; type Particle = { x: number; y: number; radius: number; speed: number; drift: number; alpha: number; hue: number; phase: number; swing: number; depth: number; }; const PARTICLE_DENSITY_MULTIPLIER = { low: 0.75, medium: 1, high: 1.35, } as const; function randomBetween(min: number, max: number) { return min + Math.random() * (max - min); } export function AgentWorkflowParticleScatter({ className, density = "medium", }: AgentWorkflowParticleScatterProps) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) { return; } const context = canvas.getContext("2d"); if (!context) { return; } const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); let width = 0; let height = 0; let animationFrameId = 0; let particles: Particle[] = []; const buildParticle = (): Particle => { const depth = randomBetween(0.65, 1.15); const spread = width * randomBetween(0.16, 0.48); const originX = width / 2 + randomBetween(-spread, spread); const originY = height * randomBetween(0.56, 1.06); return { x: originX, y: originY, radius: randomBetween(0.9, 2.8) * depth, speed: randomBetween(0.08, 0.34) * depth, drift: randomBetween(0.12, 0.38) * (Math.random() > 0.5 ? 1 : -1), alpha: randomBetween(0.16, 0.84), hue: Math.random() > 0.7 ? 0 : 160, phase: randomBetween(0, Math.PI * 2), swing: randomBetween(10, 42), depth, }; }; const resetParticle = (particle: Particle, respawnNearBottom = false) => { const next = buildParticle(); particle.x = next.x; particle.y = respawnNearBottom ? height * randomBetween(0.72, 1.04) : next.y; particle.radius = next.radius; particle.speed = next.speed; particle.drift = next.drift; particle.alpha = next.alpha; particle.hue = next.hue; particle.phase = next.phase; particle.swing = next.swing; particle.depth = next.depth; }; const resize = () => { const bounds = canvas.getBoundingClientRect(); const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(1, bounds.width); height = Math.max(1, bounds.height); canvas.width = Math.round(width * pixelRatio); canvas.height = Math.round(height * pixelRatio); context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); const baseCount = Math.round((width / 12) * PARTICLE_DENSITY_MULTIPLIER[density]); particles = Array.from( { length: Math.max(56, Math.min(220, baseCount)) }, () => buildParticle() ); }; const drawGlow = (x: number, y: number, radius: number, alpha: number, hue: number) => { const gradient = context.createRadialGradient(x, y, 0, x, y, radius * 5.5); if (hue === 160) { gradient.addColorStop(0, `rgba(110, 255, 217, ${alpha})`); gradient.addColorStop(0.35, `rgba(26, 214, 168, ${alpha * 0.7})`); gradient.addColorStop(1, "rgba(26, 214, 168, 0)"); } else { gradient.addColorStop(0, `rgba(255, 255, 255, ${alpha})`); gradient.addColorStop(0.42, `rgba(209, 255, 244, ${alpha * 0.45})`); gradient.addColorStop(1, "rgba(255, 255, 255, 0)"); } context.fillStyle = gradient; context.beginPath(); context.arc(x, y, radius * 5.5, 0, Math.PI * 2); context.fill(); }; const render = (time: number) => { context.clearRect(0, 0, width, height); const bottomGlow = context.createRadialGradient( width / 2, height * 0.92, 0, width / 2, height * 0.92, width * 0.42 ); bottomGlow.addColorStop(0, "rgba(19, 221, 179, 0.18)"); bottomGlow.addColorStop(0.5, "rgba(19, 221, 179, 0.08)"); bottomGlow.addColorStop(1, "rgba(19, 221, 179, 0)"); context.fillStyle = bottomGlow; context.fillRect(0, 0, width, height); particles.forEach((particle) => { particle.phase += 0.012; particle.y -= particle.speed; particle.x += Math.sin(time * 0.00032 + particle.phase) * particle.drift; const waveOffset = Math.sin(time * 0.001 + particle.phase * 2.4) * particle.swing * 0.018; const pulseAlpha = particle.alpha * (0.7 + 0.3 * Math.sin(time * 0.0016 + particle.phase)); const renderX = particle.x + waveOffset; const renderY = particle.y; if (renderY < -24 || renderX < -32 || renderX > width + 32) { resetParticle(particle, true); } drawGlow(renderX, renderY, particle.radius, pulseAlpha * 0.68, particle.hue); context.beginPath(); context.fillStyle = particle.hue === 160 ? `rgba(155, 255, 230, ${pulseAlpha})` : `rgba(255, 255, 255, ${pulseAlpha})`; context.arc(renderX, renderY, particle.radius, 0, Math.PI * 2); context.fill(); }); if (!reducedMotionQuery.matches) { animationFrameId = window.requestAnimationFrame(render); } }; resize(); const handleResize = () => { resize(); }; window.addEventListener("resize", handleResize); if (reducedMotionQuery.matches) { render(0); } else { animationFrameId = window.requestAnimationFrame(render); } return () => { window.removeEventListener("resize", handleResize); if (animationFrameId) { window.cancelAnimationFrame(animationFrameId); } }; }, [density]); return (
); }