62 lines
2.1 KiB
JavaScript
62 lines
2.1 KiB
JavaScript
// 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;
|
|
} |