UNPKG

aura-glass

Version:

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

562 lines (559 loc) 21.6 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useCallback, useEffect } 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'; // Move color schemes outside component to keep reference stable across renders const COLOR_SCHEMES = { blue: { primary: [100, 150, 255], secondary: [150, 200, 255], accent: [50, 100, 200] }, purple: { primary: [200, 100, 255], secondary: [255, 150, 255], accent: [150, 50, 200] }, green: { primary: [100, 255, 150], secondary: [150, 255, 200], accent: [50, 200, 100] }, red: { primary: [255, 100, 100], secondary: [255, 150, 150], accent: [200, 50, 50] }, gold: { primary: [255, 200, 100], secondary: [255, 220, 150], accent: [200, 150, 50] }, cosmic: { primary: [255, 100, 200], secondary: [100, 200, 255], accent: [200, 255, 100] } }; const GlassVortexPortal = /*#__PURE__*/forwardRef(({ width = 600, height = 600, radius = 150, ringCount = 8, rotationSpeed = 1, intensity = 0.8, depth = 10, type = "dimensional", colorScheme = "blue", active = true, opening = false, closing = false, particleCount = 100, showDistortion = true, distortionIntensity = 0.5, energyLevel = 1, pulsing = true, pulseFrequency = 2, showEventHorizon = true, interactive = true, onActivate, onDeactivate, onEntry, timeScale = 1, showControls = true, debug = false, respectMotionPreference = true, className, ...props }, ref) => { const { prefersReducedMotion, isMotionSafe } = useMotionPreferenceContext(); const { play } = useGlassSound(); const canvasRef = useRef(null); const animationRef = useRef(); const portalId = useA11yId("glass-vortex-portal"); const [rings, setRings] = useState([]); const [particles, setParticles] = useState([]); const [distortions, setDistortions] = useState([]); const [currentIntensity, setCurrentIntensity] = useState(intensity); const [currentRadius, setCurrentRadius] = useState(radius); const [portalActive, setPortalActive] = useState(active); const [animationTime, setAnimationTime] = useState(0); // Use stable color scheme map so callbacks don't churn every render const currentColors = COLOR_SCHEMES[colorScheme]; // Initialize portal rings const initializeRings = useCallback(() => { const newRings = []; for (let i = 0; i < ringCount; i++) { const ringRadius = radius / ringCount * (i + 1); const speed = (1 + i * 0.2) * rotationSpeed; const opacity = Math.max(0.1, 1 - i / ringCount * 0.8); newRings.push({ radius: ringRadius, rotation: Math.random() * Math.PI * 2, speed, opacity, color: i % 2 === 0 ? currentColors.primary : currentColors.secondary, thickness: Math.max(1, 5 - i), id: `ring-${i}` }); } setRings(newRings); }, [ringCount, radius, rotationSpeed, colorScheme]); // Initialize particles const initializeParticles = useCallback(() => { const newParticles = []; for (let i = 0; i < particleCount; i++) { const angle = Math.random() * Math.PI * 2; const particleRadius = Math.random() * radius * 1.5; newParticles.push({ x: width / 2 + Math.cos(angle) * particleRadius, y: height / 2 + Math.sin(angle) * particleRadius, angle, radius: particleRadius, speed: (Math.random() * 2 + 1) * rotationSpeed, size: Math.random() * 3 + 1, opacity: Math.random() * 0.8 + 0.2, color: [currentColors.accent[0] + Math.random() * 50, currentColors.accent[1] + Math.random() * 50, currentColors.accent[2] + Math.random() * 50], lifetime: Math.random() * 5000 + 2000, id: `particle-${i}` }); } setParticles(newParticles); }, [particleCount, radius, width, height, rotationSpeed, colorScheme]); // Initialize distortions const initializeDistortions = useCallback(() => { if (!showDistortion) return; const newDistortions = []; const distortionCount = Math.floor(distortionIntensity * 10); for (let i = 0; i < distortionCount; i++) { newDistortions.push({ x: Math.random() * width, y: Math.random() * height, intensity: Math.random() * distortionIntensity, frequency: Math.random() * 0.02 + 0.01, type: ["ripple", "spiral", "quantum", "void"][Math.floor(Math.random() * 4)], id: `distortion-${i}` }); } setDistortions(newDistortions); }, [showDistortion, distortionIntensity, width, height]); // Initialize portal useEffect(() => { initializeRings(); initializeParticles(); initializeDistortions(); }, [initializeRings, initializeParticles, initializeDistortions]); // Update portal state useEffect(() => { if (opening) { setCurrentIntensity(0); setCurrentRadius(0); const interval = setInterval(() => { setCurrentIntensity(prev => Math.min(intensity, prev + 0.02)); setCurrentRadius(prev => Math.min(radius, prev + 3)); }, 16); setTimeout(() => { clearInterval(interval); play("success"); }, 2000); return () => clearInterval(interval); } if (closing) { const interval = setInterval(() => { setCurrentIntensity(prev => Math.max(0, prev - 0.02)); setCurrentRadius(prev => Math.max(0, prev - 3)); }, 16); setTimeout(() => { clearInterval(interval); setPortalActive(false); play("error"); }, 2000); return () => clearInterval(interval); } }, [opening, closing, intensity, radius, play]); // Update rings animation const updateRings = useCallback(deltaTime => { setRings(prevRings => prevRings.map(ring => ({ ...ring, rotation: ring.rotation + ring.speed * deltaTime * timeScale * (portalActive ? 1 : 0.1), opacity: ring.opacity * currentIntensity, radius: ring.radius / radius * currentRadius }))); }, [timeScale, portalActive, currentIntensity, currentRadius, radius]); // Update particles const updateParticles = useCallback(deltaTime => { setParticles(prevParticles => prevParticles.map(particle => { const centerX = width / 2; const centerY = height / 2; // Spiral motion towards center const newAngle = particle.angle + particle.speed * deltaTime * timeScale; let newRadius = particle.radius; if (portalActive) { newRadius = Math.max(5, particle.radius - 20 * deltaTime * timeScale); // Respawn particle at edge when it reaches center if (newRadius < 10) { newRadius = radius * 1.5; particle.lifetime = Math.random() * 5000 + 2000; } } const newX = centerX + Math.cos(newAngle) * newRadius; const newY = centerY + Math.sin(newAngle) * newRadius; return { ...particle, x: newX, y: newY, angle: newAngle, radius: newRadius, opacity: particle.opacity * currentIntensity, lifetime: particle.lifetime - deltaTime }; }).filter(p => p.lifetime > 0)); // Add new particles if needed if (particles.length < particleCount && portalActive) { const newParticles = []; const needed = Math.min(5, particleCount - particles.length); for (let i = 0; i < needed; i++) { const angle = Math.random() * Math.PI * 2; const particleRadius = radius * 1.5; newParticles.push({ x: width / 2 + Math.cos(angle) * particleRadius, y: height / 2 + Math.sin(angle) * particleRadius, angle, radius: particleRadius, speed: (Math.random() * 2 + 1) * rotationSpeed, size: Math.random() * 3 + 1, opacity: Math.random() * 0.8 + 0.2, color: [currentColors.accent[0] + Math.random() * 50, currentColors.accent[1] + Math.random() * 50, currentColors.accent[2] + Math.random() * 50], lifetime: Math.random() * 5000 + 2000, id: `particle-${Date.now()}-${i}` }); } setParticles(prev => [...prev, ...newParticles]); } }, [width, height, radius, timeScale, portalActive, currentIntensity, particles.length, particleCount, rotationSpeed, colorScheme]); // Render portal const render = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const centerX = width / 2; const centerY = height / 2; // Clear canvas ctx.fillStyle = "rgba(var(--glass-color-black) / var(--glass-opacity-10))"; ctx.fillRect(0, 0, width, height); // Apply distortion effects if (showDistortion && portalActive) { distortions.forEach(distortion => { const distortionRadius = 50 * distortion.intensity; const gradient = ctx.createRadialGradient(distortion.x, distortion.y, 0, distortion.x, distortion.y, distortionRadius); gradient.addColorStop(0, "rgba(255, 255, 255, 0.02)"); gradient.addColorStop(1, "rgba(255, 255, 255, 0)"); ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); }); } // Draw event horizon if (showEventHorizon && portalActive) { const horizonGradient = ctx.createRadialGradient(centerX, centerY, currentRadius * 0.1, centerX, centerY, currentRadius); horizonGradient.addColorStop(0, `rgba(${currentColors.primary[0]}, ${currentColors.primary[1]}, ${currentColors.primary[2]}, 0.8)`); horizonGradient.addColorStop(0.7, `rgba(${currentColors.primary[0]}, ${currentColors.primary[1]}, ${currentColors.primary[2]}, 0.3)`); horizonGradient.addColorStop(1, "rgba(0, 0, 0, 0)"); ctx.fillStyle = horizonGradient; ctx.beginPath(); ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2); ctx.fill(); } // Draw vortex rings rings.forEach((ring, index) => { if (ring.opacity < 0.01) return; ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(ring.rotation); // Create ring gradient const ringGradient = ctx.createLinearGradient(-ring.radius, 0, ring.radius, 0); ringGradient.addColorStop(0, `rgba(${ring.color[0]}, ${ring.color[1]}, ${ring.color[2]}, 0)`); ringGradient.addColorStop(0.5, `rgba(${ring.color[0]}, ${ring.color[1]}, ${ring.color[2]}, ${ring.opacity})`); ringGradient.addColorStop(1, `rgba(${ring.color[0]}, ${ring.color[1]}, ${ring.color[2]}, 0)`); ctx.strokeStyle = ringGradient; ctx.lineWidth = ring.thickness; ctx.globalAlpha = ring.opacity * currentIntensity; // Draw multiple concentric lines for depth for (let i = 0; i < depth; i++) { const layerRadius = ring.radius + i * 2; const layerOpacity = (1 - i / depth) * ring.opacity; ctx.globalAlpha = layerOpacity * currentIntensity; ctx.beginPath(); ctx.arc(0, 0, layerRadius, 0, Math.PI * 2); ctx.stroke(); } ctx.restore(); }); // Draw particles particles.forEach(particle => { if (particle.opacity < 0.01) return; const particleGradient = ctx.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, particle.size); particleGradient.addColorStop(0, `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.opacity})`); particleGradient.addColorStop(1, `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, 0)`); ctx.fillStyle = particleGradient; ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fill(); }); // Draw portal center void if (portalActive && currentRadius > 10) { const voidGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, currentRadius * 0.3); voidGradient.addColorStop(0, "var(--glass-black)"); voidGradient.addColorStop(1, "rgba(0, 0, 0, 0)"); ctx.fillStyle = voidGradient; ctx.beginPath(); ctx.arc(centerX, centerY, currentRadius * 0.3, 0, Math.PI * 2); ctx.fill(); } // Pulse effect if (pulsing && portalActive) { const pulseIntensity = Math.sin(animationTime * pulseFrequency) * 0.3 + 0.7; const pulseGradient = ctx.createRadialGradient(centerX, centerY, currentRadius * 0.8, centerX, centerY, currentRadius * 1.2); pulseGradient.addColorStop(0, "rgba(255, 255, 255, 0)"); pulseGradient.addColorStop(1, `rgba(255, 255, 255, ${pulseIntensity * 0.1})`); ctx.fillStyle = pulseGradient; ctx.beginPath(); ctx.arc(centerX, centerY, currentRadius * 1.2, 0, Math.PI * 2); ctx.fill(); } // Debug info if (debug) { ctx.fillStyle = "white"; ctx.font = "12px monospace"; ctx.fillText(`Intensity: ${currentIntensity.toFixed(2)}`, 10, 20); ctx.fillText(`Radius: ${currentRadius.toFixed(0)}`, 10, 35); ctx.fillText(`Rings: ${rings.length}`, 10, 50); ctx.fillText(`Particles: ${particles.length}`, 10, 65); ctx.fillText(`Active: ${portalActive}`, 10, 80); } }, [width, height, currentRadius, currentIntensity, showDistortion, distortions, portalActive, showEventHorizon, colorScheme, rings, depth, particles, pulsing, animationTime, pulseFrequency, debug]); // Animation loop const animate = useCallback(currentTime => { if (prefersReducedMotion && respectMotionPreference) { render(); return; } const deltaTime = 0.016; // 60fps setAnimationTime(prev => prev + deltaTime); updateRings(deltaTime); updateParticles(deltaTime); render(); animationRef.current = requestAnimationFrame(animate); }, [prefersReducedMotion, respectMotionPreference, render, updateRings, updateParticles]); // Start animation useEffect(() => { animationRef.current = requestAnimationFrame(animate); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [animate]); // Canvas setup useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; canvas.width = width; canvas.height = height; }, [width, height]); // Handle portal interaction const handleCanvasClick = useCallback(event => { if (!interactive) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const centerX = width / 2; const centerY = height / 2; const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); // Check if click is within portal if (distance < currentRadius && portalActive) { onEntry?.(x, y); play("success"); } }, [interactive, width, height, currentRadius, portalActive, onEntry, play]); // Portal controls const renderControls = () => { if (!showControls) return null; return jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: "glass-portal-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: () => { setPortalActive(!portalActive); portalActive ? onDeactivate?.() : onActivate?.(); play("tap"); }, className: cn("glass-px-3 glass-py-1 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard", portalActive ? "bg-red-500/20 hover:bg-red-500/30 text-red-400" : "bg-green-500/20 hover:bg-green-500/30 text-green-400"), children: portalActive ? "Deactivate" : "Activate" }), jsx("button", { onClick: () => { initializeRings(); initializeParticles(); initializeDistortions(); play("success"); }, 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', children: "Reset" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("label", { className: "glass-text-sm", children: "Type:" }), jsxs("select", { value: type, onChange: e => {}, className: "glass-px-2 glass-py-1 glass-radius-md glass-surface-overlay glass-border glass-border-glass-border/20", children: [jsx("option", { value: "dimensional", children: "Dimensional" }), jsx("option", { value: "energy", children: "Energy" }), jsx("option", { value: "void", children: "Void" }), jsx("option", { value: "quantum", children: "Quantum" }), jsx("option", { value: "temporal", children: "Temporal" })] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("label", { className: "glass-text-sm", children: "Color:" }), jsxs("select", { value: colorScheme, onChange: e => {}, className: "glass-px-2 glass-py-1 glass-radius-md glass-surface-overlay glass-border glass-border-glass-border/20", children: [jsx("option", { value: "blue", children: "Blue" }), jsx("option", { value: "purple", children: "Purple" }), jsx("option", { value: "green", children: "Green" }), jsx("option", { value: "red", children: "Red" }), jsx("option", { value: "gold", children: "Gold" }), jsx("option", { value: "cosmic", children: "Cosmic" })] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("label", { className: "glass-text-sm", children: "Intensity:" }), jsx("input", { type: "range", min: "0", max: "1", step: "0.1", value: currentIntensity, onChange: e => setCurrentIntensity(parseFloat(e.target.value)), className: 'w-20 glass-focus glass-touch-target glass-contrast-guard' }), jsxs("span", { className: 'glass-text-sm min-w-[3ch]', children: [(currentIntensity * 100).toFixed(0), "%"] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsxs("label", { className: "glass-text-sm", children: [jsx("input", { type: "checkbox", checked: pulsing, onChange: e => {}, className: "glass-mr-1 glass-focus glass-touch-target glass-contrast-guard" }), "Pulse"] }), jsxs("label", { className: "glass-text-sm", children: [jsx("input", { type: "checkbox", checked: showDistortion, onChange: e => {}, className: "glass-mr-1 glass-focus glass-touch-target glass-contrast-guard" }), "Distortion"] }), jsxs("label", { className: "glass-text-sm", children: [jsx("input", { type: "checkbox", checked: debug, onChange: e => {}, className: "glass-mr-1 glass-focus glass-touch-target glass-contrast-guard" }), "Debug"] })] })] }); }; return jsx(OptimizedGlassCore, { ref: ref, id: portalId, elevation: "level1", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-vortex-portal 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", interactive && "cursor-pointer"), onClick: handleCanvasClick, style: { width, height } }) })] }) }); }); GlassVortexPortal.displayName = "GlassVortexPortal"; export { GlassVortexPortal, GlassVortexPortal as default }; //# sourceMappingURL=GlassVortexPortal.js.map