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