1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2const SpectrumBarsVisualizer = ({ 3 audioElement, 4 audioNode, 5 audioContext, 6 width = "100%", 7 height = "100%", 8 barCount: originalBarCount = 128, 9 gap = 2, 10 smoothingTimeConstant = 0.85, 11 fftSize = 2048, 12 isPlaying 13}) => { 14 const containerRef = reactExports.useRef(null); 15 const canvasRef = reactExports.useRef(null); 16 const requestRef = reactExports.useRef(null); 17 const isMobile = reactExports.useRef(typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window)).current; 18 const [lowPerf, setLowPerf] = reactExports.useState(() => { 19 if (typeof window === "undefined") return false; 20 const saved = localStorage.getItem("low_performance"); 21 if (saved !== null) return saved === "true"; 22 return isMobile; 23 }); 24 reactExports.useEffect(() => { 25 const handleLowPerfChange = () => { 26 const saved = localStorage.getItem("low_performance"); 27 if (saved !== null) { 28 setLowPerf(saved === "true"); 29 } else { 30 setLowPerf(isMobile); 31 } 32 }; 33 window.addEventListener("storage", handleLowPerfChange); 34 window.addEventListener("low_performance_change", handleLowPerfChange); 35 return () => { 36 window.removeEventListener("storage", handleLowPerfChange); 37 window.removeEventListener("low_performance_change", handleLowPerfChange); 38 }; 39 }, [isMobile]); 40 const audioCtxRef = reactExports.useRef(null); 41 const analyserRef = reactExports.useRef(null); 42 const sourceRef = reactExports.useRef(null); 43 const lastActiveDataRef = reactExports.useRef(null); 44 const [colorStyle, setColorStyle] = reactExports.useState("rainbow"); 45 const [showReflection, setShowReflection] = reactExports.useState(false); 46 const [intensity, setIntensity] = reactExports.useState(1.2); 47 const [showMeasurements, setShowMeasurements] = reactExports.useState(true); 48 const barCount = lowPerf ? Math.min(originalBarCount, 48) : originalBarCount; 49 reactExports.useEffect(() => { 50 const styles = ["rainbow", "fire", "ocean", "neon", "plasma", "matrix", "sunset", "electric", "grayscale", "retro"]; 51 const randomStyle = styles[Math.floor(Math.random() * styles.length)]; 52 setColorStyle(randomStyle); 53 }, []); 54 reactExports.useEffect(() => { 55 if (audioNode && audioContext) { 56 audioCtxRef.current = audioContext; 57 const ctx2 = audioContext; 58 if (ctx2.state === "suspended" && isPlaying) { 59 ctx2.resume().catch(() => { 60 }); 61 } 62 if (!analyserRef.current) { 63 analyserRef.current = ctx2.createAnalyser(); 64 analyserRef.current.fftSize = fftSize; 65 analyserRef.current.smoothingTimeConstant = smoothingTimeConstant; 66 } 67 try { 68 audioNode.connect(analyserRef.current); 69 } catch (e) { 70 console.warn("Could not connect audioNode to analyser.", e); 71 } 72 return () => { 73 if (requestRef.current) { 74 cancelAnimationFrame(requestRef.current); 75 } 76 try { 77 if (analyserRef.current) { 78 audioNode.disconnect(analyserRef.current); 79 } 80 } catch (e) { 81 } 82 }; 83 } 84 const audioEl = audioElement || document.getElementById("global-audio-element"); 85 if (!audioEl) return; 86 if (!audioCtxRef.current) { 87 const AudioContextClass = window.AudioContext || window.webkitAudioContext; 88 audioCtxRef.current = new AudioContextClass(); 89 } 90 const ctx = audioCtxRef.current; 91 if (ctx.state === "suspended" && isPlaying) { 92 ctx.resume().catch(() => { 93 }); 94 } 95 if (!analyserRef.current) { 96 analyserRef.current = ctx.createAnalyser(); 97 analyserRef.current.fftSize = fftSize; 98 analyserRef.current.smoothingTimeConstant = smoothingTimeConstant; 99 } 100 if (!sourceRef.current) { 101 try { 102 if (!audioEl._hasSourceNode) { 103 sourceRef.current = ctx.createMediaElementSource(audioEl); 104 audioEl._hasSourceNode = true; 105 sourceRef.current.connect(analyserRef.current); 106 analyserRef.current.connect(ctx.destination); 107 } 108 } catch (e) { 109 console.warn("Could not create MediaElementSourceNode.", e); 110 } 111 } 112 return () => { 113 if (requestRef.current) { 114 cancelAnimationFrame(requestRef.current); 115 } 116 }; 117 }, [audioElement, audioNode, audioContext, fftSize, smoothingTimeConstant, isPlaying]); 118 reactExports.useEffect(() => { 119 if (!containerRef.current || !canvasRef.current) return; 120 const updateDim = () => { 121 if (containerRef.current && canvasRef.current) { 122 const { width: width2, height: height2 } = containerRef.current.getBoundingClientRect(); 123 let dpr = window.devicePixelRatio || 1; 124 if (lowPerf) { 125 dpr = Math.min(dpr, 1); 126 } else if (isMobile) { 127 dpr = Math.min(dpr, 1.5); 128 } 129 canvasRef.current.width = width2 * dpr; 130 canvasRef.current.height = height2 * dpr; 131 canvasRef.current.style.width = `${width2}px`; 132 canvasRef.current.style.height = `${height2}px`; 133 } 134 }; 135 updateDim();
136 const ro = new ResizeObserver(updateDim); 137 ro.observe(containerRef.current); 138 return () => ro.disconnect(); 139 }, [lowPerf, isMobile]); 140 const animate = () => { 141 if (!canvasRef.current || !analyserRef.current) return; 142 const canvas = canvasRef.current; 143 const ctx = canvas.getContext("2d"); 144 if (!ctx) return; 145 let dpr = window.devicePixelRatio || 1; 146 if (lowPerf) { 147 dpr = Math.min(dpr, 1); 148 } else if (isMobile) { 149 dpr = Math.min(dpr, 1.5); 150 } 151 const width2 = canvas.width; 152 const height2 = canvas.height; 153 ctx.clearRect(0, 0, width2, height2); 154 ctx.fillStyle = "#0a0a0a"; 155 ctx.fillRect(0, 0, width2, height2); 156 const bufferLength = analyserRef.current.frequencyBinCount; 157 const dataArray = new Uint8Array(bufferLength); 158 if (isPlaying) { 159 analyserRef.current.getByteFrequencyData(dataArray); 160 if (!lastActiveDataRef.current || lastActiveDataRef.current.length !== bufferLength) { 161 lastActiveDataRef.current = new Uint8Array(bufferLength); 162 } 163 lastActiveDataRef.current.set(dataArray); 164 } else if (lastActiveDataRef.current && lastActiveDataRef.current.length === bufferLength) { 165 dataArray.set(lastActiveDataRef.current); 166 } else { 167 analyserRef.current.getByteFrequencyData(dataArray); 168 } 169 const actualGap = gap * dpr; 170 const usableBufferLength = Math.floor(bufferLength * 0.7); 171 const calculatedBarWidth = (width2 - (barCount - 1) * actualGap) / barCount; 172 const actualBarWidth = Math.max(1, calculatedBarWidth); 173 const totalWidth = barCount * actualBarWidth + (barCount - 1) * actualGap; 174 const startX = (width2 - totalWidth) / 2; 175 const effectiveReflection = showReflection && !lowPerf; 176 const maxBarHeight = height2 * (effectiveReflection ? 0.6 : 0.85); 177 const baselineY = effectiveReflection ? height2 * 0.65 : height2 * 0.95; 178 if (showMeasurements && !lowPerf) { 179 ctx.lineWidth = 1 * dpr; 180 ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; 181 ctx.fillStyle = "rgba(255, 255, 255, 0.4)"; 182 ctx.font = `${10 * dpr}px monospace`; 183 ctx.textAlign = "right"; 184 const dbSteps = [0, -12, -24, -36, -48, -60]; 185 dbSteps.forEach((db) => { 186 const ratio = 1 - Math.abs(db) / 60; 187 if (ratio < 0) return; 188 const y = baselineY - ratio * maxBarHeight * intensity; 189 if (y < 0) return; 190 ctx.beginPath(); 191 ctx.moveTo(0, y); 192 ctx.lineTo(width2, y); 193 ctx.stroke(); 194 ctx.fillText(`${db} dB`, width2 - 10 * dpr, y - 4 * dpr); 195 }); 196 const nyquist = audioCtxRef.current ? audioCtxRef.current.sampleRate / 2 : 24e3; 197 const freqSteps = [100, 500, 1e3, 5e3, 1e4, 2e4]; 198 ctx.textAlign = "center"; 199 freqSteps.forEach((freq) => { 200 const linearIndex = freq / nyquist * bufferLength; 201 const i = barCount * Math.sqrt(linearIndex / usableBufferLength); 202 if (i >= 0 && i < barCount) { 203 const x = startX + i * (actualBarWidth + actualGap) + actualBarWidth / 2; 204 ctx.beginPath(); 205 ctx.moveTo(x, 0); 206 ctx.lineTo(x, height2); 207 ctx.stroke(); 208 const label = freq >= 1e3 ? `${freq / 1e3}k` : `${freq}`; 209 ctx.fillText(label, x, height2 - 10 * dpr); 210 } 211 }); 212 } 213 for (let i = 0; i < barCount; i++) { 214 const logIndex = Math.floor(Math.pow(i / barCount, 2.5) * usableBufferLength); 215 const dataIndex = Math.min(usableBufferLength - 1, logIndex); 216 let value = dataArray[dataIndex] || 0; 217 const normalizedValue = value / 255; 218 const barHeight = Math.max(2 * dpr, normalizedValue * maxBarHeight * intensity); 219 const x = startX + i * (actualBarWidth + actualGap); 220 const y = baselineY - barHeight; 221 const ratio = i / barCount; 222 let colorStr = ""; 223 switch (colorStyle) { 224 case "rainbow": 225 colorStr = `hsl(${Math.floor(ratio * 300)}, 100%, 60%)`; 226 break; 227 case "fire": 228 colorStr = `hsl(${Math.floor(ratio * 60)}, 100%, 50%)`; 229 break; 230 case "ocean": 231 colorStr = `hsl(${180 + Math.floor(ratio * 60)}, 100%, 60%)`; 232 break; 233 case "neon": 234 colorStr = `hsl(${280 + Math.floor(ratio * 60)}, 100%, 65%)`; 235 break; 236 case "plasma": 237 colorStr = `hsl(${240 + Math.floor(ratio * 120)}, 100%, 60%)`; 238 break; 239 case "matrix": 240 colorStr = `hsl(120, 100%, ${20 + ratio * 60}%)`; 241 break; 242 case "sunset": 243 colorStr = `hsl(${30 + Math.floor(ratio * 60)}, 100%, 60%)`; 244 break; 245 case "electric": 246 colorStr = `hsl(${200 + Math.floor(ratio * 60)}, 100%, 60%)`; 247 break; 248 case "grayscale": 249 colorStr = `hsl(0, 0%, ${30 + ratio * 70}%)`; 250 break; 251 case "retro": 252 colorStr = ratio < 0.5 ? "#00ffff" : "#ff00ff"; 253 break; 254 default: 255 colorStr = `hsl(${Math.floor(ratio * 300)}, 100%, 60%)`; 256 } 257 ctx.beginPath(); 258 if (actualBarWidth < 4 || lowPerf) { 259 ctx.rect(x, y, actualBarWidth, barHeight); 260 } else { 261 ctx.roundRect(x, y, actualBarWidth, barHeight, [actualBarWidth / 2, actualBarWidth / 2, 0, 0]); 262 } 263 if (actualBarWidth > 3 && !lowPerf) { 264 ctx.shadowBlur = 10 * dpr; 265 ctx.shadowColor = colorStr; 266 } 267 ctx.fillStyle = colorStr; 268 ctx.fill(); 269 ctx.shadowBlur = 0; 270 if (effectiveReflection && barHeight > 2 * dpr) { 271 ctx.beginPath(); 272 if (actualBarWidth < 4) { 273 ctx.rect(x, baselineY + 2 * dpr, actualBarWidth, barHeight * 0.3); 274 } else { 275 ctx.roundRect(x, baselineY + 2 * dpr, actualBarWidth, barHeight * 0.3, [0, 0, actualBarWidth / 2, actualBarWidth / 2]); 276 } 277 ctx.globalAlpha = 0.2; 278 const gradient = ctx.createLinearGradient(0, baselineY, 0, baselineY + barHeight * 0.3); 279 gradient.addColorStop(0, colorStr); 280 gradient.addColorStop(1, "rgba(0,0,0,0)"); 281 ctx.fillStyle = gradient; 282 ctx.fill(); 283 ctx.globalAlpha = 1; 284 } 285 } 286 if (effectiveReflection) { 287 ctx.beginPath(); 288 ctx.moveTo(0, baselineY); 289 ctx.lineTo(width2, baselineY); 290 ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; 291 ctx.lineWidth = 1 * dpr; 292 ctx.stroke(); 293 } 294 requestRef.current = requestAnimationFrame(animate); 295 }; 296 reactExports.useEffect(() => { 297 var _a; 298 if (isPlaying) { 299 if (((_a = audioCtxRef.current) == null ? void 0 : _a.state) === "suspended") { 300 audioCtxRef.current.resume().catch(() => { 301 }); 302 } 303 requestRef.current = requestAnimationFrame(animate); 304 } else { 305 requestRef.current = requestAnimationFrame(animate); 306 } 307 return () => { 308 if (requestRef.current) { 309 cancelAnimationFrame(requestRef.current); 310 } 311 }; 312 }, [isPlaying, colorStyle, showReflection, intensity, barCount, gap, showMeasurements, lowPerf]); 313 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: "relative w-full h-full flex flex-col", style: { width, height }, children: [
314 /* @__PURE__ */ jsxRuntimeExports.jsx("canvas", { ref: canvasRef, className: "absolute inset-0 w-full h-full" }), 315 /* @__PURE__ */ jsxRuntimeExports.jsxs( 316 "div", 317 { 318 className: "absolute top-4 right-4 flex gap-2 opacity-0 hover:opacity-100 transition-opacity duration-300 bg-black/50 p-2 rounded-lg backdrop-blur-sm z-10", 319 onClick: (e) => e.stopPropagation(), 320 children: [ 321 /* @__PURE__ */ jsxRuntimeExports.jsxs( 322 "select", 323 { 324 value: colorStyle, 325 onChange: (e) => setColorStyle(e.target.value), 326 className: "bg-zinc-800 text-xs text-white border-none rounded px-2 py-1 outline-none cursor-pointer", 327 children: [ 328 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "rainbow", children: "Rainbow" }), 329 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "fire", children: "Fire" }), 330 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "ocean", children: "Ocean" }),
331 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "neon", children: "Neon" }), 332 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "plasma", children: "Plasma" }), 333 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "matrix", children: "Matrix" }), 334 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "sunset", children: "Sunset" }), 335 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "electric", children: "Electric" }), 336 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "grayscale", children: "Grayscale" }), 337 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "retro", children: "Retro" }) 338 ] 339 } 340 ), 341 !lowPerf && /* @__PURE__ */ jsxRuntimeExports.jsx( 342 "button", 343 { 344 onClick: () => setShowReflection(!showReflection), 345 className: `text-xs px-2 py-1 rounded transition-colors ${showReflection ? "bg-indigo-600 text-white" : "bg-zinc-800 text-zinc-400"}`, 346 children: "Reflect" 347 } 348 ), 349 !lowPerf && /* @__PURE__ */ jsxRuntimeExports.jsx( 350 "button", 351 { 352 onClick: () => setShowMeasurements(!showMeasurements), 353 className: `text-xs px-2 py-1 rounded transition-colors ${showMeasurements ? "bg-indigo-600 text-white" : "bg-zinc-800 text-zinc-400"}`, 354 children: "Grid" 355 } 356 ),
357 /* @__PURE__ */ jsxRuntimeExports.jsxs( 358 "button", 359 { 360 onClick: () => setIntensity((prev) => prev > 1.5 ? 0.8 : prev + 0.4), 361 className: "bg-zinc-800 text-xs text-white px-2 py-1 rounded hover:bg-zinc-700 transition-colors", 362 children: [ 363 "Boost: ", 364 intensity.toFixed(1), 365 "x" 366 ] 367 } 368 ) 369 ] 370 } 371 ) 372 ] }); 373}; 374export { 375 SpectrumBarsVisualizer as default 376};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.