aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
466 lines (461 loc) • 12.6 kB
JavaScript
import { isBrowser, getSafeNavigator, getSafeDocument, getSafeWindow } from './env.js';
/**
* AuraGlass Sound Design System
* Haptic and audio feedback for glass interactions
*/
class GlassSoundDesign {
constructor() {
this.audioContext = null;
this.sounds = new Map();
this.oscillators = new Map();
this.gainNodes = new Map();
this.enabled = true;
this.hapticEnabled = true;
// Predefined glass sounds
this.glassSounds = {
tap: {
frequency: 800,
duration: 50,
volume: 0.3,
type: 'sine'
},
hover: {
frequency: 400,
duration: 30,
volume: 0.1,
type: 'triangle'
},
slide: {
frequency: 600,
duration: 100,
volume: 0.2,
type: 'sine'
},
break: {
frequency: 1200,
duration: 200,
volume: 0.5,
type: 'sawtooth'
},
morph: {
frequency: 500,
duration: 150,
volume: 0.25,
type: 'sine'
},
ripple: {
frequency: 300,
duration: 300,
volume: 0.15,
type: 'sine'
},
success: {
frequency: 880,
duration: 150,
volume: 0.4,
type: 'sine'
},
error: {
frequency: 200,
duration: 200,
volume: 0.4,
type: 'square'
},
notification: {
frequency: 660,
duration: 100,
volume: 0.3,
type: 'sine'
}
};
// Haptic patterns
this.hapticPatterns = {
tap: {
intensity: 1,
duration: 10
},
hover: {
intensity: 0.3,
duration: 5
},
slide: {
intensity: 0.5,
duration: 20
},
success: {
intensity: 0.8,
duration: 50,
pattern: [10, 10, 10]
},
error: {
intensity: 1,
duration: 100,
pattern: [50, 50]
},
notification: {
intensity: 0.6,
duration: 30,
pattern: [10, 10, 10, 10]
},
longPress: {
intensity: 0.7,
duration: 200
},
swipe: {
intensity: 0.4,
duration: 15
}
};
// Do NOT create AudioContext at import time.
if (isBrowser()) {
// Set up gesture listeners to initialize audio lazily on first user interaction.
this.setupGestureListeners();
this.checkHapticSupport();
} else {
this.enabled = false;
this.hapticEnabled = false;
}
}
/**
* Attach one-time listeners to create/resume AudioContext after a user gesture
*/
setupGestureListeners() {
const doc = getSafeDocument();
const win = getSafeWindow();
if (!doc || !win) return;
const enable = async () => {
const currentWin = getSafeWindow();
const currentDoc = getSafeDocument();
if (!currentWin || !currentDoc) return;
try {
if (!this.audioContext) {
this.audioContext = new (currentWin.AudioContext || currentWin.webkitAudioContext)();
}
if (this.audioContext?.state === 'suspended') {
await this.audioContext.resume();
}
} catch (e) {
console.warn('Failed to initialize audio after gesture:', e);
} finally {
currentDoc.removeEventListener('click', enable, true);
currentDoc.removeEventListener('touchstart', enable, true);
currentDoc.removeEventListener('keydown', enable, true);
}
};
// Capture phase to run before most bubbling handlers, and once
doc.addEventListener('click', enable, {
capture: true,
once: true,
passive: true
});
doc.addEventListener('touchstart', enable, {
capture: true,
once: true,
passive: true
});
doc.addEventListener('keydown', enable, {
capture: true,
once: true,
passive: true
});
}
static getInstance() {
if (!GlassSoundDesign.instance) {
GlassSoundDesign.instance = new GlassSoundDesign();
}
return GlassSoundDesign.instance;
}
/**
* Get available glass sound types
*/
getGlassSoundTypes() {
return Object.keys(this.glassSounds);
}
/**
* Get available haptic pattern types
*/
getHapticPatternTypes() {
return Object.keys(this.hapticPatterns);
}
/**
* Initialize Web Audio API context
*/
initAudioContext() {
const win = getSafeWindow();
if (!win) return;
try {
if (!this.audioContext) {
this.audioContext = new (win.AudioContext || win.webkitAudioContext)();
}
} catch (error) {
console.warn('Web Audio API not supported:', error);
this.enabled = false;
}
}
/**
* Allow callers to explicitly request audio enablement (e.g., from a button onClick)
*/
async requestEnableAudio() {
if (!isBrowser()) return false;
try {
this.initAudioContext();
if (this.audioContext?.state === 'suspended') {
await this.audioContext.resume();
}
return !!this.audioContext && this.audioContext.state !== 'suspended';
} catch (e) {
console.warn('Could not enable audio:', e);
return false;
}
}
/**
* Check if haptic feedback is supported
*/
checkHapticSupport() {
const nav = getSafeNavigator();
this.hapticEnabled = !!nav && 'vibrate' in nav;
}
/**
* Play a synthetic glass sound
*/
playGlassSound(type, customConfig) {
if (!this.enabled || !this.audioContext) return;
const config = {
...this.glassSounds[type],
...customConfig
};
try {
// Create oscillator
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
// Configure oscillator
oscillator.type = config.type;
oscillator.frequency.setValueAtTime(config.frequency, this.audioContext.currentTime);
// Configure gain (volume)
gainNode.gain.setValueAtTime(config.volume, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + config.duration / 1000);
// Connect nodes
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// Play sound
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + config.duration / 1000);
// Cleanup
oscillator.onended = () => {
oscillator.disconnect();
gainNode.disconnect();
};
} catch (error) {
console.warn('Error playing glass sound:', error);
}
}
/**
* Play a chord (multiple frequencies)
*/
playChord(frequencies, duration = 200, volume = 0.3) {
if (!this.enabled || !this.audioContext) return;
frequencies.forEach(freq => {
this.playGlassSound('tap', {
frequency: freq,
duration,
volume: volume / frequencies.length
});
});
}
/**
* Play an arpeggio
*/
playArpeggio(frequencies, noteDuration = 100, noteDelay = 50, volume = 0.3) {
if (!this.enabled || !this.audioContext) return;
frequencies.forEach((freq, index) => {
setTimeout(() => {
this.playGlassSound('tap', {
frequency: freq,
duration: noteDuration,
volume
});
}, index * noteDelay);
});
}
/**
* Create ambient glass atmosphere
*/
createAmbientGlass(config = {}) {
if (!this.enabled || !this.audioContext) return null;
const {
baseFrequency = 200,
modulationRate = 0.5,
volume = 0.1
} = config;
try {
// Create nodes
const oscillator = this.audioContext.createOscillator();
const modulatorOsc = this.audioContext.createOscillator();
const modulatorGain = this.audioContext.createGain();
const mainGain = this.audioContext.createGain();
// Configure main oscillator
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(baseFrequency, this.audioContext.currentTime);
// Configure modulator
modulatorOsc.type = 'sine';
modulatorOsc.frequency.setValueAtTime(modulationRate, this.audioContext.currentTime);
modulatorGain.gain.setValueAtTime(50, this.audioContext.currentTime);
// Configure main gain
mainGain.gain.setValueAtTime(volume, this.audioContext.currentTime);
// Connect modulation
modulatorOsc.connect(modulatorGain);
modulatorGain.connect(oscillator.frequency);
// Connect to output
oscillator.connect(mainGain);
mainGain.connect(this.audioContext.destination);
// Start oscillators
oscillator.start();
modulatorOsc.start();
// Store for later control
const id = Date.now().toString();
this.oscillators.set(id, oscillator);
this.gainNodes.set(id, mainGain);
return {
id,
stop: () => {
oscillator.stop();
modulatorOsc.stop();
oscillator.disconnect();
modulatorOsc.disconnect();
modulatorGain.disconnect();
mainGain.disconnect();
this.oscillators.delete(id);
this.gainNodes.delete(id);
},
setVolume: newVolume => {
mainGain.gain.setValueAtTime(newVolume, this.audioContext.currentTime);
}
};
} catch (error) {
console.warn('Error creating ambient glass:', error);
return null;
}
}
/**
* Trigger haptic feedback
*/
triggerHaptic(pattern) {
if (!this.hapticEnabled) return;
const config = typeof pattern === 'string' ? this.hapticPatterns[pattern] : pattern;
try {
const nav = getSafeNavigator();
if (!nav || typeof nav.vibrate !== 'function') return;
if ('pattern' in config && config.pattern && Array.isArray(config.pattern)) {
nav.vibrate(config.pattern);
} else {
nav.vibrate(config.duration);
}
} catch (error) {
console.warn('Error triggering haptic:', error);
}
}
/**
* Combined sound and haptic feedback
*/
playFeedback(type) {
// Play sound
if (this.glassSounds[type]) {
this.playGlassSound(type);
}
// Trigger haptic
if (this.hapticPatterns[type]) {
this.triggerHaptic(type);
}
}
/**
* Create a glass resonance effect
*/
createResonance(frequency = 440, decay = 2) {
if (!this.enabled || !this.audioContext) return;
const impulseLength = this.audioContext.sampleRate * decay;
const impulse = this.audioContext.createBuffer(2, impulseLength, this.audioContext.sampleRate);
for (let channel = 0; channel < 2; channel++) {
const channelData = impulse.getChannelData(channel);
for (let i = 0; i < impulseLength; i++) {
channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / impulseLength, 2);
}
}
const convolver = this.audioContext.createConvolver();
convolver.buffer = impulse;
this.playGlassSound('tap', {
frequency
});
}
/**
* Enable/disable sound
*/
setEnabled(enabled) {
this.enabled = enabled;
}
/**
* Enable/disable haptics
*/
setHapticEnabled(enabled) {
const nav = getSafeNavigator();
this.hapticEnabled = !!nav && enabled && 'vibrate' in nav;
}
/**
* Get audio context state
*/
getState() {
return {
soundEnabled: this.enabled,
hapticEnabled: this.hapticEnabled,
audioContextState: this.audioContext?.state
};
}
/**
* Cleanup
*/
destroy() {
// Stop all oscillators
this.oscillators.forEach(osc => {
try {
osc.stop();
osc.disconnect();
} catch {}
});
// Disconnect all gain nodes
this.gainNodes.forEach(gain => {
try {
gain.disconnect();
} catch {}
});
// Clear maps
this.oscillators.clear();
this.gainNodes.clear();
this.sounds.clear();
// Close audio context
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
}
}
// Export singleton instance
const glassSoundDesign = GlassSoundDesign.getInstance();
// React hook for sound design
function useGlassSound() {
const play = type => {
glassSoundDesign.playGlassSound(type);
};
const haptic = pattern => {
glassSoundDesign.triggerHaptic(pattern);
};
const feedback = type => {
glassSoundDesign.playFeedback(type);
};
return {
play,
haptic,
feedback,
soundDesign: glassSoundDesign
};
}
export { GlassSoundDesign, glassSoundDesign, useGlassSound };
//# sourceMappingURL=soundDesign.js.map