File size: 5,697 Bytes
d5c1d05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 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)
}