aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
671 lines (668 loc) • 23.7 kB
JavaScript
'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