// Controls generated from a scene's param schema. // // Nothing here knows about any specific scene. That is the point: a new scene // gets a full editing UI for free, which is what makes "auto with override" // affordable across a library of thirty. Adding a hand-written control for a // scene would be a bug, not a feature. const FORMATTERS = { float: (v) => (typeof v === 'number' ? v.toFixed(2) : '—'), int: (v) => String(v), bool: (v) => (v ? 'on' : 'off'), vec2: (v) => (Array.isArray(v) ? `${v[0].toFixed(2)}, ${v[1].toFixed(2)}` : '—'), }; export class ParamPanel { /** * @param {HTMLElement} container * @param {(name: string, value: any) => void} onChange */ constructor(container, onChange) { this.container = container; this.onChange = onChange; this.controls = new Map(); this.module = null; } /** Rebuild for a scene module and its current values. */ build(module, values) { this.container.innerHTML = ''; this.controls.clear(); this.module = module; if (!module) return; const heading = document.createElement('div'); heading.className = 'pp-heading'; heading.innerHTML = `${module.name} ${module.family}`; this.container.appendChild(heading); for (const [name, def] of Object.entries(module.params || {})) { if (def.type === 'palette') continue; // edited in the look tab const control = this._buildControl(name, def, values[name]); if (control) { this.container.appendChild(control.element); this.controls.set(name, control); } } const reactive = module.reactive || {}; if (Object.keys(reactive).length) { const note = document.createElement('div'); note.className = 'pp-reactive'; note.innerHTML = '
audio-driven
' + Object.entries(reactive) .map(([p, r]) => `
${p}` + `${r.feature}` + `${r.amount > 0 ? '+' : ''}${r.amount}
`) .join(''); this.container.appendChild(note); } } _buildControl(name, def, value) { const element = document.createElement('div'); element.className = 'pp-row'; const label = document.createElement('label'); label.className = 'pp-label'; label.textContent = name; const readout = document.createElement('span'); readout.className = 'pp-value'; let input; if (def.type === 'bool') { input = document.createElement('input'); input.type = 'checkbox'; input.checked = !!value; input.addEventListener('change', () => { readout.textContent = FORMATTERS.bool(input.checked); this.onChange(name, input.checked); }); } else { const [lo, hi] = def.range || [0, 1]; input = document.createElement('input'); input.type = 'range'; input.min = String(lo); input.max = String(hi); input.step = def.type === 'int' ? '1' : String((hi - lo) / 200); input.value = String(def.type === 'vec2' ? (value ? value[0] : lo) : value ?? lo); input.addEventListener('input', () => { const v = def.type === 'int' ? Math.round(+input.value) : +input.value; const out = def.type === 'vec2' ? [v, v] : v; readout.textContent = (FORMATTERS[def.type] || FORMATTERS.float)(out); this.onChange(name, out); }); } input.className = 'pp-input'; readout.textContent = (FORMATTERS[def.type] || FORMATTERS.float)(value); element.append(label, input, readout); return { element, input, readout, def }; } /** Push new values into the controls without emitting change events. */ update(values) { for (const [name, control] of this.controls) { const v = values[name]; if (v === undefined) continue; if (control.def.type === 'bool') control.input.checked = !!v; else control.input.value = String(control.def.type === 'vec2' ? v[0] : v); control.readout.textContent = (FORMATTERS[control.def.type] || FORMATTERS.float)(v); } } /** Names of every control currently rendered. Used by the Phase 2 gate. */ controlNames() { return [...this.controls.keys()]; } }