// SegmentOverlay — draws structural section bands on the timeline canvas (Phase 6) // Bands are translucent, drawn below waveform. Clicking selects a section. const BAND_COLORS = [ 'rgba(0,255,255,0.15)', // --accent cyan variant 'rgba(180,0,255,0.15)', // purple complement 'rgba(0,200,200,0.12)', // darker cyan variant ]; class SegmentOverlay { constructor() { this.segments = []; // [{start, end, label}, ...] this.selectedIndex = -1; // -1 = no selection this.loading = false; this.error = null; } /** * Load segments from the API. * @param {string} audioPath — absolute path to audio file (e.g. /app/shows/file.mp3) */ async load(audioPath) { this.loading = true; this.error = null; this.segments = []; this.selectedIndex = -1; try { const res = await fetch(`/api/audio/segments?path=${encodeURIComponent(audioPath)}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); this.segments = data.segments || []; } catch (e) { this.error = `SEGMENTATION FAILED: ${e.message}`; console.warn('[segments]', e); } finally { this.loading = false; } } /** * Get the currently selected segment, or null. * @returns {{start: number, end: number, label: string} | null} */ getSelected() { if (this.selectedIndex >= 0 && this.selectedIndex < this.segments.length) { return this.segments[this.selectedIndex]; } return null; } /** * Handle a click at a given time position. Selects the segment containing that time, * or deselects if clicking outside any segment. * @param {number} time — time in seconds * @returns {boolean} — true if selection changed */ handleClick(time) { const prev = this.selectedIndex; this.selectedIndex = -1; for (let i = 0; i < this.segments.length; i++) { if (time >= this.segments[i].start && time < this.segments[i].end) { this.selectedIndex = i; break; } } return this.selectedIndex !== prev; } /** * Draw segment bands on the canvas context. * Must be called BETWEEN track row banding and waveform in the render pipeline. * * @param {CanvasRenderingContext2D} ctx * @param {number} W — canvas logical width * @param {number} H — canvas logical height * @param {number} trackCount — number of tracks * @param {Function} timeToX — converts time (seconds) to canvas X coordinate */ draw(ctx, W, H, trackCount, timeToX) { if (this.segments.length === 0) return; const totalH = trackCount * 48; // TRACK_HEIGHT const topY = 24; // TIME_AXIS_HEIGHT const headerW = 140; // HEADER_WIDTH for (let i = 0; i < this.segments.length; i++) { const seg = this.segments[i]; const x1 = timeToX(seg.start); const x2 = timeToX(seg.end); // Skip segments entirely off-screen if (x2 < headerW || x1 > W) continue; // Clamp to visible area const drawX1 = Math.max(x1, headerW); const drawX2 = Math.min(x2, W); const drawW = drawX2 - drawX1; const isSelected = this.selectedIndex === i; // D-09: Translucent colored band, full waveform height, alternating colors ctx.fillStyle = BAND_COLORS[i % BAND_COLORS.length]; ctx.fillRect(drawX1, topY, drawW, totalH); // D-11: Selected section gets accent-color border highlight if (isSelected) { ctx.strokeStyle = 'rgba(0,255,255,1.0)'; ctx.lineWidth = 1; ctx.strokeRect(drawX1, topY, drawW, totalH); } // D-10: Label — small monospace uppercase at top-left of band if (drawW > 30) { ctx.fillStyle = 'rgba(0,255,255,0.6)'; ctx.font = '9px monospace'; ctx.fillText(seg.label, Math.max(x1 + 3, headerW + 2), topY + 10); } } } } export { SegmentOverlay };