// Catalog loading + display helpers for the demo page. // The catalog itself is computed by the vite plugin (node-side analysis of the // exact same pipeline the browser uses), so the library card and the player // can never disagree about a song's shape. const LAW_COLORS = { intro: '#7d8fa3', build: '#d8a24a', drop: '#e2434b', breakdown: '#4a8fe2', sustain: '#3fbf9f', outro: '#9a7fd6', }; export function fetchCatalog() { return fetch('/songs/catalog.json').then((r) => { if (!r.ok) throw new Error(`catalog unavailable (${r.status})`); return r.json(); }); } export function lawColor(kind) { return LAW_COLORS[kind] || '#5f6673'; } /** A CSS accent (hue/sat from the track's palette, so the page wears the song). */ export function accentFor(entry) { const hue = Number(entry.hue) || 0.6; const sat = Number(entry.sat) || 0.6; return `hsl(${Math.round(hue * 360)}, ${Math.round(Math.min(1, sat) * 100)}%, 58%)`; } export function fmtTime(s) { if (!Number.isFinite(s) || s < 0) return '0:00'; const m = Math.floor(s / 60); const ss = String(Math.floor(s % 60)).padStart(2, '0'); return `${m}:${ss}`; } /** A one-line natural-language summary of what the track is. */ export function tagsFor(entry) { const q = entry.summary; const bits = []; if (q.bpm) bits.push(`${Math.round(q.bpm)} BPM`); bits.push(q.centroid > 0.5 ? 'bright' : 'dark'); bits.push(q.flatness < 0.12 ? 'tonal' : q.flatness < 0.4 ? 'grained' : 'noisy'); bits.push(q.beatStrength > 0.5 ? 'pulsing' : 'beatless'); bits.push(`${entry.laws.length} ${entry.laws.length === 1 ? 'part' : 'parts'}`); return bits.join(' ยท '); } /** Read the URL's song/seed/res and give safe defaults for the rest. */ export function parseQuery() { const p = new URLSearchParams(location.search); const res = ['640x360', '960x540', '1280x720'].includes(p.get('res')) ? p.get('res') : '640x360'; return { song: p.get('song') || 'drone', seed: parseSeed(p.get('seed')), res }; } function parseSeed(v) { const n = parseInt(v ?? '', 10); return Number.isFinite(n) && n > 0 ? n : 12345; }