@kolkrabbi/kol-hardware 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ A different TIER from `kol-component`'s app atoms: a `kol-component` Slider is a
18
18
  | | `IconButton` | 1px-bordered icon key, momentary pulse |
19
19
  | indicators | `LED` | 6 / 8px lamp — red · yellow · green · white · blue, optional hit pad |
20
20
  | | `JackSocket`, `LabeledJack` | the 3.5mm jack, presentational: ring, hole, label, a rim that glows with `signalRef`. **Routing stays in the consumer** |
21
- | | `SignalScope` | the oscilloscope — a trace of any `sample(t)`, the knob range dashed, a live playhead; zoom X/Y, drag to pan, a clock `rate`, `loop` or one-shot on `trigger`, `height="fill"` |
21
+ | | `SignalScope` | the oscilloscope — a trace of any `sample(t)`, the knob range dashed, a live playhead; zoom X/Y, drag to pan, a clock `rate` or a host's `now`, `loop` or one-shot on `trigger`, the played part in the accent, `height="fill"` |
22
22
  | panel | `PanelLabel` | label wrapper, four positions |
23
23
  | | `ModuleHeader` | Toggle + module name + edit-mode remove dot / bypass dot |
24
24
  | | `SignalReference` | an expression / envelope reference — `variant` `panel` (mirror's box) · `tabs` · `popover` (EX / REF) · `sheet` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-hardware",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "Panel controls for instruments",
6
6
  "license": "MIT",
@@ -20,9 +20,9 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.262.0",
24
- "@kolkrabbi/kol-icons": "^0.38.0",
25
- "@kolkrabbi/kol-theme": "^0.174.0"
23
+ "@kolkrabbi/kol-icons": "^0.41.0",
24
+ "@kolkrabbi/kol-component": "^0.287.0",
25
+ "@kolkrabbi/kol-theme": "^0.183.0"
26
26
  },
