@tremolo-ui/react 0.1.4 → 0.1.6

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.
Files changed (43) hide show
  1. package/dist/index.cjs +285 -155
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +80 -32
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +95 -60
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +97 -62
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +262 -139
  10. package/dist/index.js.map +1 -1
  11. package/package.json +14 -9
  12. package/src/components/AnimationCanvas/canvas.ts +16 -0
  13. package/src/components/AnimationCanvas/index.tsx +54 -71
  14. package/src/components/Knob/index.css +54 -23
  15. package/src/components/Knob/index.tsx +30 -25
  16. package/src/components/NumberInput/InternalInput.tsx +1 -1
  17. package/src/components/NumberInput/context.tsx +2 -1
  18. package/src/components/NumberInput/index.css +1 -0
  19. package/src/components/NumberInput/index.tsx +5 -5
  20. package/src/components/NumberInput/type.ts +58 -0
  21. package/src/components/Piano/context.tsx +1 -1
  22. package/src/components/Piano/index.css +24 -8
  23. package/src/components/Piano/index.tsx +254 -219
  24. package/src/components/Piano/key.tsx +3 -3
  25. package/src/components/Slider/Scale.tsx +1 -1
  26. package/src/components/Slider/ScaleOption.tsx +1 -1
  27. package/src/components/Slider/Track.tsx +2 -2
  28. package/src/components/Slider/index.tsx +3 -2
  29. package/src/components/Slider/type.ts +26 -0
  30. package/src/components/XYPad/index.tsx +2 -2
  31. package/src/hooks/useAnimationFrame.ts +3 -0
  32. package/src/hooks/useCallbackRef.ts +2 -2
  33. package/src/hooks/useDrag.ts +2 -1
  34. package/src/hooks/useDragWithElement.ts +2 -1
  35. package/src/hooks/useEventListener.ts +3 -0
  36. package/src/hooks/useInterval.ts +3 -0
  37. package/src/hooks/useLongPress.ts +3 -0
  38. package/src/hooks/useMIDIAccess.ts +40 -0
  39. package/src/hooks/useMIDIInput.ts +50 -0
  40. package/src/hooks/useMIDIMessage.ts +24 -0
  41. package/src/hooks/useRefCallbackEvent.ts +4 -0
  42. package/src/index.ts +21 -13
  43. package/src/styles/{index.css → global.css} +1 -1
package/dist/index.cjs CHANGED
@@ -21,13 +21,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
 
23
23
  //#endregion
24
- const clsx = __toESM(require("clsx"));
25
- const react = __toESM(require("react"));
26
- const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
27
- const __tremolo_ui_functions = __toESM(require("@tremolo-ui/functions"));
28
- const zustand = __toESM(require("zustand"));
29
- const __tremolo_ui_functions_NumberInput = __toESM(require("@tremolo-ui/functions/NumberInput"));
30
- const __tremolo_ui_functions_Slider = __toESM(require("@tremolo-ui/functions/Slider"));
24
+ let clsx = require("clsx");
25
+ clsx = __toESM(clsx);
26
+ let react = require("react");
27
+ react = __toESM(react);
28
+ let react_jsx_runtime = require("react/jsx-runtime");
29
+ let __tremolo_ui_functions = require("@tremolo-ui/functions");
30
+ let zustand = require("zustand");
31
31
 
32
32
  //#region src/components/AnimationCanvas/canvas.ts
