Metabolism: rule program, song-driven life, tribe VMs; M2.7 design (strains & allegiances, living wells)
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user