mindX / rack /src /studio_rack.jsx
Gregory-L's picture
mindX Space — rebuilt from mindx/godel/mindxtrain/hf/space_static
d5c1d05 verified
Raw History Blame Contribute Delete
5.7 kB
// mindX studio — the settings rack as dreamknob hardware.
//
// IT OWNS NO STATE. Every value comes from window.mindxStudio.knobs() and every turn goes back
// through window.mindxStudio.setKnob(); the rack repaints on the window 'mindx:studio' event, which
// the engine fires (once per frame at most) whenever a knob moves — by hand here, by a fallback
// slider, or by a live mapping from .pixel .gesture .mic .cam. So a knob that is following your
// microphone turns on its own, and the readout under it is what the audio graph was handed.
// Delete this script and studio.js shows plain sliders that drive the same engine.
//
// dreamknob 1.1.4 — MIT, (c) Dreamodus Software Inc. Bundled with React 18 by rack/build.mjs.
import { useEffect, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { DreamknobProvider, Rack, MetalKnob, PanKnob, Fader, IndicatorLamp, linearTaper, logTaper, powTaper } from 'dreamknob'
const MONO = "'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace"
const TAPERS = { lin: linearTaper, log: logTaper, pow2: powTaper(2) }
const SECTIONS = ['OUTPUT', 'EQ', 'FX', 'SCOPE']
const api = () => window.mindxStudio
const colors = () => (api() && api().colors ? api().colors() : {})
function themeFrom(c, light) {
return {
accent: c.gold || '#d6b25e',
track: c.line2 || 'rgba(232,230,223,.08)',
face: c.canvas || '#0a0d0c',
text: c.text || '#e8e6df',
label: c.text3 || '#6d7771',
ticks: c.text3 || '#6d7771',
focusRing: c.gold || '#d6b25e',
panel: light ? 'rgba(255,255,255,.85)' : 'rgba(14,18,17,.85)',
ledGreen: c.green || '#52d98a',
ledAmber: c.gold || '#d6b25e',
fontMono: MONO,
}
}
function useWidth(ref) {
const [w, setW] = useState(800)
useEffect(() => {
const el = ref.current
if (!el) return
const measure = () => setW(el.clientWidth || 800)
measure()
if (typeof ResizeObserver !== 'undefined') { const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect() }
window.addEventListener('resize', measure)
return () => window.removeEventListener('resize', measure)
}, [ref])
return w
}
function Cell({ k, size }) {
const set = (v) => api().setKnob(k.id, v)
const common = {
value: k.value, min: k.min, max: k.max, step: k.step || undefined, taper: TAPERS[k.taper] || linearTaper,
defaultValue: k.def, onChange: set, label: k.label, 'aria-label': k.label,
detents: k.detents, origin: k.origin, wheelRequiresFocus: true,
}
const c = colors()
let control
if (k.kind === 'fader') {
control = <Fader {...common} orientation="horizontal" fill breadth={40} showValue valuePosition="end"
format={() => k.text} color={c.gold} tickCount={9} editable />
} else if (k.kind === 'pan') {
control = <PanKnob {...common} size={size} showValue format={() => k.text} color={c.gold} />
} else {
control = <MetalKnob {...common} size={size} tone="dark" color={k.live ? c.green : c.gold} showValue format={() => k.text}
sublabel={k.sub || undefined} valueBubble bubbleFormat={() => k.text} editable />
}
return (
<div className={'sr-cell' + (k.kind === 'fader' ? ' sr-wide' : '')}>
{control}
<button type="button" className={'sr-map' + (k.mapped ? ' on' : '')} onClick={() => api().focusMapping(k.id)}
title={k.mapped ? 'mapped to ' + k.mapped + (k.live ? ' (live)' : ' (source idle: turns by hand)') : 'map this knob to a sense'}>
{k.mapped ? (k.live ? '● ' : '○ ') + k.mapped : 'map…'}
</button>
</div>
)
}
function StudioRack() {
const [, tick] = useState(0)
const [light, setLight] = useState(() => document.documentElement.style.colorScheme === 'light' || getComputedStyle(document.documentElement).colorScheme === 'light')
const ref = useRef(document.getElementById('rack'))
const width = useWidth(ref)
useEffect(() => {
const on = () => tick((n) => n + 1)
const th = () => { setLight(getComputedStyle(document.documentElement).colorScheme === 'light'); tick((n) => n + 1) }
window.addEventListener('mindx:studio', on)
window.addEventListener('mindx:studio-theme', th)
return () => { window.removeEventListener('mindx:studio', on); window.removeEventListener('mindx:studio-theme', th) }
}, [])
const knobs = api().knobs()
const size = width < 420 ? 58 : width < 760 ? 64 : 72
const anyLive = knobs.some((k) => k.live)
return (
<DreamknobProvider base={light ? 'light' : 'dark'} theme={themeFrom(colors(), light)}>
<div className="sr-wrap">
{SECTIONS.map((sec) => (
<div className="sr-sec" key={sec}>
<Rack orientation="column" title={sec} gap={8} padding={10} accentColor={colors().gold} style={{ width: '100%', boxSizing: 'border-box', minWidth: 0 }}>
<div className="sr-knobs">
{knobs.filter((k) => k.sec === sec).map((k) => <Cell key={k.id} k={k} size={size} />)}
</div>
</Rack>
</div>
))}
</div>
<div style={{ marginTop: 8 }}>
<IndicatorLamp readOnly on={anyLive} color={colors().green} label={anyLive ? 'a sense is turning a knob' : 'every knob by hand'} labelSize={11} />
</div>
</DreamknobProvider>
)
}
function mount() {
const host = document.getElementById('rack')
if (!host || !window.mindxStudio || host.__mounted) return !!(host && host.__mounted)
host.__mounted = true
window.__mindxStudioRack = true
createRoot(host).render(<StudioRack />)
return true
}
if (!mount()) {
const t0 = Date.now()
const iv = setInterval(() => { if (mount() || Date.now() - t0 > 10000) clearInterval(iv) }, 120)
}