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.