UNPKG

aura-glass

Version:

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

564 lines (561 loc) 20 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useEffect, useCallback } from 'react'; 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 { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; import { useGlassSound } from '../../utils/soundDesign.js'; // Stable defaults defined at module scope to avoid new identities each render const DEFAULT_FLUID_COLOR = [100, 150, 255, 0.8]; const GlassFluidSimulation = /*#__PURE__*/forwardRef(({ width = 800, height = 600, particleCount = 200, viscosity = 0.1, gravity = 0.5, smoothingRadius = 25, restDensity = 1000, gasConstant = 200, damping = 0.99, showTrails = true, trailLength = 10, fluidColor = DEFAULT_FLUID_COLOR, backgroundColor = "rgba(var(--glass-color-black) / var(--glass-opacity-10))", interactive = true, forceStrength = 1, forces = [], timeStep = 0.016, paused = false, maxFPS = 60, debug = false, showControls = true, preset = "water", onChange, onForceChange, respectMotionPreference = true, className, ...props }, ref) => { const { prefersReducedMotion, isMotionSafe } = useMotionPreferenceContext(); const { play } = useGlassSound(); const canvasRef = useRef(null); const animationRef = useRef(); const lastTimeRef = useRef(0); const simulationId = useA11yId("glass-fluid-simulation"); const [particles, setParticles] = useState([]); const [particleHistory, setParticleHistory] = useState([]); const [currentForces, setCurrentForces] = useState(forces); const [isRunning, setIsRunning] = useState(!paused); const [fps, setFps] = useState(60); const [currentPreset, setCurrentPreset] = useState(preset); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [isMouseDown, setIsMouseDown] = useState(false); // Stable refs to avoid re-creating callbacks on each render const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); const onForceChangeRef = useRef(onForceChange); useEffect(() => { onForceChangeRef.current = onForceChange; }, [onForceChange]); const particlesRef = useRef(particles); useEffect(() => { particlesRef.current = particles; }, [particles]); // Fluid presets const fluidPresets = { water: { viscosity: 0.1, gravity: 0.5, gasConstant: 200, damping: 0.99, color: [100, 150, 255, 0.8] }, honey: { viscosity: 0.8, gravity: 0.3, gasConstant: 150, damping: 0.95, color: [255, 200, 50, 0.9] }, mercury: { viscosity: 0.2, gravity: 0.8, gasConstant: 400, damping: 0.98, color: [192, 192, 192, 0.95] }, gas: { viscosity: 0.05, gravity: -0.1, gasConstant: 100, damping: 0.999, color: [200, 255, 200, 0.3] }, plasma: { viscosity: 0.01, gravity: 0.1, gasConstant: 300, damping: 0.997, color: [255, 100, 255, 0.7] } }; // Initialize particles const initializeParticles = useCallback(() => { const newParticles = []; for (let i = 0; i < particleCount; i++) { newParticles.push({ x: Math.random() * width * 0.8 + width * 0.1, y: Math.random() * height * 0.8 + height * 0.1, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, density: restDensity, pressure: 0, color: [...fluidColor], id: `particle-${i}` }); } setParticles(newParticles); setParticleHistory([newParticles]); onChangeRef.current?.(newParticles); }, [particleCount, width, height, restDensity]); // Initialize on mount useEffect(() => { // Initialize on mount and when core dimensions change. initializeParticles(); // Intentionally avoid `fluidColor` here to prevent an update loop // when a new array instance is created each render. }, [particleCount, width, height, restDensity, initializeParticles]); // Apply preset changes useEffect(() => { // Update fluid properties based on preset // This would typically update the simulation parameters }, [currentPreset]); // SPH density calculation const calculateDensity = useCallback(particles => { const h = smoothingRadius; const h2 = h * h; particles.forEach(particle => { let density = 0; particles.forEach(neighbor => { const dx = particle.x - neighbor.x; const dy = particle.y - neighbor.y; const distance2 = dx * dx + dy * dy; if (distance2 < h2) { const distance = Math.sqrt(distance2); const influence = Math.max(0, h - distance); density += influence * influence; } }); particle.density = Math.max(density, restDensity); particle.pressure = gasConstant * (particle.density - restDensity); }); }, [smoothingRadius, restDensity, gasConstant]); // SPH force calculation const calculateForces = useCallback(particles => { const h = smoothingRadius; particles.forEach(particle => { let fx = 0, fy = 0; // Pressure and viscosity forces particles.forEach(neighbor => { if (particle.id === neighbor.id) return; const dx = particle.x - neighbor.x; const dy = particle.y - neighbor.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < h && distance > 0) { const influence = (h - distance) / h; // Pressure force const pressureForce = (particle.pressure + neighbor.pressure) / (2 * neighbor.density) * influence; fx += dx / distance * pressureForce; fy += dy / distance * pressureForce; // Viscosity force const viscosityForce = viscosity * influence / neighbor.density; fx += (neighbor.vx - particle.vx) * viscosityForce; fy += (neighbor.vy - particle.vy) * viscosityForce; } }); // Apply external forces currentForces.forEach(force => { const dx = particle.x - force.x; const dy = particle.y - force.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < force.radius) { const influence = (force.radius - distance) / force.radius; const forceAmount = force.strength * influence * forceStrength; switch (force.type) { case "push": if (distance > 0) { fx += dx / distance * forceAmount; fy += dy / distance * forceAmount; } break; case "pull": if (distance > 0) { fx -= dx / distance * forceAmount; fy -= dy / distance * forceAmount; } break; case "vortex": fx += -dy * forceAmount * 0.01; fy += dx * forceAmount * 0.01; break; case "wave": fx += Math.sin(Date.now() * 0.001) * forceAmount; fy += Math.cos(Date.now() * 0.001) * forceAmount; break; } } }); // Interactive mouse force if (interactive && isMouseDown) { const dx = particle.x - mousePos.x; const dy = particle.y - mousePos.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 100 && distance > 0) { const influence = (100 - distance) / 100; const mouseForce = 50 * influence * forceStrength; fx -= dx / distance * mouseForce; fy -= dy / distance * mouseForce; } } // Gravity fy += gravity; // Update velocity particle.vx += fx * timeStep; particle.vy += fy * timeStep; // Apply damping particle.vx *= damping; particle.vy *= damping; }); }, [smoothingRadius, viscosity, currentForces, forceStrength, interactive, isMouseDown, mousePos, gravity, timeStep, damping]); // Update particle positions const updatePositions = useCallback(particles => { particles.forEach(particle => { // Update position particle.x += particle.vx * timeStep; particle.y += particle.vy * timeStep; // Boundary collisions if (particle.x < 10) { particle.x = 10; particle.vx = Math.abs(particle.vx) * 0.5; } if (particle.x > width - 10) { particle.x = width - 10; particle.vx = -Math.abs(particle.vx) * 0.5; } if (particle.y < 10) { particle.y = 10; particle.vy = Math.abs(particle.vy) * 0.5; } if (particle.y > height - 10) { particle.y = height - 10; particle.vy = -Math.abs(particle.vy) * 0.5; } // Update particle color based on velocity const speed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy); const speedNormalized = Math.min(speed / 10, 1); particle.color[0] = fluidColor[0] + speedNormalized * 100; particle.color[1] = fluidColor[1]; particle.color[2] = fluidColor[2] + speedNormalized * 50; }); }, [timeStep, width, height, fluidColor]); // Render simulation const render = useCallback(particles => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; // Clear canvas ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, width, height); // Draw trails if (showTrails && particleHistory.length > 1) { particleHistory.forEach((historyParticles, historyIndex) => { const alpha = historyIndex / particleHistory.length * 0.3; historyParticles.forEach(particle => { ctx.globalAlpha = alpha; ctx.fillStyle = `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${alpha})`; ctx.beginPath(); ctx.arc(particle.x, particle.y, 2, 0, Math.PI * 2); ctx.fill(); }); }); } // Draw particles ctx.globalAlpha = 1; particles.forEach(particle => { const size = Math.max(2, particle.density / restDensity * 4); // Create gradient const gradient = ctx.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, size); gradient.addColorStop(0, `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.color[3]})`); gradient.addColorStop(1, `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, 0)`); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2); ctx.fill(); }); // Draw forces currentForces.forEach(force => { ctx.strokeStyle = force.type === "push" ? "red" : force.type === "pull" ? "blue" : force.type === "vortex" ? "purple" : "green"; ctx.globalAlpha = 0.3; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(force.x, force.y, force.radius, 0, Math.PI * 2); ctx.stroke(); }); // Draw debug info if (debug) { ctx.fillStyle = "white"; ctx.font = "12px monospace"; ctx.globalAlpha = 1; ctx.fillText(`FPS: ${fps}`, 10, 20); ctx.fillText(`Particles: ${particles.length}`, 10, 40); ctx.fillText(`Forces: ${currentForces.length}`, 10, 60); } // Draw interactive mouse force if (interactive && isMouseDown) { ctx.strokeStyle = "yellow"; ctx.globalAlpha = 0.5; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(mousePos.x, mousePos.y, 100, 0, Math.PI * 2); ctx.stroke(); } }, [backgroundColor, width, height, showTrails, particleHistory, restDensity, currentForces, debug, fps, interactive, isMouseDown, mousePos]); // Animation loop const animate = useCallback(currentTime => { if (prefersReducedMotion && respectMotionPreference) { return; } if (!isRunning) return; const deltaTime = currentTime - lastTimeRef.current; if (deltaTime < 1000 / maxFPS) { animationRef.current = requestAnimationFrame(animate); return; } lastTimeRef.current = currentTime; setFps(Math.round(1000 / deltaTime)); setParticles(prevParticles => { const newParticles = [...prevParticles]; // SPH simulation steps calculateDensity(newParticles); calculateForces(newParticles); updatePositions(newParticles); // Update history for trails if (showTrails) { setParticleHistory(prevHistory => { const newHistory = [...prevHistory, [...newParticles]]; return newHistory.slice(-trailLength); }); } onChangeRef.current?.(newParticles); // keep ref in sync for rendering particlesRef.current = newParticles; return newParticles; }); render(particlesRef.current); animationRef.current = requestAnimationFrame(animate); }, [prefersReducedMotion, respectMotionPreference, isRunning, maxFPS, calculateDensity, calculateForces, updatePositions, showTrails, trailLength, render]); // Start animation useEffect(() => { if (isRunning && !prefersReducedMotion) { animationRef.current = requestAnimationFrame(animate); } return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [animate, isRunning, prefersReducedMotion]); // Canvas setup useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (ctx) { ctx.imageSmoothingEnabled = true; } }, [width, height]); // Mouse event handlers const handleMouseMove = useCallback(event => { const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); setMousePos({ x: event.clientX - rect.left, y: event.clientY - rect.top }); }, []); const handleMouseDown = useCallback(event => { setIsMouseDown(true); play("tap"); }, [play]); const handleMouseUp = useCallback(() => { setIsMouseDown(false); }, []); // Add force const addForce = useCallback(force => { const newForce = { ...force, id: `force-${Date.now()}-${Math.random()}` }; setCurrentForces(prev => { const updated = [...prev, newForce]; onForceChangeRef.current?.(updated); return updated; }); play("success"); }, [play]); // Remove force useCallback(forceId => { setCurrentForces(prev => { const updated = prev.filter(f => f.id !== forceId); onForceChangeRef.current?.(updated); return updated; }); play("error"); }, [play]); // Control panel const renderControls = () => { if (!showControls) return null; return jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: "glass-fluid-controls glass-flex glass-flex-wrap glass-items-center glass-gap-4 glass-p-4 glass-radius-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard", children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("button", { onClick: () => setIsRunning(!isRunning), className: 'glass-px-3 glass-py-1 glass-radius-md glass-surface-primary/20 hover:glass-surface-primary/30 text-primary', children: isRunning ? "Pause" : "Play" }), jsx("button", { onClick: initializeParticles, className: 'glass-px-3 glass-py-1 glass-radius-md bg-secondary/20 hover:bg-secondary/30 glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', children: "Reset" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("label", { className: "glass-text-sm", children: "Preset:" }), jsx("select", { value: currentPreset, onChange: e => setCurrentPreset(e.target.value), className: "glass-px-2 glass-py-1 glass-radius-md glass-surface-overlay glass-border glass-border-glass-border/20", children: Object.keys(fluidPresets).map(preset => jsx("option", { value: preset, children: preset.charAt(0).toUpperCase() + preset.slice(1) }, preset)) })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("button", { onClick: () => addForce({ x: width / 2, y: height / 2, strength: 10, type: "push", radius: 50 }), className: 'glass-px-2 glass-py-1 glass-radius-md glass-surface-red/20 hover:glass-surface-red/30 text-primary glass-text-sm', children: "Add Push" }), jsx("button", { onClick: () => addForce({ x: width / 2, y: height / 2, strength: 10, type: "pull", radius: 50 }), className: 'glass-px-2 glass-py-1 glass-radius-md glass-surface-blue/20 hover:glass-surface-blue/30 text-primary glass-text-sm', children: "Add Pull" }), jsx("button", { onClick: () => addForce({ x: width / 2, y: height / 2, strength: 10, type: "vortex", radius: 100 }), className: 'glass-px-2 glass-py-1 glass-radius-md glass-surface-primary/20 hover:glass-surface-primary/30 text-primary glass-text-sm', children: "Add Vortex" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsxs("label", { className: "glass-text-sm", children: [jsx("input", { type: "checkbox", checked: showTrails, onChange: e => setParticleHistory([]), className: "glass-mr-1" }), "Trails"] }), jsxs("label", { className: "glass-text-sm", children: [jsx("input", { type: "checkbox", checked: debug, onChange: e => {}, className: "glass-mr-1" }), "Debug"] })] })] }); }; return jsx(OptimizedGlassCore, { ref: ref, id: simulationId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-fluid-simulation relative glass-radius-lg glass-backdrop-blur-md border border-border/20", className), ...props, children: jsxs(MotionFramer, { preset: isMotionSafe && respectMotionPreference ? "fadeIn" : "none", className: "glass-flex glass-flex-col glass-gap-4 glass-p-4", children: [renderControls(), jsx("div", { className: 'relative', children: jsx("canvas", { ref: canvasRef, width: width, height: height, className: cn("border border-border/20 glass-radius-md bg-black/10", interactive && "cursor-crosshair"), onMouseMove: handleMouseMove, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, style: { width, height } }) })] }) }); }); GlassFluidSimulation.displayName = "GlassFluidSimulation"; export { GlassFluidSimulation, GlassFluidSimulation as default }; //# sourceMappingURL=GlassFluidSimulation.js.map