UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

265 lines (262 loc) 9.58 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useEffect, useCallback, useMemo } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; import { useA11yId } from '../../utils/a11y.js'; import styles from './ParticleBackground.module.css.js'; /** * ParticleBackground Component Implementation */ const ParticleBackgroundComponent = (props, ref) => { const { children, className, style, baseColor = 'rgba(10, 10, 20, 0.8)', particleColor = 'rgba(var(--glass-color-white) / var(--glass-opacity-70))', particleCount = 50, particleSize = 2, particleSpeed = 1, connectParticles = true, interactive = true, blur = false, blurAmount = 5, count, size, speed, color, intent = 'neutral', elevation = 'level2', tier = 'medium', respectMotionPreference = true, ...rest } = props; // Accessibility and motion preferences const componentId = useA11yId('particle-bg'); const prefersReducedMotion = useReducedMotion(); const { prefersReducedMotion: motionPrefersReduced } = useMotionPreferenceContext(); // Determine if motion should be reduced based on all preferences const shouldReduceMotion = respectMotionPreference && (prefersReducedMotion || motionPrefersReduced); // Refs const containerRef = useRef(null); const canvasRef = useRef(null); const animationRef = useRef(null); // State for mouse position const [mousePosition, setMousePosition] = useState(null); // CRITICAL SSR FIX: Initialize particles as empty array, populate in useEffect // This prevents Math.random() from generating different values on server vs client const [particles, setParticles] = useState([]); // Use correct property names with fallbacks const actualCount = count ?? particleCount ?? 50; const actualSize = size ?? particleSize ?? 2; const actualSpeed = speed ?? particleSpeed ?? 1; const actualColor = color ?? particleColor ?? 'rgba(var(--glass-color-white) / var(--glass-opacity-70))'; // Helper function to get color values const getColorValues = colorStr => { if (colorStr.startsWith('rgba(')) { const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); if (match) { return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]), a: match[4] ? parseFloat(match[4]) : 1 }; } } // Default fallback return { r: 255, g: 255, b: 255, a: 1 }; }; // CRITICAL SSR FIX: Generate particles only on client-side mount // This ensures deterministic behavior and prevents server/client mismatches useEffect(() => { const newParticles = []; for (let i = 0; i < actualCount; i++) { newParticles.push({ x: Math.random() * 100, // Percentage y: Math.random() * 100, // Percentage size: Math.random() * actualSize + 1, speedX: (Math.random() - 0.5) * actualSpeed * 0.1, speedY: (Math.random() - 0.5) * actualSpeed * 0.1, opacity: Math.random() * 0.5 + 0.3, color: actualColor }); } setParticles(newParticles); }, [actualCount, actualSize, actualSpeed, actualColor]); // Handle canvas animation useEffect(() => { if (shouldReduceMotion) return; const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const resizeCanvas = () => { const { width, height } = container.getBoundingClientRect(); canvas.width = width; canvas.height = height; }; resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Animation loop const animate = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); // Update and draw particles particles.forEach((particle, index) => { // Convert percentage to actual coordinates const x = particle.x / 100 * canvas.width; const y = particle.y / 100 * canvas.height; // Draw particle ctx.beginPath(); ctx.arc(x, y, particle.size, 0, Math.PI * 2); const colorValues = getColorValues(particle.color); ctx.fillStyle = `rgba(${colorValues.r}, ${colorValues.g}, ${colorValues.b}, ${particle.opacity})`; ctx.fill(); // Draw connections if (connectParticles) { for (let j = index + 1; j < particles.length; j++) { const otherParticle = particles[j]; const otherX = otherParticle.x / 100 * canvas.width; const otherY = otherParticle.y / 100 * canvas.height; const distance = Math.sqrt(Math.pow(x - otherX, 2) + Math.pow(y - otherY, 2)); if (distance < canvas.width * 0.07) { ctx.beginPath(); const connectionOpacity = 0.3 - distance / (canvas.width * 0.07) * 0.3; ctx.strokeStyle = `rgba(${colorValues.r}, ${colorValues.g}, ${colorValues.b}, ${connectionOpacity})`; ctx.lineWidth = 0.5; ctx.moveTo(x, y); ctx.lineTo(otherX, otherY); ctx.stroke(); } } } // Connect to mouse if interactive and motion is allowed if (interactive && mousePosition && !shouldReduceMotion) { const mouseX = mousePosition.x * canvas.width; const mouseY = mousePosition.y * canvas.height; const distance = Math.sqrt(Math.pow(x - mouseX, 2) + Math.pow(y - mouseY, 2)); if (distance < canvas.width * 0.1) { ctx.beginPath(); const mouseConnectionOpacity = 0.5 - distance / (canvas.width * 0.1) * 0.5; ctx.strokeStyle = `rgba(${colorValues.r}, ${colorValues.g}, ${colorValues.b}, ${mouseConnectionOpacity})`; ctx.lineWidth = 1; ctx.moveTo(x, y); ctx.lineTo(mouseX, mouseY); ctx.stroke(); } } // Update position particle.x += particle.speedX; particle.y += particle.speedY; // Handle boundaries if (particle.x < 0 || particle.x > 100) { particle.speedX *= -1; } if (particle.y < 0 || particle.y > 100) { particle.speedY *= -1; } }); // Continue animation animationRef.current = requestAnimationFrame(animate); }; // Start animation animate(); // Clean up return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } window.removeEventListener('resize', resizeCanvas); }; }, [particles, connectParticles, interactive, mousePosition, shouldReduceMotion]); // Handle mouse movement useEffect(() => { if (!interactive || shouldReduceMotion) return; const handleMouseMove = e => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); setMousePosition({ x: (e.clientX - rect.left) / rect.width, y: (e.clientY - rect.top) / rect.height }); }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, [interactive, shouldReduceMotion]); // Combine external ref with internal containerRef const setRefs = useCallback(node => { if (containerRef.current !== node) { containerRef.current = node; } if (ref) { if (typeof ref === 'function') { ref(node); } else { ref.current = node; } } }, [ref]); const blurStyle = useMemo(() => ({ '--particle-blur': blur ? `blur(${blurAmount}px)` : 'none' }), [blur, blurAmount]); return jsxs(OptimizedGlassCore, { ref: setRefs, className: cn(styles.container, 'glass-particle-background', className), intent: intent, elevation: elevation, tier: tier, style: style, id: componentId, role: "img", "aria-label": `Interactive particle background with ${actualCount} ${connectParticles ? 'connected' : 'floating'} particles${interactive && !shouldReduceMotion ? ', responding to mouse movement' : ''}`, tabIndex: interactive ? 0 : -1, ...rest, children: [jsx("div", { className: styles.backgroundLayer, style: { backgroundColor: baseColor } }), jsx("canvas", { ref: canvasRef, "aria-hidden": "true", role: "presentation", className: styles.canvas }), jsx("div", { "aria-hidden": "true", className: styles.blurLayer, style: blurStyle }), jsx("div", { className: styles.contentLayer, children: children })] }); }; // Wrap the component function with forwardRef const ParticleBackground = /*#__PURE__*/forwardRef(ParticleBackgroundComponent); ParticleBackground.displayName = 'ParticleBackground'; export { ParticleBackground as default }; //# sourceMappingURL=ParticleBackground.js.map