PageSourceSearch

https://www.hamienet.com/assets/SpectrumBarsVisualizer-D1eLSVtr.js

js hamienet.com collected 2026-10-02 05:00:56 UTC 15,493 bytes, 376 lines download raw bytes

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.