33
33
  const drawingState = [
@@ -50,9 +50,6 @@ const drawingState = [
50
50
  "direction",
51
51
  "imageSmoothingEnabled"
52
52
  ];
53
-
54
- //#endregion
55
- //#region src/components/AnimationCanvas/index.tsx
56
53
  function setDprConfig(canvas, context, width, height, dpr) {
57
54
  canvas.width = width * dpr;
58
55
  canvas.height = height * dpr;
@@ -61,7 +58,10 @@ function setDprConfig(canvas, context, width, height, dpr) {
61
58
  canvas.style.width = `${width}px`;
62
59
  canvas.style.height = `${height}px`;
63
60
  }
64
- function AnimationCanvas({ options, init, draw, width = 100, height = 100, relativeSize, reduceFlickering = true, className, onContextMenu = (event) => event.preventDefault(),...props }) {
61
+
62
+ //#endregion
63
+ //#region src/components/AnimationCanvas/index.tsx
64
+ function AnimationCanvas({ draw, init, animate = true, options, width: _width = 100, height: _height = 100, relativeSize, reduceFlickering = true, className, onContextMenu = (event) => event.preventDefault(),...props }) {
65
65
  const canvasRef = (0, react.useRef)(null);
66
66
  const memoCanvasRef = (0, react.useRef)(null);
67
67
  const reqIdRef = (0, react.useRef)(-1);
@@ -69,27 +69,40 @@ function AnimationCanvas({ options, init, draw, width = 100, height = 100, relat
69
69
  const heightRef = (0, react.useRef)(0);
70
70
  const deltaMemoRef = (0, react.useRef)(-1);
71
71
  const startTimeRef = (0, react.useRef)(-1);
72
- const loop = (0, react.useCallback)((context, width$1, height$1, count) => {
72
+ const loop = (0, react.useCallback)((context, width, height, count) => {
73
73
  const now = performance.now();
74
74
  const deltaTime = now - deltaMemoRef.current;
75
75
  const elapsedTime = now - startTimeRef.current;
76
76
  deltaMemoRef.current = now;
77
- reqIdRef.current = requestAnimationFrame(() => loop(context, width$1, height$1, count + 1));
77
+ if (animate) reqIdRef.current = requestAnimationFrame(() => loop(context, width, height, count + 1));
78
78
  draw(context, {
79
- width: width$1.current,
80
- height: height$1.current,
79
+ width: width.current,
80
+ height: height.current,
81
81
  count: count + 1,
82
82
  deltaTime,
83
83
  elapsedTime,
84
84
  fps: 1e3 / deltaTime
85
85
  });
86
- }, [draw]);
86
+ }, [draw, animate]);
87
87
  (0, react.useEffect)(() => {
88
88
  if (!canvasRef.current) return;
89
89
  const canvas = canvasRef.current;
90
90
  const context = canvas.getContext("2d", options);
91
91
  if (!context) throw new Error("Cannot get canvas context.");
92
92
  const dpr = globalThis.devicePixelRatio;
93
+ const firstRendering = (width, height) => {
94
+ setDprConfig(canvas, context, width, height, dpr);
95
+ widthRef.current = width;
96
+ heightRef.current = height;
97
+ if (init) init(context, {
98
+ width,
99
+ height
100
+ });
101
+ const now = performance.now();
102
+ deltaMemoRef.current = now;
103
+ startTimeRef.current = now;
104
+ loop(context, widthRef, heightRef, -1);
105
+ };
93
106
  if (relativeSize) {
94
107
  const parent = canvas.parentElement;
95
108
  if (!parent) throw new Error("Canvas doesn't have a parent element.");
@@ -97,52 +110,37 @@ function AnimationCanvas({ options, init, draw, width = 100, height = 100, relat
97
110
  const memoContext = memoCanvas?.getContext("2d", options);
98
111
  const ro = new ResizeObserver((entries) => {
99
112
  for (const entry of entries) {
113
+ const w = entry.contentRect.width;
114
+ const h = entry.contentRect.height;
100
115
  const contextMemo = {};
101
- for (const prop of drawingState) contextMemo[prop] = context[prop];
102
- const w$1 = entry.contentRect.width;
103
- const h$1 = entry.contentRect.height;
104
116
  if (reduceFlickering && memoCanvas && memoContext) {
105
- memoCanvas.width = w$1 * dpr;
106
- memoCanvas.height = h$1 * dpr;
117
+ for (const prop of drawingState) contextMemo[prop] = context[prop];
118
+ memoCanvas.width = w * dpr;
119
+ memoCanvas.height = h * dpr;
107
120
  memoContext.scale(1 / dpr, 1 / dpr);
108
121
  if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
109
122
  }
110
- setDprConfig(canvas, context, w$1, h$1, dpr);
111
- widthRef.current = w$1;
112
- heightRef.current = h$1;
113
- for (const prop of drawingState) context[prop] = contextMemo[prop];
114
- if (reduceFlickering && memoContext && memoCanvas && memoCanvas.width > 0 && memoCanvas.height > 0) context.drawImage(memoContext.canvas, 0, 0);
123
+ setDprConfig(canvas, context, w, h, dpr);
124
+ widthRef.current = w;
125
+ heightRef.current = h;
126
+ if (reduceFlickering && memoContext && memoCanvas && memoCanvas.width > 0 && memoCanvas.height > 0) {
127
+ for (const prop of drawingState) context[prop] = contextMemo[prop];
128
+ context.drawImage(memoContext.canvas, 0, 0);
129
+ }
130
+ if (!animate) loop(context, widthRef, heightRef, -1);
115
131
  }
116
132
  });
117
133
  ro.observe(parent);
118
- const w = parent.clientWidth;
119
- const h = parent.clientHeight;
120
- setDprConfig(canvas, context, w, h, dpr);
121
- if (init) init(context, {
122
- width: widthRef.current,
123
- height: heightRef.current
124
- });
125
- const now = performance.now();
126
- deltaMemoRef.current = now;
127
- startTimeRef.current = now;
128
- loop(context, widthRef, heightRef, -1);
134
+ const width = parent.clientWidth;
135
+ const height = parent.clientHeight;
136
+ firstRendering(width, height);
129
137
  return () => {
130
138
  if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
131
139
  ro.disconnect();
132
140
  };
133
141
  } else {
134
- const rect = canvas.getBoundingClientRect();
135
- setDprConfig(canvas, context, rect.width, rect.height, dpr);
136
- widthRef.current = rect.width;
137
- heightRef.current = rect.height;
138
- if (init) init(context, {
139
- width: widthRef.current,
140
- height: heightRef.current
141
- });
142
- const now = performance.now();
143
- deltaMemoRef.current = now;
144
- startTimeRef.current = now;
145
- loop(context, widthRef, heightRef, -1);
142
+ const { width, height } = canvas.getBoundingClientRect();
143
+ firstRendering(width, height);
146
144
  return () => {
147
145
  if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
148
146
  };
@@ -152,12 +150,13 @@ function AnimationCanvas({ options, init, draw, width = 100, height = 100, relat
152
150
  init,
153
151
  options,
154
152
  reduceFlickering,
155
- relativeSize
153
+ relativeSize,
154
+ animate
156
155
  ]);
157
156
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
158
157
  className: (0, clsx.default)("tremolo-animation-canvas", className),
159
- width: relativeSize ? 0 : width,
160
- height: relativeSize ? 0 : height,
158
+ width: relativeSize ? 0 : _width,
159
+ height: relativeSize ? 0 : _height,
161
160
  ref: canvasRef,
162
161
  onContextMenu,
163
162
  ...props
@@ -170,8 +169,8 @@ function AnimationCanvas({ options, init, draw, width = 100, height = 100, relat
170
169
  //#endregion
171
170
  //#region src/hooks/useCallbackRef.ts
172
171
  /**
173
- * This hook is user-land implementation of the experimental `useEffectEvent` hook.
174
- * React docs: https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
172
+ * Internal
173
+ * @private
175
174
  */
176
175
  function useCallbackRef(callback, deps = []) {
177
176
  const callbackRef = (0, react.useRef)(() => {
@@ -202,8 +201,7 @@ function useEventListener(target, event, handler, options) {
202
201
  handler
203
202
  ]);
204
203
  return () => {
205
- const node = typeof target === "function" ? target() : target ?? document;
206
- node?.removeEventListener(event, listener, options);
204
+ (typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
207
205
  };
208
206
  }
209
207
 
@@ -228,6 +226,7 @@ function useRefCallbackEvent(event, handler, options, deps = []) {
228
226
  //#endregion
229
227
  //#region src/hooks/useDrag.ts
230
228
  /**
229
+ * @category hooks
231
230
  * @returns [refCallback, pointerDownHandler]
232
231
  */
233
232
  function useDrag({ threshold = 1, onDrag, onDragStart, onDragEnd }) {
@@ -294,9 +293,7 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
294
293
  const isRelativeSize = typeof (size ?? width) == "string" || typeof height == "string";
295
294
  const onDrag = (0, react.useCallback)((_x, y) => {
296
295
  if (!onChange) return;
297
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
298
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
299
- onChange(v2);
296
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
300
297
  }, [
301
298
  max,
302
299
  min,
@@ -316,10 +313,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
316
313
  event.preventDefault();
317
314
  const x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
318
315
  let v;
319
- if (keyboard[0] == "normalized") {
320
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
321
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
322
- } else v = value + x;
316
+ if (keyboard[0] == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
317
+ else v = value + x;
323
318
  onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
324
319
  }
325
320
  }, [
@@ -346,10 +341,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
346
341
  event.preventDefault();
347
342
  const x = event.deltaY > 0 ? -wheel[1] : wheel[1];
348
343
  let v;
349
- if (wheel[0] == "normalized") {
350
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
351
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
352
- } else v = value + x;
344
+ if (wheel[0] == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
345
+ else v = value + x;
353
346
  if (onChange) onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
354
347
  }, { passive: false }, [
355
348
  wheel,
@@ -461,20 +454,12 @@ function DragObserver(props) {
461
454
  *
462
455
  * @category Knob
463
456
  */
464
- const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, onChange, onKeyDown, onPointerDown, onDoubleClick, className, classes,...props }, ref) => {
457
+ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, lineWeight = 6, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, classes,...props }, forwardedRef) => {
465
458
  const valueRef = (0, react.useRef)(0);
466
459
  const elmRef = (0, react.useRef)(null);
467
- const padding = 8;
468
- const thumbLineWeight = 6;
469
- const thumbLineLength = 35;
470
- const lineWeight = size * .06;
471
- const p = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
472
- const s = (0, __tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
473
460
  const onDrag = (0, react.useCallback)((_x, y) => {
474
461
  if (!onChange || readonly) return;
475
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
476
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
477
- onChange(v2);
462
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
478
463
  }, [
479
464
  max,
480
465
  min,
@@ -485,10 +470,8 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
485
470
  ]);
486
471
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
487
472
  let newValue;
488
- if (eventType == "normalized") {
489
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
490
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
491
- } else newValue = value + x;
473
+ if (eventType == "normalized") newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
474
+ else newValue = value + x;
492
475
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
493
476
  }, [
494
477
  max,
@@ -538,7 +521,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
538
521
  readonly,
539
522
  updateValueByEvent
540
523
  ]);
541
- (0, react.useImperativeHandle)(ref, () => {
524
+ (0, react.useImperativeHandle)(forwardedRef, () => {
542
525
  return {
543
526
  focus() {
544
527
  elmRef.current?.focus();
@@ -548,11 +531,15 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
548
531
  }
549
532
  };
550
533
  }, []);
551
- const center = size / 2;
552
- const r1 = -135;
553
- const r2 = -135 + Math.min(p, s) * 270;
554
- const r3 = -135 + Math.max(p, s) * 270;
555
- const r4 = 135;
534
+ const viewBoxSize = 100;
535
+ const center = viewBoxSize / 2;
536
+ const padding = (viewBoxSize - (0, __tremolo_ui_functions.clamp)(thumbSize, 0, 100)) / 2;
537
+ const p = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
538
+ const s = (0, __tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
539
+ const r1 = -angleRange / 2;
540
+ const r2 = r1 + Math.min(p, s) * angleRange;
541
+ const r3 = r1 + Math.max(p, s) * angleRange;
542
+ const r4 = angleRange / 2;
556
543
  const x1 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r1 - 90));
557
544
  const y1 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r1 - 90));
558
545
  const x2 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r2 - 90));
@@ -562,12 +549,13 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
562
549
  const x4 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r4 - 90));
563
550
  const y4 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r4 - 90));
564
551
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
565
- className: (0, clsx.default)("tremolo-knob", className),
566
552
  ref: (div) => {
567
553
  elmRef.current = div;
568
554
  wheelRefCallback(div);
569
555
  touchMoveRefCallback(div);
570
556
  },
557
+ className: (0, clsx.default)("tremolo-knob", className),
558
+ viewBox: `0 0 ${viewBoxSize} ${viewBoxSize}`,
571
559
  width: size,
572
560
  height: size,
573
561
  tabIndex: 0,
@@ -617,7 +605,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
617
605
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
618
606
  cx: "50%",
619
607
  cy: "50%",
620
- r: `${50 - padding}%`,
608
+ r: `${thumbSize / 2}%`,
621
609
  fill: thumb || "currentColor"
622
610
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
623
611
  className: (0, clsx.default)("tremolo-knob-thumb-line", classes?.thumbLine),
@@ -628,7 +616,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
628
616
  stroke: thumbLine || "currentColor",
629
617
  strokeWidth: `${thumbLineWeight}%`,
630
618
  style: {
631
- transform: `rotate(${-135 + p * 270}deg)`,
619
+ transform: `rotate(${r1 + p * angleRange}deg)`,
632
620
  transformOrigin: "50% 50%"
633
621
  }
634
622
  })]
@@ -637,6 +625,47 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
637
625
  });
638
626
  });
639
627
 
628
+ //#endregion
629
+ //#region src/components/NumberInput/type.ts
630
+ function selectUnit(units, value) {
631
+ let i = 0;
632
+ for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
633
+ return units[Math.max(0, i - 1)];
634
+ }
635
+ function parseValue(inputString, units, digit) {
636
+ const str = inputString.trim();
637
+ if (!units || typeof units == "string") {
638
+ const m$1 = str.match(/-?\d+(\.\d+)?/);
639
+ let v = Number(m$1?.[0] ?? "0");
640
+ v = isNaN(v) ? 0 : v;
641
+ return {
642
+ rawValue: v,
643
+ formatValue: (digit != void 0 ? v.toFixed(digit) : v) + (units ?? ""),
644
+ unit: units ?? ""
645
+ };
646
+ }
647
+ const unitList = units.map(([u, _s]) => u);
648
+ const scaleList = units.map(([_u, s]) => s);
649
+ let rawValue$5 = 0;
650
+ let unit = unitList[0];
651
+ let formatValue = (digit != void 0 ? rawValue$5.toFixed(digit) : rawValue$5) + unit;
652
+ const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
653
+ if (m) {
654
+ rawValue$5 = Number(m[1]) || 0;
655
+ const uIndex = unitList.indexOf(m[3]);
656
+ rawValue$5 *= uIndex != -1 ? scaleList[uIndex] : 1;
657
+ const [u, scale] = selectUnit(units, rawValue$5);
658
+ const v = rawValue$5 / scale;
659
+ formatValue = (digit != void 0 ? v.toFixed(digit) : v) + u;
660
+ unit = u;
661
+ }
662
+ return {
663
+ rawValue: rawValue$5,
664
+ formatValue,
665
+ unit
666
+ };
667
+ }
668
+
640
669
  //#endregion
641
670
  //#region src/components/NumberInput/context.tsx
642
671
  function safeClamp(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
@@ -656,14 +685,13 @@ const createNumberInputStore = (initProps) => {
656
685
  return (0, zustand.createStore)()((set) => ({
657
686
  ...DEFAULT_PROPS,
658
687
  ...initProps,
659
- value: (0, __tremolo_ui_functions_NumberInput.parseValue)(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
660
- valueAsNumber: (0, __tremolo_ui_functions_NumberInput.parseValue)(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
688
+ value: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
689
+ valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
661
690
  increment: () => set((state) => {
662
691
  if (state.readonly) return {};
663
- const current = (0, __tremolo_ui_functions_NumberInput.parseValue)(state.value, state.units, state.digit).rawValue;
664
- let next = current + (state.step ?? 1);
692
+ let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
665
693
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
666
- const v = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(next), state.units, state.digit).formatValue;
694
+ const v = parseValue(String(next), state.units, state.digit).formatValue;
667
695
  state?.onChange?.(next, v);
668
696
  return {
669
697
  value: v,
@@ -672,10 +700,9 @@ const createNumberInputStore = (initProps) => {
672
700
  }),
673
701
  decrement: () => set((state) => {
674
702
  if (state.readonly) return {};
675
- const current = (0, __tremolo_ui_functions_NumberInput.parseValue)(state.value, state.units, state.digit).rawValue;
676
- let next = current - (state.step ?? 1);
703
+ let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
677
704
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
678
- const v = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(next), state.units, state.digit).formatValue;
705
+ const v = parseValue(String(next), state.units, state.digit).formatValue;
679
706
  state?.onChange?.(next, v);
680
707
  return {
681
708
  value: v,
@@ -687,7 +714,7 @@ const createNumberInputStore = (initProps) => {
687
714
  const v = value ?? "";
688
715
  return {
689
716
  value: v,
690
- valueAsNumber: (0, __tremolo_ui_functions_NumberInput.parseValue)(v, state.units, state.digit).rawValue
717
+ valueAsNumber: parseValue(v, state.units, state.digit).rawValue
691
718
  };
692
719
  })
693
720
  }));
@@ -698,7 +725,7 @@ function NumberInputProvider({ children,...props }) {
698
725
  if (!storeRef.current) storeRef.current = createNumberInputStore(props);
699
726
  (0, react.useEffect)(() => {
700
727
  if (storeRef.current) {
701
- const parsed = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(props?.value || ""), props?.units, props?.digit);
728
+ const parsed = parseValue(String(props?.value || ""), props?.units, props?.digit);
702
729
  storeRef.current.setState({
703
730
  ...props,
704
731
  value: parsed.formatValue,
@@ -745,8 +772,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
745
772
  let newValue;
746
773
  if (eventType == "normalized") {
747
774
  if (!min || !max) throw new Error("required parameter: min, max");
748
- const n = (0, __tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max);
749
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max);
775
+ newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max) + x, min, max);
750
776
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
751
777
  } else {
752
778
  newValue = valueAsNumber + x;
@@ -764,7 +790,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
764
790
  if (!["ArrowUp", "ArrowDown"].includes(key)) return;
765
791
  event.preventDefault();
766
792
  const x = key == "ArrowUp" ? keyboard[1] : -keyboard[1];
767
- const parsed = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(updateValueByEvent(keyboard[0], x)));
793
+ const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
768
794
  onChange?.(parsed.rawValue, parsed.formatValue);
769
795
  }, [
770
796
  keyboard,
@@ -777,7 +803,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
777
803
  event.preventDefault();
778
804
  if (!onChange || event.deltaY == 0) return;
779
805
  const x = Math.sign(event.deltaY) * -wheel[1];
780
- const parsed = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(updateValueByEvent(wheel[0], x)));
806
+ const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
781
807
  onChange?.(parsed.rawValue, parsed.formatValue);
782
808
  }, { passive: false }, [
783
809
  wheel,
@@ -815,7 +841,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
815
841
  const v = event.currentTarget.value;
816
842
  const cursorPos = event.target.selectionStart;
817
843
  change(v);
818
- const valueAsNumber$1 = (0, __tremolo_ui_functions_NumberInput.parseValue)(v, units, digit).rawValue;
844
+ const valueAsNumber$1 = parseValue(v, units, digit).rawValue;
819
845
  onChange?.(valueAsNumber$1, v);
820
846
  if (cursorPos) {
821
847
  event.target.selectionStart = cursorPos;
@@ -825,16 +851,16 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
825
851
  onFocus: (event) => {
826
852
  setEditing(true);
827
853
  const v = event.currentTarget.value;
828
- const parsed = (0, __tremolo_ui_functions_NumberInput.parseValue)(v, units, digit);
854
+ const parsed = parseValue(v, units, digit);
829
855
  if (selectWithFocus == "all") event.currentTarget.setSelectionRange(0, v.length);
830
856
  else if (selectWithFocus == "number") event.currentTarget.setSelectionRange(0, parsed.formatValue.length - parsed.unit.length);
831
857
  onFocus?.(parsed.rawValue, parsed.formatValue, event);
832
858
  },
833
859
  onBlur: (event) => {
834
860
  setEditing(false);
835
- let v = (0, __tremolo_ui_functions_NumberInput.parseValue)(event.currentTarget.value, units, digit).rawValue;
861
+ let v = parseValue(event.currentTarget.value, units, digit).rawValue;
836
862
  if (clampValueOnBlur) v = safeClamp(v, min, max);
837
- const parsed = (0, __tremolo_ui_functions_NumberInput.parseValue)(String(v), units, digit);
863
+ const parsed = parseValue(String(v), units, digit);
838
864
  change(parsed.formatValue);
839
865
  if (clampValueOnBlur) onChange?.(parsed.rawValue, parsed.formatValue);
840
866
  onBlur?.(parsed.rawValue, parsed.formatValue, event);
@@ -862,6 +888,9 @@ function Stepper({ dynamic = true, children, className,...props }) {
862
888
 
863
889
  //#endregion
864
890
  //#region src/hooks/useInterval.ts
891
+ /**
892
+ * @category hooks
893
+ */
865
894
  function useInterval(callback, delay) {
866
895
  const savedCallback = (0, react.useRef)(callback);
867
896
  (0, react.useEffect)(() => {
@@ -880,6 +909,9 @@ function useInterval(callback, delay) {
880
909
 
881
910
  //#endregion
882
911
  //#region src/hooks/useLongPress.ts
912
+ /**
913
+ * @category hooks
914
+ */
883
915
  function useLongPress(callback, initialDelay = 500, interval = 40) {
884
916
  const [pressed, setPressed] = (0, react.useState)(false);
885
917
  const [delay, setDelay] = (0, react.useState)(initialDelay);
@@ -905,8 +937,7 @@ function IncrementStepper({ size = 12, children, className,...props }) {
905
937
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
906
938
  const readonly = useNumberInputContext((s) => s.readonly);
907
939
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
908
- const increment = useNumberInputContext((s) => s.increment);
909
- const press = useLongPress(increment);
940
+ const press = useLongPress(useNumberInputContext((s) => s.increment));
910
941
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
911
942
  className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
912
943
  role: "button",
@@ -937,8 +968,7 @@ function DecrementStepper({ size = 12, children, className,...props }) {
937
968
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
938
969
  const readonly = useNumberInputContext((s) => s.readonly);
939
970
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
940
- const decrement = useNumberInputContext((s) => s.decrement);
941
- const press = useLongPress(decrement);
971
+ const press = useLongPress(useNumberInputContext((s) => s.decrement));
942
972
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
943
973
  className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
944
974
  role: "button",
@@ -967,7 +997,7 @@ function DecrementStepper({ size = 12, children, className,...props }) {
967
997
  * Input with some useful functions for entering numerical values.
968
998
  * @category NumberInput
969
999
  */
970
- const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = "outline", selectWithFocus = "none", blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ["raw", 1], keyboard = ["raw", 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children,...props }, ref) => {
1000
+ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = "outline", selectWithFocus = "none", blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ["raw", 1], keyboard = ["raw", 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children,...props }, forwardedRef) => {
971
1001
  const colors = { "--active-color": activeColor };
972
1002
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
973
1003
  value: String(value),
@@ -986,7 +1016,7 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
986
1016
  "data-stepper": !!children,
987
1017
  "data-variant": variant,
988
1018
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InternalInput, {
989
- ref,
1019
+ ref: forwardedRef,
990
1020
  readonly,
991
1021
  disabled,
992
1022
  selectWithFocus,
@@ -1007,6 +1037,7 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
1007
1037
  //#endregion
1008
1038
  //#region src/hooks/useDragWithElement.ts
1009
1039
  /**
1040
+ * @category hooks
1010
1041
  * @returns [refCallback, pointerDownHandler]
1011
1042
  */
1012
1043
  function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
@@ -1086,9 +1117,7 @@ function usePianoContext(selector) {
1086
1117
  //#region src/components/Piano/KeyLabel.tsx
1087
1118
  /** @category Piano */
1088
1119
  function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label,...props }) {
1089
- const noteRange = usePianoContext((s) => s.noteRange);
1090
- const noteRangeArray = getNoteRangeArray(noteRange);
1091
- const index = noteRangeArray.indexOf(__note);
1120
+ const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
1092
1121
  const content = label ? label(__note, index) : __label && __label(__note, index);
1093
1122
  return (content != void 0 || content != null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1094
1123
  className: (0, clsx.default)("tremolo-piano-key-label-wrapper", wrapperClassName),
@@ -1114,8 +1143,7 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1114
1143
  const onPlayNote = usePianoContext((s) => s.onPlayNote);
1115
1144
  const onStopNote = usePianoContext((s) => s.onStopNote);
1116
1145
  const label = usePianoContext((s) => s.label);
1117
- const notePosition = usePianoContext((s) => s.notePosition);
1118
- const position = notePosition(noteNumber);
1146
+ const position = usePianoContext((s) => s.notePosition)(noteNumber);
1119
1147
  const disabled = noteNumber > midiMax;
1120
1148
  const colors = {
1121
1149
  "--bg": bg,
@@ -1125,10 +1153,10 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1125
1153
  };
1126
1154
  (0, react.useImperativeHandle)(ref, () => {
1127
1155
  return {
1128
- play() {
1156
+ play(velocity) {
1129
1157
  if (disabled) return;
1130
1158
  setPlayed(true);
1131
- if (onPlayNote) onPlayNote(noteNumber);
1159
+ if (onPlayNote) onPlayNote(noteNumber, velocity);
1132
1160
  },
1133
1161
  stop() {
1134
1162
  setPlayed(false);
@@ -1219,12 +1247,9 @@ const SHORTCUTS = { HOME_ROW: { keys: [
1219
1247
  //#endregion
1220
1248
  //#region src/components/Piano/index.tsx
1221
1249
  /**
1222
- * Piano component
1223
- *
1224
- * TODO:
1225
- * - add scale highlight
1250
+ * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
1251
+ * @category Piano
1226
1252
  */
1227
- /** @category Piano */
1228
1253
  function getNoteRangeArray(noteRange) {
1229
1254
  return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1230
1255
  }
@@ -1233,7 +1258,7 @@ const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth;
1233
1258
  * Customizable piano component.
1234
1259
  * @category Piano
1235
1260
  */
1236
- function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown,...props }) {
1261
+ const Piano = (0, react.forwardRef)(({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown,...props }, forwardedRef) => {
1237
1262
  const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
1238
1263
  const keyRefs = (0, react.useRef)([]);
1239
1264
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
@@ -1304,9 +1329,7 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1304
1329
  ]);
1305
1330
  const onDrag = (0, react.useCallback)((perX, perY) => {
1306
1331
  if (!pianoRef.current) return;
1307
- const x = perX * staticWidth;
1308
- const y = perY * pianoRef.current.clientHeight;
1309
- const note = getHitKeyIndex(x, y);
1332
+ const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
1310
1333
  const index = noteRangeArray.indexOf(note);
1311
1334
  if (index == -1) return;
1312
1335
  if (hitKeyIndex.current != index) {
@@ -1360,6 +1383,25 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1360
1383
  const index = keyboardShortcuts.keys.indexOf(e.key);
1361
1384
  if (index != -1) keyRefs.current[index]?.current?.stop();
1362
1385
  });
1386
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1387
+ return {
1388
+ playNote(note, velocity) {
1389
+ const index = noteRangeArray.indexOf(note);
1390
+ if (index != -1) {
1391
+ if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
1392
+ } else onPlayNote?.(note, velocity);
1393
+ },
1394
+ stopNote(note) {
1395
+ const index = noteRangeArray.indexOf(note);
1396
+ if (index != -1) keyRefs.current[index]?.current?.stop();
1397
+ else onStopNote?.(note);
1398
+ }
1399
+ };
1400
+ }, [
1401
+ noteRangeArray,
1402
+ onPlayNote,
1403
+ onStopNote
1404
+ ]);
1363
1405
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoProvider, {
1364
1406
  notePosition,
1365
1407
  noteRange,
@@ -1396,7 +1438,7 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1396
1438
  }, note))
1397
1439
  })
1398
1440
  });
1399
- }
1441
+ });
1400
1442
 
1401
1443
  //#endregion
1402
1444
  //#region src/components/Slider/context.tsx
@@ -1484,6 +1526,22 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
1484
1526
  });
1485
1527
  }
1486
1528
 
1529
+ //#endregion
1530
+ //#region src/components/Slider/type.ts
1531
+ function generateOptionsList(options, min, max, step) {
1532
+ const optionsList = [];
1533
+ const per = options[0] == "step" ? step : options[0];
1534
+ const count = Math.floor(max / per) - Math.ceil(min / per) + 1;
1535
+ for (let i = 0; i < count; i++) {
1536
+ const value = (0, __tremolo_ui_functions.toFixed)(per * (Math.ceil(min / per) + i), (0, __tremolo_ui_functions.decimalPart)(per)?.length);
1537
+ optionsList.push({
1538
+ value,
1539
+ type: options[1]
1540
+ });
1541
+ }
1542
+ return optionsList;
1543
+ }
1544
+
1487
1545
  //#endregion
1488
1546
  //#region src/components/Slider/Scale.tsx
1489
1547
  /** @category Slider */
@@ -1493,7 +1551,7 @@ function Scale({ gap = 6, options, children, className, style,...props }) {
1493
1551
  const step = useSliderContext((s) => s.step);
1494
1552
  const vertical = useSliderContext((s) => s.vertical);
1495
1553
  const reverse = useSliderContext((s) => s.reverse);
1496
- const optionsList = options ? (0, __tremolo_ui_functions_Slider.generateOptionsList)(options, min, max, step) : [];
1554
+ const optionsList = options ? generateOptionsList(options, min, max, step) : [];
1497
1555
  if ((0, __tremolo_ui_functions.xor)(vertical, reverse)) optionsList.reverse();
1498
1556
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1499
1557
  className: (0, clsx.default)("tremolo-slider-scale", className),
@@ -1574,7 +1632,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1574
1632
  "data-vertical": vertical,
1575
1633
  style: !defaultStyle ? style : {
1576
1634
  ...colors,
1577
- background: (0, __tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive, #eee) ${__percent}%, var(--active, #7998ec) ${__percent}%)` : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`,
1635
+ background: (0, __tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${__percent}%, var(--active) ${__percent}%)` : `linear-gradient(to ${direction}, var(--active) ${__percent}%, var(--inactive) ${__percent}%)`,
1578
1636
  borderRadius: (0, __tremolo_ui_functions.styleHelper)(thickness, "/", 2),
1579
1637
  width: !vertical ? length : thickness,
1580
1638
  height: vertical ? length : thickness,
@@ -1591,7 +1649,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1591
1649
  * Customizable slider
1592
1650
  * @category Slider
1593
1651
  */
1594
- const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, vertical = false, reverse = false, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown,...props }, ref) => {
1652
+ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, vertical = false, reverse = false, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown,...props }, forwardedRef) => {
1595
1653
  const trackElementRef = (0, react.useRef)(null);
1596
1654
  const thumbRef = (0, react.useRef)(null);
1597
1655
  const p = (0, __tremolo_ui_functions.toFixed)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) * 100);
@@ -1611,9 +1669,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1611
1669
  const onDrag = (0, react.useCallback)((nx, ny) => {
1612
1670
  if (!onChange || readonly) return;
1613
1671
  const n = vertical ? ny : nx;
1614
- const v = (0, __tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew);
1615
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
1616
- onChange(v2);
1672
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew), step), min, max));
1617
1673
  }, [
1618
1674
  displayReversed,
1619
1675
  max,
@@ -1626,10 +1682,8 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1626
1682
  ]);
1627
1683
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
1628
1684
  let newValue;
1629
- if (eventType == "normalized") {
1630
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
1631
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
1632
- } else newValue = value + x;
1685
+ if (eventType == "normalized") newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
1686
+ else newValue = value + x;
1633
1687
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
1634
1688
  }, [
1635
1689
  max,
@@ -1692,7 +1746,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1692
1746
  onChange,
1693
1747
  updateValueByEvent
1694
1748
  ]);
1695
- (0, react.useImperativeHandle)(ref, () => {
1749
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1696
1750
  return {
1697
1751
  focus() {
1698
1752
  thumbRef.current?.focus();
@@ -1776,11 +1830,10 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1776
1830
  /** @category WheelObserver */
1777
1831
  function WheelObserver(props) {
1778
1832
  const { children, onWheel, as: Component = "div",...attributes } = props;
1779
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
1780
- if (onWheel) onWheel(event);
1781
- }, { passive: false }, [onWheel]);
1782
1833
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
1783
- ref: wheelRefCallback,
1834
+ ref: useRefCallbackEvent("wheel", (event) => {
1835
+ if (onWheel) onWheel(event);
1836
+ }, { passive: false }, [onWheel]),
1784
1837
  ...attributes,
1785
1838
  children
1786
1839
  });
@@ -1853,7 +1906,7 @@ const defaultValueOptions = {
1853
1906
  * Simple XYPad
1854
1907
  * @category XYPad
1855
1908
  */
1856
- const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children,...props }, ref) => {
1909
+ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children,...props }, forwardedRef) => {
1857
1910
  const x = (0, react.useMemo)(() => {
1858
1911
  return {
1859
1912
  ...defaultValueOptions,
@@ -1884,9 +1937,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1884
1937
  if (!onChange || readonly) return;
1885
1938
  const vx = (0, __tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx$1 : nx$1, x.min, x.max, x.skew);
1886
1939
  const vy = (0, __tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny$1 : ny$1, y.min, y.max, y.skew);
1887
- const newX = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max);
1888
- const newY = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max);
1889
- onChange(newX, newY);
1940
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max), (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max));
1890
1941
  }, [
1891
1942
  onChange,
1892
1943
  readonly,
@@ -1903,10 +1954,8 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1903
1954
  ]);
1904
1955
  const updateValueByEvent = (0, react.useCallback)((eventType, target, x$1) => {
1905
1956
  let v;
1906
- if (eventType == "normalized") {
1907
- const n = (0, __tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew);
1908
- v = (0, __tremolo_ui_functions.rawValue)(n + x$1, target.min, target.max, target.skew);
1909
- } else v = target.value + x$1;
1957
+ if (eventType == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew) + x$1, target.min, target.max, target.skew);
1958
+ else v = target.value + x$1;
1910
1959
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
1911
1960
  }, []);
1912
1961
  const handleKeyDown = (0, react.useCallback)((event) => {
@@ -1973,7 +2022,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1973
2022
  readonly,
1974
2023
  updateValueByEvent
1975
2024
  ]);
1976
- (0, react.useImperativeHandle)(ref, () => {
2025
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1977
2026
  return {
1978
2027
  focus() {
1979
2028
  thumbRef.current?.focus();
@@ -2036,6 +2085,9 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2036
2085
 
2037
2086
  //#endregion
2038
2087
  //#region src/hooks/useAnimationFrame.ts
2088
+ /**
2089
+ * @category hooks
2090
+ */
2039
2091
  function useAnimationFrame(callback = () => {}, deps = []) {
2040
2092
  const reqIdRef = (0, react.useRef)(-1);
2041
2093
  const loop = (0, react.useCallback)(() => {
@@ -2048,6 +2100,79 @@ function useAnimationFrame(callback = () => {}, deps = []) {
2048
2100
  }, [loop]);
2049
2101
  }
2050
2102
 
2103
+ //#endregion
2104
+ //#region src/hooks/useMIDIAccess.ts
2105
+ /** @private */
2106
+ const PERMISSION_DENIED = "PERMISSION_DENIED";
2107
+ /** @private */
2108
+ const NOT_SUPPORTED = "NOT_SUPPORTED";
2109
+ /**
2110
+ * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
2111
+ * @category hooks
2112
+ */
2113
+ function useMIDIAccess(requestOnMount = true) {
2114
+ const [midiAccess, setMidiAccess] = (0, react.useState)(null);
2115
+ const [error, setError] = (0, react.useState)(null);
2116
+ const request = (0, react.useCallback)(() => {
2117
+ if (navigator.requestMIDIAccess) navigator.requestMIDIAccess().then((access) => {
2118
+ setMidiAccess(access);
2119
+ setError(null);
2120
+ }).catch(() => {
2121
+ setError(PERMISSION_DENIED);
2122
+ });
2123
+ else setError(NOT_SUPPORTED);
2124
+ }, []);
2125
+ (0, react.useEffect)(() => {
2126
+ if (requestOnMount) request();
2127
+ }, [requestOnMount, request]);
2128
+ return {
2129
+ request,
2130
+ midiAccess,
2131
+ error
2132
+ };
2133
+ }
2134
+
2135
+ //#endregion
2136
+ //#region src/hooks/useMIDIMessage.ts
2137
+ /**
2138
+ * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
2139
+ * @category hooks
2140
+ */
2141
+ function useMIDIMessage(midiAccess, onMIDIMessage) {
2142
+ (0, react.useEffect)(() => {
2143
+ if (!midiAccess) return;
2144
+ for (const input of midiAccess.inputs.values()) input.addEventListener("midimessage", onMIDIMessage);
2145
+ return () => {
2146
+ for (const input of midiAccess.inputs.values()) input.removeEventListener("midimessage", onMIDIMessage);
2147
+ };
2148
+ }, [midiAccess, onMIDIMessage]);
2149
+ }
2150
+
2151
+ //#endregion
2152
+ //#region src/hooks/useMIDIInput.ts
2153
+ const MIDI_EVENT_TO_NUMBER = {
2154
+ NOTE_ON: 144,
2155
+ NOTE_OFF: 128,
2156
+ PITCH_BEND: 224
2157
+ };
2158
+ /**
2159
+ * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
2160
+ * @category hooks
2161
+ */
2162
+ function useMIDIInput(midiAccess, onNoteOnEvent, onNoteOffEvent, onPitchBendEvent) {
2163
+ useMIDIMessage(midiAccess, (0, react.useCallback)((event) => {
2164
+ if (!event.data) return;
2165
+ const kind = event.data[0] & 240;
2166
+ if (kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF || kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0) onNoteOffEvent?.(event.data[1]);
2167
+ else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) onNoteOnEvent?.(event.data[1], event.data[2]);
2168
+ else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) onPitchBendEvent?.(event.data[1], event.data[2]);
2169
+ }, [
2170
+ onNoteOffEvent,
2171
+ onNoteOnEvent,
2172
+ onPitchBendEvent
2173
+ ]));
2174
+ }
2175
+
2051
2176
  //#endregion
2052
2177
  exports.AnimationCanvas = AnimationCanvas;
2053
2178
  exports.AnimationKnob = AnimationKnob;
@@ -2057,7 +2182,9 @@ exports.DragObserver = DragObserver;
2057
2182
  exports.IncrementStepper = IncrementStepper;
2058
2183
  exports.KeyLabel = KeyLabel;
2059
2184
  exports.Knob = Knob;
2185
+ exports.NOT_SUPPORTED = NOT_SUPPORTED;
2060
2186
  exports.NumberInput = NumberInput;
2187
+ exports.PERMISSION_DENIED = PERMISSION_DENIED;
2061
2188
  exports.Piano = Piano;
2062
2189
  exports.SHORTCUTS = SHORTCUTS;
2063
2190
  exports.Scale = Scale;
@@ -2078,5 +2205,8 @@ exports.useDragWithElement = useDragWithElement;
2078
2205
  exports.useEventListener = useEventListener;
2079
2206
  exports.useInterval = useInterval;
2080
2207
  exports.useLongPress = useLongPress;
2208
+ exports.useMIDIAccess = useMIDIAccess;
2209
+ exports.useMIDIInput = useMIDIInput;
2210
+ exports.useMIDIMessage = useMIDIMessage;
2081
2211
  exports.useSliderContext = useSliderContext;
2082
2212
  //# sourceMappingURL=index.cjs.map