@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 +1 -1
- package/package.json +4 -4
- package/src/indicators/SignalScope.jsx +35 -17
- package/src/value/EnvelopeGenerator.jsx +7 -4
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.
|
|
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-
|
|
24
|
-
"@kolkrabbi/kol-
|
|
25
|
-
"@kolkrabbi/kol-theme": "^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.
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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 =
|
|
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' && (
|