@vsreact/core 0.0.2 → 0.0.4
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/animation.d.ts +19 -0
- package/dist/animation.js +43 -0
- package/dist/controls.d.ts +96 -2
- package/dist/controls.js +99 -12
- package/dist/cx.d.ts +6 -0
- package/dist/cx.js +26 -0
- package/dist/hooks.d.ts +16 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +7 -4
- package/dist/meter.d.ts +37 -0
- package/dist/meter.js +48 -0
- package/dist/parameters.d.ts +14 -0
- package/dist/parameters.js +20 -0
- package/dist/theme.js +76 -1
- package/dist/tw.js +13 -1
- package/package.json +1 -1
- package/src/animation.test.tsx +89 -1
- package/src/animation.ts +71 -0
- package/src/controls.test.tsx +193 -0
- package/src/controls.tsx +392 -20
- package/src/cx.ts +33 -0
- package/src/hooks.ts +36 -0
- package/src/index.ts +54 -24
- package/src/meter.tsx +159 -0
- package/src/parameters.ts +30 -0
- package/src/theme.ts +76 -1
- package/src/tw.test.ts +38 -0
- package/src/tw.ts +10 -1
package/dist/animation.d.ts
CHANGED
|
@@ -17,3 +17,22 @@ export interface TweenOptions {
|
|
|
17
17
|
/** Eased progress 0→1 that starts when the component mounts. */
|
|
18
18
|
export declare function useTween({ duration, delay, easing, onComplete }: TweenOptions): number;
|
|
19
19
|
export declare function lerp(from: number, to: number, t: number): number;
|
|
20
|
+
export interface SpringOptions {
|
|
21
|
+
/** Spring constant — higher snaps faster. Default 170. */
|
|
22
|
+
stiffness?: number;
|
|
23
|
+
/** Velocity drag — higher settles with less bounce. Default 24. */
|
|
24
|
+
damping?: number;
|
|
25
|
+
mass?: number;
|
|
26
|
+
/** Distance from target below which the spring snaps and stops. */
|
|
27
|
+
restDelta?: number;
|
|
28
|
+
}
|
|
29
|
+
/** One semi-implicit Euler integration step; exported for tests and for
|
|
30
|
+
driving springs from your own loops. Returns [position, velocity]. */
|
|
31
|
+
export declare function springStep(position: number, velocity: number, target: number, { stiffness, damping, mass }: SpringOptions, dtMs: number): [number, number];
|
|
32
|
+
/**
|
|
33
|
+
* A value that springs toward `target` whenever it changes — for
|
|
34
|
+
* interactive motion where a fixed-duration tween feels wrong (toggle
|
|
35
|
+
* thumbs, drawers, meters chasing levels). Runs on the host timer like
|
|
36
|
+
* useTween; starts at rest on the initial target.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useSpring(target: number, options?: SpringOptions): number;
|
package/dist/animation.js
CHANGED
|
@@ -45,3 +45,46 @@ export function useTween({ duration, delay = 0, easing = Easing.outCubic, onComp
|
|
|
45
45
|
export function lerp(from, to, t) {
|
|
46
46
|
return from + (to - from) * t;
|
|
47
47
|
}
|
|
48
|
+
/** One semi-implicit Euler integration step; exported for tests and for
|
|
49
|
+
driving springs from your own loops. Returns [position, velocity]. */
|
|
50
|
+
export function springStep(position, velocity, target, { stiffness = 170, damping = 24, mass = 1 }, dtMs) {
|
|
51
|
+
const dt = dtMs / 1000;
|
|
52
|
+
const acceleration = (-stiffness * (position - target) - damping * velocity) / mass;
|
|
53
|
+
const v = velocity + acceleration * dt;
|
|
54
|
+
return [position + v * dt, v];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A value that springs toward `target` whenever it changes — for
|
|
58
|
+
* interactive motion where a fixed-duration tween feels wrong (toggle
|
|
59
|
+
* thumbs, drawers, meters chasing levels). Runs on the host timer like
|
|
60
|
+
* useTween; starts at rest on the initial target.
|
|
61
|
+
*/
|
|
62
|
+
export function useSpring(target, options = {}) {
|
|
63
|
+
const [value, setValue] = useState(target);
|
|
64
|
+
const state = useRef({ position: target, velocity: 0 });
|
|
65
|
+
const targetRef = useRef(target);
|
|
66
|
+
targetRef.current = target;
|
|
67
|
+
const optionsRef = useRef(options);
|
|
68
|
+
optionsRef.current = options;
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const restDelta = optionsRef.current.restDelta ?? 0.001;
|
|
71
|
+
if (Math.abs(state.current.position - targetRef.current) <= restDelta &&
|
|
72
|
+
Math.abs(state.current.velocity) <= restDelta) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const id = setInterval(() => {
|
|
76
|
+
const s = state.current;
|
|
77
|
+
const [p, v] = springStep(s.position, s.velocity, targetRef.current, optionsRef.current, FRAME_MS);
|
|
78
|
+
s.position = p;
|
|
79
|
+
s.velocity = v;
|
|
80
|
+
if (Math.abs(p - targetRef.current) <= restDelta && Math.abs(v) <= restDelta) {
|
|
81
|
+
s.position = targetRef.current;
|
|
82
|
+
s.velocity = 0;
|
|
83
|
+
clearInterval(id);
|
|
84
|
+
}
|
|
85
|
+
setValue(s.position);
|
|
86
|
+
}, FRAME_MS);
|
|
87
|
+
return () => clearInterval(id);
|
|
88
|
+
}, [target]);
|
|
89
|
+
return value;
|
|
90
|
+
}
|
package/dist/controls.d.ts
CHANGED
|
@@ -24,7 +24,12 @@ export interface ParamKnobProps {
|
|
|
24
24
|
export declare function ParamKnob({ paramId, label, size, trackColor, valueColor }: ParamKnobProps): import("react").JSX.Element;
|
|
25
25
|
export interface SliderProps {
|
|
26
26
|
value: number;
|
|
27
|
+
/** Track length when horizontal (default 160). */
|
|
27
28
|
width?: number;
|
|
29
|
+
/** Track length when vertical (default 160). */
|
|
30
|
+
height?: number;
|
|
31
|
+
/** Vertical fader — drag up for more, fill rises from the bottom. */
|
|
32
|
+
vertical?: boolean;
|
|
28
33
|
label?: string;
|
|
29
34
|
disabled?: boolean;
|
|
30
35
|
trackColor?: string;
|
|
@@ -33,11 +38,100 @@ export interface SliderProps {
|
|
|
33
38
|
onBegin?: () => void;
|
|
34
39
|
onEnd?: () => void;
|
|
35
40
|
}
|
|
36
|
-
export declare function Slider({ value, width, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
|
|
41
|
+
export declare function Slider({ value, width, height, vertical, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: SliderProps): import("react").JSX.Element;
|
|
37
42
|
export interface ParamSliderProps {
|
|
38
43
|
paramId: string;
|
|
39
44
|
label?: string;
|
|
40
45
|
width?: number;
|
|
46
|
+
height?: number;
|
|
47
|
+
vertical?: boolean;
|
|
41
48
|
}
|
|
42
49
|
/** A Slider bound to an APVTS parameter (via ParameterBridge). */
|
|
43
|
-
export declare function ParamSlider({ paramId, label, width }: ParamSliderProps): import("react").JSX.Element;
|
|
50
|
+
export declare function ParamSlider({ paramId, label, width, height, vertical }: ParamSliderProps): import("react").JSX.Element;
|
|
51
|
+
export interface ToggleProps {
|
|
52
|
+
on: boolean;
|
|
53
|
+
label?: string;
|
|
54
|
+
/** Track height; width is 1.8×. Default 22. */
|
|
55
|
+
size?: number;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
trackColor?: string;
|
|
58
|
+
onColor?: string;
|
|
59
|
+
thumbColor?: string;
|
|
60
|
+
onChange: (on: boolean) => void;
|
|
61
|
+
}
|
|
62
|
+
/** A switch with a spring-animated thumb — bypass, on/off, A/B. */
|
|
63
|
+
export declare function Toggle({ on, label, size, disabled, trackColor, onColor, thumbColor, onChange, }: ToggleProps): import("react").JSX.Element;
|
|
64
|
+
export interface ParamToggleProps {
|
|
65
|
+
paramId: string;
|
|
66
|
+
label?: string;
|
|
67
|
+
size?: number;
|
|
68
|
+
trackColor?: string;
|
|
69
|
+
onColor?: string;
|
|
70
|
+
thumbColor?: string;
|
|
71
|
+
}
|
|
72
|
+
/** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
|
|
73
|
+
export declare function ParamToggle({ paramId, label, ...rest }: ParamToggleProps): import("react").JSX.Element;
|
|
74
|
+
export interface XYPadProps {
|
|
75
|
+
/** Normalized 0..1. */
|
|
76
|
+
x: number;
|
|
77
|
+
/** Normalized 0..1 — 1 is the TOP of the pad (drag up for more). */
|
|
78
|
+
y: number;
|
|
79
|
+
width?: number;
|
|
80
|
+
height?: number;
|
|
81
|
+
label?: string;
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
trackColor?: string;
|
|
84
|
+
valueColor?: string;
|
|
85
|
+
onChange: (x: number, y: number) => void;
|
|
86
|
+
onBegin?: () => void;
|
|
87
|
+
onEnd?: () => void;
|
|
88
|
+
}
|
|
89
|
+
/** A 2D drag pad with crosshair — filter cutoff/resonance, pan/depth… */
|
|
90
|
+
export declare function XYPad({ x, y, width, height, label, disabled, trackColor, valueColor, onChange, onBegin, onEnd, }: XYPadProps): import("react").JSX.Element;
|
|
91
|
+
export interface ParamXYPadProps {
|
|
92
|
+
paramX: string;
|
|
93
|
+
paramY: string;
|
|
94
|
+
width?: number;
|
|
95
|
+
height?: number;
|
|
96
|
+
label?: string;
|
|
97
|
+
trackColor?: string;
|
|
98
|
+
valueColor?: string;
|
|
99
|
+
}
|
|
100
|
+
/** An XYPad driving two APVTS parameters at once. */
|
|
101
|
+
export declare function ParamXYPad({ paramX, paramY, label, ...rest }: ParamXYPadProps): import("react").JSX.Element;
|
|
102
|
+
export interface SegmentedProps {
|
|
103
|
+
options: string[];
|
|
104
|
+
index: number;
|
|
105
|
+
label?: string;
|
|
106
|
+
disabled?: boolean;
|
|
107
|
+
trackColor?: string;
|
|
108
|
+
activeColor?: string;
|
|
109
|
+
textColor?: string;
|
|
110
|
+
activeTextColor?: string;
|
|
111
|
+
onChange: (index: number) => void;
|
|
112
|
+
}
|
|
113
|
+
/** A row of exclusive options — oscillator shapes, filter modes, A/B/C. */
|
|
114
|
+
export declare function Segmented({ options, index, label, disabled, trackColor, activeColor, textColor, activeTextColor, onChange, }: SegmentedProps): import("react").JSX.Element;
|
|
115
|
+
export interface ParamSegmentedProps {
|
|
116
|
+
paramId: string;
|
|
117
|
+
options: string[];
|
|
118
|
+
label?: string;
|
|
119
|
+
trackColor?: string;
|
|
120
|
+
activeColor?: string;
|
|
121
|
+
textColor?: string;
|
|
122
|
+
activeTextColor?: string;
|
|
123
|
+
}
|
|
124
|
+
/** A Segmented bound to a choice-style APVTS parameter: the normalized
|
|
125
|
+
value maps to an option index (index / (count − 1)). */
|
|
126
|
+
export declare function ParamSegmented({ paramId, options, label, ...rest }: ParamSegmentedProps): import("react").JSX.Element;
|
|
127
|
+
export interface GenericEditorProps {
|
|
128
|
+
/** Knobs per row. Default 4. */
|
|
129
|
+
columns?: number;
|
|
130
|
+
/** Knob diameter. Default 72. */
|
|
131
|
+
size?: number;
|
|
132
|
+
trackColor?: string;
|
|
133
|
+
valueColor?: string;
|
|
134
|
+
}
|
|
135
|
+
/** The zero-effort editor: one knob per APVTS parameter, laid out in
|
|
136
|
+
rows. `render(<GenericEditor />)` is a complete, working plugin UI. */
|
|
137
|
+
export declare function GenericEditor({ columns, size, trackColor, valueColor }: GenericEditorProps): import("react").JSX.Element;
|
package/dist/controls.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// The built-in VST controls — knobs, sliders, toggles, XY pads, segmented
|
|
3
|
+
// switches — drawn by the VSReacT painter and driven by drag gestures.
|
|
4
|
+
// Each has a Param* variant bound to an APVTS parameter.
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import { View, Text } from "./primitives";
|
|
6
|
-
import { useParameter } from "./parameters";
|
|
7
|
+
import { useParameter, useParameterList } from "./parameters";
|
|
8
|
+
import { useSpring } from "./animation";
|
|
9
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
7
10
|
/** Vertical-drag-to-value mapping shared by Knob (and tested in isolation). */
|
|
8
11
|
export function dragToValue(startValue, dy, sensitivity = 0.005) {
|
|
9
|
-
return
|
|
12
|
+
return clamp01(startValue - dy * sensitivity);
|
|
10
13
|
}
|
|
11
14
|
const ARC_START = -135;
|
|
12
15
|
const ARC_END = 135;
|
|
@@ -33,20 +36,104 @@ export function ParamKnob({ paramId, label, size, trackColor, valueColor }) {
|
|
|
33
36
|
const param = useParameter(paramId);
|
|
34
37
|
return (_jsx(Knob, { value: param.value, text: param.text, label: label ?? param.name.toUpperCase(), size: size, trackColor: trackColor, valueColor: valueColor, onChange: param.set, onBegin: param.begin, onEnd: param.end }));
|
|
35
38
|
}
|
|
36
|
-
export function Slider({ value, width = 160, label, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
|
|
39
|
+
export function Slider({ value, width = 160, height = 160, vertical, label, disabled, trackColor = "#2A2F27", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
|
|
37
40
|
const startValue = useRef(0);
|
|
38
|
-
const clamped =
|
|
39
|
-
|
|
41
|
+
const clamped = clamp01(value);
|
|
42
|
+
const onDragStart = disabled
|
|
43
|
+
? undefined
|
|
44
|
+
: () => {
|
|
45
|
+
startValue.current = clamped;
|
|
46
|
+
onBegin?.();
|
|
47
|
+
};
|
|
48
|
+
const onDragEnd = disabled ? undefined : () => onEnd?.();
|
|
49
|
+
if (vertical) {
|
|
50
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `w-[18] relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { height }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current - e.dy / height)), onDragEnd: onDragEnd, children: [_jsx(View, { className: "absolute w-[4] rounded-full left-[7] top-0 bottom-0", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute w-[4] rounded-full left-[7] bottom-0", style: { height: clamped * height, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full left-[3]", style: { top: (1 - clamped) * (height - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(View, { className: "gap-2", children: [_jsxs(View, { className: `h-[18] justify-center relative ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width }, onDragStart: onDragStart, onDrag: disabled ? undefined : (e) => onChange(clamp01(startValue.current + e.dx / width)), onDragEnd: onDragEnd, children: [_jsx(View, { className: "h-[4] rounded-full", style: { backgroundColor: trackColor } }), _jsx(View, { className: "absolute h-[4] rounded-full top-[7]", style: { width: clamped * width, backgroundColor: valueColor } }), _jsx(View, { className: "absolute w-[12] h-[12] rounded-full top-[3]", style: { left: clamped * (width - 12), backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
|
|
53
|
+
}
|
|
54
|
+
/** A Slider bound to an APVTS parameter (via ParameterBridge). */
|
|
55
|
+
export function ParamSlider({ paramId, label, width, height, vertical }) {
|
|
56
|
+
const param = useParameter(paramId);
|
|
57
|
+
return (_jsx(Slider, { value: param.value, label: label ?? param.name.toUpperCase(), width: width, height: height, vertical: vertical, onChange: param.set, onBegin: param.begin, onEnd: param.end }));
|
|
58
|
+
}
|
|
59
|
+
/** A switch with a spring-animated thumb — bypass, on/off, A/B. */
|
|
60
|
+
export function Toggle({ on, label, size = 22, disabled, trackColor = "#2A2F27", onColor = "#C6F135", thumbColor = "#F4F4F5", onChange, }) {
|
|
61
|
+
const t = useSpring(on ? 1 : 0, { stiffness: 300, damping: 28 });
|
|
62
|
+
const trackWidth = Math.round(size * 1.8);
|
|
63
|
+
const thumb = size - 6;
|
|
64
|
+
const travel = trackWidth - thumb - 6;
|
|
65
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `relative rounded-full ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width: trackWidth, height: size, backgroundColor: on ? onColor : trackColor }, onClick: disabled ? undefined : () => onChange(!on), children: _jsx(View, { className: "absolute rounded-full", style: {
|
|
66
|
+
width: thumb,
|
|
67
|
+
height: thumb,
|
|
68
|
+
top: 3,
|
|
69
|
+
left: 3 + clamp01(t) * travel,
|
|
70
|
+
backgroundColor: thumbColor,
|
|
71
|
+
} }) }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
|
|
72
|
+
}
|
|
73
|
+
/** A Toggle bound to a bool-style APVTS parameter (on = value ≥ 0.5). */
|
|
74
|
+
export function ParamToggle({ paramId, label, ...rest }) {
|
|
75
|
+
const param = useParameter(paramId);
|
|
76
|
+
return (_jsx(Toggle, { on: param.value >= 0.5, label: label ?? param.name.toUpperCase(), onChange: (next) => {
|
|
77
|
+
param.begin();
|
|
78
|
+
param.set(next ? 1 : 0);
|
|
79
|
+
param.end();
|
|
80
|
+
}, ...rest }));
|
|
81
|
+
}
|
|
82
|
+
/** A 2D drag pad with crosshair — filter cutoff/resonance, pan/depth… */
|
|
83
|
+
export function XYPad({ x, y, width = 160, height = 160, label, disabled, trackColor = "#161B17", valueColor = "#C6F135", onChange, onBegin, onEnd, }) {
|
|
84
|
+
const start = useRef({ x: 0, y: 0 });
|
|
85
|
+
const cxv = clamp01(x);
|
|
86
|
+
const cyv = clamp01(y);
|
|
87
|
+
const thumb = 14;
|
|
88
|
+
const tx = cxv * (width - thumb);
|
|
89
|
+
const ty = (1 - cyv) * (height - thumb);
|
|
90
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [_jsxs(View, { className: `relative rounded-lg overflow-hidden border ${disabled ? "opacity-40" : "cursor-pointer"}`, style: { width, height, backgroundColor: trackColor, borderColor: "#00000055" }, onDragStart: disabled
|
|
40
91
|
? undefined
|
|
41
92
|
: () => {
|
|
42
|
-
|
|
93
|
+
start.current = { x: cxv, y: cyv };
|
|
43
94
|
onBegin?.();
|
|
44
95
|
}, onDrag: disabled
|
|
45
96
|
? undefined
|
|
46
|
-
: (e) => onChange(
|
|
97
|
+
: (e) => onChange(clamp01(start.current.x + e.dx / width), clamp01(start.current.y - e.dy / height)), onDragEnd: disabled ? undefined : () => onEnd?.(), children: [_jsx(View, { className: "absolute left-0 right-0 h-[1]", style: { top: ty + thumb / 2, backgroundColor: valueColor, opacity: 0.35 } }), _jsx(View, { className: "absolute top-0 bottom-0 w-[1]", style: { left: tx + thumb / 2, backgroundColor: valueColor, opacity: 0.35 } }), _jsx(View, { className: "absolute rounded-full", style: { width: thumb, height: thumb, left: tx, top: ty, backgroundColor: valueColor } })] }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
|
|
47
98
|
}
|
|
48
|
-
/**
|
|
49
|
-
export function
|
|
99
|
+
/** An XYPad driving two APVTS parameters at once. */
|
|
100
|
+
export function ParamXYPad({ paramX, paramY, label, ...rest }) {
|
|
101
|
+
const px = useParameter(paramX);
|
|
102
|
+
const py = useParameter(paramY);
|
|
103
|
+
return (_jsx(XYPad, { x: px.value, y: py.value, label: label ?? `${px.name} / ${py.name}`.toUpperCase(), onChange: (nx, ny) => {
|
|
104
|
+
px.set(nx);
|
|
105
|
+
py.set(ny);
|
|
106
|
+
}, onBegin: () => {
|
|
107
|
+
px.begin();
|
|
108
|
+
py.begin();
|
|
109
|
+
}, onEnd: () => {
|
|
110
|
+
px.end();
|
|
111
|
+
py.end();
|
|
112
|
+
}, ...rest }));
|
|
113
|
+
}
|
|
114
|
+
/** A row of exclusive options — oscillator shapes, filter modes, A/B/C. */
|
|
115
|
+
export function Segmented({ options, index, label, disabled, trackColor = "#2A2F27", activeColor = "#C6F135", textColor = "#a1a1aa", activeTextColor = "#09090b", onChange, }) {
|
|
116
|
+
const current = Math.min(options.length - 1, Math.max(0, index));
|
|
117
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [_jsx(View, { className: `flex-row rounded-lg p-[3] gap-[3] ${disabled ? "opacity-40" : ""}`, style: { backgroundColor: trackColor }, children: options.map((option, i) => (_jsx(View, { className: `px-3 py-[6] rounded-md ${disabled ? "" : "cursor-pointer"}`, style: { backgroundColor: i === current ? activeColor : "#00000000" }, onClick: disabled ? undefined : () => onChange(i), children: _jsx(Text, { className: "text-[11] font-bold tracking-wide", style: { color: i === current ? activeTextColor : textColor }, children: option }) }, `${option}-${i}`))) }), label !== undefined ? (_jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })) : null] }));
|
|
118
|
+
}
|
|
119
|
+
/** A Segmented bound to a choice-style APVTS parameter: the normalized
|
|
120
|
+
value maps to an option index (index / (count − 1)). */
|
|
121
|
+
export function ParamSegmented({ paramId, options, label, ...rest }) {
|
|
50
122
|
const param = useParameter(paramId);
|
|
51
|
-
|
|
123
|
+
const count = Math.max(1, options.length);
|
|
124
|
+
const index = Math.round(param.value * (count - 1));
|
|
125
|
+
return (_jsx(Segmented, { options: options, index: index, label: label ?? param.name.toUpperCase(), onChange: (i) => {
|
|
126
|
+
param.begin();
|
|
127
|
+
param.set(count <= 1 ? 0 : i / (count - 1));
|
|
128
|
+
param.end();
|
|
129
|
+
}, ...rest }));
|
|
130
|
+
}
|
|
131
|
+
/** The zero-effort editor: one knob per APVTS parameter, laid out in
|
|
132
|
+
rows. `render(<GenericEditor />)` is a complete, working plugin UI. */
|
|
133
|
+
export function GenericEditor({ columns = 4, size = 72, trackColor, valueColor }) {
|
|
134
|
+
const params = useParameterList();
|
|
135
|
+
const rows = [];
|
|
136
|
+
for (let i = 0; i < params.length; i += Math.max(1, columns))
|
|
137
|
+
rows.push(params.slice(i, i + Math.max(1, columns)));
|
|
138
|
+
return (_jsx(View, { className: "flex-1 flex-col items-center justify-center gap-6 p-4", children: rows.map((row, index) => (_jsx(View, { className: "flex-row gap-7", children: row.map((param) => (_jsx(ParamKnob, { paramId: param.id, size: size, trackColor: trackColor, valueColor: valueColor }, param.id))) }, index))) }));
|
|
52
139
|
}
|
package/dist/cx.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ClassValue = string | number | null | false | undefined | ClassValue[] | Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* `cx("flex", active && "bg-red-500", { "opacity-40": disabled })`
|
|
4
|
+
* → `"flex bg-red-500 opacity-40"`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cx(...inputs: ClassValue[]): string;
|
package/dist/cx.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// cx — compose conditional className strings (a tiny clsx).
|
|
2
|
+
/**
|
|
3
|
+
* `cx("flex", active && "bg-red-500", { "opacity-40": disabled })`
|
|
4
|
+
* → `"flex bg-red-500 opacity-40"`.
|
|
5
|
+
*/
|
|
6
|
+
export function cx(...inputs) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const input of inputs) {
|
|
9
|
+
if (!input && input !== 0)
|
|
10
|
+
continue;
|
|
11
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
12
|
+
out.push(String(input));
|
|
13
|
+
}
|
|
14
|
+
else if (Array.isArray(input)) {
|
|
15
|
+
const nested = cx(...input);
|
|
16
|
+
if (nested)
|
|
17
|
+
out.push(nested);
|
|
18
|
+
}
|
|
19
|
+
else if (typeof input === "object") {
|
|
20
|
+
for (const [key, value] of Object.entries(input))
|
|
21
|
+
if (value)
|
|
22
|
+
out.push(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out.join(" ");
|
|
26
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
|
|
3
|
+
* of the component. The handler can close over fresh state — it is kept
|
|
4
|
+
* current without resubscribing.
|
|
5
|
+
*
|
|
6
|
+
* useNativeEvent("download:progress", (p) => setProgress(p.ratio));
|
|
7
|
+
*/
|
|
8
|
+
export declare function useNativeEvent(name: string, handler: (payload: any) => void): void;
|
|
9
|
+
/**
|
|
10
|
+
* The value, but only after it has stopped changing for `delayMs` —
|
|
11
|
+
* classic input debouncing for expensive native calls:
|
|
12
|
+
*
|
|
13
|
+
* const query = useDebounced(text, 250);
|
|
14
|
+
* useEffect(() => { native.call("library:search", { query }); }, [query]);
|
|
15
|
+
*/
|
|
16
|
+
export declare function useDebounced<T>(value: T, delayMs: number): T;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Convenience hooks over the native bridge.
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { native } from "./native";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribes to a C++ event (RootView::sendNativeEvent) for the lifetime
|
|
6
|
+
* of the component. The handler can close over fresh state — it is kept
|
|
7
|
+
* current without resubscribing.
|
|
8
|
+
*
|
|
9
|
+
* useNativeEvent("download:progress", (p) => setProgress(p.ratio));
|
|
10
|
+
*/
|
|
11
|
+
export function useNativeEvent(name, handler) {
|
|
12
|
+
const handlerRef = useRef(handler);
|
|
13
|
+
handlerRef.current = handler;
|
|
14
|
+
useEffect(() => native.on(name, (payload) => handlerRef.current(payload)), [name]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The value, but only after it has stopped changing for `delayMs` —
|
|
18
|
+
* classic input debouncing for expensive native calls:
|
|
19
|
+
*
|
|
20
|
+
* const query = useDebounced(text, 250);
|
|
21
|
+
* useEffect(() => { native.call("library:search", { query }); }, [query]);
|
|
22
|
+
*/
|
|
23
|
+
export function useDebounced(value, delayMs) {
|
|
24
|
+
const [debounced, setDebounced] = useState(value);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
27
|
+
return () => clearTimeout(id);
|
|
28
|
+
}, [value, delayMs]);
|
|
29
|
+
return debounced;
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,17 @@ 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 } from "./hooks";
|
|
7
8
|
export { configureTheme, tw } from "./tw";
|
|
8
9
|
export type { Style, ResolvedClasses } from "./tw";
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
11
|
-
export {
|
|
12
|
-
export type {
|
|
13
|
-
export {
|
|
14
|
-
export type {
|
|
10
|
+
export { cx } from "./cx";
|
|
11
|
+
export type { ClassValue } from "./cx";
|
|
12
|
+
export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
|
|
13
|
+
export type { TweenOptions, SpringOptions, EasingFn } from "./animation";
|
|
14
|
+
export { useParameter, useParameterList } from "./parameters";
|
|
15
|
+
export type { ParameterState, ParameterHandle, ParameterInfo } from "./parameters";
|
|
16
|
+
export { Knob, Slider, Toggle, XYPad, Segmented, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, dragToValue, } from "./controls";
|
|
17
|
+
export type { KnobProps, SliderProps, ToggleProps, XYPadProps, SegmentedProps, GenericEditorProps, ParamKnobProps, ParamSliderProps, ParamToggleProps, ParamXYPadProps, ParamSegmentedProps, } from "./controls";
|
|
18
|
+
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
|
19
|
+
export type { MeterProps, PeakHoldOptions, PeakHoldState } from "./meter";
|
|
15
20
|
export type { DragEventPayload } from "./primitives";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
// vsreact public API. runtime must load first — it installs the
|
|
1
|
+
// @vsreact/core public API. runtime must load first — it installs the
|
|
2
2
|
// timer/console/microtask shims react depends on inside QuickJS.
|
|
3
3
|
import "./runtime";
|
|
4
4
|
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 } from "./hooks";
|
|
8
9
|
export { configureTheme, tw } from "./tw";
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
10
|
+
export { cx } from "./cx";
|
|
11
|
+
export { useTween, useSpring, springStep, lerp, Easing } from "./animation";
|
|
12
|
+
export { useParameter, useParameterList } from "./parameters";
|
|
13
|
+
export { Knob, Slider, Toggle, XYPad, Segmented, GenericEditor, ParamKnob, ParamSlider, ParamToggle, ParamXYPad, ParamSegmented, dragToValue, } from "./controls";
|
|
14
|
+
export { Meter, usePeakHold, peakHoldStep } from "./meter";
|
package/dist/meter.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface PeakHoldOptions {
|
|
2
|
+
/** How long a peak is held before it starts falling. Default 600ms. */
|
|
3
|
+
holdMs?: number;
|
|
4
|
+
/** Fall rate once the hold expires, in value/second. Default 1.5. */
|
|
5
|
+
decayPerSecond?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PeakHoldState {
|
|
8
|
+
peak: number;
|
|
9
|
+
heldForMs: number;
|
|
10
|
+
}
|
|
11
|
+
/** One peak-hold step (pure, testable): new peaks latch instantly and
|
|
12
|
+
reset the hold timer; after holdMs the peak decays toward the value. */
|
|
13
|
+
export declare function peakHoldStep(state: PeakHoldState, value: number, dtMs: number, { holdMs, decayPerSecond }?: PeakHoldOptions): PeakHoldState;
|
|
14
|
+
/** The held peak for a live value — drives the Meter's peak line. */
|
|
15
|
+
export declare function usePeakHold(value: number, options?: PeakHoldOptions): number;
|
|
16
|
+
export interface MeterProps {
|
|
17
|
+
/** Level 0..1. */
|
|
18
|
+
value: number;
|
|
19
|
+
/** Long-axis length. Default 120. */
|
|
20
|
+
length?: number;
|
|
21
|
+
/** Short-axis thickness. Default 10. */
|
|
22
|
+
thickness?: number;
|
|
23
|
+
/** Horizontal bar instead of the vertical default. */
|
|
24
|
+
horizontal?: boolean;
|
|
25
|
+
/** Show the peak-hold line. Default true. */
|
|
26
|
+
peak?: boolean;
|
|
27
|
+
holdMs?: number;
|
|
28
|
+
decayPerSecond?: number;
|
|
29
|
+
/** Where the fill turns hot, 0..1. Default 0.85. */
|
|
30
|
+
hotFrom?: number;
|
|
31
|
+
trackColor?: string;
|
|
32
|
+
color?: string;
|
|
33
|
+
hotColor?: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
36
|
+
/** 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;
|
package/dist/meter.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Meter — the level meter every plugin needs: painted bar with a hot zone
|
|
3
|
+
// and a peak-hold line that holds, then falls. Feed it any 0..1 value
|
|
4
|
+
// (typically pushed from C++ via useNativeEvent).
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { View, Text } from "./primitives";
|
|
7
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
8
|
+
/** One peak-hold step (pure, testable): new peaks latch instantly and
|
|
9
|
+
reset the hold timer; after holdMs the peak decays toward the value. */
|
|
10
|
+
export function peakHoldStep(state, value, dtMs, { holdMs = 600, decayPerSecond = 1.5 } = {}) {
|
|
11
|
+
if (value >= state.peak)
|
|
12
|
+
return { peak: value, heldForMs: 0 };
|
|
13
|
+
const heldForMs = state.heldForMs + dtMs;
|
|
14
|
+
if (heldForMs < holdMs)
|
|
15
|
+
return { peak: state.peak, heldForMs };
|
|
16
|
+
return { peak: Math.max(value, state.peak - decayPerSecond * (dtMs / 1000)), heldForMs };
|
|
17
|
+
}
|
|
18
|
+
const FRAME_MS = 33; // meters read fine at ~30fps
|
|
19
|
+
/** The held peak for a live value — drives the Meter's peak line. */
|
|
20
|
+
export function usePeakHold(value, options = {}) {
|
|
21
|
+
const [peak, setPeak] = useState(value);
|
|
22
|
+
const state = useRef({ peak: value, heldForMs: 0 });
|
|
23
|
+
const valueRef = useRef(value);
|
|
24
|
+
valueRef.current = value;
|
|
25
|
+
const optionsRef = useRef(options);
|
|
26
|
+
optionsRef.current = options;
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const id = setInterval(() => {
|
|
29
|
+
state.current = peakHoldStep(state.current, valueRef.current, FRAME_MS, optionsRef.current);
|
|
30
|
+
setPeak(state.current.peak);
|
|
31
|
+
}, FRAME_MS);
|
|
32
|
+
return () => clearInterval(id);
|
|
33
|
+
}, []);
|
|
34
|
+
return Math.max(peak, value);
|
|
35
|
+
}
|
|
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, }) {
|
|
38
|
+
const level = clamp01(value);
|
|
39
|
+
const held = usePeakHold(level, { holdMs, decayPerSecond });
|
|
40
|
+
const hot = clamp01(hotFrom);
|
|
41
|
+
const fill = Math.min(level, hot) * length;
|
|
42
|
+
const hotFill = level > hot ? (level - hot) * length : 0;
|
|
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 left-0 top-0 bottom-0", style: { width: fill, backgroundColor: color } }), hotFill > 0 ? (_jsx(View, { className: "absolute top-0 bottom-0", style: { left: hot * length, width: hotFill, backgroundColor: hotColor } })) : null, peak && peakAt > 2 ? (_jsx(View, { className: "absolute top-0 bottom-0 w-[2]", style: { 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 bottom-0 left-0 right-0", style: { height: fill, backgroundColor: color } }), hotFill > 0 ? (_jsx(View, { className: "absolute left-0 right-0", style: { bottom: hot * length, height: hotFill, backgroundColor: hotColor } })) : null, peak && peakAt > 2 ? (_jsx(View, { className: "absolute left-0 right-0 h-[2]", style: { bottom: peakAt - 2, backgroundColor: held >= hot ? hotColor : color } })) : null] }));
|
|
45
|
+
if (label === undefined)
|
|
46
|
+
return bar;
|
|
47
|
+
return (_jsxs(View, { className: "items-center gap-2", children: [bar, _jsx(Text, { className: "text-faint text-[10] font-bold tracking-widest", children: label })] }));
|
|
48
|
+
}
|
package/dist/parameters.d.ts
CHANGED
|
@@ -9,4 +9,18 @@ export interface ParameterHandle extends ParameterState {
|
|
|
9
9
|
begin: () => void;
|
|
10
10
|
end: () => void;
|
|
11
11
|
}
|
|
12
|
+
export interface ParameterInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Normalized 0..1 snapshot at mount — use useParameter(id) for live values. */
|
|
17
|
+
value: number;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Enumerates every APVTS parameter (via param:list) once at mount — the
|
|
22
|
+
* host's parameter set is fixed for the plugin's lifetime. Powers
|
|
23
|
+
* <GenericEditor/> and any auto-generated UI.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useParameterList(): ParameterInfo[];
|
|
12
26
|
export declare function useParameter(id: string): ParameterHandle;
|
package/dist/parameters.js
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
// parameter through vsreact::ParameterBridge. Values are normalized 0..1.
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { native } from "./native";
|
|
5
|
+
/**
|
|
6
|
+
* Enumerates every APVTS parameter (via param:list) once at mount — the
|
|
7
|
+
* host's parameter set is fixed for the plugin's lifetime. Powers
|
|
8
|
+
* <GenericEditor/> and any auto-generated UI.
|
|
9
|
+
*/
|
|
10
|
+
export function useParameterList() {
|
|
11
|
+
const [list] = useState(() => {
|
|
12
|
+
const result = native.call("param:list");
|
|
13
|
+
if (!Array.isArray(result))
|
|
14
|
+
return [];
|
|
15
|
+
return result.map((entry) => ({
|
|
16
|
+
id: String(entry?.id ?? ""),
|
|
17
|
+
name: String(entry?.name ?? entry?.id ?? ""),
|
|
18
|
+
label: String(entry?.label ?? ""),
|
|
19
|
+
value: Number(entry?.value ?? 0),
|
|
20
|
+
text: String(entry?.text ?? ""),
|
|
21
|
+
}));
|
|
22
|
+
});
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
5
25
|
export function useParameter(id) {
|
|
6
26
|
const [state, setState] = useState(() => {
|
|
7
27
|
const initial = native.call("param:get", { id });
|