UNPKG

aura-glass

Version:

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

739 lines (736 loc) 27.5 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { motion } from 'framer-motion'; import { forwardRef, useState, useRef, useCallback, useEffect } from 'react'; import { useMotionPreference } from '../../hooks/useMotionPreference.js'; 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 '../../primitives/motion/MotionFramer.js'; import { useA11yId } from '../../utils/a11y.js'; import { createGlassStyle } from '../../utils/createGlassStyle.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const defaultAudioSettings = { volume: 0.8, gain: 1.0, bassBoost: 0, trebleBoost: 0, smoothing: 0.8, fftSize: 256 }; const defaultVisualSettings = { mode: "bars", colorScheme: "rainbow", particleCount: 100, sensitivity: 1.0, symmetry: false, mirror: false }; const colorSchemes = { rainbow: ["#ff0000", "#ff8000", "#ffff00", "#80ff00", "#00ff00", "#00ff80", "#00ffff", "#0080ff", "#0000ff", "#8000ff"], monochrome: ["var(--glass-white)", "#e0e0e0", "#c0c0c0", "#a0a0a0", "#808080", "#606060", "#404040", "#202020"], neon: ["#ff00ff", "#ff0080", "#ff0040", "#ff8040", "#ffff40", "#80ff40", "#40ff40", "#40ff80", "#40ffff", "#4080ff"], fire: ["#ffff00", "#ffcc00", "#ff9900", "#ff6600", "#ff3300", "#ff0000", "#cc0000", "#990000"], ice: ["var(--glass-white)", "#e0f0ff", "#c0e0ff", "#a0d0ff", "#80c0ff", "#60b0ff", "#40a0ff", "#2090ff"], galaxy: ["#1a1a2e", "#16213e", "#0f3460", "#533483", "#7209b7", "#a663cc", "#4cc9f0"] }; const GlassMusicVisualizer = /*#__PURE__*/forwardRef(({ audioSource, audioSettings = {}, visualSettings = {}, showControls = true, showFrequencyDisplay = true, showWaveform = true, showSpectrum = true, realTimeAnalysis = true, enableInteraction = true, enableRecording = false, canvasWidth = 800, canvasHeight = 400, onAudioLoad, onFrequencyData, onBeatDetected, className = "", ...props }, ref) => { useReducedMotion(); const [isPlaying, setIsPlaying] = useState(false); const [isRecording, setIsRecording] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [frequencyData, setFrequencyData] = useState(new Uint8Array(128)); const [waveformData, setWaveformData] = useState(new Uint8Array(128)); const [beatIntensity, setBeatIntensity] = useState(0); const [audioConfig, setAudioConfig] = useState({ ...defaultAudioSettings, ...audioSettings }); const [visualConfig, setVisualConfig] = useState({ ...defaultVisualSettings, ...visualSettings }); const canvasRef = useRef(null); const audioRef = useRef(null); const audioContextRef = useRef(null); const analyserRef = useRef(null); const sourceRef = useRef(null); const animationFrameRef = useRef(); const particles = useRef([]); useA11yId("glass-music-visualizer"); const { shouldAnimate } = useMotionPreference(); const { play } = useGlassSound(); // Initialize audio context and analyser const initializeAudio = useCallback(async () => { try { if (!audioContextRef.current) { audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); } const context = audioContextRef.current; const analyser = context.createAnalyser(); analyser.fftSize = audioConfig.fftSize; analyser.smoothingTimeConstant = audioConfig.smoothing; analyserRef.current = analyser; // Initialize frequency and waveform data arrays const bufferLength = analyser.frequencyBinCount; setFrequencyData(new Uint8Array(bufferLength)); setWaveformData(new Uint8Array(bufferLength)); // Connect audio source if provided if (audioSource && typeof audioSource === "string") { const audio = audioRef.current; if (audio) { audio.src = audioSource; const source = context.createMediaElementSource(audio); source.connect(analyser); analyser.connect(context.destination); sourceRef.current = source; } } else if (audioSource instanceof MediaStream) { const source = context.createMediaStreamSource(audioSource); source.connect(analyser); analyser.connect(context.destination); sourceRef.current = source; } } catch (error) { console.error("Failed to initialize audio:", error); } }, [audioSource, audioConfig.fftSize, audioConfig.smoothing]); // Beat detection algorithm const detectBeat = useCallback(frequencyData => { const bassRange = Math.floor(frequencyData.length * 0.1); const midRange = Math.floor(frequencyData.length * 0.3); let bassSum = 0; let midSum = 0; for (let i = 0; i < bassRange; i++) { bassSum += frequencyData[i]; } for (let i = bassRange; i < midRange; i++) { midSum += frequencyData[i]; } const bassAvg = bassSum / bassRange; const midAvg = midSum / (midRange - bassRange); const intensity = (bassAvg + midAvg) / 2 / 255; setBeatIntensity(intensity); if (intensity > 0.7) { onBeatDetected?.(intensity); } return intensity; }, [onBeatDetected]); // Visualization rendering const renderVisualization = useCallback(() => { const canvas = canvasRef.current; const analyser = analyserRef.current; if (!canvas || !analyser) return; const ctx = canvas.getContext("2d"); if (!ctx) return; // Update data arrays const frequencyArray = new Uint8Array(analyser.frequencyBinCount); const waveformArray = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(frequencyArray); analyser.getByteTimeDomainData(waveformArray); setFrequencyData(frequencyArray); setWaveformData(waveformArray); onFrequencyData?.(frequencyArray); // Detect beats const beat = detectBeat(frequencyArray); // Clear canvas ctx.fillStyle = "rgba(var(--glass-color-black) / var(--glass-opacity-10))"; ctx.fillRect(0, 0, canvas.width, canvas.height); // Get colors for current scheme const colors = colorSchemes[visualConfig.colorScheme] || colorSchemes.rainbow; switch (visualConfig.mode) { case "bars": renderBars(ctx, frequencyArray, colors, beat); break; case "wave": renderWave(ctx, waveformArray, colors, beat); break; case "circular": renderCircular(ctx, frequencyArray, colors, beat); break; case "spectrum": renderSpectrum(ctx, frequencyArray, colors); break; case "particles": renderParticles(ctx, frequencyArray, colors); break; case "ripples": renderRipples(ctx, frequencyArray, colors, beat); break; } if (realTimeAnalysis && isPlaying) { animationFrameRef.current = requestAnimationFrame(renderVisualization); } }, [visualConfig, realTimeAnalysis, isPlaying, detectBeat, onFrequencyData]); // Visualization modes const renderBars = (ctx, data, colors, beat) => { const barWidth = ctx.canvas.width / data.length; const heightScale = visualConfig.sensitivity; for (let i = 0; i < data.length; i++) { const barHeight = data[i] / 255 * ctx.canvas.height * heightScale; const colorIndex = Math.floor(i / data.length * colors.length); // Add beat intensity to color brightness const alpha = Math.max(0.3, beat); ctx.fillStyle = colors[colorIndex] + Math.floor(alpha * 255).toString(16).padStart(2, "0"); ctx.fillRect(i * barWidth, ctx.canvas.height - barHeight, barWidth - 1, barHeight); // Mirror effect if (visualConfig.mirror) { ctx.fillRect(i * barWidth, 0, barWidth - 1, barHeight); } } }; const renderWave = (ctx, data, colors, beat) => { ctx.lineWidth = 2 + beat * 3; ctx.strokeStyle = colors[Math.floor(beat * colors.length)]; ctx.beginPath(); const sliceWidth = ctx.canvas.width / data.length; let x = 0; for (let i = 0; i < data.length; i++) { const v = data[i] / 128.0 * visualConfig.sensitivity; const y = v * ctx.canvas.height / 2; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } x += sliceWidth; } ctx.stroke(); }; const renderCircular = (ctx, data, colors, beat) => { const centerX = ctx.canvas.width / 2; const centerY = ctx.canvas.height / 2; const radius = Math.min(centerX, centerY) * 0.7; for (let i = 0; i < data.length; i++) { const angle = i / data.length * Math.PI * 2; const amplitude = data[i] / 255 * radius * visualConfig.sensitivity * 0.5; const x1 = centerX + Math.cos(angle) * radius; const y1 = centerY + Math.sin(angle) * radius; const x2 = centerX + Math.cos(angle) * (radius + amplitude); const y2 = centerY + Math.sin(angle) * (radius + amplitude); const colorIndex = Math.floor(i / data.length * colors.length); ctx.strokeStyle = colors[colorIndex]; ctx.lineWidth = 1 + beat * 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } }; const renderSpectrum = (ctx, data, colors, beat) => { const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); const pixels = imageData.data; // Shift existing data left for (let x = 0; x < ctx.canvas.width - 1; x++) { for (let y = 0; y < ctx.canvas.height; y++) { const sourceIndex = (y * ctx.canvas.width + x + 1) * 4; const targetIndex = (y * ctx.canvas.width + x) * 4; pixels[targetIndex] = pixels[sourceIndex]; pixels[targetIndex + 1] = pixels[sourceIndex + 1]; pixels[targetIndex + 2] = pixels[sourceIndex + 2]; pixels[targetIndex + 3] = pixels[sourceIndex + 3]; } } // Add new column const x = ctx.canvas.width - 1; for (let i = 0; i < data.length; i++) { const y = Math.floor(i / data.length * ctx.canvas.height); const intensity = data[i] / 255; const colorIndex = Math.floor(intensity * colors.length); const color = colors[colorIndex] || "var(--glass-white)"; const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); const index = (y * ctx.canvas.width + x) * 4; pixels[index] = r * intensity; pixels[index + 1] = g * intensity; pixels[index + 2] = b * intensity; pixels[index + 3] = 255 * intensity; } ctx.putImageData(imageData, 0, 0); }; const renderParticles = (ctx, data, colors, beat) => { // Update existing particles particles.current = particles.current.filter(particle => { particle.x += particle.vx; particle.y += particle.vy; particle.life -= 0.01; particle.vy += 0.1; // gravity return particle.life > 0 && particle.x >= 0 && particle.x <= ctx.canvas.width && particle.y >= 0 && particle.y <= ctx.canvas.height; }); // Create new particles based on frequency data for (let i = 0; i < data.length; i += 4) { if (particles.current.length < visualConfig.particleCount) { const intensity = data[i] / 255; if (intensity > 0.1) { particles.current.push({ x: i / data.length * ctx.canvas.width, y: ctx.canvas.height - intensity * ctx.canvas.height * 0.5, vx: (Math.random() - 0.5) * 4, vy: -Math.random() * intensity * 5, size: intensity * 5 + 1, color: colors[Math.floor(intensity * colors.length)], life: 1.0 }); } } } // Render particles particles.current.forEach(particle => { ctx.save(); ctx.globalAlpha = particle.life; ctx.fillStyle = particle.color; ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }); }; const renderRipples = (ctx, data, colors, beat) => { const centerX = ctx.canvas.width / 2; const centerY = ctx.canvas.height / 2; // Calculate average intensity data.reduce((sum, val) => sum + val, 0) / data.length / 255; // Draw ripples based on beat intensity if (beat > 0.3) { const rippleCount = 5; for (let i = 0; i < rippleCount; i++) { const radius = beat * 200 + i * 50; const alpha = Math.max(0, 1 - radius / 300); ctx.strokeStyle = colors[i % colors.length] + Math.floor(alpha * 255).toString(16).padStart(2, "0"); ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.stroke(); } } // Add frequency-based elements for (let i = 0; i < data.length; i += 8) { const angle = i / data.length * Math.PI * 2; const intensity = data[i] / 255; const radius = intensity * 100 + 50; const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; ctx.fillStyle = colors[Math.floor(intensity * colors.length)]; ctx.beginPath(); ctx.arc(x, y, intensity * 5 + 1, 0, Math.PI * 2); ctx.fill(); } }; // Audio controls const handlePlay = useCallback(async () => { if (!audioContextRef.current) { await initializeAudio(); } if (audioRef.current) { try { await audioRef.current.play(); setIsPlaying(true); renderVisualization(); play("play"); } catch (error) { console.error("Failed to play audio:", error); } } }, [initializeAudio, renderVisualization, play]); const handlePause = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); setIsPlaying(false); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } play("pause"); } }, [play]); const handleStop = useCallback(() => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; setIsPlaying(false); setCurrentTime(0); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } play("stop"); } }, [play]); // Initialize on mount useEffect(() => { if (audioSource) { initializeAudio(); } return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } if (audioContextRef.current) { audioContextRef.current.close(); } }; }, [audioSource, initializeAudio]); // Update canvas size useEffect(() => { const canvas = canvasRef.current; if (canvas) { canvas.width = canvasWidth; canvas.height = canvasHeight; } }, [canvasWidth, canvasHeight]); const Controls = () => jsxs("div", { className: 'glass-flex glass-items-center space-x-4', children: [jsx(motion.button, { className: 'glass-p-2 glass-surface-blue hover:glass-surface-blue text-primary glass-radius-lg transition-colors', whileHover: shouldAnimate ? { scale: 1.1 } : {}, whileTap: shouldAnimate ? { scale: 0.9 } : {}, onClick: isPlaying ? handlePause : handlePlay, children: isPlaying ? "⏸️" : "▶️" }), jsx(motion.button, { className: 'glass-p-2 glass-surface-primary hover:glass-surface-primary text-primary glass-radius-lg transition-colors', whileHover: shouldAnimate ? { scale: 1.1 } : {}, whileTap: shouldAnimate ? { scale: 0.9 } : {}, onClick: handleStop, children: "\u23F9\uFE0F" }), jsxs("div", { className: 'glass-flex glass-items-center space-x-2', children: [jsxs("span", { className: 'glass-text-xs text-primary/60', children: [Math.floor(currentTime / 60), ":", Math.floor(currentTime % 60).toString().padStart(2, "0")] }), jsx("span", { className: 'text-primary/40', children: "/" }), jsxs("span", { className: 'glass-text-xs text-primary/60', children: [Math.floor(duration / 60), ":", Math.floor(duration % 60).toString().padStart(2, "0")] })] }), jsxs("div", { className: 'glass-flex glass-items-center space-x-2', children: [jsx("span", { className: 'glass-text-xs text-primary/80', children: "Volume:" }), jsx("input", { type: "range", min: "0", max: "1", step: "0.1", value: audioConfig.volume, onChange: e => { const volume = parseFloat(e.target.value); setAudioConfig(prev => ({ ...prev, volume })); if (audioRef.current) { audioRef.current.volume = volume; } }, className: 'w-16 h-2 glass-surface-subtle/20 glass-radius-lg appearance-none cursor-pointer' })] })] }); return jsxs(OptimizedGlassCore, { ref: ref, variant: "frosted", className: `p-6 space-y-6 ${className}`, ...props, children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("div", { children: [jsx("h3", { className: 'glass-text-xl font-semibold text-primary/90', children: "Music Visualizer" }), jsx("p", { className: 'glass-text-sm text-primary/60', children: "Real-time audio visualization and analysis" })] }), jsxs("div", { className: 'glass-flex glass-items-center space-x-2', children: [realTimeAnalysis && jsxs("div", { className: 'glass-flex glass-items-center space-x-1 text-primary', children: [jsx("div", { className: 'w-2 h-2 glass-surface-green glass-radius-full animate-pulse' }), jsx("span", { className: "glass-text-xs", children: "Live" })] }), isRecording && jsxs("div", { className: 'glass-flex glass-items-center space-x-1 text-primary', children: [jsx("div", { className: 'w-2 h-2 glass-surface-red glass-radius-full animate-pulse' }), jsx("span", { className: "glass-text-xs", children: "Recording" })] })] })] }), audioSource && typeof audioSource === "string" && jsx("audio", { ref: audioRef, src: audioSource, onLoadedMetadata: () => { if (audioRef.current) { setDuration(audioRef.current.duration); onAudioLoad?.(audioRef.current.duration); } }, onTimeUpdate: () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); } } }), showControls && jsx(Controls, {}), jsxs("div", { className: 'relative', children: [jsx("canvas", { ref: canvasRef, width: canvasWidth, height: canvasHeight, className: ` w-full border border-white/20 rounded-lg bg-black/20 ${enableInteraction ? "cursor-pointer" : ""} `, onClick: enableInteraction ? isPlaying ? handlePause : handlePlay : undefined }), jsx("div", { className: 'absolute glass-top-2 right-2', children: jsx("div", { className: 'w-4 h-4 glass-radius-full glass-surface-red', style: { opacity: beatIntensity, transform: `scale(${1 + beatIntensity})` } }) })] }), jsxs("div", { className: 'glass-grid glass-grid-cols-1 md:grid-cols-2 glass-gap-6', children: [jsxs("div", { className: 'space-y-4', children: [jsx("h4", { className: 'glass-text-sm font-medium text-primary/80', children: "Visualization" }), jsxs("div", { className: 'space-y-3', children: [jsxs("div", { children: [jsx("label", { className: 'block glass-text-xs text-primary/70 mb-1', children: "Mode" }), jsxs("select", { value: visualConfig.mode, onChange: e => setVisualConfig(prev => ({ ...prev, mode: e.target.value })), className: 'glass-w-full glass-p-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-lg text-primary/90 glass-text-sm', children: [jsx("option", { value: "bars", children: "Frequency Bars" }), jsx("option", { value: "wave", children: "Waveform" }), jsx("option", { value: "circular", children: "Circular" }), jsx("option", { value: "spectrum", children: "Spectrum" }), jsx("option", { value: "particles", children: "Particles" }), jsx("option", { value: "ripples", children: "Ripples" })] })] }), jsxs("div", { children: [jsx("label", { className: 'block glass-text-xs text-primary/70 mb-1', children: "Color Scheme" }), jsxs("select", { value: visualConfig.colorScheme, onChange: e => setVisualConfig(prev => ({ ...prev, colorScheme: e.target.value })), className: 'glass-w-full glass-p-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-lg text-primary/90 glass-text-sm', children: [jsx("option", { value: "rainbow", children: "Rainbow" }), jsx("option", { value: "monochrome", children: "Monochrome" }), jsx("option", { value: "neon", children: "Neon" }), jsx("option", { value: "fire", children: "Fire" }), jsx("option", { value: "ice", children: "Ice" }), jsx("option", { value: "galaxy", children: "Galaxy" })] })] }), jsxs("div", { children: [jsxs("label", { className: 'block glass-text-xs text-primary/70 mb-1', children: ["Sensitivity: ", visualConfig.sensitivity.toFixed(1)] }), jsx("input", { type: "range", min: "0.1", max: "3.0", step: "0.1", value: visualConfig.sensitivity, onChange: e => setVisualConfig(prev => ({ ...prev, sensitivity: parseFloat(e.target.value) })), className: 'glass-w-full h-2 glass-surface-subtle/20 glass-radius-lg appearance-none cursor-pointer' })] })] })] }), jsxs("div", { className: 'space-y-4', children: [jsx("h4", { className: 'glass-text-sm font-medium text-primary/80', children: "Audio Settings" }), jsxs("div", { className: 'space-y-3', children: [jsxs("div", { children: [jsxs("label", { className: 'block glass-text-xs text-primary/70 mb-1', children: ["Smoothing: ", audioConfig.smoothing.toFixed(1)] }), jsx("input", { type: "range", min: "0.0", max: "1.0", step: "0.1", value: audioConfig.smoothing, onChange: e => { const smoothing = parseFloat(e.target.value); setAudioConfig(prev => ({ ...prev, smoothing })); if (analyserRef.current) { analyserRef.current.smoothingTimeConstant = smoothing; } }, className: 'glass-w-full h-2 glass-surface-subtle/20 glass-radius-lg appearance-none cursor-pointer' })] }), jsxs("div", { children: [jsx("label", { className: 'block glass-text-xs text-primary/70 mb-1', children: "FFT Size" }), jsxs("select", { value: audioConfig.fftSize, onChange: e => setAudioConfig(prev => ({ ...prev, fftSize: parseInt(e.target.value) })), className: 'glass-w-full glass-p-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-lg text-primary/90 glass-text-sm', children: [jsx("option", { value: "128", children: "128" }), jsx("option", { value: "256", children: "256" }), jsx("option", { value: "512", children: "512" }), jsx("option", { value: "1024", children: "1024" }), jsx("option", { value: "2048", children: "2048" })] })] })] })] })] }), showFrequencyDisplay && jsxs("div", { className: ` p-3 rounded-lg border border-white/10 ${createGlassStyle({ blur: "sm", opacity: 0.6 }).background} `, children: [jsx("h4", { className: 'glass-text-sm font-medium text-primary/80 mb-2', children: "Frequency Analysis" }), jsxs("div", { className: "glass-grid glass-grid-cols-4 glass-gap-4 glass-text-sm", children: [jsxs("div", { children: [jsx("span", { className: 'text-primary/60', children: "Bass:" }), jsxs("div", { className: 'text-primary/90 font-medium', children: [Math.round(frequencyData.slice(0, 8).reduce((a, b) => a + b, 0) / 8 / 255 * 100), "%"] })] }), jsxs("div", { children: [jsx("span", { className: 'text-primary/60', children: "Mid:" }), jsxs("div", { className: 'text-primary/90 font-medium', children: [Math.round(frequencyData.slice(8, 32).reduce((a, b) => a + b, 0) / 24 / 255 * 100), "%"] })] }), jsxs("div", { children: [jsx("span", { className: 'text-primary/60', children: "Treble:" }), jsxs("div", { className: 'text-primary/90 font-medium', children: [Math.round(frequencyData.slice(32).reduce((a, b) => a + b, 0) / (frequencyData.length - 32) / 255 * 100), "%"] })] }), jsxs("div", { children: [jsx("span", { className: 'text-primary/60', children: "Beat:" }), jsxs("div", { className: 'text-primary/90 font-medium', children: [Math.round(beatIntensity * 100), "%"] })] })] })] })] }); }); GlassMusicVisualizer.displayName = "GlassMusicVisualizer"; export { GlassMusicVisualizer }; //# sourceMappingURL=GlassMusicVisualizer.js.map