PageSourceSearch

https://www.hamienet.com/assets/PitchFollowerVisualizer-C65xBjxl.js

js hamienet.com collected 2026-10-02 05:00:57 UTC 24,429 bytes, 653 lines download raw bytes

1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js";
2import { k as getTrackColor } from "./utils-CEVb7nN1.js";
3import { b6 as LoaderCircle } from "./vendor-D0P4XgNj.js";
4import "./index-w5lXls15.js";
5import "./vendor-i18n-BC7ZMXWQ.js";
6const PALETTE = [
7  "#facc15",
8  // Warm Yellow / Gold (upper track in reference)
9  "#f472b6",
10  // Bright Rose Pink (lower track in reference)
11  "#38bdf8",
12  // Electric Cyan
13  "#4ade80",
14  // Emerald Green
15  "#c084fc",
16  // Vibrant Purple
17  "#fb923c",
18  // Tangerine Orange
19  "#2dd4bf",
20  // Teal
21  "#e879f9",
22  // Magenta
23  "#a3e635",
24  // Lime
25  "#60a5fa"
26  // Blue
27];
28const PitchFollowerVisualizer = ({
29  data,
30  currentTime,
31  duration,
32  isPlaying,
33  hiddenTracks,
34  colorMap,
35  processedInstruments,
36  uniqueNames,
37  onSeek,
38  onPlayPause,
39  theme = "dark",
40  playbackRate = 1
41}) => {
42  const containerRef = reactExports.useRef(null);
43  const canvasRef = reactExports.useRef(null);
44  const requestRef = reactExports.useRef(null);
45  const lastTimeRef = reactExports.useRef(currentTime);
46  const lastTickRef = reactExports.useRef(performance.now());
47  const animPhaseRef = reactExports.useRef(0);
48  reactExports.useEffect(() => {
49    lastTimeRef.current = currentTime;
50    lastTickRef.current = performance.now();
51  }, [currentTime]);
52  const isMobile = reactExports.useRef(typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || "ontouchstart" in window)).current;
53  const [lowPerf] = reactExports.useState(() => {
54    if (typeof window === "undefined") return false;
55    const saved = localStorage.getItem("low_performance");
56    return saved === "true" || isMobile;
57  });
58  const [canvasDimensions, setCanvasDimensions] = reactExports.useState({ width: 800, height: 450 });
59  reactExports.useEffect(() => {
60    const updateSize = () => {
61      if (!containerRef.current || !canvasRef.current) return;
62      const rect = containerRef.current.getBoundingClientRect();
63      const dpr = Math.min(window.devicePixelRatio || 1, lowPerf ? 1 : 2);
64      const w = Math.max(rect.width, 320);
65      const h = Math.max(rect.height, 240);
66      setCanvasDimensions({ width: w, height: h });
67      canvasRef.current.width = Math.round(w * dpr);
68      canvasRef.current.height = Math.round(h * dpr);
69      canvasRef.current.style.width = `${w}px`;
70      canvasRef.current.style.height = `${h}px`;
71    };
72    updateSize();
73    const ro = new ResizeObserver(updateSize);
74    if (containerRef.current) ro.observe(containerRef.current);
75    return () => ro.disconnect();
76  }, [lowPerf]);
77  const musicalInstruments = reactExports.useMemo(() => {
78    return (processedInstruments == null ? void 0 : processedInstruments.filter((inst) => inst.midiChannel !== -1)) || [];
79  }, [processedInstruments]);
80  const isPercussionNote = reactExports.useCallback((note) => {
81    if (!processedInstruments) return false;
82    let inst = musicalInstruments[note.trackIdx];
83    if (!inst || inst.midiChannel !== 9) {
84      inst = processedInstruments.find((i) => i.trackId === note.trackIdx);
85    }
86    if (!inst || inst.midiChannel !== 9) {
87      inst = processedInstruments.find((i) => i.originalName === note.i || i.displayName === note.i || i.name === note.i);
88    }
89    if (inst) {
90      return inst.midiChannel === 9;
91    }
92    const nameLower = (note.i || "").toLowerCase();
93    return nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal");
94  }, [processedInstruments, musicalInstruments]);
95  const getNoteKey = reactExports.useCallback((note) => {
96    if (!note) return "";
97    if (note.i && colorMap && colorMap[note.i]) {
98      return note.i;
99    }
100    if (processedInstruments && note.trackIdx !== void 0 && musicalInstruments[note.trackIdx]) {
101      const inst = musicalInstruments[note.trackIdx];
102      const foundIdx = processedInstruments.findIndex((i) => i.trackId === inst.trackId);
103      if (foundIdx !== -1 && uniqueNames && uniqueNames[foundIdx]) {
104        return uniqueNames[foundIdx];
105      }
106    }
107    return note.i || `Track ${note.trackIdx ?? 0}`;
108  }, [colorMap, processedInstruments, musicalInstruments, uniqueNames]);
109  const trackPaths = reactExports.useMemo(() => {
110    if (!data || !Array.isArray(data) || data.length === 0) return [];
111    const validNotes = [];
112    for (let i = 0; i < data.length; i++) {
113      const n = data[i];
114      if (!n || n.n === void 0 || n.t === void 0) continue;
115      if (isPercussionNote(n)) continue;
116      validNotes.push({
117        t: Number(n.t),
118        d: Math.max(0.04, Number(n.d || 0.1)),
119        n: Math.round(Number(n.n)),
120        v: n.v,
121        trackIdx: n.trackIdx !== void 0 ? n.trackIdx : 0,
122        i: n.i
123      });
124    }
125    if (validNotes.length === 0) return [];
126    const uniqueTrackIndices = new Set(validNotes.map((n) => n.trackIdx));
127    const uniqueInstrumentNames = new Set(validNotes.map((n) =>
127 n.i).filter(Boolean));
128    const groups = [];
129    if (uniqueTrackIndices.size > 1) {
130      const notesByTrack = /* @__PURE__ */ new Map();
131      for (const n of validNotes) {
132        if (!notesByTrack.has(n.trackIdx)) notesByTrack.set(n.trackIdx, []);
133        notesByTrack.get(n.trackIdx).push(n);
134      }
135      notesByTrack.forEach((notes, tIdx) => {
136        let name = getNoteKey(notes[0]);
137        let color = void 0;
138        if (processedInstruments && musicalInstruments[tIdx]) {
139          const inst = musicalInstruments[tIdx];
140          const foundIdx = processedInstruments.findIndex((i) => i.trackId === inst.trackId);
141          if (foundIdx !== -1 && uniqueNames && uniqueNames[foundIdx]) {
142            name = uniqueNames[foundIdx];
143            color = colorMap[name] || getTrackColor(foundIdx);
144          } else {
145            name = inst.displayName || inst.name || name;
146            color = colorMap[name] || getTrackColor(tIdx);
147          }
148        } else {
149          color = colorMap[name] || getTrackColor(tIdx);
150        }
151        groups.push({ trackIdx: tIdx, name, color, notes });
152      });
153    } else if (uniqueInstrumentNames.size > 1) {
154      const notesByName = /* @__PURE__ */ new Map();
155      for (const n of validNotes) {
156        const key = n.i || "Track 1";
157        if (!notesByName.has(key)) notesByName.set(key, []);
158        notesByName.get(key).push(n);
159      }
160      let idx = 0;
161      notesByName.forEach((notes, name) => {
162        const color = colorMap[name] || getTrackColor(idx);
163        groups.push({ trackIdx: idx++, name, color, notes });
164      });
165    } else {
166      validNotes.sort((a, b) => a.t === b.t ? b.n - a.n : a.t - b.t);
167      let overlapCount = 0;
168      for (let i = 1; i < validNotes.length; i++) {
169        if (validNotes[i].t < validNotes[i - 1].t + validNotes[i - 1].d - 0.05) {
170          overlapCount++;
171        }
172      }
173      const isPolyphonic = validNotes.length > 8 && overlapCount / validNotes.length > 0.12;
174      if (isPolyphonic) {
175        const trebleNotes = [];
176        const bassNotes = [];
177        let cluster = [];
178        const clusters = [];
179        for (let i = 0; i < validNotes.length; i++) {
180          const n = validNotes[i];
181          if (cluster.length === 0) {
182            cluster.push(n);
183          } else if (Math.abs(n.t - cluster[0].t) <= 0.035) {
184            cluster.push(n);
185          } else {
186            clusters.push(cluster);
187            cluster = [n];
188          }
189        }
190        if (cluster.length > 0) clusters.push(cluster);
191        for (const cl of clusters) {
192          cl.sort((a, b) => b.n - a.n);
193          const highest = cl[0];
194          trebleNotes.push({ ...highest });
195          if (cl.length > 1) {
196            const lowest = cl[cl.length - 1];
197            if (highest.n - lowest.n >= 4) {
198              bassNotes.push({ ...lowest });
199            }
200          } else {
201            if (highest.n < 56) {
202              bassNotes.push({ ...highest });
203            }
204          }
205        }
206        if (trebleNotes.length > 0) {
207          const primaryName = (uniqueNames == null ? void 0 : uniqueNames[0]) || "Treble / Melody";
208          const primaryColor = colorMap[primaryName] || getTrackColor(0);
209          groups.push({
210            trackIdx: 0,
211            name: primaryName,
212            color: primaryColor,
213            notes: trebleNotes
214          });
215        }
216        if (bassNotes.length > 0) {
217          const secondaryName = uniqueNames && uniqueNames.length > 1 ? uniqueNames[1] : "Bass / Harmony";
218          const secondaryColor = uniqueNames && uniqueNames.length > 1 && colorMap[secondaryName] ? colorMap[secondaryName] : getTrackColor(1);
219          groups.push({
220            trackIdx: 1,
221            name: secondaryName,
222            color: secondaryColor,
223            notes: bassNotes
224          });
225        }
226      } else {
227        const primaryName = (uniqueNames == null ? void 0 : uniqueNames[0]) || validNotes[0].i || "Melody";
228        const primaryColor = colorMap[primaryName] || getTrackColor(0);
229        groups.push({
230          trackIdx: 0,
231          name: primaryName,
232          color: primaryColor,
233          notes: validNotes
234        });
235      }
236    }
237    const paths = [];
238    let paletteIdx = 0;
239    const maxAllowedTime = duration && duration > 0 ? duration : Infinity;
240    for (const grp of groups) {
241      const rawNotes = grp.notes;
242      if (rawNotes.length === 0) continue;
243      rawNotes.sort((a, b) => a.t === b.t ? b.n - a.n : a.t - b.t);
244      const monoNotes = [];
245      for (let i = 0; i < rawNotes.length; i++) {
246        const cur = rawNotes[i];
247        if (cur.t >= maxAllowedTime) continue;
248        if (monoNotes.length === 0) {
249          monoNotes.push({ ...cur });
250          continue;
251        }
252        const prev = monoNotes[monoNotes.length - 1];
253        if (Math.abs(cur.t - prev.t) < 0.025) {
254          if (cur.n > prev.n) {
255            monoNotes[monoNotes.length - 1] = { ...cur };
256          }
257          continue;
258        }
259        if (cur.t < prev.t + prev.d) {
260          prev.d = Math.max(0.04, cur.t - prev.t);
261        }
262        monoNotes.push({ ...cur });
263      }
264      if (monoNotes.length === 0) continue;
265      const segments = [];
266      let curSegmentVertices = [];
267      let tMinPitch = Infinity;
268      let tMaxPitch = -Infinity;
269      for (let i = 0; i < monoNotes.length; i++) {
270        const note = monoNotes[i];
271        const startTime = note.t;
272        const endTime = Math.min(maxAllowedTime, note.t + note.d);
273        const pitch = note.n;
274        tMinPitch = Math.min(tMinPitch, pitch);
275        tMaxPitch = Math.max(tMaxPitch, pitch);
276        if (i > 0 && curSegmentVertices.length > 0) {
277          const prevNote = monoNotes[i - 1];
278          const prevEnd = Math.min(maxAllowedTime, prevNote.t + prevNote.d);
279          if (startTime - prevEnd > 2) {
280            segments.push({
281              vertices: curSegmentVertices,
282              startTime: curSegmentVertices[0].t,
283              endTime: curSegmentVertices[curSegmentVertices.length - 1].t
284            });
285            curSegmentVertices = [];
286          }
287        }
288        curSegmentVertices.push({
289          t: startTime,
290          pitch,
291          isNoteStart: true,
292          duration: note.d
293        });
294        curSegmentVertices.push({
295          t: endTime,
296          pitch,
297          isNoteEnd: true,
298          duration: note.d
299        });
300      }
301      if (curSegmentVertices.length > 0) {
302        segments.push({
303          vertices: curSegmentVertices,
304          startTime: curSegmentVertices[0].t,
305          endTime: curSegmentVertices[curSegmentVertices.length - 1].t
306        });
307      }
308      if (segments.length === 0) continue;
309      let color = grp.color;
310      if (!color) {
311        const sampleNote = monoNotes[0];
312        const trackKey = getNoteKey(sampleNote);
313        color = colorMap[trackKey];
314      }
315      if (!color) {
316        color = getTrackColor(grp.trackIdx) || PALETTE[paletteIdx % PALETTE.length];
317        paletteIdx++;
318      }
319      paths.push({
320        trackIdx: grp.trackIdx,
321        name: grp.name,
322        color,
323        segments,
324        minPitch: tMinPitch === Infinity ? 48 : tMinPitch,
325        maxPitch: tMaxPitch === -Infinity ? 72 : tMaxPitch
326      });
327    }
328    return paths;
329  }, [data, isPercussionNote, getNoteKey, colorMap, processedInstruments, musicalInstruments, uniqueNames, duration]);
330  const { globalMinPitch, globalMaxPitch } = reactExports.useMemo(() => {
331    let min = Infinity;
332    let max = -Infinity;
333    trackPaths.forEach((p) => {
334      const key = p.name;
335      if (hiddenTracks.has(key)) return;
336      min = Math.min(min, p.minPitch);
337      max = Math.max(max, p.maxPitch);
338    });
339    if (min === Infinity || max === -Infinity) {
340      return { globalMinPitch: 48, globalMaxPitch: 72 };
341    }
342    return {
343      globalMinPitch: Math.max(21, min - 3),
344      globalMaxPitch: Math.min(108, max + 3)
345    };
346  }, [trackPaths, hiddenTracks]);
347  const getPitchAtTime = reactExports.useCallback((segments, time) => {
348    if (!segments || segments.length === 0) return null;
349    for (let s = 0; s < segments.length; s++) {
350      const seg = segments[s];
351      if (time < seg.startTime) {
352        return null;
353      }
354      if (time >= seg.startTime && time <= seg.endTime) {
355        const verts = seg.vertices;
356        if (!verts || verts.length === 0) return null;
357        if (time === verts[0].t) return verts[0].pitch;
358        if (time === verts[verts.length - 1].t) return verts[verts.length - 1].pitch;
359        let low = 0;
360        let high = verts.length - 1;
361        while (low <= high) {
362          const mid = Math.floor((low + high) / 2);
363          const v = verts[mid];
364          if (v.t === time) return v.pitch;
365          if (v.t < time) {
366            if (mid < verts.length - 1 && verts[mid + 1].t >= time) {
367              const next = verts[mid + 1];
368              if (next.t === v.t) return v.pitch;
369              const frac = (time - v.t) / (next.t - v.t);
370              return v.pitch + (next.pitch - v.pitch) * frac;
371            }
372            low = mid + 1;
373          } else {
374            if (mid > 0 && verts[mid - 1].t <= time) {
375              const prev = verts[mid - 1];
376              if (v.t === prev.t) return v.pitch;
377              const frac = (time - prev.t) / (v.t - prev.t);
378              return prev.pitch + (v.pitch - prev.pitch) * frac;
379            }
380            high = mid - 1;
381          }
382        }
383        return verts[0].pitch;
384      }
385    }
386    return null;
387  }, []);
388  const drawMultiStrokeRail = reactExports.useCallback((ctx, x1, y1, x2, y2, trackColor) => {
389    const dx = x2 - x1;
390    const dy = y2 - y1;
391    const len = Math.hypot(dx, dy);
392    if (len < 0.1) return;
393    const nx = -dy / len;
394    const ny = dx / len;
395    const offsets = [-2.2, 0, 2.2];
396    ctx.strokeStyle = trackColor;
397    ctx.lineWidth = 1.2;
398    ctx.globalAlpha = 0.75;
399    for (let i = 0; i < offsets.length; i++) {
400      const off = offsets[i];
401      ctx.beginPath();
402      ctx.moveTo(x1 + nx * off, y1 + ny * off);
403      ctx.lineTo(x2 + nx * off, y2 + ny * off);
404      ctx.stroke();
405    }
406    ctx.strokeStyle = "#ffffff";
407    ctx.lineWidth = 1;
408    ctx.globalAlpha = 0.95;
409    ctx.beginPath();
410    ctx.moveTo(x1, y1);
411    ctx.lineTo(x2, y2);
412    ctx.stroke();
413  }, []);
414  reactExports.useEffect(() => {
415    let isCancelled = false;
416    const render = () => {
417      if (isCancelled) return;
418      const canvas = canvasRef.current;
419      if (!canvas) {
420        requestRef.current = requestAnimationFrame(render);
421        return;
422      }
423      const ctx = canvas.getContext("2d");
424      if (!ctx) {
425        requestRef.current = requestAnimationFrame(render);
426        return;
427      }
428      const now = performance.now();
429      let effectiveTime = lastTimeRef.current;
430      if (isPlaying) {
431        const deltaSec = (now - lastTickRef.current) / 1e3 * (playbackRate || 1);
432        effectiveTime = Math.min(duration || 9999, lastTimeRef.current + deltaSec);
433      }
434      animPhaseRef.current += 0.05;
435      const dpr = Math.min(window.devicePixelRatio || 1, lowPerf ? 1 : 2);
436      const width = canvas.width / dpr;
437      const height = canvas.height / dpr;
438      ctx.save();
439      ctx.scale(dpr, dpr);
440      const bgGrad = ctx.createLinearGradient(0, 0, 0, height);
441      bgGrad.addColorStop(0, "#09090d");
442      bgGrad.addColorStop(0.5, "#0e0e16");
443      bgGrad.addColorStop(1, "#08080c");
444      ctx.fillStyle = bgGrad;
445      ctx.fillRect(0, 0, width, height);
446      ctx.strokeStyle = "rgba(255, 255, 255, 0.035)";
447      ctx.lineWidth = 1;
448      const pitchRange = Math.max(12, globalMaxPitch - globalMinPitch);
449      const topMargin = height * 0.12;
450      const bottomMargin = height * 0.12;
451      const usableHeight = height - topMargin - bottomMargin;
452      const pitchToY = (pitch) => {
453        const ratio = (pitch - globalMinPitch) / pitchRange;
454        return height - bottomMargin - ratio * usableHeight;
455      };
456      for (let p = Math.ceil(globalMinPitch / 12) * 12; p <= globalMaxPitch; p += 12) {
457        const y = pitchToY(p);
458        ctx.beginPath();
459        ctx.moveTo(0, y);
460        ctx.lineTo(width, y);
461        ctx.stroke();
462        const octaveNum = Math.floor(p / 12) - 1;
463        ctx.fillStyle = "rgba(255, 255, 255, 0.18)";
464        ctx.font = "10px monospace";
465        ctx.fillText(`C${octaveNum}`, 10, y - 4);
466      }
467      const centerX = width * 0.5;
468      const pixelsPerSecond = Math.max(100, Math.min(220, width * 0.22));
469      const timeToX = (t) => centerX + (t - effectiveTime) * pixelsPerSecond;
470      const tMin = effectiveTime - centerX / pixelsPerSecond - 1.5;
471      const tMax = effectiveTime + (width - centerX) / pixelsPerSecond + 1.5;
472      ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
473      ctx.lineWidth = 1;
474      ctx.setLineDash([4, 6]);
475      ctx.beginPath();
476      ctx.moveTo(centerX, 0);
477      ctx.lineTo(centerX, height);
478      ctx.stroke();
479      ctx.setLineDash([]);
480      const visibleFollowers = [];
481      for (let t = 0; t < trackPaths.length; t++) {
482        const track = trackPaths[t];
483        if (hiddenTracks.has(track.name)) continue;
484        const trackColor = track.color;
485        for (let s = 0; s < track.segments.length; s++) {
486          const seg = track.segments[s];
487          if (seg.endTime < tMin || seg.startTime > tMax) continue;
488          const vertices = seg.vertices;
489          if (!vertices || vertices.length < 2) continue;
490          const visibleVertices = [];
491          for (let i = 0; i < vertices.length; i++) {
492            const v = vertices[i];
493            if (v.t >= tMin && v.t <= tMax) {
494              visibleVertices.push(v);
495            } else if (v.t < tMin && i < vertices.length - 1 && vertices[i + 1].t > tMin) {
496              visibleVertices.push(v);
497            } else if (v.t > tMax && i > 0 && vertices[i - 1].t < tMax) {
498              visibleVertices.push(v);
499            }
500          }
501          if (visibleVertices.length < 2) continue;
502          for (let i = 0; i < visibleVertices.length - 1; i++) {
503            const v1 = visibleVertices[i];
504            const v2 = visibleVertices[i + 1];
505            const x1 = timeToX(v1.t);
506            const y1 = pitchToY(v1.pitch);
507            const x2 = timeToX(v2.t);
508            const y2 = pitchToY(v2.pitch);
509            drawMultiStrokeRail(ctx, x1, y1, x2, y2, trackColor);
510          }
511          ctx.globalAlpha = 1;
512          for (let i = 0; i < visibleVertices.length; i++) {
513            const v = visibleVertices[i];
514            if (!v.isNoteStart && !(v.isNoteEnd && v.duration && v.duration > 0.75)) continue;
515            const vx = timeToX(v.t);
516            const vy = pitchToY(v.pitch);
517            if (vx < -30 || vx > width + 30) continue;
518            const isReached = vx <= centerX + 6;
519            ctx.strokeStyle = isReached ? trackColor : "rgba(255, 255, 255, 0.4)";
520            ctx.lineWidth = 1.2;
521            ctx.beginPath();
522            ctx.arc(vx, vy, 11, 0, Math.PI * 2);
523            ctx.stroke();
524            ctx.strokeStyle = isReached ? "#ffffff" : "rgba(255, 255, 255, 0.6)";
525            ctx.lineWidth = 1.2;
526            ctx.beginPath();
527            ctx.arc(vx, vy, 7, 0, Math.PI * 2);
528            ctx.stroke();
529            if (isReached) {
530              if (!lowPerf) {
531                ctx.shadowBlur = 10;
532                ctx.shadowColor = trackColor;
533              }
534              ctx.fillStyle = trackColor;
535              ctx.beginPath();
536              ctx.arc(vx, vy, 4.5, 0, Math.PI * 2);
537              ctx.fill();
538              ctx.shadowBlur = 0;
539              ctx.fillStyle = "#ffffff";
540              ctx.beginPath();
541              ctx.arc(vx, vy, 2, 0, Math.PI * 2);
542              ctx.fill();
543            } else {
544              ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
545              ctx.lineWidth = 1;
546              ctx.beginPath();
547              ctx.arc(vx, vy, 3.5, 0, Math.PI * 2);
548              ctx.stroke();
549            }
550          }
551        }
552        const currentPitch = getPitchAtTime(track.segments, effectiveTime);
553        if (currentPitch !== null) {
554          const followerY = pitchToY(currentPitch);
555          visibleFollowers.push({
556            x: centerX,
557            y: followerY,
558            color: trackColor,
559            name: track.name
560          });
561        }
562      }
563      visibleFollowers.forEach((f) => {
564        const pulse = Math.sin(animPhaseRef.current) * 3;
565        ctx.strokeStyle = f.color;
566        ctx.lineWidth = 1.4;
567        ctx.globalAlpha = 0.5 + Math.sin(animPhaseRef.current) * 0.2;
568        ctx.beginPath();
569        ctx.arc(f.x, f.y, 16 + pulse, 0, Math.PI * 2);
570        ctx.stroke();
571        if (!lowPerf) {
572          const haloGrad = ctx.createRadialGradient(f.x, f.y, 2, f.x, f.y, 14);
573          haloGrad.addColorStop(0, f.color);
574          haloGrad.addColorStop(0.6, f.color);
575          haloGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
576          ctx.fillStyle = haloGrad;
577          ctx.globalAlpha = 0.4;
578          ctx.beginPath();
579          ctx.arc(f.x, f.y, 14, 0, Math.PI * 2);
580          ctx.fill();
581        }
582        ctx.globalAlpha = 1;
583        if (!lowPerf) {
584          ctx.shadowBlur = 14;
585          ctx.shadowColor = f.color;
586        }
587        ctx.fillStyle = f.color;
588        ctx.beginPath();
589        ctx.arc(f.x, f.y, 7, 0, Math.PI * 2);
590        ctx.fill();
591        ctx.shadowBlur = 0;
592        ctx.fillStyle = "#ffffff";
593        ctx.beginPath();
594        ctx.arc(f.x, f.y, 3.5, 0, Math.PI * 2);
595        ctx.fill();
596      });
597      ctx.restore();
598      requestRef.current = requestAnimationFrame(render);
599    };
600    requestRef.current = requestAnimationFrame(render);
601    return () => {
602      isCancelled = true;
603      if (requestRef.current) cancelAnimationFrame(requestRef.current);
604    };
605  }, [
606    canvasDimensions,
607    trackPaths,
608    hiddenTracks,
609    isPlaying,
610    playbackRate,
611    globalMinPitch,
612    globalMaxPitch,
613    getPitchAtTime,
614    drawMultiStrokeRail,
615    lowPerf,
616    duration
617  ]);
618  const handleCanvasClick = (e) => {
619    if (!containerRef.current || !onSeek) return;
620    const rect = containerRef.current.getBoundingClientRect();
621    const clickX = e.clientX - rect.left;
622    const width = rect.width;
623    const centerX = width * 0.5;
624    const pixelsPerSecond = Math.max(100, Math.min(220, width * 0.22));
625    const seekTime = Math.max(0, Math.min(duration || 9999, currentTime + (clickX - centerX) / pixelsPerSecond));
626    onSeek(seekTime);
627  };
628  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
629    "div",
630    {
631      ref: containerRef,
632      className: "relative w-full h-full overflow-hidden select-none bg-[#09090d] flex items-center justify-center",
633      children: [
634        /* @__PURE__ */ jsxRuntimeExports.jsx(
635          "canvas",
636          {
637            ref: canvasRef,
638            onClick: handleCanvasClick,
639            className: "w-full h-full cursor-pointer block",
640            title: "Click anywhere to seek to that note in time"
641          }
642        ),
643        (!data || data.length === 0) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm text-zinc-400 gap-3", children: [
644          /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-8 h-8 animate-spin text-amber-400" }),
645          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-medium tracking-wide", children: "Loading track melody data..." })
646        ] })
647      ]
648    }
649  );
650};
651export {
652  PitchFollowerVisualizer as default
653};

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.