@tremolo-ui/react 0.3.0 → 0.5.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/dist/index.cjs +1162 -1554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +592 -278
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +592 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1163 -1555
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/components/AnimationCanvas/index.tsx +84 -155
- package/src/components/Knob/ActiveLine.tsx +6 -6
- package/src/components/Knob/InactiveLine.tsx +9 -11
- package/src/components/Knob/SVGRoot.tsx +9 -18
- package/src/components/Knob/context.tsx +57 -89
- package/src/components/Knob/index.css +6 -2
- package/src/components/Knob/index.tsx +95 -83
- package/src/components/NumberInput/DecrementStepper.tsx +17 -50
- package/src/components/NumberInput/IncrementStepper.tsx +17 -50
- package/src/components/NumberInput/InputField.tsx +123 -0
- package/src/components/NumberInput/Stepper.tsx +90 -12
- package/src/components/NumberInput/context.tsx +74 -146
- package/src/components/NumberInput/index.css +38 -102
- package/src/components/NumberInput/index.tsx +252 -98
- package/src/components/NumberInput/stepperButton.tsx +90 -0
- package/src/components/Piano/index.tsx +272 -288
- package/src/components/Piano/keyboardShortcuts.ts +34 -4
- package/src/components/PointsEditor/Background.tsx +8 -2
- package/src/components/PointsEditor/Container.tsx +19 -9
- package/src/components/PointsEditor/Point.tsx +117 -52
- package/src/components/PointsEditor/context.tsx +28 -74
- package/src/components/PointsEditor/index.css +5 -0
- package/src/components/PointsEditor/index.tsx +77 -43
- package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
- package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
- package/src/components/Slider/Thumb.tsx +55 -68
- package/src/components/Slider/Track.tsx +15 -15
- package/src/components/Slider/context.tsx +36 -51
- package/src/components/Slider/index.css +18 -5
- package/src/components/Slider/index.tsx +136 -170
- package/src/components/Slider/type.ts +4 -4
- package/src/components/XYPad/Area.tsx +16 -7
- package/src/components/XYPad/Thumb.tsx +62 -74
- package/src/components/XYPad/context.tsx +46 -0
- package/src/components/XYPad/index.css +12 -0
- package/src/components/XYPad/index.tsx +193 -250
- package/src/hooks/useDrag.ts +41 -76
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +51 -0
- package/src/index.ts +15 -13
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/DragObserver/index.tsx +0 -58
- package/src/components/NumberInput/InternalInput.tsx +0 -221
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/WheelObserver/index.tsx +0 -48
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -72
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -25,147 +25,78 @@ let clsx = require("clsx");
|
|
|
25
25
|
clsx = __toESM(clsx);
|
|
26
26
|
let react = require("react");
|
|
27
27
|
react = __toESM(react);
|
|
28
|
+
let _tremolo_ui_dom = require("@tremolo-ui/dom");
|
|
28
29
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
30
|
let _tremolo_ui_functions = require("@tremolo-ui/functions");
|
|
30
|
-
let zustand = require("zustand");
|
|
31
|
-
let _tremolo_ui_dom = require("@tremolo-ui/dom");
|
|
32
|
-
//#region src/components/AnimationCanvas/canvas.ts
|
|
33
|
-
const drawingState = [
|
|
34
|
-
"strokeStyle",
|
|
35
|
-
"fillStyle",
|
|
36
|
-
"globalAlpha",
|
|
37
|
-
"lineWidth",
|
|
38
|
-
"lineCap",
|
|
39
|
-
"lineJoin",
|
|
40
|
-
"miterLimit",
|
|
41
|
-
"lineDashOffset",
|
|
42
|
-
"shadowOffsetX",
|
|
43
|
-
"shadowOffsetY",
|
|
44
|
-
"shadowBlur",
|
|
45
|
-
"shadowColor",
|
|
46
|
-
"globalCompositeOperation",
|
|
47
|
-
"font",
|
|
48
|
-
"textAlign",
|
|
49
|
-
"textBaseline",
|
|
50
|
-
"direction",
|
|
51
|
-
"imageSmoothingEnabled"
|
|
52
|
-
];
|
|
53
|
-
function setDprConfig(canvas, context, width, height, dpr) {
|
|
54
|
-
canvas.width = width * dpr;
|
|
55
|
-
canvas.height = height * dpr;
|
|
56
|
-
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
57
|
-
context.scale(dpr, dpr);
|
|
58
|
-
canvas.style.width = `${width}px`;
|
|
59
|
-
canvas.style.height = `${height}px`;
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
31
|
//#region src/components/AnimationCanvas/index.tsx
|
|
63
|
-
function AnimationCanvas({ draw, init, animate = true, options, width
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
32
|
+
function AnimationCanvas({ draw, init, animate = true, options, width = 100, height = 100, relativeSize = false, reduceFlickering = true, className, onContextMenu = (event) => event.preventDefault(), ...props }) {
|
|
33
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
34
|
+
const latest = (0, react.useRef)({
|
|
35
|
+
draw,
|
|
36
|
+
init,
|
|
37
|
+
animate,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
reduceFlickering,
|
|
41
|
+
options
|
|
42
|
+
});
|
|
43
|
+
const instanceRef = (0, react.useRef)(null);
|
|
44
|
+
/** Set while the instance below is newer than the effect that pushes into it. */
|
|
45
|
+
const justCreated = (0, react.useRef)(false);
|
|
46
|
+
(0, react.useEffect)(() => {
|
|
47
|
+
if (!node) return;
|
|
48
|
+
const current = latest.current;
|
|
49
|
+
const instance = (0, _tremolo_ui_dom.createAnimationCanvas)(node, {
|
|
50
|
+
draw: (context, frame) => latest.current.draw(context, frame),
|
|
51
|
+
init: (context, size) => latest.current.init?.(context, size),
|
|
52
|
+
animate: current.animate,
|
|
53
|
+
size: {
|
|
54
|
+
width: current.width,
|
|
55
|
+
height: current.height
|
|
56
|
+
},
|
|
57
|
+
reduceFlickering: current.reduceFlickering,
|
|
58
|
+
relativeSize,
|
|
59
|
+
contextAttributes: current.options
|
|
84
60
|
});
|
|
85
|
-
|
|
61
|
+
instanceRef.current = instance;
|
|
62
|
+
justCreated.current = true;
|
|
63
|
+
return () => {
|
|
64
|
+
instanceRef.current = null;
|
|
65
|
+
instance.destroy();
|
|
66
|
+
};
|
|
67
|
+
}, [node, relativeSize]);
|
|
86
68
|
(0, react.useEffect)(() => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
heightRef.current = height;
|
|
96
|
-
if (init) init(context, {
|
|
97
|
-
width,
|
|
98
|
-
height
|
|
99
|
-
});
|
|
100
|
-
const now = performance.now();
|
|
101
|
-
deltaMemoRef.current = now;
|
|
102
|
-
startTimeRef.current = now;
|
|
103
|
-
loop(context, widthRef, heightRef, -1);
|
|
69
|
+
latest.current = {
|
|
70
|
+
draw,
|
|
71
|
+
init,
|
|
72
|
+
animate,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
reduceFlickering,
|
|
76
|
+
options
|
|
104
77
|
};
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const memoCanvas = memoCanvasRef.current;
|
|
109
|
-
const memoContext = memoCanvas?.getContext("2d", options);
|
|
110
|
-
const ro = new ResizeObserver((entries) => {
|
|
111
|
-
for (const entry of entries) {
|
|
112
|
-
const w = entry.contentRect.width;
|
|
113
|
-
const h = entry.contentRect.height;
|
|
114
|
-
const contextMemo = {};
|
|
115
|
-
if (reduceFlickering && memoCanvas && memoContext) {
|
|
116
|
-
for (const prop of drawingState) contextMemo[prop] = context[prop];
|
|
117
|
-
memoCanvas.width = w * dpr;
|
|
118
|
-
memoCanvas.height = h * dpr;
|
|
119
|
-
memoContext.scale(1 / dpr, 1 / dpr);
|
|
120
|
-
if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
|
|
121
|
-
}
|
|
122
|
-
setDprConfig(canvas, context, w, h, dpr);
|
|
123
|
-
widthRef.current = w;
|
|
124
|
-
heightRef.current = h;
|
|
125
|
-
if (reduceFlickering && memoContext && memoCanvas && memoCanvas.width > 0 && memoCanvas.height > 0) {
|
|
126
|
-
for (const prop of drawingState) context[prop] = contextMemo[prop];
|
|
127
|
-
context.drawImage(memoContext.canvas, 0, 0);
|
|
128
|
-
}
|
|
129
|
-
if (!animate) loop(context, widthRef, heightRef, -1);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
ro.observe(parent);
|
|
133
|
-
const width = parent.clientWidth;
|
|
134
|
-
const height = parent.clientHeight;
|
|
135
|
-
firstRendering(width, height);
|
|
136
|
-
return () => {
|
|
137
|
-
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
|
|
138
|
-
ro.disconnect();
|
|
139
|
-
};
|
|
140
|
-
} else {
|
|
141
|
-
const { width, height } = canvas.getBoundingClientRect();
|
|
142
|
-
firstRendering(width, height);
|
|
143
|
-
return () => {
|
|
144
|
-
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
|
|
145
|
-
};
|
|
78
|
+
if (justCreated.current) {
|
|
79
|
+
justCreated.current = false;
|
|
80
|
+
return;
|
|
146
81
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
82
|
+
instanceRef.current?.update({
|
|
83
|
+
animate,
|
|
84
|
+
size: {
|
|
85
|
+
width,
|
|
86
|
+
height
|
|
87
|
+
},
|
|
88
|
+
reduceFlickering
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
156
92
|
className: (0, clsx.default)("tremolo-animation-canvas", className),
|
|
157
|
-
|
|
158
|
-
height: relativeSize ? 0 : _height,
|
|
159
|
-
ref: canvasRef,
|
|
93
|
+
ref: setNode,
|
|
160
94
|
onContextMenu,
|
|
161
95
|
...props
|
|
162
|
-
})
|
|
163
|
-
style: { display: "none" },
|
|
164
|
-
ref: memoCanvasRef
|
|
165
|
-
})] });
|
|
96
|
+
});
|
|
166
97
|
}
|
|
167
98
|
//#endregion
|
|
168
|
-
//#region src/hooks/useCallbackRef.ts
|
|
99
|
+
//#region src/hooks/_internal/useCallbackRef.ts
|
|
169
100
|
/**
|
|
170
101
|
* Internal
|
|
171
102
|
* @private
|
|
@@ -180,110 +111,96 @@ function useCallbackRef(callback, deps = []) {
|
|
|
180
111
|
return (0, react.useCallback)((...args) => callbackRef.current?.(...args), deps);
|
|
181
112
|
}
|
|
182
113
|
//#endregion
|
|
183
|
-
//#region src/hooks/
|
|
184
|
-
|
|
185
|
-
|
|
114
|
+
//#region src/hooks/useDragValue.ts
|
|
115
|
+
/**
|
|
116
|
+
* Drive a value with a pointer drag.
|
|
117
|
+
*
|
|
118
|
+
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
119
|
+
*/
|
|
120
|
+
function useDragValue(options) {
|
|
121
|
+
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor } = options;
|
|
122
|
+
if (!baseElementRef && !options.getValue) throw new Error("useDragValue: give either baseElementRef or getValue, so that the drag has something to move");
|
|
123
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
124
|
+
const valueGetter = useCallbackRef(options.getValue);
|
|
125
|
+
const changeHandler = useCallbackRef(options.onChange);
|
|
126
|
+
const dragStartHandler = useCallbackRef(options.onDragStart);
|
|
127
|
+
const dragEndHandler = useCallbackRef(options.onDragEnd);
|
|
128
|
+
const latest = (0, react.useRef)(options);
|
|
129
|
+
const instanceRef = (0, react.useRef)(null);
|
|
130
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
131
|
+
const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange) ? pixelRange : [pixelRange, pixelRange];
|
|
186
132
|
(0, react.useEffect)(() => {
|
|
187
|
-
const node = typeof target === "function" ? target() : target ?? document;
|
|
188
133
|
if (!node) return;
|
|
189
|
-
|
|
134
|
+
const instance = (0, _tremolo_ui_dom.createDragValue)(node, {
|
|
135
|
+
axis: latest.current.axis,
|
|
136
|
+
mapping: baseElementRef ? (0, _tremolo_ui_dom.elementMapping)(() => baseElementRef.current) : (0, _tremolo_ui_dom.relativeMapping)({ pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100] }),
|
|
137
|
+
getValue: () => valueGetter() ?? [0, 0],
|
|
138
|
+
updateOnPointerDown: latest.current.updateOnPointerDown,
|
|
139
|
+
threshold: latest.current.threshold,
|
|
140
|
+
cursor: latest.current.cursor,
|
|
141
|
+
onChange: (value) => changeHandler(value),
|
|
142
|
+
onDragStart: (value) => {
|
|
143
|
+
setDragging(true);
|
|
144
|
+
dragStartHandler(value);
|
|
145
|
+
},
|
|
146
|
+
onDragEnd: (value) => {
|
|
147
|
+
setDragging(false);
|
|
148
|
+
dragEndHandler(value);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
instanceRef.current = instance;
|
|
190
152
|
return () => {
|
|
191
|
-
|
|
153
|
+
instanceRef.current = null;
|
|
154
|
+
instance.destroy();
|
|
192
155
|
};
|
|
193
156
|
}, [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
157
|
+
node,
|
|
158
|
+
baseElementRef,
|
|
159
|
+
pixelRangeX,
|
|
160
|
+
pixelRangeY,
|
|
161
|
+
valueGetter,
|
|
162
|
+
changeHandler,
|
|
163
|
+
dragStartHandler,
|
|
164
|
+
dragEndHandler
|
|
198
165
|
]);
|
|
199
|
-
|
|
200
|
-
|
|
166
|
+
(0, react.useEffect)(() => {
|
|
167
|
+
latest.current = options;
|
|
168
|
+
instanceRef.current?.update({
|
|
169
|
+
axis,
|
|
170
|
+
updateOnPointerDown,
|
|
171
|
+
threshold,
|
|
172
|
+
cursor
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
refCallback: setNode,
|
|
177
|
+
dragging
|
|
201
178
|
};
|
|
202
179
|
}
|
|
203
180
|
//#endregion
|
|
204
|
-
//#region src/hooks/
|
|
205
|
-
function useRefCallbackEvent(event, handler, options, deps = []) {
|
|
206
|
-
return (0, react.useMemo)(() => {
|
|
207
|
-
let cleanup;
|
|
208
|
-
return (node) => {
|
|
209
|
-
if (!node) {
|
|
210
|
-
cleanup?.();
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
node.addEventListener(event, handler, options);
|
|
214
|
-
cleanup = () => {
|
|
215
|
-
node.removeEventListener(event, handler, options);
|
|
216
|
-
};
|
|
217
|
-
};
|
|
218
|
-
}, deps);
|
|
219
|
-
}
|
|
220
|
-
//#endregion
|
|
221
|
-
//#region src/hooks/useDrag.ts
|
|
181
|
+
//#region src/hooks/useWheel.ts
|
|
222
182
|
/**
|
|
223
|
-
*
|
|
183
|
+
* Listen to wheel events on an element.
|
|
184
|
+
*
|
|
185
|
+
* The listener is not passive, so the handler may call `preventDefault()`.
|
|
186
|
+
*
|
|
187
|
+
* @returns a ref callback to attach to the element, unused when `target` is given
|
|
224
188
|
*/
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
dragStartY.current = screenY;
|
|
241
|
-
}
|
|
242
|
-
if (dragOffsetX.current) {
|
|
243
|
-
deltaX = screenX - dragOffsetX.current;
|
|
244
|
-
dragOffsetX.current = screenX;
|
|
245
|
-
}
|
|
246
|
-
if (dragOffsetY.current) {
|
|
247
|
-
deltaY = screenY - dragOffsetY.current;
|
|
248
|
-
dragOffsetY.current = screenY;
|
|
249
|
-
}
|
|
250
|
-
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
251
|
-
onDrag?.(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
|
|
252
|
-
}, [threshold, onDrag]);
|
|
253
|
-
const pointerDownHandler = (0, react.useCallback)((event) => {
|
|
254
|
-
dragOffsetX.current = event.screenX;
|
|
255
|
-
dragOffsetY.current = event.screenY;
|
|
256
|
-
handleDrag(event, true);
|
|
257
|
-
onDragStart?.();
|
|
258
|
-
}, [handleDrag, onDragStart]);
|
|
259
|
-
const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
|
|
260
|
-
useEventListener(globalThis.window, "mousemove", handleDrag);
|
|
261
|
-
useEventListener(globalThis.window, "pointerup", () => {
|
|
262
|
-
dragOffsetX.current = void 0;
|
|
263
|
-
dragOffsetY.current = void 0;
|
|
264
|
-
onDragEnd?.();
|
|
265
|
-
});
|
|
266
|
-
return [refHandler, pointerDownHandler];
|
|
267
|
-
}
|
|
268
|
-
//#endregion
|
|
269
|
-
//#region src/components/DragObserver/index.tsx
|
|
270
|
-
function DragObserver(props) {
|
|
271
|
-
const { as: Component = "div", threshold = 1, onDrag, onDragStart, onDragEnd, children, onPointerDown, ...attributes } = props;
|
|
272
|
-
const [refCallback, pointerDownHandler] = useDrag({
|
|
273
|
-
threshold,
|
|
274
|
-
onDrag,
|
|
275
|
-
onDragStart,
|
|
276
|
-
onDragEnd
|
|
277
|
-
});
|
|
278
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
|
|
279
|
-
ref: refCallback,
|
|
280
|
-
onPointerDown: (e) => {
|
|
281
|
-
pointerDownHandler(e);
|
|
282
|
-
onPointerDown?.(e);
|
|
283
|
-
},
|
|
284
|
-
...attributes,
|
|
285
|
-
children
|
|
286
|
-
});
|
|
189
|
+
function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
190
|
+
const wheelHandler = useCallbackRef(onWheel);
|
|
191
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
192
|
+
(0, react.useEffect)(() => {
|
|
193
|
+
const element = target ? target.current : node;
|
|
194
|
+
if (!element) return;
|
|
195
|
+
const instance = (0, _tremolo_ui_dom.createWheel)(element, (event) => wheelHandler(event), { requireFocus });
|
|
196
|
+
return () => instance.destroy();
|
|
197
|
+
}, [
|
|
198
|
+
node,
|
|
199
|
+
target,
|
|
200
|
+
wheelHandler,
|
|
201
|
+
requireFocus
|
|
202
|
+
]);
|
|
203
|
+
return setNode;
|
|
287
204
|
}
|
|
288
205
|
//#endregion
|
|
289
206
|
//#region src/components/_util/index.ts
|
|
@@ -293,84 +210,92 @@ function addUserSelectNone() {
|
|
|
293
210
|
function removeUserSelectNone() {
|
|
294
211
|
window.document.body.classList.remove("tremolo-user-select-none");
|
|
295
212
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
window.document.body.classList.add(cursorStyles[type]);
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/components/_util/composeRefs.tsx
|
|
215
|
+
/**
|
|
216
|
+
* Set a given ref to a given value
|
|
217
|
+
* This utility takes care of different types of refs: callback refs and RefObject(s)
|
|
218
|
+
*/
|
|
219
|
+
function setRef(ref, value) {
|
|
220
|
+
if (typeof ref === "function") return ref(value);
|
|
221
|
+
else if (ref !== null && ref !== void 0) ref.current = value;
|
|
306
222
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
223
|
+
/**
|
|
224
|
+
* A utility to compose multiple refs together
|
|
225
|
+
* Accepts callback refs and RefObject(s)
|
|
226
|
+
*/
|
|
227
|
+
function composeRefs(...refs) {
|
|
228
|
+
return (node) => {
|
|
229
|
+
let hasCleanup = false;
|
|
230
|
+
const cleanups = refs.map((ref) => {
|
|
231
|
+
const cleanup = setRef(ref, node);
|
|
232
|
+
if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
|
|
233
|
+
return cleanup;
|
|
234
|
+
});
|
|
235
|
+
if (hasCleanup) return () => {
|
|
236
|
+
for (let i = 0; i < cleanups.length; i++) {
|
|
237
|
+
const cleanup = cleanups[i];
|
|
238
|
+
if (typeof cleanup == "function") cleanup();
|
|
239
|
+
else setRef(refs[i], null);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* A custom hook that composes multiple refs
|
|
246
|
+
* Accepts callback refs and RefObject(s)
|
|
247
|
+
*/
|
|
248
|
+
function useComposedRefs(...refs) {
|
|
249
|
+
return (0, react.useCallback)(composeRefs(...refs), refs);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 角度から円弧上の点を求める。
|
|
253
|
+
*
|
|
254
|
+
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
255
|
+
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
256
|
+
*/
|
|
257
|
+
function pointOnArc(angle, radius) {
|
|
258
|
+
return {
|
|
259
|
+
x: 50 + radius * Math.cos((0, _tremolo_ui_functions.radian)(angle - 90)),
|
|
260
|
+
y: 50 + radius * Math.sin((0, _tremolo_ui_functions.radian)(angle - 90))
|
|
261
|
+
};
|
|
311
262
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
263
|
+
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
264
|
+
function arcRadius(strokeWidth) {
|
|
265
|
+
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
266
|
+
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
267
|
+
}
|
|
268
|
+
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
269
|
+
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
270
|
+
const p = scale.normalize(value, min, max);
|
|
271
|
+
const s = scale.normalize(startValue, min, max);
|
|
315
272
|
const r1 = -angleRange / 2;
|
|
316
|
-
const r2 = r1 + Math.min(p, s) * angleRange;
|
|
317
|
-
const r3 = r1 + Math.max(p, s) * angleRange;
|
|
318
|
-
const r4 = angleRange / 2;
|
|
319
273
|
return {
|
|
320
274
|
p,
|
|
321
275
|
r1,
|
|
322
|
-
r2,
|
|
323
|
-
r3,
|
|
324
|
-
r4
|
|
325
|
-
x1: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r1 - 90)),
|
|
326
|
-
y1: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r1 - 90)),
|
|
327
|
-
x2: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r2 - 90)),
|
|
328
|
-
y2: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r2 - 90)),
|
|
329
|
-
x3: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r3 - 90)),
|
|
330
|
-
y3: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r3 - 90)),
|
|
331
|
-
x4: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r4 - 90)),
|
|
332
|
-
y4: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r4 - 90))
|
|
276
|
+
r2: r1 + Math.min(p, s) * angleRange,
|
|
277
|
+
r3: r1 + Math.max(p, s) * angleRange,
|
|
278
|
+
r4: angleRange / 2
|
|
333
279
|
};
|
|
334
280
|
}
|
|
335
|
-
const createKnobStore = (initProps) => {
|
|
336
|
-
return (0, zustand.createStore)()(() => ({
|
|
337
|
-
...initProps,
|
|
338
|
-
...calcDrawingState(initProps)
|
|
339
|
-
}));
|
|
340
|
-
};
|
|
341
281
|
const KnobContext = (0, react.createContext)(null);
|
|
342
|
-
|
|
343
|
-
const storeRef = (0, react.useRef)(null);
|
|
344
|
-
if (!storeRef.current) storeRef.current = createKnobStore(props);
|
|
345
|
-
(0, react.useEffect)(() => {
|
|
346
|
-
if (storeRef.current) storeRef.current.setState({
|
|
347
|
-
...props,
|
|
348
|
-
...calcDrawingState(props)
|
|
349
|
-
});
|
|
350
|
-
else storeRef.current = createKnobStore(props);
|
|
351
|
-
}, [props]);
|
|
352
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobContext.Provider, {
|
|
353
|
-
value: storeRef.current,
|
|
354
|
-
children
|
|
355
|
-
});
|
|
356
|
-
}
|
|
282
|
+
const KnobProvider = KnobContext.Provider;
|
|
357
283
|
function useKnobContext(selector) {
|
|
358
|
-
const
|
|
359
|
-
if (!
|
|
360
|
-
return
|
|
284
|
+
const context = (0, react.useContext)(KnobContext);
|
|
285
|
+
if (!context) throw new Error("Missing KnobContext.Provider in the tree");
|
|
286
|
+
return selector ? selector(context) : context;
|
|
361
287
|
}
|
|
362
288
|
//#endregion
|
|
363
289
|
//#region src/components/Knob/ActiveLine.tsx
|
|
364
290
|
function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
365
291
|
const r2 = useKnobContext((s) => s.r2);
|
|
366
292
|
const r3 = useKnobContext((s) => s.r3);
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const y3 = useKnobContext((s) => s.y3);
|
|
293
|
+
const radius = arcRadius(strokeWidth);
|
|
294
|
+
const start = pointOnArc(r2, radius);
|
|
295
|
+
const end = pointOnArc(r3, radius);
|
|
371
296
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
372
297
|
className: (0, clsx.default)("tremolo-knob-active-line", className),
|
|
373
|
-
d: `M ${
|
|
298
|
+
d: `M ${start.x} ${start.y} A ${radius} ${radius} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${end.x} ${end.y}`,
|
|
374
299
|
fill: "none",
|
|
375
300
|
stroke,
|
|
376
301
|
strokeWidth,
|
|
@@ -387,24 +312,21 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
387
312
|
const r2 = useKnobContext((s) => s.r2);
|
|
388
313
|
const r3 = useKnobContext((s) => s.r3);
|
|
389
314
|
const r4 = useKnobContext((s) => s.r4);
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
const y3 = useKnobContext((s) => s.y3);
|
|
396
|
-
const x4 = useKnobContext((s) => s.x4);
|
|
397
|
-
const y4 = useKnobContext((s) => s.y4);
|
|
315
|
+
const radius = arcRadius(strokeWidth);
|
|
316
|
+
const p1 = pointOnArc(r1, radius);
|
|
317
|
+
const p2 = pointOnArc(r2, radius);
|
|
318
|
+
const p3 = pointOnArc(r3, radius);
|
|
319
|
+
const p4 = pointOnArc(r4, radius);
|
|
398
320
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [startValue > min && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
399
321
|
className: (0, clsx.default)("tremolo-knob-inactive-line", className),
|
|
400
|
-
d: `M ${
|
|
322
|
+
d: `M ${p1.x} ${p1.y} A ${radius} ${radius} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${p2.x} ${p2.y}`,
|
|
401
323
|
fill: "none",
|
|
402
324
|
stroke,
|
|
403
325
|
strokeWidth,
|
|
404
326
|
...props
|
|
405
327
|
}), startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
406
328
|
className: (0, clsx.default)("tremolo-knob-inactive-line", className),
|
|
407
|
-
d: `M ${
|
|
329
|
+
d: `M ${p3.x} ${p3.y} A ${radius} ${radius} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${p4.x} ${p4.y}`,
|
|
408
330
|
fill: "none",
|
|
409
331
|
stroke,
|
|
410
332
|
strokeWidth,
|
|
@@ -412,6 +334,19 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
412
334
|
})] });
|
|
413
335
|
}
|
|
414
336
|
//#endregion
|
|
337
|
+
//#region src/components/Knob/SVGRoot.tsx
|
|
338
|
+
function SVGRoot({ children, style, ...props }) {
|
|
339
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
340
|
+
viewBox: `0 0 100 100`,
|
|
341
|
+
style: {
|
|
342
|
+
display: "block",
|
|
343
|
+
...style
|
|
344
|
+
},
|
|
345
|
+
...props,
|
|
346
|
+
children
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
415
350
|
//#region src/components/Knob/Thumb.tsx
|
|
416
351
|
function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
|
|
417
352
|
const angleRange = useKnobContext((s) => s.angleRange);
|
|
@@ -441,63 +376,36 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
441
376
|
});
|
|
442
377
|
}
|
|
443
378
|
//#endregion
|
|
444
|
-
//#region src/components/Knob/SVGRoot.tsx
|
|
445
|
-
function SVGRoot({ children, style, ...props }) {
|
|
446
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
447
|
-
viewBox: `0 0 100 100`,
|
|
448
|
-
style: {
|
|
449
|
-
display: "block",
|
|
450
|
-
overflow: "visible",
|
|
451
|
-
...style
|
|
452
|
-
},
|
|
453
|
-
...props,
|
|
454
|
-
children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
455
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InactiveLine, {}),
|
|
456
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveLine, {}),
|
|
457
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$2, {})
|
|
458
|
-
] })
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
//#endregion
|
|
462
379
|
//#region src/components/Knob/index.tsx
|
|
463
|
-
const defaultExternalStyles$
|
|
380
|
+
const defaultExternalStyles$2 = {
|
|
464
381
|
userSelectNone: true,
|
|
465
382
|
cursor: "grabbing"
|
|
466
383
|
};
|
|
467
384
|
/**
|
|
385
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
386
|
+
* the control does not change its value.
|
|
387
|
+
*/
|
|
388
|
+
const WHEEL_OPTIONS$2 = { requireFocus: true };
|
|
389
|
+
/**
|
|
468
390
|
* Interactive rotary knob component implemented in SVG.
|
|
469
391
|
*/
|
|
470
392
|
const Knob = {
|
|
471
|
-
Root: (0, react.forwardRef)(({ value, min, max, step = 1,
|
|
472
|
-
const [dragging, setDragging] = (0, react.useState)(false);
|
|
473
|
-
const valueRef = (0, react.useRef)(0);
|
|
393
|
+
Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
|
|
474
394
|
const elmRef = (0, react.useRef)(null);
|
|
475
395
|
const externalStyles = {
|
|
476
|
-
...defaultExternalStyles$
|
|
396
|
+
...defaultExternalStyles$2,
|
|
477
397
|
..._externalStyles
|
|
478
398
|
};
|
|
479
|
-
const
|
|
480
|
-
if (!onChange || readonly) return;
|
|
481
|
-
onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
|
|
482
|
-
}, [
|
|
483
|
-
max,
|
|
399
|
+
const range = (0, react.useMemo)(() => ({
|
|
484
400
|
min,
|
|
485
|
-
onChange,
|
|
486
|
-
readonly,
|
|
487
|
-
skew,
|
|
488
|
-
step
|
|
489
|
-
]);
|
|
490
|
-
const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
|
|
491
|
-
let newValue;
|
|
492
|
-
if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
|
|
493
|
-
else newValue = value + x;
|
|
494
|
-
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
|
|
495
|
-
}, [
|
|
496
401
|
max,
|
|
402
|
+
step,
|
|
403
|
+
scale
|
|
404
|
+
}), [
|
|
497
405
|
min,
|
|
498
|
-
|
|
406
|
+
max,
|
|
499
407
|
step,
|
|
500
|
-
|
|
408
|
+
scale
|
|
501
409
|
]);
|
|
502
410
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
503
411
|
if (!keyboard || !onChange || readonly) return;
|
|
@@ -509,40 +417,62 @@ const Knob = {
|
|
|
509
417
|
"ArrowDown"
|
|
510
418
|
].includes(key)) {
|
|
511
419
|
event.preventDefault();
|
|
512
|
-
|
|
513
|
-
onChange(updateValueByEvent(keyboard[0], x));
|
|
420
|
+
onChange((0, _tremolo_ui_functions.applyDelta)(value, key == "ArrowRight" || key == "ArrowUp" ? 1 : -1, keyboard, range));
|
|
514
421
|
}
|
|
515
422
|
}, [
|
|
516
423
|
keyboard,
|
|
517
424
|
onChange,
|
|
518
425
|
readonly,
|
|
519
|
-
|
|
426
|
+
value,
|
|
427
|
+
range
|
|
520
428
|
]);
|
|
521
|
-
const
|
|
522
|
-
|
|
429
|
+
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
430
|
+
axis: (0, react.useMemo)(() => [range, {
|
|
431
|
+
...range,
|
|
432
|
+
reverse: true
|
|
433
|
+
}], [range]),
|
|
434
|
+
getValue: () => [value, value],
|
|
435
|
+
threshold: 1,
|
|
436
|
+
cursor: readonly ? void 0 : externalStyles.cursor,
|
|
437
|
+
onChange: (v) => {
|
|
438
|
+
if (readonly) return;
|
|
439
|
+
onChange?.(v[1]);
|
|
440
|
+
},
|
|
523
441
|
onDragStart: () => {
|
|
524
|
-
setDragging(true);
|
|
525
|
-
valueRef.current = (0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew);
|
|
526
442
|
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
527
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
|
|
528
443
|
},
|
|
529
444
|
onDragEnd: () => {
|
|
530
|
-
setDragging(false);
|
|
531
445
|
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
532
|
-
if (externalStyles.cursor) resetCursorStyle();
|
|
533
446
|
}
|
|
534
447
|
});
|
|
535
|
-
const
|
|
448
|
+
const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, useWheel((event) => {
|
|
536
449
|
if (!wheel || readonly) return;
|
|
537
450
|
event.preventDefault();
|
|
538
451
|
if (!onChange || event.deltaY == 0) return;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
452
|
+
onChange((0, _tremolo_ui_functions.applyDelta)(value, -Math.sign(event.deltaY), wheel, range));
|
|
453
|
+
}, WHEEL_OPTIONS$2));
|
|
454
|
+
const context = (0, react.useMemo)(() => {
|
|
455
|
+
const config = {
|
|
456
|
+
value,
|
|
457
|
+
min,
|
|
458
|
+
max,
|
|
459
|
+
step,
|
|
460
|
+
scale,
|
|
461
|
+
startValue,
|
|
462
|
+
angleRange
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
...config,
|
|
466
|
+
...calcAngles(config)
|
|
467
|
+
};
|
|
468
|
+
}, [
|
|
469
|
+
value,
|
|
470
|
+
min,
|
|
471
|
+
max,
|
|
472
|
+
step,
|
|
473
|
+
scale,
|
|
474
|
+
startValue,
|
|
475
|
+
angleRange
|
|
546
476
|
]);
|
|
547
477
|
(0, react.useImperativeHandle)(forwardedRef, () => {
|
|
548
478
|
return {
|
|
@@ -555,19 +485,9 @@ const Knob = {
|
|
|
555
485
|
};
|
|
556
486
|
}, []);
|
|
557
487
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobProvider, {
|
|
558
|
-
value,
|
|
559
|
-
min,
|
|
560
|
-
max,
|
|
561
|
-
step,
|
|
562
|
-
skew,
|
|
563
|
-
startValue,
|
|
564
|
-
angleRange,
|
|
488
|
+
value: context,
|
|
565
489
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
566
|
-
ref:
|
|
567
|
-
elmRef.current = elm;
|
|
568
|
-
wheelRefCallback(elm);
|
|
569
|
-
touchMoveRefCallback(elm);
|
|
570
|
-
},
|
|
490
|
+
ref: rootRefCallback,
|
|
571
491
|
className: (0, clsx.default)("tremolo-knob", className),
|
|
572
492
|
tabIndex: 0,
|
|
573
493
|
role: "slider",
|
|
@@ -582,10 +502,7 @@ const Knob = {
|
|
|
582
502
|
height: size,
|
|
583
503
|
...style
|
|
584
504
|
},
|
|
585
|
-
onPointerDown
|
|
586
|
-
pointerDownHandler(event);
|
|
587
|
-
onPointerDown?.(event);
|
|
588
|
-
},
|
|
505
|
+
onPointerDown,
|
|
589
506
|
onDoubleClick: (event) => {
|
|
590
507
|
if (enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
591
508
|
onDoubleClick?.(event);
|
|
@@ -595,7 +512,7 @@ const Knob = {
|
|
|
595
512
|
onKeyDown?.(event);
|
|
596
513
|
},
|
|
597
514
|
...props,
|
|
598
|
-
children
|
|
515
|
+
children
|
|
599
516
|
})
|
|
600
517
|
});
|
|
601
518
|
}),
|
|
@@ -605,121 +522,39 @@ const Knob = {
|
|
|
605
522
|
Thumb: Thumb$2
|
|
606
523
|
};
|
|
607
524
|
//#endregion
|
|
608
|
-
//#region src/components/NumberInput/
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
525
|
+
//#region src/components/NumberInput/context.tsx
|
|
526
|
+
const NumberInputContext = (0, react.createContext)(null);
|
|
527
|
+
const NumberInputProvider = NumberInputContext.Provider;
|
|
528
|
+
function useNumberInputContext(selector) {
|
|
529
|
+
const context = (0, react.useContext)(NumberInputContext);
|
|
530
|
+
if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
|
|
531
|
+
return selector ? selector(context) : context;
|
|
613
532
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
let v = Number(m?.[0] ?? "0");
|
|
619
|
-
v = isNaN(v) ? 0 : v;
|
|
620
|
-
return {
|
|
621
|
-
rawValue: v,
|
|
622
|
-
formatValue: (digit != void 0 ? v.toFixed(digit) : v) + (units ?? ""),
|
|
623
|
-
unit: units ?? ""
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
const unitList = units.map(([u, _s]) => u);
|
|
627
|
-
const scaleList = units.map(([_u, s]) => s);
|
|
628
|
-
let rawValue = 0;
|
|
629
|
-
let unit = unitList[0];
|
|
630
|
-
let formatValue = (digit != void 0 ? rawValue.toFixed(digit) : rawValue) + unit;
|
|
631
|
-
const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
|
|
632
|
-
if (m) {
|
|
633
|
-
rawValue = Number(m[1]) || 0;
|
|
634
|
-
const uIndex = unitList.indexOf(m[3]);
|
|
635
|
-
rawValue *= uIndex != -1 ? scaleList[uIndex] : 1;
|
|
636
|
-
const [u, scale] = selectUnit(units, rawValue);
|
|
637
|
-
const v = rawValue / scale;
|
|
638
|
-
formatValue = (digit != void 0 ? v.toFixed(digit) : v) + u;
|
|
639
|
-
unit = u;
|
|
640
|
-
}
|
|
641
|
-
return {
|
|
642
|
-
rawValue,
|
|
643
|
-
formatValue,
|
|
644
|
-
unit
|
|
645
|
-
};
|
|
533
|
+
const StepperContext = (0, react.createContext)(null);
|
|
534
|
+
const StepperProvider = StepperContext.Provider;
|
|
535
|
+
function useStepperContext() {
|
|
536
|
+
return (0, react.useContext)(StepperContext);
|
|
646
537
|
}
|
|
647
538
|
//#endregion
|
|
648
|
-
//#region src/
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
const createNumberInputStore = (initProps) => {
|
|
653
|
-
const DEFAULT_PROPS = {
|
|
654
|
-
value: "",
|
|
655
|
-
valueAsNumber: 0,
|
|
656
|
-
min: 0,
|
|
657
|
-
max: 0,
|
|
658
|
-
step: 1,
|
|
659
|
-
readonly: false,
|
|
660
|
-
keepWithinRange: true,
|
|
661
|
-
onChange: (_v, _t) => {}
|
|
662
|
-
};
|
|
663
|
-
return (0, zustand.createStore)()((set) => ({
|
|
664
|
-
...DEFAULT_PROPS,
|
|
665
|
-
...initProps,
|
|
666
|
-
value: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
|
|
667
|
-
valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
|
|
668
|
-
increment: () => set((state) => {
|
|
669
|
-
if (state.readonly) return {};
|
|
670
|
-
let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
|
|
671
|
-
if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
|
|
672
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue;
|
|
673
|
-
state?.onChange?.(next, v);
|
|
674
|
-
return {
|
|
675
|
-
value: v,
|
|
676
|
-
valueAsNumber: next
|
|
677
|
-
};
|
|
678
|
-
}),
|
|
679
|
-
decrement: () => set((state) => {
|
|
680
|
-
if (state.readonly) return {};
|
|
681
|
-
let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
|
|
682
|
-
if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
|
|
683
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue;
|
|
684
|
-
state?.onChange?.(next, v);
|
|
685
|
-
return {
|
|
686
|
-
value: v,
|
|
687
|
-
valueAsNumber: next
|
|
688
|
-
};
|
|
689
|
-
}),
|
|
690
|
-
change: (value) => set((state) => {
|
|
691
|
-
if (state.readonly) return {};
|
|
692
|
-
const v = value ?? "";
|
|
693
|
-
return {
|
|
694
|
-
value: v,
|
|
695
|
-
valueAsNumber: parseValue(v, state.units, state.digit).rawValue
|
|
696
|
-
};
|
|
697
|
-
})
|
|
698
|
-
}));
|
|
699
|
-
};
|
|
700
|
-
const NumberInputContext = (0, react.createContext)(null);
|
|
701
|
-
function NumberInputProvider({ children, ...props }) {
|
|
702
|
-
const storeRef = (0, react.useRef)(null);
|
|
703
|
-
if (!storeRef.current) storeRef.current = createNumberInputStore(props);
|
|
539
|
+
//#region src/hooks/useEventListener.ts
|
|
540
|
+
function useEventListener(target, event, handler, options) {
|
|
541
|
+
const listener = useCallbackRef(handler);
|
|
704
542
|
(0, react.useEffect)(() => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const store = (0, react.useContext)(NumberInputContext);
|
|
721
|
-
if (!store) throw new Error("Missing NumberInputContext.Provider in the tree");
|
|
722
|
-
return (0, zustand.useStore)(store, selector);
|
|
543
|
+
const node = typeof target === "function" ? target() : target ?? document;
|
|
544
|
+
if (!node) return;
|
|
545
|
+
node.addEventListener(event, listener, options);
|
|
546
|
+
return () => {
|
|
547
|
+
node.removeEventListener(event, listener, options);
|
|
548
|
+
};
|
|
549
|
+
}, [
|
|
550
|
+
event,
|
|
551
|
+
target,
|
|
552
|
+
options,
|
|
553
|
+
listener
|
|
554
|
+
]);
|
|
555
|
+
return () => {
|
|
556
|
+
(typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
|
|
557
|
+
};
|
|
723
558
|
}
|
|
724
559
|
//#endregion
|
|
725
560
|
//#region src/hooks/useInterval.ts
|
|
@@ -757,641 +592,554 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
|
757
592
|
}, [callback]);
|
|
758
593
|
}
|
|
759
594
|
//#endregion
|
|
760
|
-
//#region src/components/NumberInput/
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
595
|
+
//#region src/components/NumberInput/stepperButton.tsx
|
|
596
|
+
/**
|
|
597
|
+
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
598
|
+
* in which way they move the value and which arrow they draw.
|
|
599
|
+
*/
|
|
600
|
+
function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
|
|
601
|
+
const { step, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
602
|
+
const stepper = useStepperContext();
|
|
603
|
+
const press = useLongPress((0, react.useCallback)(() => {
|
|
604
|
+
if (stepper?.draggingRef.current) return;
|
|
605
|
+
nudge(direction, ["raw", step]);
|
|
606
|
+
}, [
|
|
607
|
+
stepper,
|
|
608
|
+
nudge,
|
|
609
|
+
direction,
|
|
610
|
+
step
|
|
611
|
+
]));
|
|
767
612
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
768
|
-
className: (0, clsx.default)(
|
|
613
|
+
className: (0, clsx.default)(`tremolo-number-input-${variant}-stepper`, className),
|
|
769
614
|
role: "button",
|
|
770
615
|
tabIndex: -1,
|
|
771
|
-
"aria-
|
|
616
|
+
"aria-label": direction > 0 ? "Increment" : "Decrement",
|
|
617
|
+
"aria-disabled": direction > 0 ? atMax : atMin,
|
|
772
618
|
"aria-readonly": readonly,
|
|
773
|
-
|
|
619
|
+
style,
|
|
620
|
+
onPointerDown: (event) => {
|
|
621
|
+
if (!readonly) press();
|
|
622
|
+
onPointerDown?.(event);
|
|
623
|
+
},
|
|
774
624
|
...props,
|
|
775
|
-
children: children
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
625
|
+
children: children ?? icon
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function StepperArrow({ up }) {
|
|
629
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
630
|
+
className: "tremolo-number-input-stepper-icon",
|
|
631
|
+
viewBox: "0 0 24 24",
|
|
632
|
+
fill: "none",
|
|
633
|
+
stroke: "currentColor",
|
|
634
|
+
strokeWidth: "2",
|
|
635
|
+
strokeLinecap: "round",
|
|
636
|
+
strokeLinejoin: "round",
|
|
637
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: up ? "18 15 12 9 6 15" : "6 9 12 15 18 9" })
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/components/NumberInput/DecrementStepper.tsx
|
|
642
|
+
/** Lowers the value by one `step`, repeating while held. */
|
|
643
|
+
function DecrementStepper(props) {
|
|
644
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperButton, {
|
|
645
|
+
direction: -1,
|
|
646
|
+
variant: "decrement",
|
|
647
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperArrow, { up: false }),
|
|
648
|
+
...props
|
|
786
649
|
});
|
|
787
650
|
}
|
|
788
651
|
//#endregion
|
|
789
652
|
//#region src/components/NumberInput/IncrementStepper.tsx
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
|
|
798
|
-
role: "button",
|
|
799
|
-
tabIndex: -1,
|
|
800
|
-
"aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
|
|
801
|
-
"aria-readonly": readonly,
|
|
802
|
-
onPointerDown: press,
|
|
803
|
-
...props,
|
|
804
|
-
children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
805
|
-
width: size,
|
|
806
|
-
height: size,
|
|
807
|
-
viewBox: "0 0 24 24",
|
|
808
|
-
fill: "none",
|
|
809
|
-
stroke: "currentColor",
|
|
810
|
-
strokeWidth: "2",
|
|
811
|
-
strokeLinecap: "round",
|
|
812
|
-
strokeLinejoin: "round",
|
|
813
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "18 15 12 9 6 15" })
|
|
814
|
-
})
|
|
653
|
+
/** Raises the value by one `step`, repeating while held. */
|
|
654
|
+
function IncrementStepper(props) {
|
|
655
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperButton, {
|
|
656
|
+
direction: 1,
|
|
657
|
+
variant: "increment",
|
|
658
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperArrow, { up: true }),
|
|
659
|
+
...props
|
|
815
660
|
});
|
|
816
661
|
}
|
|
817
662
|
//#endregion
|
|
818
|
-
//#region src/components/NumberInput/
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
const change = useNumberInputContext((s) => s.change);
|
|
830
|
-
const error = (0, react.useMemo)(() => {
|
|
831
|
-
if (editing) return false;
|
|
832
|
-
const v = valueAsNumber;
|
|
833
|
-
return (min ?? -Infinity) > v || v > (max ?? Infinity);
|
|
834
|
-
}, [
|
|
835
|
-
editing,
|
|
836
|
-
max,
|
|
837
|
-
min,
|
|
838
|
-
valueAsNumber
|
|
839
|
-
]);
|
|
840
|
-
const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
|
|
841
|
-
let newValue;
|
|
842
|
-
if (eventType == "normalized") {
|
|
843
|
-
if (!min || !max) throw new Error("[NumberInput] \"min\" and \"max\" are required when InputEventOption[0] is set to \"normalized\".");
|
|
844
|
-
newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max) + x, min, max);
|
|
845
|
-
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
|
|
846
|
-
} else {
|
|
847
|
-
newValue = valueAsNumber + x;
|
|
848
|
-
return (0, _tremolo_ui_functions.stepValue)(newValue, step);
|
|
849
|
-
}
|
|
850
|
-
}, [
|
|
851
|
-
max,
|
|
852
|
-
min,
|
|
853
|
-
step,
|
|
854
|
-
valueAsNumber
|
|
855
|
-
]);
|
|
856
|
-
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
857
|
-
if (!keyboard || !onChange || readonly) return;
|
|
858
|
-
const key = event.key;
|
|
859
|
-
if (!["ArrowUp", "ArrowDown"].includes(key)) return;
|
|
860
|
-
event.preventDefault();
|
|
861
|
-
const x = key == "ArrowUp" ? keyboard[1] : -keyboard[1];
|
|
862
|
-
const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
|
|
863
|
-
onChange?.(parsed.rawValue, parsed.formatValue);
|
|
864
|
-
}, [
|
|
865
|
-
keyboard,
|
|
866
|
-
readonly,
|
|
867
|
-
onChange,
|
|
868
|
-
updateValueByEvent
|
|
869
|
-
]);
|
|
870
|
-
const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
|
|
871
|
-
if (!wheel || readonly) return;
|
|
872
|
-
event.preventDefault();
|
|
873
|
-
if (!onChange || event.deltaY == 0) return;
|
|
874
|
-
const x = Math.sign(event.deltaY) * -wheel[1];
|
|
875
|
-
const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
|
|
876
|
-
onChange?.(parsed.rawValue, parsed.formatValue);
|
|
877
|
-
}, { passive: false }, [
|
|
878
|
-
wheel,
|
|
879
|
-
readonly,
|
|
880
|
-
onChange,
|
|
881
|
-
updateValueByEvent
|
|
882
|
-
]);
|
|
883
|
-
(0, react.useImperativeHandle)(ref, () => {
|
|
884
|
-
return {
|
|
885
|
-
focus() {
|
|
886
|
-
elmRef.current?.focus();
|
|
887
|
-
},
|
|
888
|
-
blur() {
|
|
889
|
-
elmRef.current?.blur();
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
}, []);
|
|
663
|
+
//#region src/components/NumberInput/InputField.tsx
|
|
664
|
+
/** The leading number of the displayed text, whatever the format put around it. */
|
|
665
|
+
const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
|
|
666
|
+
/**
|
|
667
|
+
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
668
|
+
*
|
|
669
|
+
* While the user types, their own text stands rather than `format(value)`, so
|
|
670
|
+
* that a half-finished entry is not rewritten under the caret.
|
|
671
|
+
*/
|
|
672
|
+
function InputField({ selectOnFocus = "none", blurOnEnter = true, className, style, ref, onFocus, onBlur, onKeyDown, ...props }) {
|
|
673
|
+
const { value, min, max, step, disabled, readonly, keyboard, text, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
893
674
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
894
|
-
ref: (
|
|
895
|
-
|
|
896
|
-
wheelRefCallback(input);
|
|
897
|
-
},
|
|
898
|
-
className: (0, clsx.default)("tremolo-number-input", className),
|
|
899
|
-
value,
|
|
900
|
-
min,
|
|
901
|
-
max,
|
|
902
|
-
step,
|
|
675
|
+
ref: useComposedRefs(ref, inputRef),
|
|
676
|
+
className: (0, clsx.default)("tremolo-number-input-field", className),
|
|
903
677
|
type: "text",
|
|
678
|
+
inputMode: "decimal",
|
|
679
|
+
role: "spinbutton",
|
|
680
|
+
value: text,
|
|
681
|
+
readOnly: readonly,
|
|
904
682
|
"aria-disabled": disabled,
|
|
905
683
|
"aria-readonly": readonly,
|
|
906
|
-
|
|
907
|
-
"
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
onChange?.(valueAsNumber, v);
|
|
915
|
-
if (cursorPos) {
|
|
916
|
-
event.target.selectionStart = cursorPos;
|
|
917
|
-
event.target.selectionEnd = cursorPos;
|
|
918
|
-
}
|
|
919
|
-
},
|
|
684
|
+
"aria-valuenow": value,
|
|
685
|
+
"aria-valuemin": min,
|
|
686
|
+
"aria-valuemax": max,
|
|
687
|
+
"aria-valuetext": text,
|
|
688
|
+
step,
|
|
689
|
+
"data-out-of-range": outOfRange,
|
|
690
|
+
style,
|
|
691
|
+
onChange: (event) => setDraft(event.currentTarget.value),
|
|
920
692
|
onFocus: (event) => {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
else if (selectWithFocus == "number") event.currentTarget.setSelectionRange(0, parsed.formatValue.length - parsed.unit.length);
|
|
926
|
-
onFocus?.(parsed.rawValue, parsed.formatValue, event);
|
|
693
|
+
const input = event.currentTarget;
|
|
694
|
+
if (selectOnFocus == "all") input.setSelectionRange(0, input.value.length);
|
|
695
|
+
else if (selectOnFocus == "number") input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
|
|
696
|
+
onFocus?.(event);
|
|
927
697
|
},
|
|
928
698
|
onBlur: (event) => {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (clampValueOnBlur) v = safeClamp(v, min, max);
|
|
932
|
-
const parsed = parseValue(String(v), units, digit);
|
|
933
|
-
change(parsed.formatValue);
|
|
934
|
-
if (clampValueOnBlur) onChange?.(parsed.rawValue, parsed.formatValue);
|
|
935
|
-
onBlur?.(parsed.rawValue, parsed.formatValue, event);
|
|
699
|
+
commitDraft();
|
|
700
|
+
onBlur?.(event);
|
|
936
701
|
},
|
|
937
702
|
onKeyDown: (event) => {
|
|
938
|
-
|
|
939
|
-
|
|
703
|
+
const key = event.key;
|
|
704
|
+
if (key == "Enter") {
|
|
705
|
+
commitDraft();
|
|
706
|
+
if (blurOnEnter) event.currentTarget.blur();
|
|
707
|
+
} else if (keyboard && !readonly && (key == "ArrowUp" || key == "ArrowDown")) {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
nudge(key == "ArrowUp" ? 1 : -1, keyboard);
|
|
710
|
+
}
|
|
940
711
|
onKeyDown?.(event);
|
|
941
712
|
},
|
|
942
713
|
...props
|
|
943
714
|
});
|
|
944
|
-
}
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/hooks/useDrag.ts
|
|
718
|
+
/**
|
|
719
|
+
* Track a pointer drag on an element.
|
|
720
|
+
*
|
|
721
|
+
* @returns a ref callback to attach to the element being dragged
|
|
722
|
+
*/
|
|
723
|
+
function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
724
|
+
const dragHandler = useCallbackRef(onDrag);
|
|
725
|
+
const dragStartHandler = useCallbackRef(onDragStart);
|
|
726
|
+
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
727
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
728
|
+
(0, react.useEffect)(() => {
|
|
729
|
+
if (!node) return;
|
|
730
|
+
const instance = (0, _tremolo_ui_dom.createDrag)(node, {
|
|
731
|
+
threshold,
|
|
732
|
+
cursor,
|
|
733
|
+
onDragStart: () => dragStartHandler(),
|
|
734
|
+
onDrag: ({ x, y, deltaX, deltaY }) => dragHandler(x, y, deltaX, deltaY),
|
|
735
|
+
onDragEnd: () => dragEndHandler()
|
|
736
|
+
});
|
|
737
|
+
return () => instance.destroy();
|
|
738
|
+
}, [
|
|
739
|
+
node,
|
|
740
|
+
threshold,
|
|
741
|
+
cursor,
|
|
742
|
+
dragHandler,
|
|
743
|
+
dragStartHandler,
|
|
744
|
+
dragEndHandler
|
|
745
|
+
]);
|
|
746
|
+
return setNode;
|
|
747
|
+
}
|
|
945
748
|
//#endregion
|
|
946
749
|
//#region src/components/NumberInput/Stepper.tsx
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
750
|
+
/**
|
|
751
|
+
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
752
|
+
* up and down moves the value one `step` every `drag` pixels.
|
|
753
|
+
*
|
|
754
|
+
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
755
|
+
* off text selection on whatever element it is attached to.
|
|
756
|
+
*/
|
|
757
|
+
function Stepper({ className, style, children, ref, ...props }) {
|
|
758
|
+
const { value, step, readonly, drag, range, changeValue } = useNumberInputContext();
|
|
759
|
+
/**
|
|
760
|
+
* Set once the drag has actually moved the value, so that the press-and-hold
|
|
761
|
+
* repeat of a stepper stands down and leaves the value to the drag. A ref,
|
|
762
|
+
* because it is read from inside the repeat rather than rendered.
|
|
763
|
+
*/
|
|
764
|
+
const draggingRef = (0, react.useRef)(false);
|
|
765
|
+
/**
|
|
766
|
+
* Where the drag started, taken on the first move rather than on pointer
|
|
767
|
+
* down: the steppers act on pointer down, so by then the value may already
|
|
768
|
+
* have been nudged once, and the drag should carry on from there.
|
|
769
|
+
*/
|
|
770
|
+
const originRef = (0, react.useRef)(null);
|
|
771
|
+
const dragRefCallback = useDrag({
|
|
772
|
+
threshold: 1,
|
|
773
|
+
cursor: readonly ? void 0 : "ns-resize",
|
|
774
|
+
onDragStart: () => {
|
|
775
|
+
originRef.current = null;
|
|
776
|
+
draggingRef.current = false;
|
|
777
|
+
},
|
|
778
|
+
onDrag: (_x, y) => {
|
|
779
|
+
if (readonly || drag == null) return;
|
|
780
|
+
if (!originRef.current) {
|
|
781
|
+
originRef.current = {
|
|
782
|
+
y,
|
|
783
|
+
value
|
|
784
|
+
};
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
788
|
+
if (steps == 0) return;
|
|
789
|
+
draggingRef.current = true;
|
|
790
|
+
changeValue((0, _tremolo_ui_functions.applyDelta)(originRef.current.value, steps, ["raw", step], range));
|
|
791
|
+
},
|
|
792
|
+
onDragEnd: () => {
|
|
793
|
+
draggingRef.current = false;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
const composedRef = useComposedRefs(ref, drag != null && !readonly ? dragRefCallback : void 0);
|
|
797
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperProvider, {
|
|
798
|
+
value: (0, react.useMemo)(() => ({ draggingRef }), []),
|
|
799
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
800
|
+
ref: composedRef,
|
|
801
|
+
className: (0, clsx.default)("tremolo-number-input-stepper", className),
|
|
802
|
+
style,
|
|
803
|
+
...props,
|
|
804
|
+
children
|
|
805
|
+
})
|
|
953
806
|
});
|
|
954
807
|
}
|
|
955
808
|
/**
|
|
956
809
|
* Input with some useful functions for entering numerical values.
|
|
957
810
|
*/
|
|
958
811
|
const NumberInput = {
|
|
959
|
-
Root: (0, react.forwardRef)(({ value, min, max, step = 1,
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
812
|
+
Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, units, digit, format: formatProp, parse: parseProp, clampValue = true, wheel = ["raw", 1], keyboard = ["raw", 1], drag = 1, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
|
|
813
|
+
const inputRef = (0, react.useRef)(null);
|
|
814
|
+
/**
|
|
815
|
+
* The text being typed. The only state here: it is not a copy of `value`,
|
|
816
|
+
* but the half-finished entry that has no value to be derived from yet.
|
|
817
|
+
*/
|
|
818
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
819
|
+
const format = (0, react.useCallback)((v) => formatProp ? formatProp(v) : (0, _tremolo_ui_functions.formatValue)(v, units, digit), [
|
|
820
|
+
formatProp,
|
|
821
|
+
units,
|
|
822
|
+
digit
|
|
823
|
+
]);
|
|
824
|
+
const parse = (0, react.useCallback)((t) => parseProp ? parseProp(t) : (0, _tremolo_ui_functions.parseValue)(t, units), [parseProp, units]);
|
|
825
|
+
const range = (0, react.useMemo)(() => ({
|
|
826
|
+
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
827
|
+
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
828
|
+
step,
|
|
829
|
+
scale
|
|
830
|
+
}), [
|
|
831
|
+
clampValue,
|
|
963
832
|
min,
|
|
964
833
|
max,
|
|
965
834
|
step,
|
|
966
|
-
|
|
967
|
-
|
|
835
|
+
scale
|
|
836
|
+
]);
|
|
837
|
+
const text = draft ?? format(value);
|
|
838
|
+
const editing = draft != null;
|
|
839
|
+
const outOfRange = !editing && (min != void 0 && value < min || max != void 0 && value > max);
|
|
840
|
+
const handleDraft = (0, react.useCallback)((next) => {
|
|
841
|
+
if (readonly) return;
|
|
842
|
+
setDraft(next);
|
|
843
|
+
const parsed = parse(next);
|
|
844
|
+
if (Number.isFinite(parsed)) onChange?.(parsed);
|
|
845
|
+
}, [
|
|
968
846
|
readonly,
|
|
969
|
-
|
|
970
|
-
onChange
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
847
|
+
parse,
|
|
848
|
+
onChange
|
|
849
|
+
]);
|
|
850
|
+
const changeValue = (0, react.useCallback)((next) => {
|
|
851
|
+
if (readonly) return;
|
|
852
|
+
setDraft(null);
|
|
853
|
+
if (next != value) onChange?.(next);
|
|
854
|
+
}, [
|
|
855
|
+
readonly,
|
|
856
|
+
value,
|
|
857
|
+
onChange
|
|
858
|
+
]);
|
|
859
|
+
const commitDraft = (0, react.useCallback)(() => {
|
|
860
|
+
if (draft == null || readonly) return;
|
|
861
|
+
changeValue((0, _tremolo_ui_functions.clamp)(parse(draft), range.min, range.max));
|
|
862
|
+
}, [
|
|
863
|
+
draft,
|
|
864
|
+
readonly,
|
|
865
|
+
parse,
|
|
866
|
+
range,
|
|
867
|
+
changeValue
|
|
868
|
+
]);
|
|
869
|
+
const nudge = (0, react.useCallback)((direction, option) => {
|
|
870
|
+
changeValue((0, _tremolo_ui_functions.applyDelta)(value, direction, option, range));
|
|
871
|
+
}, [
|
|
872
|
+
changeValue,
|
|
873
|
+
value,
|
|
874
|
+
range
|
|
875
|
+
]);
|
|
876
|
+
const wheelRefCallback = useWheel((event) => {
|
|
877
|
+
if (!wheel || readonly || event.deltaY == 0) return;
|
|
878
|
+
event.preventDefault();
|
|
879
|
+
nudge(-Math.sign(event.deltaY), wheel);
|
|
880
|
+
}, { requireFocus: true });
|
|
881
|
+
const context = (0, react.useMemo)(() => ({
|
|
882
|
+
value,
|
|
883
|
+
min,
|
|
884
|
+
max,
|
|
885
|
+
step,
|
|
886
|
+
scale,
|
|
887
|
+
disabled,
|
|
888
|
+
readonly,
|
|
889
|
+
clampValue,
|
|
890
|
+
range,
|
|
891
|
+
keyboard,
|
|
892
|
+
text,
|
|
893
|
+
editing,
|
|
894
|
+
outOfRange,
|
|
895
|
+
atMin: clampValue && min != void 0 && value <= min,
|
|
896
|
+
atMax: clampValue && max != void 0 && value >= max,
|
|
897
|
+
drag,
|
|
898
|
+
format,
|
|
899
|
+
parse,
|
|
900
|
+
setDraft: handleDraft,
|
|
901
|
+
commitDraft,
|
|
902
|
+
changeValue,
|
|
903
|
+
nudge,
|
|
904
|
+
inputRef
|
|
905
|
+
}), [
|
|
906
|
+
value,
|
|
907
|
+
min,
|
|
908
|
+
max,
|
|
909
|
+
step,
|
|
910
|
+
scale,
|
|
911
|
+
disabled,
|
|
912
|
+
readonly,
|
|
913
|
+
clampValue,
|
|
914
|
+
range,
|
|
915
|
+
keyboard,
|
|
916
|
+
text,
|
|
917
|
+
editing,
|
|
918
|
+
outOfRange,
|
|
919
|
+
drag,
|
|
920
|
+
format,
|
|
921
|
+
parse,
|
|
922
|
+
handleDraft,
|
|
923
|
+
commitDraft,
|
|
924
|
+
changeValue,
|
|
925
|
+
nudge
|
|
926
|
+
]);
|
|
927
|
+
(0, react.useImperativeHandle)(forwardedRef, () => {
|
|
928
|
+
return {
|
|
929
|
+
focus() {
|
|
930
|
+
inputRef.current?.focus();
|
|
931
|
+
},
|
|
932
|
+
blur() {
|
|
933
|
+
inputRef.current?.blur();
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}, []);
|
|
937
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
|
|
938
|
+
value: context,
|
|
939
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
940
|
+
ref: wheelRefCallback,
|
|
941
|
+
className: (0, clsx.default)("tremolo-number-input", className),
|
|
942
|
+
"aria-disabled": disabled,
|
|
943
|
+
"aria-readonly": readonly,
|
|
944
|
+
style,
|
|
945
|
+
...props,
|
|
946
|
+
children
|
|
992
947
|
})
|
|
993
948
|
});
|
|
994
949
|
}),
|
|
950
|
+
InputField,
|
|
995
951
|
Stepper,
|
|
996
952
|
IncrementStepper,
|
|
997
953
|
DecrementStepper
|
|
998
954
|
};
|
|
999
955
|
//#endregion
|
|
1000
|
-
//#region src/
|
|
956
|
+
//#region src/components/Piano/keyboardShortcuts.ts
|
|
1001
957
|
/**
|
|
1002
|
-
*
|
|
1003
|
-
* @returns [refCallback, pointerDownHandler]
|
|
1004
|
-
* @private
|
|
958
|
+
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
1005
959
|
*/
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
first: 0,
|
|
1046
|
-
last: 127
|
|
1047
|
-
},
|
|
1048
|
-
glissando: true,
|
|
1049
|
-
midiMax: 127,
|
|
1050
|
-
fill: false,
|
|
1051
|
-
label: () => void 0,
|
|
1052
|
-
onPlayNote: () => {},
|
|
1053
|
-
onStopNote: () => {}
|
|
1054
|
-
};
|
|
1055
|
-
return (0, zustand.createStore)()(() => ({
|
|
1056
|
-
...DEFAULT_PROPS,
|
|
1057
|
-
...initProps,
|
|
1058
|
-
notePosition: initProps.notePosition
|
|
1059
|
-
}));
|
|
960
|
+
const SHORTCUTS = {
|
|
961
|
+
HOME_ROW: { keys: [
|
|
962
|
+
"a",
|
|
963
|
+
"w",
|
|
964
|
+
"s",
|
|
965
|
+
"e",
|
|
966
|
+
"d",
|
|
967
|
+
"f",
|
|
968
|
+
"t",
|
|
969
|
+
"g",
|
|
970
|
+
"y",
|
|
971
|
+
"h",
|
|
972
|
+
"u",
|
|
973
|
+
"j",
|
|
974
|
+
"k",
|
|
975
|
+
"o",
|
|
976
|
+
"l",
|
|
977
|
+
"p",
|
|
978
|
+
";"
|
|
979
|
+
] },
|
|
980
|
+
HOME_ROW_NATURAL: { keys: [
|
|
981
|
+
"a",
|
|
982
|
+
"",
|
|
983
|
+
"s",
|
|
984
|
+
"",
|
|
985
|
+
"d",
|
|
986
|
+
"f",
|
|
987
|
+
"",
|
|
988
|
+
"g",
|
|
989
|
+
"",
|
|
990
|
+
"h",
|
|
991
|
+
"",
|
|
992
|
+
"j",
|
|
993
|
+
"k",
|
|
994
|
+
"",
|
|
995
|
+
"l",
|
|
996
|
+
"",
|
|
997
|
+
";"
|
|
998
|
+
] }
|
|
1060
999
|
};
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
(0, react.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1000
|
+
/**
|
|
1001
|
+
* Customizable piano component.
|
|
1002
|
+
*/
|
|
1003
|
+
const Piano = { Root: (0, react.forwardRef)(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, height = fill ? "100%" : 160, style, className, label, keyProps, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
1004
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
1005
|
+
const [activeNotes, setActiveNotes] = (0, react.useState)([]);
|
|
1006
|
+
/** Set while `fill` is on, measured from the parent. */
|
|
1007
|
+
const [filledKeyWidth, setFilledKeyWidth] = (0, react.useState)(whiteKeyWidth);
|
|
1008
|
+
const notes = (0, react.useMemo)(() => (0, _tremolo_ui_functions.getNoteRangeArray)(noteRange), [noteRange]);
|
|
1009
|
+
const whiteKeyCount = (0, react.useMemo)(() => notes.filter(_tremolo_ui_functions.isWhiteKey).length, [notes]);
|
|
1010
|
+
const layout = (0, react.useMemo)(() => ({
|
|
1011
|
+
noteRange,
|
|
1012
|
+
whiteKeyWidth: fill ? filledKeyWidth : whiteKeyWidth,
|
|
1013
|
+
keyGap,
|
|
1014
|
+
blackKeyWidthRatio,
|
|
1015
|
+
blackKeyHeightRatio
|
|
1016
|
+
}), [
|
|
1017
|
+
noteRange,
|
|
1018
|
+
fill,
|
|
1019
|
+
filledKeyWidth,
|
|
1020
|
+
whiteKeyWidth,
|
|
1021
|
+
keyGap,
|
|
1022
|
+
blackKeyWidthRatio,
|
|
1023
|
+
blackKeyHeightRatio
|
|
1024
|
+
]);
|
|
1025
|
+
const latest = (0, react.useRef)({
|
|
1026
|
+
layout,
|
|
1027
|
+
glissando,
|
|
1028
|
+
midiMax,
|
|
1029
|
+
onPlayNote,
|
|
1030
|
+
onStopNote
|
|
1092
1031
|
});
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
};
|
|
1109
|
-
(0, react.useImperativeHandle)(ref, () => {
|
|
1110
|
-
return {
|
|
1111
|
-
play(velocity) {
|
|
1112
|
-
if (disabled) return;
|
|
1113
|
-
setPlayed(true);
|
|
1114
|
-
if (onPlayNote) onPlayNote(noteNumber, velocity);
|
|
1115
|
-
},
|
|
1116
|
-
stop() {
|
|
1117
|
-
setPlayed(false);
|
|
1118
|
-
if (onStopNote) onStopNote(noteNumber);
|
|
1119
|
-
},
|
|
1120
|
-
played() {
|
|
1121
|
-
return played;
|
|
1122
|
-
}
|
|
1032
|
+
const instanceRef = (0, react.useRef)(null);
|
|
1033
|
+
(0, react.useEffect)(() => {
|
|
1034
|
+
if (!node) return;
|
|
1035
|
+
const instance = (0, _tremolo_ui_dom.createPianoInput)(node, {
|
|
1036
|
+
layout: latest.current.layout,
|
|
1037
|
+
glissando: latest.current.glissando,
|
|
1038
|
+
midiMax: latest.current.midiMax,
|
|
1039
|
+
onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
|
|
1040
|
+
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
1041
|
+
onActiveNotesChange: setActiveNotes
|
|
1042
|
+
});
|
|
1043
|
+
instanceRef.current = instance;
|
|
1044
|
+
return () => {
|
|
1045
|
+
instanceRef.current = null;
|
|
1046
|
+
instance.destroy();
|
|
1123
1047
|
};
|
|
1048
|
+
}, [node]);
|
|
1049
|
+
(0, react.useEffect)(() => {
|
|
1050
|
+
latest.current = {
|
|
1051
|
+
layout,
|
|
1052
|
+
glissando,
|
|
1053
|
+
midiMax,
|
|
1054
|
+
onPlayNote,
|
|
1055
|
+
onStopNote
|
|
1056
|
+
};
|
|
1057
|
+
instanceRef.current?.update({
|
|
1058
|
+
layout,
|
|
1059
|
+
glissando,
|
|
1060
|
+
midiMax
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
(0, react.useEffect)(() => {
|
|
1064
|
+
if (!fill || !node) return;
|
|
1065
|
+
const parent = node.parentElement;
|
|
1066
|
+
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1067
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1068
|
+
setFilledKeyWidth(node.clientWidth / whiteKeyCount - keyGap);
|
|
1069
|
+
});
|
|
1070
|
+
resizeObserver.observe(parent);
|
|
1071
|
+
return () => resizeObserver.disconnect();
|
|
1124
1072
|
}, [
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
played
|
|
1073
|
+
fill,
|
|
1074
|
+
node,
|
|
1075
|
+
whiteKeyCount,
|
|
1076
|
+
keyGap
|
|
1130
1077
|
]);
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (
|
|
1134
|
-
|
|
1135
|
-
|
|
1078
|
+
/** The note a shortcut key plays, or null when it has none. */
|
|
1079
|
+
function shortcutNote(key) {
|
|
1080
|
+
if (!keyboardShortcuts || key === "") return null;
|
|
1081
|
+
const index = keyboardShortcuts.keys.indexOf(key);
|
|
1082
|
+
return index === -1 ? null : noteRange.first + index;
|
|
1083
|
+
}
|
|
1084
|
+
useEventListener(globalThis.window, "keydown", (e) => {
|
|
1085
|
+
if (e.repeat) return;
|
|
1086
|
+
const note = shortcutNote(e.key);
|
|
1087
|
+
if (note !== null) instanceRef.current?.noteOn(note, { source: "keyboard" });
|
|
1136
1088
|
});
|
|
1089
|
+
useEventListener(globalThis.window, "keyup", (e) => {
|
|
1090
|
+
const note = shortcutNote(e.key);
|
|
1091
|
+
if (note !== null) instanceRef.current?.noteOff(note, { source: "keyboard" });
|
|
1092
|
+
});
|
|
1093
|
+
(0, react.useImperativeHandle)(forwardedRef, () => ({
|
|
1094
|
+
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1095
|
+
source: "api",
|
|
1096
|
+
velocity
|
|
1097
|
+
}),
|
|
1098
|
+
stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
|
|
1099
|
+
}), []);
|
|
1137
1100
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1138
|
-
|
|
1139
|
-
"
|
|
1140
|
-
"data-active": played,
|
|
1101
|
+
ref: setNode,
|
|
1102
|
+
className: (0, clsx.default)("tremolo-piano", className),
|
|
1141
1103
|
style: {
|
|
1142
|
-
|
|
1143
|
-
left: position,
|
|
1144
|
-
width: fill ? __width : width,
|
|
1104
|
+
width: fill ? "100%" : (0, _tremolo_ui_functions.pianoWidth)(layout),
|
|
1145
1105
|
height,
|
|
1146
1106
|
...style
|
|
1147
|
-
},
|
|
1148
|
-
...props,
|
|
1149
|
-
children:
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const WhiteKey = (0, react.forwardRef)(({ width = 40, height = "100%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
|
|
1157
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
|
|
1158
|
-
ref,
|
|
1159
|
-
keyType: "white",
|
|
1160
|
-
width,
|
|
1161
|
-
height,
|
|
1162
|
-
onContextMenu,
|
|
1163
|
-
...props
|
|
1164
|
-
});
|
|
1165
|
-
});
|
|
1166
|
-
const BlackKey = (0, react.forwardRef)(({ width = 26, height = "60%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
|
|
1167
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
|
|
1168
|
-
ref,
|
|
1169
|
-
keyType: "black",
|
|
1170
|
-
width,
|
|
1171
|
-
height,
|
|
1172
|
-
onContextMenu,
|
|
1173
|
-
...props
|
|
1174
|
-
});
|
|
1175
|
-
});
|
|
1176
|
-
//#endregion
|
|
1177
|
-
//#region src/components/Piano/keyboardShortcuts.ts
|
|
1178
|
-
const SHORTCUTS = { HOME_ROW: { keys: [
|
|
1179
|
-
"a",
|
|
1180
|
-
"w",
|
|
1181
|
-
"s",
|
|
1182
|
-
"e",
|
|
1183
|
-
"d",
|
|
1184
|
-
"f",
|
|
1185
|
-
"t",
|
|
1186
|
-
"g",
|
|
1187
|
-
"y",
|
|
1188
|
-
"h",
|
|
1189
|
-
"u",
|
|
1190
|
-
"j",
|
|
1191
|
-
"k",
|
|
1192
|
-
"o",
|
|
1193
|
-
"l",
|
|
1194
|
-
"p",
|
|
1195
|
-
";"
|
|
1196
|
-
] } };
|
|
1197
|
-
//#endregion
|
|
1198
|
-
//#region src/components/Piano/index.tsx
|
|
1199
|
-
/**
|
|
1200
|
-
* [noteRange.first, noteRange.first + 1 ..., noteRange.last]
|
|
1201
|
-
*/
|
|
1202
|
-
function getNoteRangeArray(noteRange) {
|
|
1203
|
-
return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
|
|
1204
|
-
}
|
|
1205
|
-
const blackPerWhiteWidth = 26 / 40;
|
|
1206
|
-
/**
|
|
1207
|
-
* Customizable piano component.
|
|
1208
|
-
*/
|
|
1209
|
-
const Piano = {
|
|
1210
|
-
Root: (0, react.forwardRef)(({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = 40, style, className, onPlayNote, onStopNote, label, children, onPointerDown, ...props }, forwardedRef) => {
|
|
1211
|
-
const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
|
|
1212
|
-
const keyRefs = (0, react.useRef)([]);
|
|
1213
|
-
for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
|
|
1214
|
-
const pianoRef = (0, react.useRef)(null);
|
|
1215
|
-
const hitKeyIndex = (0, react.useRef)(-1);
|
|
1216
|
-
const noteRangeArray = getNoteRangeArray(noteRange);
|
|
1217
|
-
const whiteNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isWhiteKey)(v));
|
|
1218
|
-
const blackNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isBlackKey)(v));
|
|
1219
|
-
const whiteNoteCount = whiteNotes.length;
|
|
1220
|
-
const padding = 1;
|
|
1221
|
-
const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount;
|
|
1222
|
-
const childrenWithProps = (0, react.useMemo)(() => {
|
|
1223
|
-
return children && react.default.Children.map(children, (child, index) => {
|
|
1224
|
-
if (react.default.isValidElement(child)) {
|
|
1225
|
-
const __width = child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * .65;
|
|
1226
|
-
const props = {
|
|
1227
|
-
ref: keyRefs.current[index],
|
|
1228
|
-
__width
|
|
1229
|
-
};
|
|
1230
|
-
return react.default.cloneElement(child, props);
|
|
1231
|
-
}
|
|
1232
|
-
return child;
|
|
1233
|
-
});
|
|
1234
|
-
}, [children, whiteNoteWidth]);
|
|
1235
|
-
const notePosition = (0, react.useCallback)((note) => {
|
|
1236
|
-
const pitchPositions = {
|
|
1237
|
-
C: 0,
|
|
1238
|
-
"C#": 1,
|
|
1239
|
-
D: 1,
|
|
1240
|
-
"D#": 2,
|
|
1241
|
-
E: 2,
|
|
1242
|
-
F: 3,
|
|
1243
|
-
"F#": 4,
|
|
1244
|
-
G: 4,
|
|
1245
|
-
"G#": 5,
|
|
1246
|
-
A: 5,
|
|
1247
|
-
"A#": 6,
|
|
1248
|
-
B: 6
|
|
1249
|
-
};
|
|
1250
|
-
const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth;
|
|
1251
|
-
const padding = 1;
|
|
1252
|
-
const targetNoteKey = (0, _tremolo_ui_functions.noteKey)(note);
|
|
1253
|
-
const firstNoteKey = (0, _tremolo_ui_functions.noteKey)(noteRange.first);
|
|
1254
|
-
const octave = Math.floor((note - noteRange.first) / 12);
|
|
1255
|
-
const octaveOffset = _tremolo_ui_functions.noteKeys.indexOf(firstNoteKey) > _tremolo_ui_functions.noteKeys.indexOf(targetNoteKey) ? 1 : 0;
|
|
1256
|
-
const w = whiteNoteWidth + padding;
|
|
1257
|
-
const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
|
|
1258
|
-
const blackKeyOffset = (0, _tremolo_ui_functions.isBlackKey)(note) ? blackNoteWidth / 2 : 0;
|
|
1259
|
-
return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
|
|
1260
|
-
}, [noteRange.first, whiteNoteWidth]);
|
|
1261
|
-
const getHitKeyIndex = (0, react.useCallback)((x, y) => {
|
|
1262
|
-
if (!pianoRef.current) return -1;
|
|
1263
|
-
const containerHeight = pianoRef.current.clientHeight;
|
|
1264
|
-
const notes = [...blackNotes, ...whiteNotes];
|
|
1265
|
-
for (let i = 0; i < notes.length; i++) {
|
|
1266
|
-
const note = notes[i];
|
|
1267
|
-
const pos = notePosition(note);
|
|
1268
|
-
const w = (0, _tremolo_ui_functions.isWhiteKey)(note) ? whiteNoteWidth : whiteNoteWidth * blackPerWhiteWidth;
|
|
1269
|
-
const h = (0, _tremolo_ui_functions.isWhiteKey)(note) ? containerHeight : containerHeight * .6;
|
|
1270
|
-
if (pos <= x && x < pos + w && 0 <= y && y < h) return note;
|
|
1271
|
-
}
|
|
1272
|
-
return -1;
|
|
1273
|
-
}, [
|
|
1274
|
-
blackNotes,
|
|
1275
|
-
notePosition,
|
|
1276
|
-
whiteNoteWidth,
|
|
1277
|
-
whiteNotes
|
|
1278
|
-
]);
|
|
1279
|
-
const onDrag = (0, react.useCallback)((perX, perY) => {
|
|
1280
|
-
if (!pianoRef.current) return;
|
|
1281
|
-
const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
|
|
1282
|
-
const index = noteRangeArray.indexOf(note);
|
|
1283
|
-
if (index == -1) return;
|
|
1284
|
-
if (hitKeyIndex.current != index) {
|
|
1285
|
-
keyRefs.current[hitKeyIndex.current]?.current?.stop();
|
|
1286
|
-
keyRefs.current[index]?.current?.play();
|
|
1287
|
-
hitKeyIndex.current = index;
|
|
1288
|
-
}
|
|
1289
|
-
}, [
|
|
1290
|
-
getHitKeyIndex,
|
|
1291
|
-
noteRangeArray,
|
|
1292
|
-
staticWidth
|
|
1293
|
-
]);
|
|
1294
|
-
const onDragEnd = (0, react.useCallback)(() => {
|
|
1295
|
-
if (keyRefs.current[hitKeyIndex.current]?.current?.played()) keyRefs.current[hitKeyIndex.current]?.current?.stop();
|
|
1296
|
-
hitKeyIndex.current = -1;
|
|
1297
|
-
}, [keyRefs]);
|
|
1298
|
-
(0, react.useEffect)(() => {
|
|
1299
|
-
if (fill && pianoRef.current) {
|
|
1300
|
-
const parent = pianoRef.current.parentElement;
|
|
1301
|
-
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1302
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
1303
|
-
const w = pianoRef.current.clientWidth;
|
|
1304
|
-
setWhiteNoteWidth(w / whiteNoteCount - padding);
|
|
1305
|
-
});
|
|
1306
|
-
resizeObserver.observe(parent);
|
|
1307
|
-
return () => {
|
|
1308
|
-
resizeObserver.unobserve(parent);
|
|
1309
|
-
};
|
|
1310
|
-
} else setWhiteNoteWidth(_whiteNoteWidth);
|
|
1311
|
-
}, [
|
|
1312
|
-
fill,
|
|
1313
|
-
_whiteNoteWidth,
|
|
1314
|
-
whiteNoteCount
|
|
1315
|
-
]);
|
|
1316
|
-
const { refHandler: touchMoveRefCallback, pointerDownHandler } = usePianoDrag({
|
|
1317
|
-
baseElementRef: pianoRef,
|
|
1318
|
-
onDrag,
|
|
1319
|
-
onDragEnd
|
|
1320
|
-
});
|
|
1321
|
-
useEventListener(globalThis.window, "keydown", (e) => {
|
|
1322
|
-
if (e.repeat) return;
|
|
1323
|
-
if (!keyboardShortcuts) return;
|
|
1324
|
-
const index = keyboardShortcuts.keys.indexOf(e.key);
|
|
1325
|
-
if (index != -1) {
|
|
1326
|
-
if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play();
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
useEventListener(globalThis.window, "keyup", (e) => {
|
|
1330
|
-
if (e.repeat) return;
|
|
1331
|
-
if (!keyboardShortcuts) return;
|
|
1332
|
-
const index = keyboardShortcuts.keys.indexOf(e.key);
|
|
1333
|
-
if (index != -1) keyRefs.current[index]?.current?.stop();
|
|
1334
|
-
});
|
|
1335
|
-
(0, react.useImperativeHandle)(forwardedRef, () => {
|
|
1336
|
-
return {
|
|
1337
|
-
playNote(note, velocity) {
|
|
1338
|
-
const index = noteRangeArray.indexOf(note);
|
|
1339
|
-
if (index != -1) {
|
|
1340
|
-
if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
|
|
1341
|
-
} else onPlayNote?.(note, velocity);
|
|
1342
|
-
},
|
|
1343
|
-
stopNote(note) {
|
|
1344
|
-
const index = noteRangeArray.indexOf(note);
|
|
1345
|
-
if (index != -1) keyRefs.current[index]?.current?.stop();
|
|
1346
|
-
else onStopNote?.(note);
|
|
1347
|
-
}
|
|
1107
|
+
},
|
|
1108
|
+
...props,
|
|
1109
|
+
children: notes.map((note, index) => {
|
|
1110
|
+
const keyType = (0, _tremolo_ui_functions.isWhiteKey)(note) ? "white" : "black";
|
|
1111
|
+
const state = {
|
|
1112
|
+
index,
|
|
1113
|
+
keyType,
|
|
1114
|
+
active: activeNotes.includes(note),
|
|
1115
|
+
disabled: note > midiMax
|
|
1348
1116
|
};
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
midiMax,
|
|
1359
|
-
fill,
|
|
1360
|
-
onPlayNote,
|
|
1361
|
-
onStopNote,
|
|
1362
|
-
label,
|
|
1363
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1364
|
-
ref: (div) => {
|
|
1365
|
-
pianoRef.current = div;
|
|
1366
|
-
touchMoveRefCallback(div);
|
|
1367
|
-
},
|
|
1368
|
-
className: (0, clsx.default)("tremolo-piano", className),
|
|
1117
|
+
const { className: keyClassName, style: keyStyle, ...rest } = keyProps?.(note, state) ?? {};
|
|
1118
|
+
const content = label?.(note, state);
|
|
1119
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1120
|
+
className: (0, clsx.default)(`tremolo-piano-${keyType}-key`, keyClassName),
|
|
1121
|
+
"data-note": note,
|
|
1122
|
+
"data-note-key": (0, _tremolo_ui_functions.noteKey)(note),
|
|
1123
|
+
"data-active": state.active,
|
|
1124
|
+
"aria-disabled": state.disabled,
|
|
1125
|
+
...rest,
|
|
1369
1126
|
style: {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
onPointerDown: (event) => {
|
|
1375
|
-
pointerDownHandler(event);
|
|
1376
|
-
onPointerDown?.(event);
|
|
1127
|
+
...keyStyle,
|
|
1128
|
+
left: (0, _tremolo_ui_functions.notePosition)(note, layout),
|
|
1129
|
+
width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_functions.blackKeyWidth)(layout),
|
|
1130
|
+
height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
|
|
1377
1131
|
},
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
});
|
|
1390
|
-
}),
|
|
1391
|
-
WhiteKey,
|
|
1392
|
-
BlackKey,
|
|
1393
|
-
KeyLabel
|
|
1394
|
-
};
|
|
1132
|
+
children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1133
|
+
className: "tremolo-piano-key-label-wrapper",
|
|
1134
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1135
|
+
className: "tremolo-piano-key-label",
|
|
1136
|
+
children: content
|
|
1137
|
+
})
|
|
1138
|
+
})
|
|
1139
|
+
}, note);
|
|
1140
|
+
})
|
|
1141
|
+
});
|
|
1142
|
+
}) };
|
|
1395
1143
|
//#endregion
|
|
1396
1144
|
//#region src/components/PointsEditor/Background.tsx
|
|
1397
1145
|
function Background({ className, children, ...props }) {
|
|
@@ -1403,96 +1151,24 @@ function Background({ className, children, ...props }) {
|
|
|
1403
1151
|
}
|
|
1404
1152
|
//#endregion
|
|
1405
1153
|
//#region src/components/PointsEditor/context.tsx
|
|
1406
|
-
const createPointsEditorStore = (initProps) => {
|
|
1407
|
-
const DEFAULT_PROPS = {
|
|
1408
|
-
containerElementRef: (0, react.createRef)(),
|
|
1409
|
-
disabled: false,
|
|
1410
|
-
readonly: false,
|
|
1411
|
-
externalStyles: {
|
|
1412
|
-
userSelectNone: true,
|
|
1413
|
-
cursor: "grabbing"
|
|
1414
|
-
}
|
|
1415
|
-
};
|
|
1416
|
-
return (0, zustand.createStore)()((set) => ({
|
|
1417
|
-
...DEFAULT_PROPS,
|
|
1418
|
-
...initProps,
|
|
1419
|
-
setContainerElementRef: (containerElementRef) => {
|
|
1420
|
-
set(() => ({ containerElementRef }));
|
|
1421
|
-
}
|
|
1422
|
-
}));
|
|
1423
|
-
};
|
|
1424
1154
|
const PointsEditorContext = (0, react.createContext)(null);
|
|
1425
|
-
|
|
1426
|
-
const storeRef = (0, react.useRef)(null);
|
|
1427
|
-
if (!storeRef.current) storeRef.current = createPointsEditorStore(props);
|
|
1428
|
-
(0, react.useEffect)(() => {
|
|
1429
|
-
if (storeRef.current) storeRef.current.setState(props);
|
|
1430
|
-
else storeRef.current = createPointsEditorStore(props);
|
|
1431
|
-
}, [props]);
|
|
1432
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorContext.Provider, {
|
|
1433
|
-
value: storeRef.current,
|
|
1434
|
-
children
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1155
|
+
const PointsEditorProvider = PointsEditorContext.Provider;
|
|
1437
1156
|
function usePointsEditorContext(selector) {
|
|
1438
|
-
const
|
|
1439
|
-
if (!
|
|
1440
|
-
return
|
|
1157
|
+
const context = (0, react.useContext)(PointsEditorContext);
|
|
1158
|
+
if (!context) throw new Error("Missing PointsEditorContext.Provider in the tree");
|
|
1159
|
+
return selector ? selector(context) : context;
|
|
1441
1160
|
}
|
|
1442
1161
|
//#endregion
|
|
1443
1162
|
//#region src/components/PointsEditor/Container.tsx
|
|
1444
|
-
function Container({ className,
|
|
1445
|
-
const containerElementRef = (0, react.useRef)(null);
|
|
1446
|
-
usePointsEditorContext((s) => s.setContainerElementRef)(containerElementRef);
|
|
1163
|
+
function Container({ children, className, ref, ...props }) {
|
|
1447
1164
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1448
|
-
ref:
|
|
1165
|
+
ref: useComposedRefs(ref, usePointsEditorContext((s) => s.containerRef)),
|
|
1449
1166
|
className: (0, clsx.default)("tremolo-points-editor-container", className),
|
|
1450
1167
|
...props,
|
|
1451
1168
|
children
|
|
1452
1169
|
});
|
|
1453
1170
|
}
|
|
1454
1171
|
//#endregion
|
|
1455
|
-
//#region src/hooks/useDragWithElement.ts
|
|
1456
|
-
function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
|
|
1457
|
-
const [dragging, setDragging] = (0, react.useState)(false);
|
|
1458
|
-
const normalizedX = (0, react.useRef)(0);
|
|
1459
|
-
const normalizedY = (0, react.useRef)(0);
|
|
1460
|
-
const handleDrag = (0, react.useCallback)((event) => {
|
|
1461
|
-
if (!baseElementRef.current || !dragging) return;
|
|
1462
|
-
const isTouch = event instanceof TouchEvent;
|
|
1463
|
-
if (isTouch && event.cancelable) event.preventDefault();
|
|
1464
|
-
const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
|
|
1465
|
-
const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
|
|
1466
|
-
const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
|
|
1467
|
-
const nx = (0, _tremolo_ui_functions.normalizeValue)(mouseX, left, right);
|
|
1468
|
-
const ny = (0, _tremolo_ui_functions.normalizeValue)(mouseY, top, bottom);
|
|
1469
|
-
normalizedX.current = nx;
|
|
1470
|
-
normalizedY.current = ny;
|
|
1471
|
-
onDrag(nx, ny);
|
|
1472
|
-
}, [
|
|
1473
|
-
onDrag,
|
|
1474
|
-
baseElementRef,
|
|
1475
|
-
dragging
|
|
1476
|
-
]);
|
|
1477
|
-
const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
|
|
1478
|
-
const pointerDownHandler = (0, react.useCallback)((event) => {
|
|
1479
|
-
setDragging(true);
|
|
1480
|
-
handleDrag(event);
|
|
1481
|
-
onDragStart?.(normalizedX.current, normalizedY.current);
|
|
1482
|
-
}, [handleDrag, onDragStart]);
|
|
1483
|
-
useEventListener(globalThis.window, "pointermove", handleDrag);
|
|
1484
|
-
useEventListener(globalThis.window, "pointerup", () => {
|
|
1485
|
-
if (!dragging) return;
|
|
1486
|
-
setDragging(false);
|
|
1487
|
-
onDragEnd?.(normalizedX.current, normalizedY.current);
|
|
1488
|
-
});
|
|
1489
|
-
return {
|
|
1490
|
-
refHandler,
|
|
1491
|
-
pointerDownHandler,
|
|
1492
|
-
dragging
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
//#endregion
|
|
1496
1172
|
//#region src/components/PointsEditor/Point.tsx
|
|
1497
1173
|
function clampPoint(point, min, max) {
|
|
1498
1174
|
const { x, y } = point;
|
|
@@ -1501,48 +1177,88 @@ function clampPoint(point, min, max) {
|
|
|
1501
1177
|
y: (0, _tremolo_ui_functions.clamp)(y, min?.y ?? 0, max?.y ?? 1)
|
|
1502
1178
|
};
|
|
1503
1179
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1180
|
+
/**
|
|
1181
|
+
* A point is placed by its position within the container, so its value is a
|
|
1182
|
+
* position: 0..1 on each axis, with y growing downwards.
|
|
1183
|
+
*/
|
|
1184
|
+
const AXIS = {
|
|
1185
|
+
min: 0,
|
|
1186
|
+
max: 1
|
|
1187
|
+
};
|
|
1188
|
+
function Point({ value, min, max, size, width = 16, height = 16, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, ...props }) {
|
|
1189
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard } = usePointsEditorContext();
|
|
1190
|
+
const disabled = _disabled ?? rootDisabled;
|
|
1191
|
+
const readonly = _readonly ?? rootReadonly;
|
|
1192
|
+
const wheel = _wheel === void 0 ? rootWheel : _wheel;
|
|
1193
|
+
const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
|
|
1194
|
+
const [element, setElement] = (0, react.useState)(null);
|
|
1195
|
+
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1196
|
+
axis: AXIS,
|
|
1197
|
+
baseElementRef: containerRef,
|
|
1198
|
+
cursor: readonly ? void 0 : externalStyles.cursor,
|
|
1199
|
+
onChange: ([x, y]) => {
|
|
1512
1200
|
if (readonly) return;
|
|
1513
1201
|
onChange?.(clampPoint({
|
|
1514
1202
|
x,
|
|
1515
1203
|
y
|
|
1516
1204
|
}, min, max));
|
|
1517
1205
|
},
|
|
1518
|
-
onDragStart: (x, y) => {
|
|
1206
|
+
onDragStart: ([x, y]) => {
|
|
1519
1207
|
if (readonly) return;
|
|
1520
1208
|
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
1521
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
|
|
1522
1209
|
onDragStart?.(clampPoint({
|
|
1523
1210
|
x,
|
|
1524
1211
|
y
|
|
1525
1212
|
}, min, max));
|
|
1526
1213
|
},
|
|
1527
|
-
onDragEnd: (x, y) => {
|
|
1214
|
+
onDragEnd: ([x, y]) => {
|
|
1528
1215
|
if (readonly) return;
|
|
1529
1216
|
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
1530
|
-
if (externalStyles.cursor) resetCursorStyle();
|
|
1531
1217
|
onDragEnd?.(clampPoint({
|
|
1532
1218
|
x,
|
|
1533
1219
|
y
|
|
1534
1220
|
}, min, max));
|
|
1535
1221
|
}
|
|
1536
1222
|
});
|
|
1223
|
+
const nudge = (0, react.useCallback)((axis, direction, option) => {
|
|
1224
|
+
const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, option, AXIS);
|
|
1225
|
+
onChange?.(clampPoint({
|
|
1226
|
+
...value,
|
|
1227
|
+
[axis]: next
|
|
1228
|
+
}, min, max));
|
|
1229
|
+
}, [
|
|
1230
|
+
value,
|
|
1231
|
+
min,
|
|
1232
|
+
max,
|
|
1233
|
+
onChange
|
|
1234
|
+
]);
|
|
1235
|
+
useWheel((event) => {
|
|
1236
|
+
if (!onChange || readonly || !wheel) return;
|
|
1237
|
+
if (!element || element.ownerDocument.activeElement !== element) return;
|
|
1238
|
+
event.preventDefault();
|
|
1239
|
+
nudge(event.shiftKey ? "x" : "y", event.deltaY < 0 ? -1 : 1, wheel);
|
|
1240
|
+
}, { target: containerRef });
|
|
1241
|
+
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1242
|
+
const handleKeyDown = (event) => {
|
|
1243
|
+
if (!onChange || readonly || !keyboard) return;
|
|
1244
|
+
const key = event.key;
|
|
1245
|
+
if ([
|
|
1246
|
+
"ArrowRight",
|
|
1247
|
+
"ArrowLeft",
|
|
1248
|
+
"ArrowUp",
|
|
1249
|
+
"ArrowDown"
|
|
1250
|
+
].includes(key)) {
|
|
1251
|
+
event.preventDefault();
|
|
1252
|
+
nudge(key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y", key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1, keyboard);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1537
1255
|
const colors = { "--color": color };
|
|
1538
1256
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1539
|
-
ref:
|
|
1540
|
-
touchMoveRefCallback(div);
|
|
1541
|
-
},
|
|
1257
|
+
ref: refCallback,
|
|
1542
1258
|
className: (0, clsx.default)("tremolo-points-editor-point", className),
|
|
1543
1259
|
tabIndex: 0,
|
|
1544
|
-
"aria-disabled": disabled
|
|
1545
|
-
"aria-readonly": readonly
|
|
1260
|
+
"aria-disabled": disabled,
|
|
1261
|
+
"aria-readonly": readonly,
|
|
1546
1262
|
"data-dragging": dragging,
|
|
1547
1263
|
style: {
|
|
1548
1264
|
...colors,
|
|
@@ -1552,34 +1268,52 @@ function Point({ value, min, max, size, width = 16, height = 16, color, disabled
|
|
|
1552
1268
|
top: `${value.y * 100}%`,
|
|
1553
1269
|
...style
|
|
1554
1270
|
},
|
|
1555
|
-
onPointerDown
|
|
1556
|
-
|
|
1557
|
-
|
|
1271
|
+
onPointerDown,
|
|
1272
|
+
onKeyDown: (event) => {
|
|
1273
|
+
handleKeyDown(event);
|
|
1274
|
+
onKeyDown?.(event);
|
|
1558
1275
|
},
|
|
1559
1276
|
...props
|
|
1560
1277
|
});
|
|
1561
1278
|
}
|
|
1562
1279
|
//#endregion
|
|
1563
1280
|
//#region src/components/PointsEditor/index.tsx
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1281
|
+
/**
|
|
1282
|
+
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1283
|
+
* in a hundred steps whatever its pixel size.
|
|
1284
|
+
*/
|
|
1285
|
+
const DEFAULT_INPUT_EVENT_OPTION = ["normalized", .01];
|
|
1568
1286
|
/**
|
|
1569
1287
|
* Multiple Point Controller
|
|
1570
1288
|
*/
|
|
1571
1289
|
const PointsEditor = {
|
|
1572
|
-
Root: (0, react.forwardRef)(({ width = 200, height = 100, disabled = false, readonly = false,
|
|
1290
|
+
Root: (0, react.forwardRef)(({ width = 200, height = 100, disabled = false, readonly = false, wheel = DEFAULT_INPUT_EVENT_OPTION, keyboard = DEFAULT_INPUT_EVENT_OPTION, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
1291
|
+
const containerRef = (0, react.useRef)(null);
|
|
1292
|
+
const { userSelectNone = true, cursor = "grabbing" } = externalStyles ?? {};
|
|
1573
1293
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorProvider, {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1294
|
+
value: (0, react.useMemo)(() => ({
|
|
1295
|
+
disabled,
|
|
1296
|
+
readonly,
|
|
1297
|
+
wheel,
|
|
1298
|
+
keyboard,
|
|
1299
|
+
externalStyles: {
|
|
1300
|
+
userSelectNone,
|
|
1301
|
+
cursor
|
|
1302
|
+
},
|
|
1303
|
+
containerRef
|
|
1304
|
+
}), [
|
|
1305
|
+
disabled,
|
|
1306
|
+
readonly,
|
|
1307
|
+
wheel,
|
|
1308
|
+
keyboard,
|
|
1309
|
+
userSelectNone,
|
|
1310
|
+
cursor
|
|
1311
|
+
]),
|
|
1580
1312
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1581
1313
|
ref: forwardedRef,
|
|
1582
1314
|
className: (0, clsx.default)("tremolo-points-editor", className),
|
|
1315
|
+
"aria-disabled": disabled,
|
|
1316
|
+
"aria-readonly": readonly,
|
|
1583
1317
|
style: {
|
|
1584
1318
|
width,
|
|
1585
1319
|
height,
|
|
@@ -1596,59 +1330,33 @@ const PointsEditor = {
|
|
|
1596
1330
|
};
|
|
1597
1331
|
//#endregion
|
|
1598
1332
|
//#region src/components/Slider/context.tsx
|
|
1599
|
-
const createSliderStore = (initProps) => {
|
|
1600
|
-
const DEFAULT_PROPS = {
|
|
1601
|
-
min: 0,
|
|
1602
|
-
max: 0,
|
|
1603
|
-
step: 0,
|
|
1604
|
-
skew: 0,
|
|
1605
|
-
vertical: false,
|
|
1606
|
-
reverse: false,
|
|
1607
|
-
disabled: false,
|
|
1608
|
-
readonly: false
|
|
1609
|
-
};
|
|
1610
|
-
return (0, zustand.createStore)()(() => ({
|
|
1611
|
-
...DEFAULT_PROPS,
|
|
1612
|
-
...initProps
|
|
1613
|
-
}));
|
|
1614
|
-
};
|
|
1615
1333
|
const SliderContext = (0, react.createContext)(null);
|
|
1616
|
-
|
|
1617
|
-
const storeRef = (0, react.useRef)(null);
|
|
1618
|
-
if (!storeRef.current) storeRef.current = createSliderStore(props);
|
|
1619
|
-
(0, react.useEffect)(() => {
|
|
1620
|
-
if (storeRef.current) storeRef.current.setState(props);
|
|
1621
|
-
else storeRef.current = createSliderStore(props);
|
|
1622
|
-
}, [props]);
|
|
1623
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderContext.Provider, {
|
|
1624
|
-
value: storeRef.current,
|
|
1625
|
-
children
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1334
|
+
const SliderProvider = SliderContext.Provider;
|
|
1628
1335
|
function useSliderContext(selector) {
|
|
1629
|
-
const
|
|
1630
|
-
if (!
|
|
1631
|
-
return
|
|
1336
|
+
const context = (0, react.useContext)(SliderContext);
|
|
1337
|
+
if (!context) throw new Error("Missing SliderContext.Provider in the tree");
|
|
1338
|
+
return selector ? selector(context) : context;
|
|
1632
1339
|
}
|
|
1633
1340
|
//#endregion
|
|
1634
|
-
//#region src/components/Slider/
|
|
1635
|
-
function
|
|
1341
|
+
//#region src/components/Slider/MarksOption.tsx
|
|
1342
|
+
function MarksOption({ value, type = "mark-number", label, thickness = 1, length = "0.5rem", gap = 2, labelWidth, classes, styles, className, style, ...props }) {
|
|
1636
1343
|
const min = useSliderContext((s) => s.min);
|
|
1637
1344
|
const max = useSliderContext((s) => s.max);
|
|
1638
|
-
const
|
|
1345
|
+
const scale = useSliderContext((s) => s.scale);
|
|
1639
1346
|
const vertical = useSliderContext((s) => s.vertical);
|
|
1640
1347
|
const reverse = useSliderContext((s) => s.reverse);
|
|
1641
|
-
const calcPercent = (0, react.useCallback)((
|
|
1642
|
-
|
|
1348
|
+
const calcPercent = (0, react.useCallback)((value) => {
|
|
1349
|
+
const percent = scale.normalize(value, min, max) * 100;
|
|
1350
|
+
return (0, _tremolo_ui_functions.toFixed)((0, _tremolo_ui_functions.xor)(vertical, reverse) ? 100 - percent : percent);
|
|
1643
1351
|
}, [
|
|
1644
1352
|
vertical,
|
|
1645
1353
|
reverse,
|
|
1646
1354
|
max,
|
|
1647
1355
|
min,
|
|
1648
|
-
|
|
1356
|
+
scale
|
|
1649
1357
|
]);
|
|
1650
1358
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1651
|
-
className: (0, clsx.default)("tremolo-slider-
|
|
1359
|
+
className: (0, clsx.default)("tremolo-slider-marks-option", className),
|
|
1652
1360
|
style: {
|
|
1653
1361
|
left: !vertical ? `${calcPercent(value)}%` : void 0,
|
|
1654
1362
|
top: vertical ? `${calcPercent(value)}%` : void 0,
|
|
@@ -1657,7 +1365,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
|
|
|
1657
1365
|
"data-vertical": vertical,
|
|
1658
1366
|
...props,
|
|
1659
1367
|
children: [type != "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1660
|
-
className: (0, clsx.default)("tremolo-slider-
|
|
1368
|
+
className: (0, clsx.default)("tremolo-slider-marks-option-mark", classes?.mark),
|
|
1661
1369
|
style: {
|
|
1662
1370
|
width: !vertical ? thickness : length,
|
|
1663
1371
|
height: vertical ? thickness : length,
|
|
@@ -1667,7 +1375,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
|
|
|
1667
1375
|
},
|
|
1668
1376
|
"data-vertical": vertical
|
|
1669
1377
|
}), type != "mark" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1670
|
-
className: (0, clsx.default)("tremolo-slider-
|
|
1378
|
+
className: (0, clsx.default)("tremolo-slider-marks-option-label", classes?.label),
|
|
1671
1379
|
style: {
|
|
1672
1380
|
width: labelWidth,
|
|
1673
1381
|
...styles?.label
|
|
@@ -1692,8 +1400,8 @@ function generateOptionsList(options, min, max, step) {
|
|
|
1692
1400
|
return optionsList;
|
|
1693
1401
|
}
|
|
1694
1402
|
//#endregion
|
|
1695
|
-
//#region src/components/Slider/
|
|
1696
|
-
function
|
|
1403
|
+
//#region src/components/Slider/Marks.tsx
|
|
1404
|
+
function Marks({ gap = 6, options, children, className, style, ...props }) {
|
|
1697
1405
|
const min = useSliderContext((s) => s.min);
|
|
1698
1406
|
const max = useSliderContext((s) => s.max);
|
|
1699
1407
|
const step = useSliderContext((s) => s.step);
|
|
@@ -1702,7 +1410,7 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
|
|
|
1702
1410
|
const optionsList = options ? generateOptionsList(options, min, max, step) : [];
|
|
1703
1411
|
if ((0, _tremolo_ui_functions.xor)(vertical, reverse)) optionsList.reverse();
|
|
1704
1412
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1705
|
-
className: (0, clsx.default)("tremolo-slider-
|
|
1413
|
+
className: (0, clsx.default)("tremolo-slider-marks", className),
|
|
1706
1414
|
style: {
|
|
1707
1415
|
marginLeft: vertical ? gap : void 0,
|
|
1708
1416
|
marginTop: !vertical ? gap : void 0,
|
|
@@ -1710,72 +1418,70 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
|
|
|
1710
1418
|
},
|
|
1711
1419
|
...props,
|
|
1712
1420
|
children: options ? optionsList.map(({ value, type }, index) => {
|
|
1713
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1421
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarksOption, {
|
|
1714
1422
|
value,
|
|
1715
1423
|
type
|
|
1716
1424
|
}, index);
|
|
1717
1425
|
}) : children
|
|
1718
1426
|
});
|
|
1719
1427
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
const
|
|
1724
|
-
const readonly = useSliderContext(
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
focus()
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
blur()
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/components/Slider/Thumb.tsx
|
|
1430
|
+
function Thumb$1({ color, children, className, style, ref }) {
|
|
1431
|
+
const elementRef = (0, react.useRef)(null);
|
|
1432
|
+
const { vertical, disabled, readonly, percent, thumbRef } = useSliderContext();
|
|
1433
|
+
const methods = () => ({
|
|
1434
|
+
focus() {
|
|
1435
|
+
elementRef.current?.focus();
|
|
1436
|
+
},
|
|
1437
|
+
blur() {
|
|
1438
|
+
elementRef.current?.blur();
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
(0, react.useImperativeHandle)(ref, methods, []);
|
|
1442
|
+
(0, react.useImperativeHandle)(thumbRef, methods, []);
|
|
1735
1443
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1736
1444
|
className: "tremolo-slider-thumb-wrapper",
|
|
1737
1445
|
style: {
|
|
1738
|
-
top: vertical ? `${
|
|
1739
|
-
left: !vertical ? `${
|
|
1446
|
+
top: vertical ? `${percent}%` : "50%",
|
|
1447
|
+
left: !vertical ? `${percent}%` : "50%"
|
|
1740
1448
|
},
|
|
1741
1449
|
children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1742
|
-
ref:
|
|
1450
|
+
ref: elementRef,
|
|
1743
1451
|
className: (0, clsx.default)("tremolo-slider-thumb", className),
|
|
1744
1452
|
tabIndex: 0,
|
|
1745
1453
|
"aria-disabled": disabled,
|
|
1746
1454
|
"aria-readonly": readonly,
|
|
1747
1455
|
style: {
|
|
1748
1456
|
"--color": color,
|
|
1749
|
-
width: size ?? width,
|
|
1750
|
-
height: size ?? height,
|
|
1751
1457
|
...style
|
|
1752
1458
|
}
|
|
1753
1459
|
})
|
|
1754
1460
|
});
|
|
1755
|
-
}
|
|
1756
|
-
function Track({ length = 140, thickness = 10, active, inactive, children, className, style, defaultStyle = true,
|
|
1757
|
-
const vertical = useSliderContext(
|
|
1758
|
-
const
|
|
1759
|
-
const disabled = useSliderContext((s) => s.disabled);
|
|
1461
|
+
}
|
|
1462
|
+
function Track({ length = 140, thickness = 10, active, inactive, children, className, style, defaultStyle = true, ref, ...props }) {
|
|
1463
|
+
const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
|
|
1464
|
+
const composedRef = useComposedRefs(ref, trackRef);
|
|
1760
1465
|
const direction = vertical ? "bottom" : "right";
|
|
1761
1466
|
const colors = {
|
|
1762
1467
|
"--active": active,
|
|
1763
1468
|
"--inactive": inactive
|
|
1764
1469
|
};
|
|
1765
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1470
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1471
|
+
ref: composedRef,
|
|
1766
1472
|
className: (0, clsx.default)("tremolo-slider-track", className),
|
|
1767
1473
|
"aria-disabled": disabled,
|
|
1768
1474
|
"data-vertical": vertical,
|
|
1769
1475
|
style: !defaultStyle ? style : {
|
|
1770
1476
|
...colors,
|
|
1771
|
-
background: (0, _tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${
|
|
1477
|
+
background: (0, _tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${percent}%, var(--active) ${percent}%)` : `linear-gradient(to ${direction}, var(--active) ${percent}%, var(--inactive) ${percent}%)`,
|
|
1772
1478
|
borderRadius: (0, _tremolo_ui_functions.styleHelper)(thickness, "/", 2),
|
|
1773
1479
|
width: !vertical ? length : thickness,
|
|
1774
1480
|
height: vertical ? length : thickness,
|
|
1775
1481
|
...style
|
|
1776
1482
|
},
|
|
1777
1483
|
...props,
|
|
1778
|
-
children
|
|
1484
|
+
children
|
|
1779
1485
|
});
|
|
1780
1486
|
}
|
|
1781
1487
|
//#endregion
|
|
@@ -1785,55 +1491,37 @@ const defaultExternalStyles$1 = {
|
|
|
1785
1491
|
cursor: "pointer"
|
|
1786
1492
|
};
|
|
1787
1493
|
/**
|
|
1494
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1495
|
+
* the control does not change its value.
|
|
1496
|
+
*/
|
|
1497
|
+
const WHEEL_OPTIONS$1 = { requireFocus: true };
|
|
1498
|
+
/**
|
|
1788
1499
|
* Customizable slider
|
|
1789
1500
|
*/
|
|
1790
1501
|
const Slider = {
|
|
1791
|
-
Root: (0, react.forwardRef)(({ value, min, max, step = 1,
|
|
1792
|
-
const
|
|
1502
|
+
Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
|
|
1503
|
+
const trackRef = (0, react.useRef)(null);
|
|
1793
1504
|
const thumbRef = (0, react.useRef)(null);
|
|
1794
1505
|
const externalStyles = {
|
|
1795
1506
|
...defaultExternalStyles$1,
|
|
1796
1507
|
..._externalStyles
|
|
1797
1508
|
};
|
|
1798
|
-
const p = (0, _tremolo_ui_functions.toFixed)(
|
|
1509
|
+
const p = (0, _tremolo_ui_functions.toFixed)(scale.normalize(value, min, max) * 100);
|
|
1799
1510
|
const rev = (0, _tremolo_ui_functions.toFixed)(100 - p);
|
|
1800
1511
|
const displayReversed = (0, react.useMemo)(() => (0, _tremolo_ui_functions.xor)(vertical, reverse), [vertical, reverse]);
|
|
1801
1512
|
const percent = displayReversed ? rev : p;
|
|
1802
|
-
|
|
1803
|
-
let thumbProps = {};
|
|
1804
|
-
let scaleComponent = void 0;
|
|
1805
|
-
if (children != void 0) react.default.Children.forEach(children, (child) => {
|
|
1806
|
-
if (react.default.isValidElement(child)) if (child.type == Thumb$1) thumbProps = child.props;
|
|
1807
|
-
else if (child.type == Track) trackProps = child.props;
|
|
1808
|
-
else if (child.type == Scale) scaleComponent = child;
|
|
1809
|
-
else throw new Error("only <SliderThumb> or <SliderTrack>");
|
|
1810
|
-
else throw new Error("children is an invalid element.");
|
|
1811
|
-
});
|
|
1812
|
-
const onDrag = (0, react.useCallback)((nx, ny) => {
|
|
1813
|
-
if (!onChange || readonly) return;
|
|
1814
|
-
const n = vertical ? ny : nx;
|
|
1815
|
-
onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew), step), min, max));
|
|
1816
|
-
}, [
|
|
1817
|
-
displayReversed,
|
|
1818
|
-
max,
|
|
1513
|
+
const axis = (0, react.useMemo)(() => ({
|
|
1819
1514
|
min,
|
|
1820
|
-
onChange,
|
|
1821
|
-
readonly,
|
|
1822
|
-
skew,
|
|
1823
|
-
step,
|
|
1824
|
-
vertical
|
|
1825
|
-
]);
|
|
1826
|
-
const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
|
|
1827
|
-
let newValue;
|
|
1828
|
-
if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
|
|
1829
|
-
else newValue = value + x;
|
|
1830
|
-
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
|
|
1831
|
-
}, [
|
|
1832
1515
|
max,
|
|
1516
|
+
step,
|
|
1517
|
+
scale,
|
|
1518
|
+
reverse: displayReversed
|
|
1519
|
+
}), [
|
|
1833
1520
|
min,
|
|
1834
|
-
|
|
1521
|
+
max,
|
|
1835
1522
|
step,
|
|
1836
|
-
|
|
1523
|
+
scale,
|
|
1524
|
+
displayReversed
|
|
1837
1525
|
]);
|
|
1838
1526
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
1839
1527
|
if (!keyboard || !onChange || readonly) return;
|
|
@@ -1845,51 +1533,76 @@ const Slider = {
|
|
|
1845
1533
|
"ArrowDown"
|
|
1846
1534
|
].includes(key)) {
|
|
1847
1535
|
event.preventDefault();
|
|
1848
|
-
let
|
|
1849
|
-
if (reverse)
|
|
1850
|
-
onChange(
|
|
1536
|
+
let direction = key == "ArrowRight" || key == "ArrowUp" ? 1 : -1;
|
|
1537
|
+
if (reverse) direction *= -1;
|
|
1538
|
+
onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, keyboard, axis));
|
|
1851
1539
|
}
|
|
1852
1540
|
}, [
|
|
1853
1541
|
keyboard,
|
|
1854
1542
|
onChange,
|
|
1855
1543
|
readonly,
|
|
1856
1544
|
reverse,
|
|
1857
|
-
|
|
1545
|
+
value,
|
|
1546
|
+
axis
|
|
1858
1547
|
]);
|
|
1859
|
-
const
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1548
|
+
const valueOf = (v) => v[vertical ? 1 : 0];
|
|
1549
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1550
|
+
axis,
|
|
1551
|
+
baseElementRef: trackRef,
|
|
1552
|
+
cursor: readonly ? void 0 : externalStyles.cursor,
|
|
1553
|
+
updateOnPointerDown: true,
|
|
1554
|
+
onChange: (v) => {
|
|
1555
|
+
if (readonly) return;
|
|
1556
|
+
onChange?.(valueOf(v));
|
|
1557
|
+
},
|
|
1558
|
+
onDragStart: (v) => {
|
|
1863
1559
|
if (readonly) return;
|
|
1864
1560
|
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
1865
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
|
|
1866
1561
|
thumbRef.current?.focus();
|
|
1867
|
-
onDragStart?.((
|
|
1562
|
+
onDragStart?.(valueOf(v));
|
|
1868
1563
|
},
|
|
1869
|
-
onDragEnd: (
|
|
1564
|
+
onDragEnd: (v) => {
|
|
1870
1565
|
if (readonly) return;
|
|
1871
1566
|
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
1872
|
-
|
|
1873
|
-
onDragEnd?.((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(vertical ? ny : nx, min, max, skew), step), min, max));
|
|
1567
|
+
onDragEnd?.(valueOf(v));
|
|
1874
1568
|
}
|
|
1875
1569
|
});
|
|
1876
|
-
const
|
|
1570
|
+
const rootRefCallback = useComposedRefs(dragRefCallback, useWheel((event) => {
|
|
1877
1571
|
if (!wheel || !onChange || readonly) return;
|
|
1878
1572
|
event.preventDefault();
|
|
1879
|
-
let
|
|
1880
|
-
if (!vertical && event.deltaX != 0)
|
|
1573
|
+
let direction;
|
|
1574
|
+
if (!vertical && event.deltaX != 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
1881
1575
|
else {
|
|
1882
1576
|
if (event.deltaY == 0) return;
|
|
1883
|
-
|
|
1577
|
+
direction = event.deltaY > 0 ? -1 : 1;
|
|
1884
1578
|
}
|
|
1885
|
-
if (vertical && reverse)
|
|
1886
|
-
onChange(
|
|
1887
|
-
},
|
|
1888
|
-
|
|
1579
|
+
if (vertical && reverse) direction *= -1;
|
|
1580
|
+
onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, wheel, axis));
|
|
1581
|
+
}, WHEEL_OPTIONS$1));
|
|
1582
|
+
const context = (0, react.useMemo)(() => ({
|
|
1583
|
+
value,
|
|
1584
|
+
min,
|
|
1585
|
+
max,
|
|
1586
|
+
step,
|
|
1587
|
+
scale,
|
|
1889
1588
|
vertical,
|
|
1589
|
+
reverse,
|
|
1590
|
+
disabled,
|
|
1890
1591
|
readonly,
|
|
1891
|
-
|
|
1892
|
-
|
|
1592
|
+
percent,
|
|
1593
|
+
trackRef,
|
|
1594
|
+
thumbRef
|
|
1595
|
+
}), [
|
|
1596
|
+
value,
|
|
1597
|
+
min,
|
|
1598
|
+
max,
|
|
1599
|
+
step,
|
|
1600
|
+
scale,
|
|
1601
|
+
vertical,
|
|
1602
|
+
reverse,
|
|
1603
|
+
disabled,
|
|
1604
|
+
readonly,
|
|
1605
|
+
percent
|
|
1893
1606
|
]);
|
|
1894
1607
|
(0, react.useImperativeHandle)(forwardedRef, () => {
|
|
1895
1608
|
return {
|
|
@@ -1902,20 +1615,10 @@ const Slider = {
|
|
|
1902
1615
|
};
|
|
1903
1616
|
}, []);
|
|
1904
1617
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderProvider, {
|
|
1905
|
-
|
|
1906
|
-
max,
|
|
1907
|
-
step,
|
|
1908
|
-
skew,
|
|
1909
|
-
vertical,
|
|
1910
|
-
reverse,
|
|
1911
|
-
disabled,
|
|
1912
|
-
readonly,
|
|
1618
|
+
value: context,
|
|
1913
1619
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1914
1620
|
className: (0, clsx.default)("tremolo-slider", className),
|
|
1915
|
-
ref:
|
|
1916
|
-
wheelRefCallback(div);
|
|
1917
|
-
touchMoveRefCallback(div);
|
|
1918
|
-
},
|
|
1621
|
+
ref: rootRefCallback,
|
|
1919
1622
|
tabIndex: -1,
|
|
1920
1623
|
role: "slider",
|
|
1921
1624
|
"aria-valuenow": value,
|
|
@@ -1924,14 +1627,9 @@ const Slider = {
|
|
|
1924
1627
|
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
1925
1628
|
"aria-disabled": disabled,
|
|
1926
1629
|
"aria-readonly": readonly,
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
},
|
|
1931
|
-
onPointerDown: (event) => {
|
|
1932
|
-
pointerDownHandler(event);
|
|
1933
|
-
onPointerDown?.(event);
|
|
1934
|
-
},
|
|
1630
|
+
"data-vertical": vertical,
|
|
1631
|
+
style,
|
|
1632
|
+
onPointerDown,
|
|
1935
1633
|
onKeyDown: (event) => {
|
|
1936
1634
|
handleKeyDown(event);
|
|
1937
1635
|
onKeyDown?.(event);
|
|
@@ -1945,93 +1643,30 @@ const Slider = {
|
|
|
1945
1643
|
onBlur?.(event);
|
|
1946
1644
|
},
|
|
1947
1645
|
...props,
|
|
1948
|
-
children
|
|
1949
|
-
style: {
|
|
1950
|
-
display: "flex",
|
|
1951
|
-
flexDirection: vertical ? "row" : "column",
|
|
1952
|
-
width: !vertical ? "100%" : void 0,
|
|
1953
|
-
height: vertical ? "100%" : void 0
|
|
1954
|
-
},
|
|
1955
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1956
|
-
className: "tremolo-slider-track-wrapper",
|
|
1957
|
-
ref: trackElementRef,
|
|
1958
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Track, {
|
|
1959
|
-
__percent: percent,
|
|
1960
|
-
__thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$1, {
|
|
1961
|
-
ref: thumbRef,
|
|
1962
|
-
__percent: percent,
|
|
1963
|
-
...thumbProps
|
|
1964
|
-
}),
|
|
1965
|
-
...trackProps
|
|
1966
|
-
})
|
|
1967
|
-
}), scaleComponent]
|
|
1968
|
-
})
|
|
1646
|
+
children
|
|
1969
1647
|
})
|
|
1970
1648
|
});
|
|
1971
1649
|
}),
|
|
1972
1650
|
Thumb: Thumb$1,
|
|
1973
1651
|
Track,
|
|
1974
|
-
|
|
1975
|
-
|
|
1652
|
+
Marks,
|
|
1653
|
+
MarksOption
|
|
1976
1654
|
};
|
|
1977
1655
|
//#endregion
|
|
1978
|
-
//#region src/components/
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
else if (ref !== null && ref !== void 0) ref.current = value;
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* A utility to compose multiple refs together
|
|
1989
|
-
* Accepts callback refs and RefObject(s)
|
|
1990
|
-
*/
|
|
1991
|
-
function composeRefs(...refs) {
|
|
1992
|
-
return (node) => {
|
|
1993
|
-
let hasCleanup = false;
|
|
1994
|
-
const cleanups = refs.map((ref) => {
|
|
1995
|
-
const cleanup = setRef(ref, node);
|
|
1996
|
-
if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
|
|
1997
|
-
return cleanup;
|
|
1998
|
-
});
|
|
1999
|
-
if (hasCleanup) return () => {
|
|
2000
|
-
for (let i = 0; i < cleanups.length; i++) {
|
|
2001
|
-
const cleanup = cleanups[i];
|
|
2002
|
-
if (typeof cleanup == "function") cleanup();
|
|
2003
|
-
else setRef(refs[i], null);
|
|
2004
|
-
}
|
|
2005
|
-
};
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
//#endregion
|
|
2009
|
-
//#region src/components/WheelObserver/index.tsx
|
|
2010
|
-
/**
|
|
2011
|
-
* @example
|
|
2012
|
-
* <WheelObserver
|
|
2013
|
-
* onWheel={(event) => {
|
|
2014
|
-
* event.preventDefault()
|
|
2015
|
-
* console.log(event.deltaY)
|
|
2016
|
-
* }
|
|
2017
|
-
* >
|
|
2018
|
-
* <div>Wheel here</div>
|
|
2019
|
-
* </WheelObserver>
|
|
2020
|
-
*/
|
|
2021
|
-
function WheelObserver(props) {
|
|
2022
|
-
const { as: Component = "div", children, onWheel, ref, ...attributes } = props;
|
|
2023
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
|
|
2024
|
-
ref: composeRefs(ref, useRefCallbackEvent("wheel", (event) => {
|
|
2025
|
-
if (onWheel) onWheel(event);
|
|
2026
|
-
}, { passive: false }, [onWheel])),
|
|
2027
|
-
...attributes,
|
|
2028
|
-
children
|
|
2029
|
-
});
|
|
1656
|
+
//#region src/components/XYPad/context.tsx
|
|
1657
|
+
const XYPadContext = (0, react.createContext)(null);
|
|
1658
|
+
const XYPadProvider = XYPadContext.Provider;
|
|
1659
|
+
function useXYPadContext(selector) {
|
|
1660
|
+
const context = (0, react.useContext)(XYPadContext);
|
|
1661
|
+
if (!context) throw new Error("Missing XYPadContext.Provider in the tree");
|
|
1662
|
+
return selector ? selector(context) : context;
|
|
2030
1663
|
}
|
|
2031
1664
|
//#endregion
|
|
2032
1665
|
//#region src/components/XYPad/Area.tsx
|
|
2033
|
-
function Area({ width = 120, height = 120, color, children, className, style,
|
|
2034
|
-
|
|
1666
|
+
function Area({ width = 120, height = 120, color, children, className, style, ref, ...props }) {
|
|
1667
|
+
const { areaRef } = useXYPadContext();
|
|
1668
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1669
|
+
ref: useComposedRefs(ref, areaRef),
|
|
2035
1670
|
className: (0, clsx.default)("tremolo-xy-pad-area", className),
|
|
2036
1671
|
style: {
|
|
2037
1672
|
"--color": color,
|
|
@@ -2040,121 +1675,105 @@ function Area({ width = 120, height = 120, color, children, className, style, __
|
|
|
2040
1675
|
...style
|
|
2041
1676
|
},
|
|
2042
1677
|
...props,
|
|
2043
|
-
children
|
|
1678
|
+
children
|
|
2044
1679
|
});
|
|
2045
1680
|
}
|
|
2046
1681
|
//#endregion
|
|
2047
1682
|
//#region src/components/XYPad/Thumb.tsx
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
1683
|
+
function Thumb({ color, children, wrapperClassName, wrapperStyle, className, style, ref, ...props }) {
|
|
1684
|
+
const elementRef = (0, react.useRef)(null);
|
|
1685
|
+
const { disabled, readonly, percent, thumbRef } = useXYPadContext();
|
|
1686
|
+
const methods = () => ({
|
|
1687
|
+
focus() {
|
|
1688
|
+
elementRef.current?.focus();
|
|
1689
|
+
},
|
|
1690
|
+
blur() {
|
|
1691
|
+
elementRef.current?.blur();
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
(0, react.useImperativeHandle)(ref, methods, []);
|
|
1695
|
+
(0, react.useImperativeHandle)(thumbRef, methods, []);
|
|
2060
1696
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2061
1697
|
className: (0, clsx.default)("tremolo-xy-pad-thumb-wrapper", wrapperClassName),
|
|
2062
1698
|
style: {
|
|
2063
|
-
|
|
1699
|
+
left: `${percent[0]}%`,
|
|
1700
|
+
top: `${percent[1]}%`,
|
|
2064
1701
|
...wrapperStyle
|
|
2065
1702
|
},
|
|
2066
1703
|
...props,
|
|
2067
1704
|
children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2068
|
-
ref:
|
|
1705
|
+
ref: elementRef,
|
|
2069
1706
|
className: (0, clsx.default)("tremolo-xy-pad-thumb", className),
|
|
2070
1707
|
tabIndex: 0,
|
|
2071
|
-
"aria-disabled":
|
|
2072
|
-
"aria-readonly":
|
|
1708
|
+
"aria-disabled": disabled,
|
|
1709
|
+
"aria-readonly": readonly,
|
|
2073
1710
|
style: {
|
|
2074
1711
|
"--color": color,
|
|
2075
|
-
width: size ?? width,
|
|
2076
|
-
height: size ?? height,
|
|
2077
1712
|
...style
|
|
2078
1713
|
}
|
|
2079
1714
|
})
|
|
2080
1715
|
});
|
|
2081
|
-
}
|
|
1716
|
+
}
|
|
2082
1717
|
//#endregion
|
|
2083
1718
|
//#region src/components/XYPad/index.tsx
|
|
2084
|
-
const defaultValueOptions = {
|
|
2085
|
-
skew: 1,
|
|
2086
|
-
step: 1,
|
|
2087
|
-
reverse: false,
|
|
2088
|
-
wheel: ["raw", 1],
|
|
2089
|
-
keyboard: ["raw", 1]
|
|
2090
|
-
};
|
|
2091
1719
|
const defaultExternalStyles = {
|
|
2092
1720
|
userSelectNone: true,
|
|
2093
1721
|
cursor: "pointer"
|
|
2094
1722
|
};
|
|
2095
1723
|
/**
|
|
1724
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1725
|
+
* the control does not change its value.
|
|
1726
|
+
*/
|
|
1727
|
+
const WHEEL_OPTIONS = { requireFocus: true };
|
|
1728
|
+
/**
|
|
2096
1729
|
* Simple XYPad
|
|
2097
1730
|
*/
|
|
2098
1731
|
const XYPad = {
|
|
2099
|
-
Root: (0, react.forwardRef)(({
|
|
2100
|
-
const x = (0, react.useMemo)(() => {
|
|
2101
|
-
return {
|
|
2102
|
-
...defaultValueOptions,
|
|
2103
|
-
..._x
|
|
2104
|
-
};
|
|
2105
|
-
}, [_x]);
|
|
2106
|
-
const y = (0, react.useMemo)(() => {
|
|
2107
|
-
return {
|
|
2108
|
-
...defaultValueOptions,
|
|
2109
|
-
..._y
|
|
2110
|
-
};
|
|
2111
|
-
}, [_y]);
|
|
1732
|
+
Root: (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = ["raw", 1], keyboard = ["raw", 1], className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
|
|
2112
1733
|
const rootRef = (0, react.useRef)(null);
|
|
2113
|
-
const
|
|
1734
|
+
const areaRef = (0, react.useRef)(null);
|
|
2114
1735
|
const thumbRef = (0, react.useRef)(null);
|
|
2115
1736
|
const externalStyles = {
|
|
2116
1737
|
...defaultExternalStyles,
|
|
2117
1738
|
..._externalStyles
|
|
2118
1739
|
};
|
|
2119
|
-
const
|
|
2120
|
-
const
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
else if (child.type == Area) areaProps = child.props;
|
|
2128
|
-
else throw new Error("only <XYPadThumb> or <XYPadArea>");
|
|
2129
|
-
else throw new Error("children is an invalid element.");
|
|
2130
|
-
});
|
|
2131
|
-
const onDrag = (0, react.useCallback)((nx, ny) => {
|
|
2132
|
-
if (!onChange || readonly) return;
|
|
2133
|
-
const vx = (0, _tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
|
|
2134
|
-
const vy = (0, _tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
|
|
2135
|
-
onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max), (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max));
|
|
1740
|
+
const min = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_min), [_min]);
|
|
1741
|
+
const max = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_max), [_max]);
|
|
1742
|
+
const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_step), [_step]);
|
|
1743
|
+
const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_scale), [_scale]);
|
|
1744
|
+
const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_reverse), [_reverse]);
|
|
1745
|
+
const percent = (0, react.useMemo)(() => {
|
|
1746
|
+
const normalized = [0, 1].map((i) => scale[i].normalize(value[i], min[i], max[i]));
|
|
1747
|
+
return [0, 1].map((i) => (0, _tremolo_ui_functions.toFixed)((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100));
|
|
2136
1748
|
}, [
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
1749
|
+
value,
|
|
1750
|
+
min,
|
|
1751
|
+
max,
|
|
1752
|
+
scale,
|
|
1753
|
+
reverse
|
|
1754
|
+
]);
|
|
1755
|
+
const axis = (0, react.useMemo)(() => [0, 1].map((i) => ({
|
|
1756
|
+
min: min[i],
|
|
1757
|
+
max: max[i],
|
|
1758
|
+
step: step[i],
|
|
1759
|
+
scale: scale[i],
|
|
1760
|
+
reverse: reverse[i]
|
|
1761
|
+
})), [
|
|
1762
|
+
min,
|
|
1763
|
+
max,
|
|
1764
|
+
step,
|
|
1765
|
+
scale,
|
|
1766
|
+
reverse
|
|
1767
|
+
]);
|
|
1768
|
+
const withAxis = (0, react.useCallback)((axis, next) => axis == 0 ? [next, value[1]] : [value[0], next], [value]);
|
|
1769
|
+
/** @param i 0 = x, 1 = y */
|
|
1770
|
+
const nudge = (0, react.useCallback)((i, direction, option) => withAxis(i, (0, _tremolo_ui_functions.applyDelta)(value[i], direction, option, axis[i])), [
|
|
1771
|
+
value,
|
|
1772
|
+
axis,
|
|
1773
|
+
withAxis
|
|
2149
1774
|
]);
|
|
2150
|
-
const updateValueByEvent = (0, react.useCallback)((eventType, target, x) => {
|
|
2151
|
-
let v;
|
|
2152
|
-
if (eventType == "normalized") v = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew) + x, target.min, target.max, target.skew);
|
|
2153
|
-
else v = target.value + x;
|
|
2154
|
-
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
|
|
2155
|
-
}, []);
|
|
2156
1775
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
2157
|
-
if (!onChange || readonly) return;
|
|
1776
|
+
if (!onChange || readonly || !keyboard) return;
|
|
2158
1777
|
const key = event.key;
|
|
2159
1778
|
if ([
|
|
2160
1779
|
"ArrowRight",
|
|
@@ -2162,62 +1781,72 @@ const XYPad = {
|
|
|
2162
1781
|
"ArrowUp",
|
|
2163
1782
|
"ArrowDown"
|
|
2164
1783
|
].includes(key)) {
|
|
2165
|
-
const
|
|
2166
|
-
const target = isHorizontal ? x : y;
|
|
2167
|
-
if (!target.keyboard) return;
|
|
1784
|
+
const i = key == "ArrowRight" || key == "ArrowLeft" ? 0 : 1;
|
|
2168
1785
|
event.preventDefault();
|
|
2169
|
-
let
|
|
2170
|
-
if (key == "ArrowLeft" || key == "ArrowUp")
|
|
2171
|
-
if (
|
|
2172
|
-
|
|
2173
|
-
if (isHorizontal) onChange(newValue, y.value);
|
|
2174
|
-
else onChange(x.value, newValue);
|
|
1786
|
+
let direction = 1;
|
|
1787
|
+
if (key == "ArrowLeft" || key == "ArrowUp") direction *= -1;
|
|
1788
|
+
if (reverse[i]) direction *= -1;
|
|
1789
|
+
onChange(nudge(i, direction, keyboard));
|
|
2175
1790
|
}
|
|
2176
1791
|
}, [
|
|
2177
1792
|
onChange,
|
|
2178
1793
|
readonly,
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
1794
|
+
keyboard,
|
|
1795
|
+
reverse,
|
|
1796
|
+
nudge
|
|
2182
1797
|
]);
|
|
2183
|
-
const {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
1798
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1799
|
+
axis,
|
|
1800
|
+
baseElementRef: areaRef,
|
|
1801
|
+
updateOnPointerDown: true,
|
|
1802
|
+
cursor: readonly ? void 0 : externalStyles.cursor,
|
|
1803
|
+
onChange: (v) => {
|
|
1804
|
+
if (readonly) return;
|
|
1805
|
+
onChange?.(v);
|
|
1806
|
+
},
|
|
1807
|
+
onDragStart: (v) => {
|
|
2187
1808
|
if (readonly) return;
|
|
2188
1809
|
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
2189
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
|
|
2190
1810
|
thumbRef.current?.focus();
|
|
2191
|
-
|
|
2192
|
-
const valueY = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
|
|
2193
|
-
onDragStart?.(valueX, valueY);
|
|
1811
|
+
onDragStart?.(v);
|
|
2194
1812
|
},
|
|
2195
|
-
onDragEnd: (
|
|
1813
|
+
onDragEnd: (v) => {
|
|
2196
1814
|
if (readonly) return;
|
|
2197
1815
|
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
2198
|
-
|
|
2199
|
-
const valueX = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
|
|
2200
|
-
const valueY = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
|
|
2201
|
-
onDragEnd?.(valueX, valueY);
|
|
1816
|
+
onDragEnd?.(v);
|
|
2202
1817
|
}
|
|
2203
1818
|
});
|
|
2204
|
-
const
|
|
2205
|
-
if (!onChange || readonly) return;
|
|
2206
|
-
const
|
|
2207
|
-
if (!target.wheel) return;
|
|
1819
|
+
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, useWheel((event) => {
|
|
1820
|
+
if (!onChange || readonly || !wheel) return;
|
|
1821
|
+
const i = event.shiftKey ? 0 : 1;
|
|
2208
1822
|
event.preventDefault();
|
|
2209
|
-
let
|
|
2210
|
-
if (event.deltaY < 0)
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
1823
|
+
let direction = 1;
|
|
1824
|
+
if (event.deltaY < 0) direction *= -1;
|
|
1825
|
+
if (reverse[i]) direction *= -1;
|
|
1826
|
+
onChange(nudge(i, direction, wheel));
|
|
1827
|
+
}, WHEEL_OPTIONS));
|
|
1828
|
+
const context = (0, react.useMemo)(() => ({
|
|
1829
|
+
value,
|
|
1830
|
+
min,
|
|
1831
|
+
max,
|
|
1832
|
+
step,
|
|
1833
|
+
scale,
|
|
1834
|
+
reverse,
|
|
1835
|
+
disabled,
|
|
1836
|
+
readonly,
|
|
1837
|
+
percent,
|
|
1838
|
+
areaRef,
|
|
1839
|
+
thumbRef
|
|
1840
|
+
}), [
|
|
1841
|
+
value,
|
|
1842
|
+
min,
|
|
1843
|
+
max,
|
|
1844
|
+
step,
|
|
1845
|
+
scale,
|
|
1846
|
+
reverse,
|
|
1847
|
+
disabled,
|
|
2219
1848
|
readonly,
|
|
2220
|
-
|
|
1849
|
+
percent
|
|
2221
1850
|
]);
|
|
2222
1851
|
(0, react.useImperativeHandle)(forwardedRef, () => {
|
|
2223
1852
|
return {
|
|
@@ -2230,51 +1859,30 @@ const XYPad = {
|
|
|
2230
1859
|
original: rootRef
|
|
2231
1860
|
};
|
|
2232
1861
|
}, []);
|
|
2233
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2234
|
-
|
|
2235
|
-
ref: composeRefs(rootRef, wheelRefCallback, touchMoveRefCallback),
|
|
2236
|
-
tabIndex: -1,
|
|
2237
|
-
"aria-disabled": disabled,
|
|
2238
|
-
"aria-readonly": readonly,
|
|
2239
|
-
style: {
|
|
2240
|
-
margin: `calc(${(0, _tremolo_ui_functions.styleHelper)(thumbProps?.size ?? 22)} / 2)`,
|
|
2241
|
-
cursor: readonly ? "not-allowed" : "pointer",
|
|
2242
|
-
WebkitTapHighlightColor: "transparent",
|
|
2243
|
-
...style
|
|
2244
|
-
},
|
|
2245
|
-
onPointerDown: (event) => {
|
|
2246
|
-
pointerDownHandler(event);
|
|
2247
|
-
onPointerDown?.(event);
|
|
2248
|
-
},
|
|
2249
|
-
onKeyDown: (event) => {
|
|
2250
|
-
handleKeyDown(event);
|
|
2251
|
-
onKeyDown?.(event);
|
|
2252
|
-
},
|
|
2253
|
-
onFocus: (event) => {
|
|
2254
|
-
thumbRef.current?.focus();
|
|
2255
|
-
onFocus?.(event);
|
|
2256
|
-
},
|
|
2257
|
-
onBlur: (event) => {
|
|
2258
|
-
thumbRef.current?.blur();
|
|
2259
|
-
onBlur?.(event);
|
|
2260
|
-
},
|
|
2261
|
-
...props,
|
|
1862
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadProvider, {
|
|
1863
|
+
value: context,
|
|
2262
1864
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2263
|
-
className: "tremolo-xy-pad
|
|
2264
|
-
ref:
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
}
|
|
1865
|
+
className: (0, clsx.default)("tremolo-xy-pad", className),
|
|
1866
|
+
ref: rootRefCallback,
|
|
1867
|
+
tabIndex: -1,
|
|
1868
|
+
"aria-disabled": disabled,
|
|
1869
|
+
"aria-readonly": readonly,
|
|
1870
|
+
style,
|
|
1871
|
+
onPointerDown,
|
|
1872
|
+
onKeyDown: (event) => {
|
|
1873
|
+
handleKeyDown(event);
|
|
1874
|
+
onKeyDown?.(event);
|
|
1875
|
+
},
|
|
1876
|
+
onFocus: (event) => {
|
|
1877
|
+
thumbRef.current?.focus();
|
|
1878
|
+
onFocus?.(event);
|
|
1879
|
+
},
|
|
1880
|
+
onBlur: (event) => {
|
|
1881
|
+
thumbRef.current?.blur();
|
|
1882
|
+
onBlur?.(event);
|
|
1883
|
+
},
|
|
1884
|
+
...props,
|
|
1885
|
+
children
|
|
2278
1886
|
})
|
|
2279
1887
|
});
|
|
2280
1888
|
}),
|
|
@@ -2347,7 +1955,6 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
|
2347
1955
|
}
|
|
2348
1956
|
//#endregion
|
|
2349
1957
|
exports.AnimationCanvas = AnimationCanvas;
|
|
2350
|
-
exports.DragObserver = DragObserver;
|
|
2351
1958
|
exports.Knob = Knob;
|
|
2352
1959
|
Object.defineProperty(exports, "NOT_SUPPORTED", {
|
|
2353
1960
|
enumerable: true,
|
|
@@ -2366,19 +1973,20 @@ exports.Piano = Piano;
|
|
|
2366
1973
|
exports.PointsEditor = PointsEditor;
|
|
2367
1974
|
exports.SHORTCUTS = SHORTCUTS;
|
|
2368
1975
|
exports.Slider = Slider;
|
|
2369
|
-
exports.WheelObserver = WheelObserver;
|
|
2370
1976
|
exports.XYPad = XYPad;
|
|
2371
1977
|
exports.clampPoint = clampPoint;
|
|
2372
|
-
exports.getNoteRangeArray = getNoteRangeArray;
|
|
2373
1978
|
exports.useAnimationFrame = useAnimationFrame;
|
|
2374
1979
|
exports.useDrag = useDrag;
|
|
2375
|
-
exports.
|
|
1980
|
+
exports.useDragValue = useDragValue;
|
|
2376
1981
|
exports.useEventListener = useEventListener;
|
|
2377
1982
|
exports.useInterval = useInterval;
|
|
2378
1983
|
exports.useLongPress = useLongPress;
|
|
2379
1984
|
exports.useMIDIAccess = useMIDIAccess;
|
|
2380
1985
|
exports.useMIDIInput = useMIDIInput;
|
|
2381
1986
|
exports.useMIDIMessage = useMIDIMessage;
|
|
1987
|
+
exports.useNumberInputContext = useNumberInputContext;
|
|
1988
|
+
exports.usePointsEditorContext = usePointsEditorContext;
|
|
2382
1989
|
exports.useSliderContext = useSliderContext;
|
|
1990
|
+
exports.useWheel = useWheel;
|
|
2383
1991
|
|
|
2384
1992
|
//# sourceMappingURL=index.cjs.map
|