UNPKG

aura-glass

Version:

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

671 lines (668 loc) 23.7 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useRef, useState, useEffect, useCallback, useContext, createContext } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; // Pre-defined glass sounds const GLASS_SOUNDS = { tap: { name: "Glass Tap", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmUaBy2I1/LNdCYCKnbK7+OUQwouetDrQVNFSYd8LE/T9YNJOgVMnN/xy2MaCUaQ2fLKdCYEKnbK7+GUQQsUXrTp66lWFApBn+TyvWUaByuI1/PNdCYCK3fK7+OUQgsuetDryVNFN45+LE/T9YVJOgVMnt/xy2IdCkaQ2fLKdCgEKnbK7+GUQQsUXrTp65ZWFApGn+TyvWUaBiuI1/PNdCgDKnfK7+OUQgouetDryVNFTYd8LE/T9YFJOgVRnt/xy2MdCkaQ2fLKdCgEKnbK7+GUQQsUXrTp66hWFApGn+TyvWUaCy2I1/PNdCcCKnfK7+OUQgsuetDryVNFSId8LE/T9YVJOgVMn9/xy2MdCkaQ2fLKdCgEKnbK7+GUQQsUXrTp66lWFApGn+TyvWUaCy2I1/PNdCcCKnfK7+OUQgsuetDryVNFT4d8LE/T9YVJOgVMnt/xy2MdCkaQ2fLKdCgEKnbK7+GUQQsUXrTp66lWFApGn+TyvWUaBy2I1/PNdCcCKnfK7+OUQgsuetDryVNFTId8LE/T9YRJOgVMn9/xy2MdCkaQ2fLKdCcEKnbK7+GUQgouetDryVNFSId8LE/T9YVJOgVMntr/uuUaBy2I1/PNdCcCKnbK7+OUQgsuetDryVNFTId8LE/T9YVJOgVMntr/xy2MdCkaQ2fLKdCgEKnbK7+GUQgouetDryVNFTId8LE/T9YVJOgVMntr/xy2MdCkaQ2fLKdCgEKnbK7+GUQgouetDryVNFTId8LE/T9YVJOgVMntr/xy2MdCkaQ2fLKdCgEKnbK7+GUQgouetDryVNFTId8", volume: 0.7, category: "ui", spatial: true, variations: ["tap1", "tap2", "tap3"] }, hover: { name: "Glass Hover", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj", volume: 0.4, category: "ui", spatial: true }, slide: { name: "Glass Slide", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAAC", volume: 0.6, category: "ui", spatial: true }, break: { name: "Glass Break", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQ", volume: 0.8, category: "feedback", spatial: true }, ambientGlass: { name: "Ambient Glass", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0", volume: 0.3, category: "ambient", spatial: true }, notification: { name: "Glass Bell", url: "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgA", volume: 0.7, category: "notification", spatial: true } }; // Spatial audio engine class SpatialAudioEngine { constructor(settings = {}) { this.context = null; this.listener = null; this.sources = new Map(); this.buffers = new Map(); this.masterGain = null; this.isInitialized = false; this.settings = { masterVolume: 0.7, enableSpatial: true, enableHRTF: true, distanceModel: "inverse", maxDistance: 10000, rolloffFactor: 1, doppler: false, ...settings }; } async initialize() { try { // Create audio context this.context = new (window.AudioContext || window.webkitAudioContext)(); if (!this.context) { throw new Error("Web Audio API not supported"); } // Resume context if suspended if (this.context.state === "suspended") { await this.context.resume(); } // Get listener this.listener = this.context.listener; // Create master gain this.masterGain = this.context.createGain(); this.masterGain.gain.value = this.settings.masterVolume; this.masterGain.connect(this.context.destination); // Set up spatial audio if supported if (this.listener && this.settings.enableSpatial) { this.setupSpatialAudio(); } // Preload glass sounds await this.preloadSounds(); this.isInitialized = true; return true; } catch (error) { console.error("Failed to initialize spatial audio:", error); return false; } } // Public getter for context getContext() { return this.context; } // Public getter for sources getSources() { return this.sources; } setupSpatialAudio() { if (!this.listener) return; // Set listener orientation (forward and up vectors) if (this.listener.forwardX) { // New Web Audio API this.listener.forwardX.value = 0; this.listener.forwardY.value = 0; this.listener.forwardZ.value = -1; this.listener.upX.value = 0; this.listener.upY.value = 1; this.listener.upZ.value = 0; } else { // Legacy Web Audio API this.listener.setOrientation?.(0, 0, -1, 0, 1, 0); } // Set listener position if (this.listener.positionX) { this.listener.positionX.value = 0; this.listener.positionY.value = 0; this.listener.positionZ.value = 0; } else { this.listener.setPosition?.(0, 0, 0); } } async preloadSounds() { const loadPromises = Object.entries(GLASS_SOUNDS).map(async ([key, sound]) => { try { // For demo purposes, we'll create synthetic glass sounds const buffer = await this.createSyntheticGlassSound(sound.name, sound.category); this.buffers.set(key, buffer); } catch (error) { console.warn(`Failed to load sound ${key}:`, error); } }); await Promise.all(loadPromises); } async createSyntheticGlassSound(name, category) { if (!this.context) { throw new Error("Audio context not initialized"); } const sampleRate = this.context.sampleRate; const duration = category === "ambient" ? 2.0 : 0.5; const buffer = this.context.createBuffer(2, sampleRate * duration, sampleRate); // Generate glass-like sounds based on category for (let channel = 0; channel < buffer.numberOfChannels; channel++) { const channelData = buffer.getChannelData(channel); for (let i = 0; i < channelData.length; i++) { const t = i / sampleRate; let sample = 0; switch (category) { case "ui": // Sharp, metallic sound sample = Math.sin(2 * Math.PI * 2000 * t) * Math.exp(-t * 8) * 0.3; sample += Math.sin(2 * Math.PI * 3000 * t) * Math.exp(-t * 12) * 0.2; break; case "feedback": // Glass breaking sound sample = (Math.random() - 0.5) * Math.exp(-t * 3) * 0.6; sample += Math.sin(2 * Math.PI * 1500 * t) * Math.exp(-t * 5) * 0.4; break; case "ambient": // Soft, ethereal glass ambient sample = Math.sin(2 * Math.PI * 220 * t) * Math.sin(t * 0.5) * 0.1; sample += Math.sin(2 * Math.PI * 440 * t) * Math.sin(t * 0.3) * 0.05; break; case "notification": // Bell-like glass sound sample = Math.sin(2 * Math.PI * 800 * t) * Math.exp(-t * 2) * 0.4; sample += Math.sin(2 * Math.PI * 1200 * t) * Math.exp(-t * 3) * 0.3; break; } channelData[i] = sample; } } return buffer; } createSource(id, soundKey, position) { if (!this.context || !this.masterGain) return null; const buffer = this.buffers.get(soundKey); const soundConfig = GLASS_SOUNDS[soundKey]; if (!buffer || !soundConfig) return null; // Create audio nodes const source = this.context.createBufferSource(); const gain = this.context.createGain(); const panner = this.context.createPanner(); // Configure buffer source source.buffer = buffer; source.loop = false; // Configure gain gain.gain.value = soundConfig.volume; // Configure panner for spatial audio if (this.settings.enableSpatial && soundConfig.spatial) { panner.panningModel = this.settings.enableHRTF ? "HRTF" : "equalpower"; panner.distanceModel = this.settings.distanceModel; panner.maxDistance = this.settings.maxDistance; panner.rolloffFactor = this.settings.rolloffFactor; // Set position if (panner.positionX) { panner.positionX.value = position.x; panner.positionY.value = position.y; panner.positionZ.value = position.z; } else { panner.setPosition?.(position.x, position.y, position.z); } // Connect: source -> gain -> panner -> master gain -> destination source.connect(gain); gain.connect(panner); panner.connect(this.masterGain); } else { // Non-spatial: source -> gain -> master gain -> destination source.connect(gain); gain.connect(this.masterGain); } const audioSource = { id, position, volume: soundConfig.volume, buffer, source, panner: this.settings.enableSpatial && soundConfig.spatial ? panner : null, gain, isPlaying: false, loop: false, category: soundConfig.category }; this.sources.set(id, audioSource); return audioSource; } playSound(id, soundKey, position, options = {}) { if (!this.isInitialized) return; // Stop existing source if playing this.stopSound(id); const source = this.createSource(id, soundKey, position); if (!source) return; // Apply options if (options.volume !== undefined && source.gain) { source.gain.gain.value = options.volume; } if (options.loop && source.source) { source.source.loop = true; source.loop = true; } // Play if (source.source) { const when = this.context.currentTime + (options.delay || 0); source.source.start(when); source.isPlaying = true; // Handle ended event for non-looping sounds if (!source.loop) { source.source.addEventListener("ended", () => { this.sources.delete(id); }); } } } stopSound(id) { const source = this.sources.get(id); if (source && source.source && source.isPlaying) { try { source.source.stop(); } catch (error) { // Source might already be stopped } source.isPlaying = false; this.sources.delete(id); } } updateSourcePosition(id, position) { const source = this.sources.get(id); if (source && source.panner) { if (source.panner.positionX) { source.panner.positionX.value = position.x; source.panner.positionY.value = position.y; source.panner.positionZ.value = position.z; } else { source.panner.setPosition?.(position.x, position.y, position.z); } source.position = position; } } updateListenerPosition(position) { if (!this.listener) return; if (this.listener.positionX) { this.listener.positionX.value = position.x; this.listener.positionY.value = position.y; this.listener.positionZ.value = position.z; } else { this.listener.setPosition?.(position.x, position.y, position.z); } } updateListenerOrientation(forward, up) { if (!this.listener) return; if (this.listener.forwardX) { this.listener.forwardX.value = forward.x; this.listener.forwardY.value = forward.y; this.listener.forwardZ.value = forward.z; this.listener.upX.value = up.x; this.listener.upY.value = up.y; this.listener.upZ.value = up.z; } else { this.listener.setOrientation?.(forward.x, forward.y, forward.z, up.x, up.y, up.z); } } setMasterVolume(volume) { this.settings.masterVolume = Math.max(0, Math.min(1, volume)); if (this.masterGain) { this.masterGain.gain.value = this.settings.masterVolume; } } getMasterVolume() { return this.settings.masterVolume; } getActiveSources() { return Array.from(this.sources.values()); } cleanup() { // Stop all sources this.sources.forEach((source, id) => { this.stopSound(id); }); // Close context if (this.context && this.context.state !== "closed") { this.context.close(); } this.sources.clear(); this.buffers.clear(); this.isInitialized = false; } } // React context for spatial audio const SpatialAudioContext = /*#__PURE__*/createContext({ engine: null, isInitialized: false, masterVolume: 0.7, playGlassSound: () => "", stopGlassSound: () => {}, setMasterVolume: () => {} }); // Provider component function GlassSpatialAudioProvider({ children, settings, autoInitialize = true }) { useReducedMotion(); const engineRef = useRef(); const [isInitialized, setIsInitialized] = useState(false); const [masterVolume, setMasterVolumeState] = useState(0.7); const soundIdCounter = useRef(0); // Initialize engine useEffect(() => { engineRef.current = new SpatialAudioEngine(settings); if (autoInitialize) { // Initialize on first user interaction const initializeOnInteraction = () => { if (engineRef.current && !isInitialized) { engineRef.current.initialize().then(success => { setIsInitialized(success); if (success) { setMasterVolumeState(engineRef.current.getMasterVolume()); } }); } document.removeEventListener("click", initializeOnInteraction); document.removeEventListener("keydown", initializeOnInteraction); document.removeEventListener("touchstart", initializeOnInteraction); }; document.addEventListener("click", initializeOnInteraction); document.addEventListener("keydown", initializeOnInteraction); document.addEventListener("touchstart", initializeOnInteraction); return () => { document.removeEventListener("click", initializeOnInteraction); document.removeEventListener("keydown", initializeOnInteraction); document.removeEventListener("touchstart", initializeOnInteraction); }; } return () => { if (engineRef.current) { engineRef.current.cleanup(); } }; }, [autoInitialize, isInitialized, settings]); const playGlassSound = useCallback((soundKey, position = { x: 0, y: 0, z: 0 }, options = {}) => { if (!engineRef.current || !isInitialized) return ""; const id = `sound_${++soundIdCounter.current}`; engineRef.current.playSound(id, soundKey, position, options); return id; }, [isInitialized]); const stopGlassSound = useCallback(id => { if (engineRef.current) { engineRef.current.stopSound(id); } }, []); const setMasterVolume = useCallback(volume => { if (engineRef.current) { engineRef.current.setMasterVolume(volume); setMasterVolumeState(volume); } }, []); const value = { engine: engineRef.current || null, isInitialized, masterVolume, playGlassSound, stopGlassSound, setMasterVolume }; return jsx(SpatialAudioContext.Provider, { value: value, children: children }); } const GlassSpatialAudio = ({ settings, autoInitialize = true, className, children, ...rest }) => { return jsx(GlassSpatialAudioProvider, { settings: settings, autoInitialize: autoInitialize, children: jsx("div", { "data-glass-component": true, className: cn('relative', className), ...rest, children: children ?? jsx("span", { className: "sr-only", children: "Glass spatial audio provider active" }) }) }); }; // Hook to use spatial audio function useSpatialAudio() { const context = useContext(SpatialAudioContext); if (!context) { throw new Error("useSpatialAudio must be used within GlassSpatialAudioProvider"); } return context; } // Component for audio-reactive glass effects function GlassAudioReactive({ children, className, soundKey = "ambientGlass", position, reactToVolume = true, reactToFrequency = false, intensityMultiplier = 1 }) { const prefersReducedMotion = useReducedMotion(); const { engine, playGlassSound, stopGlassSound } = useSpatialAudio(); const [audioIntensity, setAudioIntensity] = useState(0); const soundIdRef = useRef(""); const analyzerRef = useRef(); // Start ambient sound and analysis useEffect(() => { if (!engine || !position) return; // Play ambient sound soundIdRef.current = playGlassSound(soundKey, position, { loop: true, volume: 0.3 }); // Set up audio analysis if needed const audioContext = engine.getContext(); if ((reactToVolume || reactToFrequency) && audioContext) { analyzerRef.current = audioContext.createAnalyser(); analyzerRef.current.fftSize = 256; const source = engine.getSources().get(soundIdRef.current); if (source && source.gain) { source.gain.connect(analyzerRef.current); } // Start analysis loop const analyze = () => { if (!analyzerRef.current) return; const dataArray = new Uint8Array(analyzerRef.current.frequencyBinCount); if (reactToVolume) { analyzerRef.current.getByteFrequencyData(dataArray); const average = dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length; setAudioIntensity(average / 255 * intensityMultiplier); } requestAnimationFrame(analyze); }; analyze(); } return () => { if (soundIdRef.current) { stopGlassSound(soundIdRef.current); } }; }, [engine, soundKey, position, playGlassSound, stopGlassSound, reactToVolume, reactToFrequency, intensityMultiplier]); return jsx(motion.div, { className: cn("glass-audio-reactive", className), animate: { scale: 1 + audioIntensity * 0.1, opacity: 0.8 + audioIntensity * 0.2, backgroundColor: `rgba(255, 255, 255, ${0.1 + audioIntensity * 0.1})` }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: children }); } // 3D Audio visualizer function GlassSpatialVisualizer({ className, show = true }) { const prefersReducedMotion = useReducedMotion(); const { engine } = useSpatialAudio(); const [sources, setSources] = useState([]); useEffect(() => { if (!engine || !show) return; const interval = setInterval(() => { setSources(engine.getActiveSources()); }, 100); return () => clearInterval(interval); }, [engine, show]); if (!show) return null; return jsxs("div", { className: cn("glass-fixed glass-bottom-4 glass-right-4 glass-w-64 glass-h-64 glass-surface-primary glass-elev-3 glass-radius-lg glass-p-4", className), children: [jsx("div", { className: 'glass-text-xs text-primary mb-2', children: "Spatial Audio" }), jsxs("div", { className: 'relative glass-w-full glass-h-full glass-gradient-primary glass-gradient-primary glass-gradient-primary glass-radius-lg overflow-hidden', children: [jsxs("div", { className: 'absolute inset-0', children: [Array.from({ length: 8 }, (_, i) => jsx("div", { className: "glass-absolute glass-w-full glass-h-px glass-surface-subtle/10", ref: el => { if (el) el.style.top = `${(i + 1) * 12.5}%`; } }, `h-${i}`)), Array.from({ length: 8 }, (_, i) => jsx("div", { className: "glass-absolute glass-h-full glass-w-px glass-surface-subtle/10", ref: el => { if (el) el.style.left = `${(i + 1) * 12.5}%`; } }, `v-${i}`))] }), jsx("div", { className: "glass-absolute glass-w-2 glass-h-2 glass-surface-green glass-radius-full glass-top-1-2 glass-left-1-2 glass-translate-x-1/2-neg glass-translate-y-1/2-neg", children: jsx("div", { className: 'absolute w-4 h-4 glass-border glass-border-green glass-radius-full glass-top-1 -left-1 animate-pulse' }) }), jsx(AnimatePresence, { children: sources.map(source => { // Convert 3D position to 2D screen position const screenX = (source.position.x + 1) * 50; // -1 to 1 -> 0 to 100% const screenY = (1 - source.position.y) * 50; // -1 to 1 -> 100 to 0% return jsx(motion.div, { className: 'absolute w-3 h-3 glass-radius-full transform -translate-x-1/2 -translate-y-1/2', style: { left: `${screenX}%`, top: `${screenY}%`, backgroundColor: source.category === "ambient" ? "var(--glass-color-primary)" : source.category === "ui" ? "var(--glass-color-success)" : source.category === "feedback" ? "var(--glass-color-warning)" : "#ec4899" }, initial: { scale: 0, opacity: 0 }, animate: prefersReducedMotion ? {} : { scale: source.isPlaying ? [1, 1.2, 1] : 1, opacity: source.isPlaying ? 1 : 0.5 }, exit: { scale: 0, opacity: 0 }, transition: { scale: { repeat: source.isPlaying ? Infinity : 0, duration: 1 } }, children: source.isPlaying && jsx(motion.div, { className: 'absolute inset-0 glass-radius-full glass-border opacity-30', style: { borderColor: source.category === "ambient" ? "var(--glass-color-primary)" : source.category === "ui" ? "var(--glass-color-success)" : source.category === "feedback" ? "var(--glass-color-warning)" : "#ec4899" }, animate: prefersReducedMotion ? {} : { scale: [1, 3, 1], opacity: [0.3, 0, 0.3] }, transition: prefersReducedMotion ? { duration: 0 } : { repeat: Infinity, duration: 2, ease: "easeOut" } }) }, source.id); }) }), jsxs("div", { className: 'absolute bottom-2 left-2 glass-text-xs glass-text-secondary', children: [jsxs("div", { className: 'glass-flex glass-items-center glass-gap-1 mb-1', children: [jsx("div", { className: 'w-2 h-2 glass-surface-green glass-radius-full' }), jsx("span", { children: "Listener" })] }), jsxs("div", { className: "glass-grid glass-grid-cols-2 glass-gap-1", children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx("div", { className: 'w-2 h-2 glass-surface-blue glass-radius-full' }), jsx("span", { children: "Ambient" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx("div", { className: 'w-2 h-2 bg-emerald-500 glass-radius-full' }), jsx("span", { children: "UI" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx("div", { className: 'w-2 h-2 bg-amber-500 glass-radius-full' }), jsx("span", { children: "Feedback" })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx("div", { className: 'w-2 h-2 bg-pink-500 glass-radius-full' }), jsx("span", { children: "Alert" })] })] })] })] })] }); } export { GlassAudioReactive, GlassSpatialAudio, GlassSpatialAudioProvider, GlassSpatialVisualizer, useSpatialAudio }; //# sourceMappingURL=GlassSpatialAudio.js.map