@vsreact/core 0.0.12 → 0.0.14
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/dist/components.d.ts +4 -0
- package/dist/components.js +2 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +9 -0
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.js +12 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/layout.d.ts +37 -0
- package/dist/layout.js +33 -0
- package/dist/meter.d.ts +4 -1
- package/dist/meter.js +10 -2
- package/dist/synth.d.ts +79 -0
- package/dist/synth.js +144 -0
- package/package.json +1 -1
- package/src/components.ts +19 -0
- package/src/format.ts +11 -0
- package/src/hooks.ts +13 -0
- package/src/index.ts +23 -0
- package/src/layout.test.tsx +127 -0
- package/src/layout.tsx +136 -0
- package/src/meter.tsx +28 -8
- package/src/synth.test.tsx +168 -0
- package/src/synth.tsx +370 -0
package/dist/components.d.ts
CHANGED
|
@@ -16,5 +16,9 @@ export { PianoKeyboard } from "./keyboard";
|
|
|
16
16
|
export type { PianoKeyboardProps } from "./keyboard";
|
|
17
17
|
export { StepSequencer } from "./sequencer";
|
|
18
18
|
export type { StepSequencerProps } from "./sequencer";
|
|
19
|
+
export { Tabs, Disclosure } from "./layout";
|
|
20
|
+
export type { TabsProps, DisclosureProps } from "./layout";
|
|
21
|
+
export { ADSREnvelope, ParamADSREnvelope, PitchBend, ModWheel, ParamModWheel, ParamPitchBend, } from "./synth";
|
|
22
|
+
export type { ADSRKey, ADSREnvelopeProps, ParamADSREnvelopeProps, PitchBendProps, ModWheelProps, ParamModWheelProps, ParamPitchBendProps, } from "./synth";
|
|
19
23
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
|
20
24
|
export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
|
package/dist/components.js
CHANGED
|
@@ -14,4 +14,6 @@ export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRa
|
|
|
14
14
|
export { ProgressBar, Spinner } from "./feedback";
|
|
15
15
|
export { PianoKeyboard } from "./keyboard";
|
|
16
16
|
export { StepSequencer } from "./sequencer";
|
|
17
|
+
export { Tabs, Disclosure } from "./layout";
|
|
18
|
+
export { ADSREnvelope, ParamADSREnvelope, PitchBend, ModWheel, ParamModWheel, ParamPitchBend, } from "./synth";
|
|
17
19
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
package/dist/format.d.ts
CHANGED
|
@@ -14,3 +14,8 @@ export declare function formatPercent(value01: number, decimals?: number): strin
|
|
|
14
14
|
export declare function formatSemitones(semitones: number, decimals?: number): string;
|
|
15
15
|
/** A MIDI note number as scientific pitch: 60 → "C4", 69 → "A4". */
|
|
16
16
|
export declare function midiNoteName(note: number): string;
|
|
17
|
+
/** A MIDI note as a frequency (equal temperament): 69 → 440. Feed your
|
|
18
|
+
oscillator straight from PianoKeyboard's onNoteOn. */
|
|
19
|
+
export declare function midiNoteToHz(note: number, a4?: number): number;
|
|
20
|
+
/** The nearest MIDI note for a frequency: 440 → 69. */
|
|
21
|
+
export declare function hzToMidiNote(hz: number, a4?: number): number;
|
package/dist/format.js
CHANGED
|
@@ -48,3 +48,12 @@ export function midiNoteName(note) {
|
|
|
48
48
|
const n = Math.round(note);
|
|
49
49
|
return `${NOTE_NAMES[((n % 12) + 12) % 12]}${Math.floor(n / 12) - 1}`;
|
|
50
50
|
}
|
|
51
|
+
/** A MIDI note as a frequency (equal temperament): 69 → 440. Feed your
|
|
52
|
+
oscillator straight from PianoKeyboard's onNoteOn. */
|
|
53
|
+
export function midiNoteToHz(note, a4 = 440) {
|
|
54
|
+
return a4 * Math.pow(2, (note - 69) / 12);
|
|
55
|
+
}
|
|
56
|
+
/** The nearest MIDI note for a frequency: 440 → 69. */
|
|
57
|
+
export function hzToMidiNote(hz, a4 = 440) {
|
|
58
|
+
return Math.round(69 + 12 * Math.log2(hz / a4));
|
|
59
|
+
}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export declare function useLayoutRect(): [LayoutRect | null, (rect: LayoutRect)
|
|
|
14
14
|
* useNativeEvent("download:progress", (p) => setProgress(p.ratio));
|
|
15
15
|
*/
|
|
16
16
|
export declare function useNativeEvent(name: string, handler: (payload: any) => void): void;
|
|
17
|
+
/**
|
|
18
|
+
* The latest payload of a C++ event, held as state — the one-liner for
|
|
19
|
+
* native → UI data feeds:
|
|
20
|
+
*
|
|
21
|
+
* const meter = useNativeValue("meter", { level: 0 });
|
|
22
|
+
* <Meter value={meter.level} />
|
|
23
|
+
*/
|
|
24
|
+
export declare function useNativeValue<T = any>(name: string, initial: T): T;
|
|
17
25
|
/**
|
|
18
26
|
* The value, but only after it has stopped changing for `delayMs` —
|
|
19
27
|
* classic input debouncing for expensive native calls:
|
package/dist/hooks.js
CHANGED
|
@@ -23,6 +23,18 @@ export function useNativeEvent(name, handler) {
|
|
|
23
23
|
handlerRef.current = handler;
|
|
24
24
|
useEffect(() => native.on(name, (payload) => handlerRef.current(payload)), [name]);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The latest payload of a C++ event, held as state — the one-liner for
|
|
28
|
+
* native → UI data feeds:
|
|
29
|
+
*
|
|
30
|
+
* const meter = useNativeValue("meter", { level: 0 });
|
|
31
|
+
* <Meter value={meter.level} />
|
|
32
|
+
*/
|
|
33
|
+
export function useNativeValue(name, initial) {
|
|
34
|
+
const [value, setValue] = useState(initial);
|
|
35
|
+
useNativeEvent(name, setValue);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
26
38
|
/**
|
|
27
39
|
* The value, but only after it has stopped changing for `delayMs` —
|
|
28
40
|
* classic input debouncing for expensive native calls:
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
|
4
4
|
export type { CommonProps, TextProps, ImageProps, TextInputProps, NativeViewProps, } from "./primitives";
|
|
5
5
|
export { render, unmount } from "./render";
|
|
6
6
|
export { native } from "./native";
|
|
7
|
-
export { useNativeEvent, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
|
|
7
|
+
export { useNativeEvent, useNativeValue, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
|
|
8
8
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
9
9
|
export { Tooltip, Modal } from "./popover";
|
|
10
10
|
export type { TooltipProps, ModalProps } from "./popover";
|
|
@@ -32,7 +32,11 @@ export { PianoKeyboard } from "./keyboard";
|
|
|
32
32
|
export type { PianoKeyboardProps } from "./keyboard";
|
|
33
33
|
export { StepSequencer } from "./sequencer";
|
|
34
34
|
export type { StepSequencerProps } from "./sequencer";
|
|
35
|
-
export {
|
|
35
|
+
export { Tabs, Disclosure } from "./layout";
|
|
36
|
+
export type { TabsProps, DisclosureProps } from "./layout";
|
|
37
|
+
export { ADSREnvelope, ParamADSREnvelope, PitchBend, ModWheel, ParamModWheel, ParamPitchBend, adsrLevelAt, } from "./synth";
|
|
38
|
+
export type { ADSRKey, ADSREnvelopeProps, ParamADSREnvelopeProps, PitchBendProps, ModWheelProps, ParamModWheelProps, ParamPitchBendProps, } from "./synth";
|
|
39
|
+
export { mapRange, formatDb, formatHz, formatMs, formatPercent, formatSemitones, midiNoteName, midiNoteToHz, hzToMidiNote, } from "./format";
|
|
36
40
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
|
37
41
|
export type { MacroPadProps, ParamMacroPadProps, HardwareKnobProps, ParamHardwareKnobProps, CrossfaderProps, ParamCrossfaderProps, PulseOrbProps, } from "./specialty";
|
|
38
42
|
export type { DragEventPayload, LayoutRect, WheelEventPayload } from "./primitives";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import "./bridge";
|
|
|
5
5
|
export { View, Text, Image, TextInput, NativeView } from "./primitives";
|
|
6
6
|
export { render, unmount } from "./render";
|
|
7
7
|
export { native } from "./native";
|
|
8
|
-
export { useNativeEvent, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
|
|
8
|
+
export { useNativeEvent, useNativeValue, useDebounced, useThrottled, usePrevious, useToggle, useInterval, useHover, useLayoutRect, } from "./hooks";
|
|
9
9
|
export { useOverlay, OverlayLayer } from "./overlay";
|
|
10
10
|
export { Tooltip, Modal } from "./popover";
|
|
11
11
|
export { Button } from "./button";
|
|
@@ -20,5 +20,7 @@ export { NumberBox, Checkbox, RadioGroup, ParamNumberBox, ParamCheckbox, ParamRa
|
|
|
20
20
|
export { ProgressBar, Spinner } from "./feedback";
|
|
21
21
|
export { PianoKeyboard } from "./keyboard";
|
|
22
22
|
export { StepSequencer } from "./sequencer";
|
|
23
|
-
export {
|
|
23
|
+
export { Tabs, Disclosure } from "./layout";
|
|
24
|
+
export { ADSREnvelope, ParamADSREnvelope, PitchBend, ModWheel, ParamModWheel, ParamPitchBend, adsrLevelAt, } from "./synth";
|
|
25
|
+
export { mapRange, formatDb, formatHz, formatMs, formatPercent, formatSemitones, midiNoteName, midiNoteToHz, hzToMidiNote, } from "./format";
|
|
24
26
|
export { MacroPad, ParamMacroPad, HardwareKnob, ParamHardwareKnob, Crossfader, ParamCrossfader, PulseOrb, } from "./specialty";
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface TabsProps {
|
|
3
|
+
labels: string[];
|
|
4
|
+
/** Controlled active tab; omit to let Tabs manage it. */
|
|
5
|
+
index?: number;
|
|
6
|
+
/** Starting tab when uncontrolled. Default 0. */
|
|
7
|
+
defaultIndex?: number;
|
|
8
|
+
onChange?: (index: number) => void;
|
|
9
|
+
/** Fixed width; defaults to content width. */
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Space between the bar and the panel. Default 12. */
|
|
12
|
+
gap?: number;
|
|
13
|
+
accentColor?: string;
|
|
14
|
+
textColor?: string;
|
|
15
|
+
activeTextColor?: string;
|
|
16
|
+
trackColor?: string;
|
|
17
|
+
/** One panel per label (a lone child works for label-driven UIs). */
|
|
18
|
+
children?: ReactNode | ReactNode[];
|
|
19
|
+
}
|
|
20
|
+
/** The page switcher — MAIN / FX / SETTINGS. A themed tab bar with an
|
|
21
|
+
underline indicator; renders the active panel below it. */
|
|
22
|
+
export declare function Tabs({ labels, index, defaultIndex, onChange, width, gap, accentColor, textColor, activeTextColor, trackColor, children, }: TabsProps): import("react").JSX.Element;
|
|
23
|
+
export interface DisclosureProps {
|
|
24
|
+
title: string;
|
|
25
|
+
/** Controlled open state; omit to let Disclosure manage it. */
|
|
26
|
+
open?: boolean;
|
|
27
|
+
/** Starting state when uncontrolled. Default false. */
|
|
28
|
+
defaultOpen?: boolean;
|
|
29
|
+
onChange?: (open: boolean) => void;
|
|
30
|
+
width?: number;
|
|
31
|
+
textColor?: string;
|
|
32
|
+
accentColor?: string;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
/** A collapsible section row — "ADVANCED", "MODULATION". Click the
|
|
36
|
+
header to fold the content in and out. */
|
|
37
|
+
export declare function Disclosure({ title, open, defaultOpen, onChange, width, textColor, accentColor, children, }: DisclosureProps): import("react").JSX.Element;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Workspace structure — tabs for multi-page plugin UIs and the
|
|
3
|
+
// disclosure row for collapsible settings sections.
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { View, Text } from "./primitives";
|
|
6
|
+
const clampIndex = (i, count) => Math.min(Math.max(0, count - 1), Math.max(0, i));
|
|
7
|
+
/** The page switcher — MAIN / FX / SETTINGS. A themed tab bar with an
|
|
8
|
+
underline indicator; renders the active panel below it. */
|
|
9
|
+
export function Tabs({ labels, index, defaultIndex = 0, onChange, width, gap = 12, accentColor = "#C6F135", textColor = "#7c8087", activeTextColor = "#ECF2E8", trackColor = "#FFFFFF14", children, }) {
|
|
10
|
+
const [internal, setInternal] = useState(defaultIndex);
|
|
11
|
+
const current = clampIndex(index ?? internal, labels.length);
|
|
12
|
+
const select = (i) => {
|
|
13
|
+
setInternal(i);
|
|
14
|
+
onChange?.(i);
|
|
15
|
+
};
|
|
16
|
+
const panels = Array.isArray(children) ? children : children !== undefined ? [children] : [];
|
|
17
|
+
return (_jsxs(View, { style: width !== undefined ? { width } : undefined, children: [_jsxs(View, { className: "flex-row relative", style: { columnGap: 4 }, children: [_jsx(View, { className: "absolute left-0 right-0 h-[1]", style: { bottom: 0, backgroundColor: trackColor } }), labels.map((label, i) => (_jsxs(View, { className: "items-center cursor-pointer px-3 pt-1", onClick: () => select(i), children: [_jsx(Text, { className: "text-[11] font-bold tracking-widest", style: { color: i === current ? activeTextColor : textColor }, children: label }), _jsx(View, { className: "rounded-full self-stretch", style: {
|
|
18
|
+
height: 2,
|
|
19
|
+
marginTop: 6,
|
|
20
|
+
backgroundColor: i === current ? accentColor : "#00000000",
|
|
21
|
+
} })] }, `${label}-${i}`)))] }), panels.length > 0 ? (_jsx(View, { style: { marginTop: gap }, children: panels[Math.min(current, panels.length - 1)] })) : null] }));
|
|
22
|
+
}
|
|
23
|
+
/** A collapsible section row — "ADVANCED", "MODULATION". Click the
|
|
24
|
+
header to fold the content in and out. */
|
|
25
|
+
export function Disclosure({ title, open, defaultOpen = false, onChange, width, textColor = "#a1a1aa", accentColor = "#C6F135", children, }) {
|
|
26
|
+
const [internal, setInternal] = useState(defaultOpen);
|
|
27
|
+
const isOpen = open ?? internal;
|
|
28
|
+
const toggle = () => {
|
|
29
|
+
setInternal(!isOpen);
|
|
30
|
+
onChange?.(!isOpen);
|
|
31
|
+
};
|
|
32
|
+
return (_jsxs(View, { style: width !== undefined ? { width } : undefined, children: [_jsxs(View, { className: "flex-row items-center gap-2 cursor-pointer py-1", onClick: toggle, children: [_jsx(Text, { className: "text-[10] font-bold", style: { color: accentColor, width: 10 }, children: isOpen ? "▾" : "▸" }), _jsx(Text, { className: "text-[11] font-bold tracking-widest", style: { color: textColor }, children: title })] }), isOpen ? _jsx(View, { className: "pt-2 pl-5", children: children }) : null] }));
|
|
33
|
+
}
|
package/dist/meter.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface MeterProps {
|
|
|
22
22
|
thickness?: number;
|
|
23
23
|
/** Horizontal bar instead of the vertical default. */
|
|
24
24
|
horizontal?: boolean;
|
|
25
|
+
/** Fill from the top (vertical) / right (horizontal) — gain-reduction
|
|
26
|
+
meters. Default false. */
|
|
27
|
+
reverse?: boolean;
|
|
25
28
|
/** Show the peak-hold line. Default true. */
|
|
26
29
|
peak?: boolean;
|
|
27
30
|
holdMs?: number;
|
|
@@ -34,4 +37,4 @@ export interface MeterProps {
|
|
|
34
37
|
label?: string;
|
|
35
38
|
}
|
|
36
39
|
/** A natively painted level meter with hot zone + peak hold. */
|
|
37
|
-
export declare function Meter({ value, length, thickness, horizontal, peak, holdMs, decayPerSecond, hotFrom, trackColor, color, hotColor, label, }: MeterProps): import("react").JSX.Element;
|
|
40
|
+
export declare function Meter({ value, length, thickness, horizontal, reverse, peak, holdMs, decayPerSecond, hotFrom, trackColor, color, hotColor, label, }: MeterProps): import("react").JSX.Element;
|
package/dist/meter.js
CHANGED
|
@@ -34,14 +34,22 @@ export function usePeakHold(value, options = {}) {
|
|
|
34
34
|
return Math.max(peak, value);
|
|
35
35
|
}
|
|
36
36
|
/** A natively painted level meter with hot zone + peak hold. */
|
|
37
|
-
export function Meter({ value, length = 120, thickness = 10, horizontal, peak = true, holdMs, decayPerSecond, hotFrom = 0.85, trackColor = "#141714", color = "#C6F135", hotColor = "#FF4545", label, }) {
|
|
37
|
+
export function Meter({ value, length = 120, thickness = 10, horizontal, reverse = false, peak = true, holdMs, decayPerSecond, hotFrom = 0.85, trackColor = "#141714", color = "#C6F135", hotColor = "#FF4545", label, }) {
|
|
38
38
|
const level = clamp01(value);
|
|
39
39
|
const held = usePeakHold(level, { holdMs, decayPerSecond });
|
|
40
40
|
const hot = clamp01(hotFrom);
|
|
41
41
|
const fill = Math.min(level, hot) * length;
|
|
42
42
|
const hotFill = level > hot ? (level - hot) * length : 0;
|
|
43
43
|
const peakAt = clamp01(held) * length;
|
|
44
|
-
const bar = horizontal ? (_jsxs(View, { className: "relative rounded overflow-hidden", style: { width: length, height: thickness, backgroundColor: trackColor }, children: [_jsx(View, { className: "absolute
|
|
44
|
+
const bar = horizontal ? (_jsxs(View, { className: "relative rounded overflow-hidden", style: { width: length, height: thickness, backgroundColor: trackColor }, children: [_jsx(View, { className: "absolute top-0 bottom-0", style: reverse ? { right: 0, width: fill, backgroundColor: color } : { left: 0, width: fill, backgroundColor: color } }), hotFill > 0 ? (_jsx(View, { className: "absolute top-0 bottom-0", style: reverse
|
|
45
|
+
? { right: hot * length, width: hotFill, backgroundColor: hotColor }
|
|
46
|
+
: { left: hot * length, width: hotFill, backgroundColor: hotColor } })) : null, peak && peakAt > 2 ? (_jsx(View, { className: "absolute top-0 bottom-0 w-[2]", style: reverse
|
|
47
|
+
? { right: peakAt - 2, backgroundColor: held >= hot ? hotColor : color }
|
|
48
|
+
: { left: peakAt - 2, backgroundColor: held >= hot ? hotColor : color } })) : null] })) : (_jsxs(View, { className: "relative rounded overflow-hidden", style: { width: thickness, height: length, backgroundColor: trackColor }, children: [_jsx(View, { className: "absolute left-0 right-0", style: reverse ? { top: 0, height: fill, backgroundColor: color } : { bottom: 0, height: fill, backgroundColor: color } }), hotFill > 0 ? (_jsx(View, { className: "absolute left-0 right-0", style: reverse
|
|
49
|
+
? { top: hot * length, height: hotFill, backgroundColor: hotColor }
|
|
50
|
+
: { bottom: hot * length, height: hotFill, backgroundColor: hotColor } })) : null, peak && peakAt > 2 ? (_jsx(View, { className: "absolute left-0 right-0 h-[2]", style: reverse
|
|
51
|
+
? { top: peakAt - 2, backgroundColor: held >= hot ? hotColor : color }
|
|
52
|
+
: { bottom: peakAt - 2, backgroundColor: held >= hot ? hotColor : color } })) : null] }));
|
|
45
53
|
if (label === undefined)
|
|
46
54
|
return bar;
|
|
47
55
|
return (_jsxs(View, { className: "items-center gap-2", children: [bar, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
|
package/dist/synth.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type ADSRKey = "attack" | "decay" | "sustain" | "release";
|
|
2
|
+
export interface ADSREnvelopeProps {
|
|
3
|
+
/** All 0..1: times are a fraction of each stage's max width. */
|
|
4
|
+
attack: number;
|
|
5
|
+
decay: number;
|
|
6
|
+
sustain: number;
|
|
7
|
+
release: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
/** Fill resolution. Default 44 columns. */
|
|
11
|
+
columns?: number;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
trackColor?: string;
|
|
14
|
+
/** The filled envelope body. */
|
|
15
|
+
color?: string;
|
|
16
|
+
handleColor?: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
onChange: (key: ADSRKey, value: number) => void;
|
|
19
|
+
onBegin?: (key: ADSRKey) => void;
|
|
20
|
+
onEnd?: (key: ADSRKey) => void;
|
|
21
|
+
}
|
|
22
|
+
/** The envelope level at x, piecewise over A → D → S plateau → R. */
|
|
23
|
+
export declare function adsrLevelAt(x: number, width: number, attack: number, decay: number, sustain: number, release: number): number;
|
|
24
|
+
/** The classic four-corner envelope editor: drag the attack peak, the
|
|
25
|
+
decay/sustain corner (both axes), and the release corner. */
|
|
26
|
+
export declare function ADSREnvelope({ attack, decay, sustain, release, width, height, columns, disabled, trackColor, color, handleColor, label, onChange, onBegin, onEnd, }: ADSREnvelopeProps): import("react").JSX.Element;
|
|
27
|
+
export interface ParamADSREnvelopeProps {
|
|
28
|
+
attackId: string;
|
|
29
|
+
decayId: string;
|
|
30
|
+
sustainId: string;
|
|
31
|
+
releaseId: string;
|
|
32
|
+
width?: number;
|
|
33
|
+
height?: number;
|
|
34
|
+
color?: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
}
|
|
37
|
+
/** An ADSREnvelope over four host parameters, gestures opened per
|
|
38
|
+
handle (the decay/sustain corner drives both together). */
|
|
39
|
+
export declare function ParamADSREnvelope({ attackId, decayId, sustainId, releaseId, ...visual }: ParamADSREnvelopeProps): import("react").JSX.Element;
|
|
40
|
+
interface WheelSkin {
|
|
41
|
+
width?: number;
|
|
42
|
+
height?: number;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
trackColor?: string;
|
|
45
|
+
thumbColor?: string;
|
|
46
|
+
accentColor?: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface PitchBendProps extends WheelSkin {
|
|
50
|
+
/** Bend, −1..+1; fires continuously and again as the spring returns. */
|
|
51
|
+
onChange?: (value: number) => void;
|
|
52
|
+
/** Snap back to center on release. Default true. */
|
|
53
|
+
springBack?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** The pitch wheel: drag up/down from center, springs back to 0 on
|
|
56
|
+
release (momentary — it owns its own state). */
|
|
57
|
+
export declare function PitchBend({ onChange, springBack, ...skin }: PitchBendProps): import("react").JSX.Element;
|
|
58
|
+
export interface ModWheelProps extends WheelSkin {
|
|
59
|
+
/** 0..1, controlled — mod wheels stay where you leave them. */
|
|
60
|
+
value: number;
|
|
61
|
+
onChange: (value: number) => void;
|
|
62
|
+
onBegin?: () => void;
|
|
63
|
+
onEnd?: () => void;
|
|
64
|
+
}
|
|
65
|
+
/** The mod wheel: a vertical strip that holds its position. */
|
|
66
|
+
export declare function ModWheel({ value, onChange, onBegin, onEnd, ...skin }: ModWheelProps): import("react").JSX.Element;
|
|
67
|
+
export interface ParamModWheelProps extends WheelSkin {
|
|
68
|
+
paramId: string;
|
|
69
|
+
}
|
|
70
|
+
/** A ModWheel bound to a host parameter. */
|
|
71
|
+
export declare function ParamModWheel({ paramId, ...skin }: ParamModWheelProps): import("react").JSX.Element;
|
|
72
|
+
export interface ParamPitchBendProps extends WheelSkin {
|
|
73
|
+
/** A 0..1 host parameter with 0.5 center. */
|
|
74
|
+
paramId: string;
|
|
75
|
+
}
|
|
76
|
+
/** A PitchBend writing 0.5 ± bend/2 to a host parameter; release closes
|
|
77
|
+
the gesture at dead center (the visual spring is cosmetic). */
|
|
78
|
+
export declare function ParamPitchBend({ paramId, ...skin }: ParamPitchBendProps): import("react").JSX.Element;
|
|
79
|
+
export {};
|
package/dist/synth.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Synth-surface controls — the ADSR envelope editor and the pitch/mod
|
|
3
|
+
// wheels. Views only: the envelope is a filled column fill (like
|
|
4
|
+
// <Waveform>) with draggable corner handles riding on top.
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { View, Text } from "./primitives";
|
|
7
|
+
import { useSpring } from "./animation";
|
|
8
|
+
import { useParameter } from "./parameters";
|
|
9
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
10
|
+
/** The envelope level at x, piecewise over A → D → S plateau → R. */
|
|
11
|
+
export function adsrLevelAt(x, width, attack, decay, sustain, release) {
|
|
12
|
+
const segW = width * 0.27;
|
|
13
|
+
const ax = attack * segW;
|
|
14
|
+
const dw = decay * segW;
|
|
15
|
+
const rw = release * segW;
|
|
16
|
+
const rx = width - rw;
|
|
17
|
+
if (x <= ax)
|
|
18
|
+
return ax === 0 ? 1 : x / ax;
|
|
19
|
+
if (x <= ax + dw)
|
|
20
|
+
return 1 - (1 - sustain) * ((x - ax) / (dw || 1));
|
|
21
|
+
if (x <= rx)
|
|
22
|
+
return sustain;
|
|
23
|
+
return rw === 0 ? 0 : Math.max(0, sustain * (1 - (x - rx) / rw));
|
|
24
|
+
}
|
|
25
|
+
/** The classic four-corner envelope editor: drag the attack peak, the
|
|
26
|
+
decay/sustain corner (both axes), and the release corner. */
|
|
27
|
+
export function ADSREnvelope({ attack, decay, sustain, release, width = 220, height = 96, columns = 44, disabled, trackColor = "#141714", color = "#C6F13566", handleColor = "#ECF2E8", label, onChange, onBegin, onEnd, }) {
|
|
28
|
+
const start = useRef({ attack: 0, decay: 0, sustain: 0, release: 0 });
|
|
29
|
+
const a = clamp01(attack);
|
|
30
|
+
const d = clamp01(decay);
|
|
31
|
+
const s = clamp01(sustain);
|
|
32
|
+
const r = clamp01(release);
|
|
33
|
+
const segW = width * 0.27;
|
|
34
|
+
const HANDLE = 18;
|
|
35
|
+
const handleProps = (keys, move) => disabled
|
|
36
|
+
? {}
|
|
37
|
+
: {
|
|
38
|
+
onDragStart: () => {
|
|
39
|
+
start.current = { attack: a, decay: d, sustain: s, release: r };
|
|
40
|
+
for (const key of keys)
|
|
41
|
+
onBegin?.(key);
|
|
42
|
+
},
|
|
43
|
+
onDrag: (e) => move(e.dx, e.dy),
|
|
44
|
+
onDragEnd: () => {
|
|
45
|
+
for (const key of keys)
|
|
46
|
+
onEnd?.(key);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const dot = (left, top, keys, move) => (_jsx(View, { className: `absolute items-center justify-center ${disabled ? "" : "cursor-pointer"}`, style: { left: left - HANDLE / 2, top: top - HANDLE / 2, width: HANDLE, height: HANDLE }, ...handleProps(keys, move), children: _jsx(View, { className: "rounded-full border", style: { width: 9, height: 9, backgroundColor: handleColor, borderColor: "#00000088" } }) }));
|
|
50
|
+
const body = (_jsxs(View, { className: `relative rounded overflow-hidden ${disabled ? "opacity-40" : ""}`, style: { width, height, backgroundColor: trackColor }, children: [_jsx(View, { className: "absolute inset-0 flex-row items-end px-[1]", style: { columnGap: 1 }, children: Array.from({ length: columns }, (_, i) => {
|
|
51
|
+
const level = adsrLevelAt(((i + 0.5) / columns) * width, width, a, d, s, r);
|
|
52
|
+
return (_jsx(View, { className: "flex-1 rounded-[1]", style: { height: Math.max(1, level * (height - 4)), backgroundColor: color } }, i));
|
|
53
|
+
}) }), dot(a * segW, 4, ["attack"], (dx) => onChange("attack", clamp01(start.current.attack + dx / segW))), dot(a * segW + d * segW, 4 + (1 - s) * (height - 8), ["decay", "sustain"], (dx, dy) => {
|
|
54
|
+
onChange("decay", clamp01(start.current.decay + dx / segW));
|
|
55
|
+
onChange("sustain", clamp01(start.current.sustain - dy / (height - 8)));
|
|
56
|
+
}), dot(width - r * segW, 4 + (1 - s) * (height - 8), ["release"], (dx) => onChange("release", clamp01(start.current.release - dx / segW)))] }));
|
|
57
|
+
if (label === undefined)
|
|
58
|
+
return body;
|
|
59
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [body, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
|
|
60
|
+
}
|
|
61
|
+
/** An ADSREnvelope over four host parameters, gestures opened per
|
|
62
|
+
handle (the decay/sustain corner drives both together). */
|
|
63
|
+
export function ParamADSREnvelope({ attackId, decayId, sustainId, releaseId, ...visual }) {
|
|
64
|
+
const params = {
|
|
65
|
+
attack: useParameter(attackId),
|
|
66
|
+
decay: useParameter(decayId),
|
|
67
|
+
sustain: useParameter(sustainId),
|
|
68
|
+
release: useParameter(releaseId),
|
|
69
|
+
};
|
|
70
|
+
return (_jsx(ADSREnvelope, { attack: params.attack.value, decay: params.decay.value, sustain: params.sustain.value, release: params.release.value, onChange: (key, value) => params[key].set(value), onBegin: (key) => params[key].begin(), onEnd: (key) => params[key].end(), ...visual }));
|
|
71
|
+
}
|
|
72
|
+
function WheelChrome({ value01, width = 34, height = 110, disabled, trackColor = "#141714", thumbColor = "#3A4038", accentColor = "#C6F135", label, centerMark, handlers, }) {
|
|
73
|
+
const THUMB = 16;
|
|
74
|
+
const travel = height - 4 - THUMB;
|
|
75
|
+
const wheel = (_jsxs(View, { className: `relative rounded-lg border overflow-hidden ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width, height, backgroundColor: trackColor, borderColor: "#00000066" }, ...handlers, children: [centerMark ? (_jsx(View, { className: "absolute left-0 right-0 h-[1]", style: { top: height / 2, backgroundColor: "#FFFFFF2E" } })) : null, _jsx(View, { className: "absolute left-[2] right-[2] rounded", style: { top: 2 + (1 - clamp01(value01)) * travel, height: THUMB, backgroundColor: thumbColor }, children: _jsx(View, { className: "absolute left-[2] right-[2] rounded-full", style: { top: THUMB / 2 - 1.25, height: 2.5, backgroundColor: accentColor } }) })] }));
|
|
76
|
+
if (label === undefined)
|
|
77
|
+
return wheel;
|
|
78
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [wheel, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
|
|
79
|
+
}
|
|
80
|
+
/** The pitch wheel: drag up/down from center, springs back to 0 on
|
|
81
|
+
release (momentary — it owns its own state). */
|
|
82
|
+
export function PitchBend({ onChange, springBack = true, ...skin }) {
|
|
83
|
+
const [drag, setDrag] = useState(null);
|
|
84
|
+
const sprung = useSpring(drag ?? 0, { stiffness: 320, damping: 24 });
|
|
85
|
+
const value = drag ?? (springBack ? sprung : 0);
|
|
86
|
+
const onChangeRef = useRef(onChange);
|
|
87
|
+
onChangeRef.current = onChange;
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
onChangeRef.current?.(value);
|
|
90
|
+
}, [value]);
|
|
91
|
+
const startValue = useRef(0);
|
|
92
|
+
const height = skin.height ?? 110;
|
|
93
|
+
const handlers = skin.disabled
|
|
94
|
+
? {}
|
|
95
|
+
: {
|
|
96
|
+
onDragStart: () => {
|
|
97
|
+
startValue.current = drag ?? 0;
|
|
98
|
+
setDrag(startValue.current);
|
|
99
|
+
},
|
|
100
|
+
onDrag: (e) => setDrag(Math.min(1, Math.max(-1, startValue.current - e.dy / (height / 2)))),
|
|
101
|
+
onDragEnd: () => setDrag(null),
|
|
102
|
+
};
|
|
103
|
+
return _jsx(WheelChrome, { value01: (value + 1) / 2, centerMark: true, handlers: handlers, ...skin });
|
|
104
|
+
}
|
|
105
|
+
/** The mod wheel: a vertical strip that holds its position. */
|
|
106
|
+
export function ModWheel({ value, onChange, onBegin, onEnd, ...skin }) {
|
|
107
|
+
const startValue = useRef(0);
|
|
108
|
+
const height = skin.height ?? 110;
|
|
109
|
+
const handlers = skin.disabled
|
|
110
|
+
? {}
|
|
111
|
+
: {
|
|
112
|
+
onDragStart: () => {
|
|
113
|
+
startValue.current = clamp01(value);
|
|
114
|
+
onBegin?.();
|
|
115
|
+
},
|
|
116
|
+
onDrag: (e) => onChange(clamp01(startValue.current - e.dy / (height - 20))),
|
|
117
|
+
onDragEnd: () => onEnd?.(),
|
|
118
|
+
};
|
|
119
|
+
return _jsx(WheelChrome, { value01: clamp01(value), handlers: handlers, ...skin });
|
|
120
|
+
}
|
|
121
|
+
/** A ModWheel bound to a host parameter. */
|
|
122
|
+
export function ParamModWheel({ paramId, ...skin }) {
|
|
123
|
+
const param = useParameter(paramId);
|
|
124
|
+
return (_jsx(ModWheel, { value: param.value, onChange: param.set, onBegin: param.begin, onEnd: param.end, label: skin.label ?? param.name.toUpperCase(), ...skin }));
|
|
125
|
+
}
|
|
126
|
+
/** A PitchBend writing 0.5 ± bend/2 to a host parameter; release closes
|
|
127
|
+
the gesture at dead center (the visual spring is cosmetic). */
|
|
128
|
+
export function ParamPitchBend({ paramId, ...skin }) {
|
|
129
|
+
const param = useParameter(paramId);
|
|
130
|
+
const active = useRef(false);
|
|
131
|
+
return (_jsx(PitchBend, { onChange: (v) => {
|
|
132
|
+
if (v === 0 && !active.current)
|
|
133
|
+
return; // at rest (incl. mount) — nothing to write
|
|
134
|
+
if (!active.current) {
|
|
135
|
+
active.current = true;
|
|
136
|
+
param.begin();
|
|
137
|
+
}
|
|
138
|
+
param.set(0.5 + v / 2);
|
|
139
|
+
if (v === 0) {
|
|
140
|
+
active.current = false;
|
|
141
|
+
param.end();
|
|
142
|
+
}
|
|
143
|
+
}, ...skin }));
|
|
144
|
+
}
|
package/package.json
CHANGED
package/src/components.ts
CHANGED
|
@@ -68,6 +68,25 @@ export { PianoKeyboard } from "./keyboard";
|
|
|
68
68
|
export type { PianoKeyboardProps } from "./keyboard";
|
|
69
69
|
export { StepSequencer } from "./sequencer";
|
|
70
70
|
export type { StepSequencerProps } from "./sequencer";
|
|
71
|
+
export { Tabs, Disclosure } from "./layout";
|
|
72
|
+
export type { TabsProps, DisclosureProps } from "./layout";
|
|
73
|
+
export {
|
|
74
|
+
ADSREnvelope,
|
|
75
|
+
ParamADSREnvelope,
|
|
76
|
+
PitchBend,
|
|
77
|
+
ModWheel,
|
|
78
|
+
ParamModWheel,
|
|
79
|
+
ParamPitchBend,
|
|
80
|
+
} from "./synth";
|
|
81
|
+
export type {
|
|
82
|
+
ADSRKey,
|
|
83
|
+
ADSREnvelopeProps,
|
|
84
|
+
ParamADSREnvelopeProps,
|
|
85
|
+
PitchBendProps,
|
|
86
|
+
ModWheelProps,
|
|
87
|
+
ParamModWheelProps,
|
|
88
|
+
ParamPitchBendProps,
|
|
89
|
+
} from "./synth";
|
|
71
90
|
export {
|
|
72
91
|
MacroPad,
|
|
73
92
|
ParamMacroPad,
|
package/src/format.ts
CHANGED
|
@@ -60,3 +60,14 @@ export function midiNoteName(note: number): string {
|
|
|
60
60
|
const n = Math.round(note);
|
|
61
61
|
return `${NOTE_NAMES[((n % 12) + 12) % 12]}${Math.floor(n / 12) - 1}`;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/** A MIDI note as a frequency (equal temperament): 69 → 440. Feed your
|
|
65
|
+
oscillator straight from PianoKeyboard's onNoteOn. */
|
|
66
|
+
export function midiNoteToHz(note: number, a4 = 440): number {
|
|
67
|
+
return a4 * Math.pow(2, (note - 69) / 12);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The nearest MIDI note for a frequency: 440 → 69. */
|
|
71
|
+
export function hzToMidiNote(hz: number, a4 = 440): number {
|
|
72
|
+
return Math.round(69 + 12 * Math.log2(hz / a4));
|
|
73
|
+
}
|
package/src/hooks.ts
CHANGED
|
@@ -29,6 +29,19 @@ export function useNativeEvent(name: string, handler: (payload: any) => void): v
|
|
|
29
29
|
useEffect(() => native.on(name, (payload) => handlerRef.current(payload)), [name]);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The latest payload of a C++ event, held as state — the one-liner for
|
|
34
|
+
* native → UI data feeds:
|
|
35
|
+
*
|
|
36
|
+
* const meter = useNativeValue("meter", { level: 0 });
|
|
37
|
+
* <Meter value={meter.level} />
|
|
38
|
+
*/
|
|
39
|
+
export function useNativeValue<T = any>(name: string, initial: T): T {
|
|
40
|
+
const [value, setValue] = useState<T>(initial);
|
|
41
|
+
useNativeEvent(name, setValue);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* The value, but only after it has stopped changing for `delayMs` —
|
|
34
47
|
* classic input debouncing for expensive native calls:
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { render, unmount } from "./render";
|
|
|
15
15
|
export { native } from "./native";
|
|
16
16
|
export {
|
|
17
17
|
useNativeEvent,
|
|
18
|
+
useNativeValue,
|
|
18
19
|
useDebounced,
|
|
19
20
|
useThrottled,
|
|
20
21
|
usePrevious,
|
|
@@ -94,6 +95,26 @@ export { PianoKeyboard } from "./keyboard";
|
|
|
94
95
|
export type { PianoKeyboardProps } from "./keyboard";
|
|
95
96
|
export { StepSequencer } from "./sequencer";
|
|
96
97
|
export type { StepSequencerProps } from "./sequencer";
|
|
98
|
+
export { Tabs, Disclosure } from "./layout";
|
|
99
|
+
export type { TabsProps, DisclosureProps } from "./layout";
|
|
100
|
+
export {
|
|
101
|
+
ADSREnvelope,
|
|
102
|
+
ParamADSREnvelope,
|
|
103
|
+
PitchBend,
|
|
104
|
+
ModWheel,
|
|
105
|
+
ParamModWheel,
|
|
106
|
+
ParamPitchBend,
|
|
107
|
+
adsrLevelAt,
|
|
108
|
+
} from "./synth";
|
|
109
|
+
export type {
|
|
110
|
+
ADSRKey,
|
|
111
|
+
ADSREnvelopeProps,
|
|
112
|
+
ParamADSREnvelopeProps,
|
|
113
|
+
PitchBendProps,
|
|
114
|
+
ModWheelProps,
|
|
115
|
+
ParamModWheelProps,
|
|
116
|
+
ParamPitchBendProps,
|
|
117
|
+
} from "./synth";
|
|
97
118
|
export {
|
|
98
119
|
mapRange,
|
|
99
120
|
formatDb,
|
|
@@ -102,6 +123,8 @@ export {
|
|
|
102
123
|
formatPercent,
|
|
103
124
|
formatSemitones,
|
|
104
125
|
midiNoteName,
|
|
126
|
+
midiNoteToHz,
|
|
127
|
+
hzToMidiNote,
|
|
105
128
|
} from "./format";
|
|
106
129
|
export {
|
|
107
130
|
MacroPad,
|