27
27
  "files": [
28
28
  "src",
@@ -16,7 +16,13 @@ import { useEffect, useRef } from 'react'
16
16
  * THE CLOCK is the scope's: `rate` is time units per second (BPM / 60 for a beat clock), and
17
17
  * `playing` stops it where it is. `loop` (default) wraps the playhead over `sec`; off, the
18
18
  * playhead runs `0 → sec` once and holds at the end, and every change of `trigger` fires it
19
- * again — a one-shot envelope's gate.
19
+ * again — a one-shot envelope's gate. `now` hands the clock to the host instead: a
20
+ * `() => t` read every frame (mirror's desk transport), and `rate` / `playing` stop applying.
21
+ *
22
+ * THE PLAYED PART IS THE ACCENT (SignalScopePlayedTraceAndClock, kol-mirror): while live, the
23
+ * curve is faint and the trace up to the playhead, the playhead and its dot take
24
+ * `--kol-slider-playhead` (the accent unless a host overrides it) — where the position is in the
25
+ * signal, at a glance.
20
26
  *
21
27
  * @param {(t:number)=>number} sample
22
28
  * @param {number} min · max the view (default 0…100)
@@ -30,20 +36,21 @@ import { useEffect, useRef } from 'react'
30
36
  * @param {number} rate time units per second (default 1)
31
37
  * @param {boolean} loop wrap the playhead (default true); off = one pass per `trigger`
32
38
  * @param {number} trigger a counter — each change restarts the pass
39
+ * @param {()=>number} now an external clock (time units); overrides the scope's own
33
40
  * @param {number|'fill'} height px (default 320), or `fill` to take the parent's height
34
41
  */
35
42
  const css = (el, name, fallback) => getComputedStyle(el).getPropertyValue(name).trim() || fallback
36
43
 
37
44
  export default function SignalScope({
38
45
  sample, min = 0, max = 100, sec = 5, ofs = 0, zoomX = 1, zoomY = 1, panY = 0, onPan,
39
- refLines = [0, 100], live = true, playing = true, rate = 1, loop = true, trigger = 0,
46
+ refLines = [0, 100], live = true, playing = true, rate = 1, loop = true, trigger = 0, now,
40
47
  height = 320, className = '',
41
48
  }) {
42
49
  const ref = useRef(null)
43
50
  const args = useRef({})
44
51
  const drawRef = useRef(null)
45
52
  const clock = useRef({ t: 0, last: 0 })
46
- args.current = { sample, min, max, sec, ofs, zoomX, zoomY, panY, refLines, live, playing, rate, loop }
53
+ args.current = { sample, min, max, sec, ofs, zoomX, zoomY, panY, refLines, live, playing, rate, loop, now }
47
54
 
48
55
  // a trigger restarts the pass
49
56
  useEffect(() => { clock.current.t = 0 }, [trigger])
@@ -59,7 +66,8 @@ export default function SignalScope({
59
66
  const now = performance.now()
60
67
  const dt = (now - clock.current.last) / 1000
61
68
  clock.current.last = now
62
- if (a.live && a.playing) clock.current.t += dt * a.rate
69
+ if (a.now) clock.current.t = a.now()
70
+ else if (a.live && a.playing) clock.current.t += dt * a.rate
63
71
  const fn = a.sample
64
72
  const dpr = window.devicePixelRatio || 1
65
73
  const { width: w, height: h } = canvas.getBoundingClientRect()
@@ -71,6 +79,7 @@ export default function SignalScope({
71
79
  ctx.clearRect(0, 0, w, h)
72
80
  const ink = css(canvas, '--kol-surface-on-primary', '#e8e4dc')
73
81
  const red = css(canvas, '--kol-ctl-led-red', '#e74c3c')
82
+ const accent = css(canvas, '--kol-slider-playhead', css(canvas, '--kol-accent-primary', ink))
74
83
  const pad = 12
75
84
  const span = (a.max - a.min) / (a.zoomY || 1) || 1
76
85
  const lo = (a.min + a.max) / 2 + a.panY - span / 2
@@ -101,26 +110,35 @@ export default function SignalScope({
101
110
  ctx.fillStyle = ink
102
111
  ctx.fillText(fmt(hi), 4, pad + 9)
103
112
  ctx.fillText(fmt(lo), 4, h - pad - 3)
104
- // trace
105
- ctx.globalAlpha = 1
106
- ctx.strokeStyle = ink
107
- ctx.lineWidth = 1.5
108
- ctx.beginPath()
109
- for (let x = 0; x <= w; x++) {
110
- const y = toY(fn ? fn(a.ofs + (x / w) * dur) : lo)
111
- if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y)
113
+ const trace = (to) => {
114
+ ctx.beginPath()
115
+ for (let x = 0; x <= to; x++) {
116
+ const y = toY(fn ? fn(a.ofs + (x / w) * dur) : lo)
117
+ if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y)
118
+ }
119
+ ctx.stroke()
112
120
  }
113
- ctx.stroke()
121
+ const played = a.live && fn
122
+ // trace — faint under a playhead, the played part drawn over it
123
+ ctx.globalAlpha = played ? 0.15 : 1
124
+ ctx.strokeStyle = ink
125
+ ctx.lineWidth = played ? 1 : 1.5
126
+ trace(w)
127
+ ctx.globalAlpha = 1
114
128
  // playhead — over the base window, whatever the zoom (mirror's)
115
- if (a.live && fn) {
116
- const t = a.loop ? clock.current.t % a.sec : Math.min(clock.current.t, a.sec)
129
+ if (played) {
130
+ const t = a.loop ? ((clock.current.t % a.sec) + a.sec) % a.sec : Math.max(0, Math.min(clock.current.t, a.sec))
117
131
  const px = toX(t)
132
+ ctx.strokeStyle = accent
133
+ ctx.lineWidth = 2
134
+ if (px > 0) trace(Math.min(px, w))
118
135
  if (px >= 0 && px <= w) {
119
136
  const py = toY(fn(t))
120
- ctx.globalAlpha = 0.25
137
+ ctx.globalAlpha = 0.4
138
+ ctx.lineWidth = 1
121
139
  ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke()
122
140
  ctx.globalAlpha = 1
123
- ctx.fillStyle = ink
141
+ ctx.fillStyle = accent
124
142
  ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2); ctx.fill()
125
143
  }
126
144
  }
@@ -27,11 +27,11 @@ const adsrWindow = (adsr) => envelopeLength(adsr) * 1.15
27
27
 
28
28
  /**
29
29
  * useEnvelopeGenerator(options) — the generator's state, for a host that arranges the parts.
30
- * Takes the component's own options (`default*`, `saved`, `savedAdsr`, `onSave`, `onChange`).
30
+ * Takes the component's own options (`default*`, `saved`, `savedAdsr`, `onSave`, `onChange`, `now`).
31
31
  */
32
32
  export function useEnvelopeGenerator({
33
33
  defaultMode = 'equation', defaultExpr = 'wave(t)', defaultAdsr = ADSR_DEFAULTS,
34
- saved, savedAdsr, onSave, onChange,
34
+ saved, savedAdsr, onSave, onChange, now,
35
35
  } = {}) {
36
36
  const [mode, setModeState] = useState(defaultMode)
37
37
  const [expr, setExprState] = useState(defaultExpr)
@@ -69,7 +69,7 @@ export function useEnvelopeGenerator({
69
69
  return {
70
70
  mode, setMode, expr, setExpr, compiled, adsr, setAdsr, view, setView, fit, reset,
71
71
  cycle, setCycle, playing, setPlaying, bpm, setBpm, trigger, fire: () => setTrigger((n) => n + 1),
72
- heldSec, setHeldSec, save,
72
+ heldSec, setHeldSec, save, now,
73
73
  /* spread into <SignalReference> — the current mode's data, whichever shape it takes. Key the
74
74
  * element on `mode` so its open tab resets when the data changes under it. */
75
75
  reference: {
@@ -188,7 +188,8 @@ function EnvelopeHandles({ adsr, sec, onDragStart, onDrag, onDragEnd }) {
188
188
  * itself.
189
189
  *
190
190
  * TIME is the scope's clock: `bpm` sets its speed (60 BPM = one unit a second, so `t` reads as
191
- * seconds at the default and as beats at any other). Play / pause stops it where it is.
191
+ * seconds at the default and as beats at any other). Play / pause stops it where it is. `now`
192
+ * (`() => t`) hands the clock to the host — a desk transport — and the scope reads it instead.
192
193
  *
193
194
  * ADSR: four handles on the scope — A (attack time), D (decay time), S (at the end of the hold:
194
195
  * sideways = how long the gate holds, up/down = the sustain level), R (release time). The scope
@@ -205,6 +206,7 @@ function EnvelopeHandles({ adsr, sec, onDragStart, onDrag, onDragEnd }) {
205
206
  * @param {string} defaultExpr
206
207
  * @param {object} defaultAdsr knob values 0…100, `hold` in seconds (`ADSR_DEFAULTS`)
207
208
  * @param {Array} saved · savedAdsr the host's own rows for each mode's Saved tab
209
+ * @param {()=>number} now an external clock for the scope (overrides play / BPM)
208
210
  * @param {Function} onSave `({ mode, code }) => void` — shows a Save button; `code` is the
209
211
  * expression, or the envelope as `a10 d30 s70 r50 h1`
210
212
  */
@@ -270,6 +272,7 @@ export default function EnvelopeGenerator({ generator, reference = 'panel', clas
270
272
  rate={g.bpm / 60}
271
273
  loop={mode === 'equation' || g.cycle}
272
274
  trigger={g.trigger}
275
+ now={g.now}
273
276
  className="absolute inset-0"
274
277
  />
275
278
  {mode === 'adsr' && (