@tremolo-ui/react 0.1.5 → 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.
package/dist/index.cjs CHANGED
@@ -21,11 +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"));
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");
29
31
 
30
32
  //#region src/components/AnimationCanvas/canvas.ts
31
33
  const drawingState = [
@@ -167,8 +169,8 @@ function AnimationCanvas({ draw, init, animate = true, options, width: _width =
167
169
  //#endregion
168
170
  //#region src/hooks/useCallbackRef.ts
169
171
  /**
170
- * This hook is user-land implementation of the experimental `useEffectEvent` hook.
171
- * React docs: https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
172
+ * Internal
173
+ * @private
172
174
  */
173
175
  function useCallbackRef(callback, deps = []) {
174
176
  const callbackRef = (0, react.useRef)(() => {
@@ -199,8 +201,7 @@ function useEventListener(target, event, handler, options) {
199
201
  handler
200
202
  ]);
201
203
  return () => {
202
- const node = typeof target === "function" ? target() : target ?? document;
203
- node?.removeEventListener(event, listener, options);
204
+ (typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
204
205
  };
205
206
  }
206
207
 
@@ -225,6 +226,7 @@ function useRefCallbackEvent(event, handler, options, deps = []) {
225
226
  //#endregion
226
227
  //#region src/hooks/useDrag.ts
227
228
  /**
229
+ * @category hooks
228
230
  * @returns [refCallback, pointerDownHandler]
229
231
  */
230
232
  function useDrag({ threshold = 1, onDrag, onDragStart, onDragEnd }) {
@@ -291,9 +293,7 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
291
293
  const isRelativeSize = typeof (size ?? width) == "string" || typeof height == "string";
292
294
  const onDrag = (0, react.useCallback)((_x, y) => {
293
295
  if (!onChange) return;
294
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
295
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
296
- 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));
297
297
  }, [
298
298
  max,
299
299
  min,
@@ -313,10 +313,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
313
313
  event.preventDefault();
314
314
  const x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
315
315
  let v;
316
- if (keyboard[0] == "normalized") {
317
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
318
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
319
- } 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;
320
318
  onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
321
319
  }
322
320
  }, [
@@ -343,10 +341,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
343
341
  event.preventDefault();
344
342
  const x = event.deltaY > 0 ? -wheel[1] : wheel[1];
345
343
  let v;
346
- if (wheel[0] == "normalized") {
347
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
348
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
349
- } 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;
350
346
  if (onChange) onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
351
347
  }, { passive: false }, [
352
348
  wheel,
@@ -458,20 +454,12 @@ function DragObserver(props) {
458
454
  *
459
455
  * @category Knob
460
456
  */
461
- 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) => {
462
458
  const valueRef = (0, react.useRef)(0);
463
459
  const elmRef = (0, react.useRef)(null);
464
- const padding = 8;
465
- const thumbLineWeight = 6;
466
- const thumbLineLength = 35;
467
- const lineWeight = size * .06;
468
- const p = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
469
- const s = (0, __tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
470
460
  const onDrag = (0, react.useCallback)((_x, y) => {
471
461
  if (!onChange || readonly) return;
472
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
473
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
474
- 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));
475
463
  }, [
476
464
  max,
477
465
  min,
@@ -482,10 +470,8 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
482
470
  ]);
483
471
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
484
472
  let newValue;
485
- if (eventType == "normalized") {
486
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
487
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
488
- } 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;
489
475
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
490
476
  }, [
491
477
  max,
@@ -535,7 +521,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
535
521
  readonly,
536
522
  updateValueByEvent
537
523
  ]);
538
- (0, react.useImperativeHandle)(ref, () => {
524
+ (0, react.useImperativeHandle)(forwardedRef, () => {
539
525
  return {
540
526
  focus() {
541
527
  elmRef.current?.focus();
@@ -545,11 +531,15 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
545
531
  }
546
532
  };
547
533
  }, []);
548
- const center = size / 2;
549
- const r1 = -135;
550
- const r2 = -135 + Math.min(p, s) * 270;
551
- const r3 = -135 + Math.max(p, s) * 270;
552
- 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;
553
543
  const x1 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r1 - 90));
554
544
  const y1 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r1 - 90));
555
545
  const x2 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r2 - 90));
@@ -559,12 +549,13 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
559
549
  const x4 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r4 - 90));
560
550
  const y4 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r4 - 90));
561
551
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
562
- className: (0, clsx.default)("tremolo-knob", className),
563
552
  ref: (div) => {
564
553
  elmRef.current = div;
565
554
  wheelRefCallback(div);
566
555
  touchMoveRefCallback(div);
567
556
  },
557
+ className: (0, clsx.default)("tremolo-knob", className),
558
+ viewBox: `0 0 ${viewBoxSize} ${viewBoxSize}`,
568
559
  width: size,
569
560
  height: size,
570
561
  tabIndex: 0,
@@ -614,7 +605,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
614
605
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
615
606
  cx: "50%",
616
607
  cy: "50%",
617
- r: `${50 - padding}%`,
608
+ r: `${thumbSize / 2}%`,
618
609
  fill: thumb || "currentColor"
619
610
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
620
611
  className: (0, clsx.default)("tremolo-knob-thumb-line", classes?.thumbLine),
@@ -625,7 +616,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
625
616
  stroke: thumbLine || "currentColor",
626
617
  strokeWidth: `${thumbLineWeight}%`,
627
618
  style: {
628
- transform: `rotate(${-135 + p * 270}deg)`,
619
+ transform: `rotate(${r1 + p * angleRange}deg)`,
629
620
  transformOrigin: "50% 50%"
630
621
  }
631
622
  })]
@@ -698,8 +689,7 @@ const createNumberInputStore = (initProps) => {
698
689
  valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
699
690
  increment: () => set((state) => {
700
691
  if (state.readonly) return {};
701
- const current = parseValue(state.value, state.units, state.digit).rawValue;
702
- let next = current + (state.step ?? 1);
692
+ let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
703
693
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
704
694
  const v = parseValue(String(next), state.units, state.digit).formatValue;
705
695
  state?.onChange?.(next, v);
@@ -710,8 +700,7 @@ const createNumberInputStore = (initProps) => {
710
700
  }),
711
701
  decrement: () => set((state) => {
712
702
  if (state.readonly) return {};
713
- const current = parseValue(state.value, state.units, state.digit).rawValue;
714
- let next = current - (state.step ?? 1);
703
+ let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
715
704
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
716
705
  const v = parseValue(String(next), state.units, state.digit).formatValue;
717
706
  state?.onChange?.(next, v);
@@ -783,8 +772,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
783
772
  let newValue;
784
773
  if (eventType == "normalized") {
785
774
  if (!min || !max) throw new Error("required parameter: min, max");
786
- const n = (0, __tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max);
787
- 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);
788
776
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
789
777
  } else {
790
778
  newValue = valueAsNumber + x;
@@ -900,6 +888,9 @@ function Stepper({ dynamic = true, children, className,...props }) {
900
888
 
901
889
  //#endregion
902
890
  //#region src/hooks/useInterval.ts
891
+ /**
892
+ * @category hooks
893
+ */
903
894
  function useInterval(callback, delay) {
904
895
  const savedCallback = (0, react.useRef)(callback);
905
896
  (0, react.useEffect)(() => {
@@ -918,6 +909,9 @@ function useInterval(callback, delay) {
918
909
 
919
910
  //#endregion
920
911
  //#region src/hooks/useLongPress.ts
912
+ /**
913
+ * @category hooks
914
+ */
921
915
  function useLongPress(callback, initialDelay = 500, interval = 40) {
922
916
  const [pressed, setPressed] = (0, react.useState)(false);
923
917
  const [delay, setDelay] = (0, react.useState)(initialDelay);
@@ -943,8 +937,7 @@ function IncrementStepper({ size = 12, children, className,...props }) {
943
937
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
944
938
  const readonly = useNumberInputContext((s) => s.readonly);
945
939
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
946
- const increment = useNumberInputContext((s) => s.increment);
947
- const press = useLongPress(increment);
940
+ const press = useLongPress(useNumberInputContext((s) => s.increment));
948
941
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
949
942
  className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
950
943
  role: "button",
@@ -975,8 +968,7 @@ function DecrementStepper({ size = 12, children, className,...props }) {
975
968
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
976
969
  const readonly = useNumberInputContext((s) => s.readonly);
977
970
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
978
- const decrement = useNumberInputContext((s) => s.decrement);
979
- const press = useLongPress(decrement);
971
+ const press = useLongPress(useNumberInputContext((s) => s.decrement));
980
972
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
981
973
  className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
982
974
  role: "button",
@@ -1005,7 +997,7 @@ function DecrementStepper({ size = 12, children, className,...props }) {
1005
997
  * Input with some useful functions for entering numerical values.
1006
998
  * @category NumberInput
1007
999
  */
1008
- 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) => {
1009
1001
  const colors = { "--active-color": activeColor };
1010
1002
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
1011
1003
  value: String(value),
@@ -1024,7 +1016,7 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
1024
1016
  "data-stepper": !!children,
1025
1017
  "data-variant": variant,
1026
1018
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InternalInput, {
1027
- ref,
1019
+ ref: forwardedRef,
1028
1020
  readonly,
1029
1021
  disabled,
1030
1022
  selectWithFocus,
@@ -1045,6 +1037,7 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
1045
1037
  //#endregion
1046
1038
  //#region src/hooks/useDragWithElement.ts
1047
1039
  /**
1040
+ * @category hooks
1048
1041
  * @returns [refCallback, pointerDownHandler]
1049
1042
  */
1050
1043
  function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
@@ -1124,9 +1117,7 @@ function usePianoContext(selector) {
1124
1117
  //#region src/components/Piano/KeyLabel.tsx
1125
1118
  /** @category Piano */
1126
1119
  function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label,...props }) {
1127
- const noteRange = usePianoContext((s) => s.noteRange);
1128
- const noteRangeArray = getNoteRangeArray(noteRange);
1129
- const index = noteRangeArray.indexOf(__note);
1120
+ const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
1130
1121
  const content = label ? label(__note, index) : __label && __label(__note, index);
1131
1122
  return (content != void 0 || content != null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1132
1123
  className: (0, clsx.default)("tremolo-piano-key-label-wrapper", wrapperClassName),
@@ -1152,8 +1143,7 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1152
1143
  const onPlayNote = usePianoContext((s) => s.onPlayNote);
1153
1144
  const onStopNote = usePianoContext((s) => s.onStopNote);
1154
1145
  const label = usePianoContext((s) => s.label);
1155
- const notePosition = usePianoContext((s) => s.notePosition);
1156
- const position = notePosition(noteNumber);
1146
+ const position = usePianoContext((s) => s.notePosition)(noteNumber);
1157
1147
  const disabled = noteNumber > midiMax;
1158
1148
  const colors = {
1159
1149
  "--bg": bg,
@@ -1163,10 +1153,10 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1163
1153
  };
1164
1154
  (0, react.useImperativeHandle)(ref, () => {
1165
1155
  return {
1166
- play() {
1156
+ play(velocity) {
1167
1157
  if (disabled) return;
1168
1158
  setPlayed(true);
1169
- if (onPlayNote) onPlayNote(noteNumber);
1159
+ if (onPlayNote) onPlayNote(noteNumber, velocity);
1170
1160
  },
1171
1161
  stop() {
1172
1162
  setPlayed(false);
@@ -1257,12 +1247,9 @@ const SHORTCUTS = { HOME_ROW: { keys: [
1257
1247
  //#endregion
1258
1248
  //#region src/components/Piano/index.tsx
1259
1249
  /**
1260
- * Piano component
1261
- *
1262
- * TODO:
1263
- * - add scale highlight
1250
+ * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
1251
+ * @category Piano
1264
1252
  */
1265
- /** @category Piano */
1266
1253
  function getNoteRangeArray(noteRange) {
1267
1254
  return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1268
1255
  }
@@ -1271,7 +1258,7 @@ const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth;
1271
1258
  * Customizable piano component.
1272
1259
  * @category Piano
1273
1260
  */
1274
- 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) => {
1275
1262
  const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
1276
1263
  const keyRefs = (0, react.useRef)([]);
1277
1264
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
@@ -1342,9 +1329,7 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1342
1329
  ]);
1343
1330
  const onDrag = (0, react.useCallback)((perX, perY) => {
1344
1331
  if (!pianoRef.current) return;
1345
- const x = perX * staticWidth;
1346
- const y = perY * pianoRef.current.clientHeight;
1347
- const note = getHitKeyIndex(x, y);
1332
+ const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
1348
1333
  const index = noteRangeArray.indexOf(note);
1349
1334
  if (index == -1) return;
1350
1335
  if (hitKeyIndex.current != index) {
@@ -1398,6 +1383,25 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1398
1383
  const index = keyboardShortcuts.keys.indexOf(e.key);
1399
1384
  if (index != -1) keyRefs.current[index]?.current?.stop();
1400
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
+ ]);
1401
1405
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoProvider, {
1402
1406
  notePosition,
1403
1407
  noteRange,
@@ -1434,7 +1438,7 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1434
1438
  }, note))
1435
1439
  })
1436
1440
  });
1437
- }
1441
+ });
1438
1442
 
1439
1443
  //#endregion
1440
1444
  //#region src/components/Slider/context.tsx
@@ -1628,7 +1632,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1628
1632
  "data-vertical": vertical,
1629
1633
  style: !defaultStyle ? style : {
1630
1634
  ...colors,
1631
- 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}%)`,
1632
1636
  borderRadius: (0, __tremolo_ui_functions.styleHelper)(thickness, "/", 2),
1633
1637
  width: !vertical ? length : thickness,
1634
1638
  height: vertical ? length : thickness,
@@ -1645,7 +1649,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1645
1649
  * Customizable slider
1646
1650
  * @category Slider
1647
1651
  */
1648
- 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) => {
1649
1653
  const trackElementRef = (0, react.useRef)(null);
1650
1654
  const thumbRef = (0, react.useRef)(null);
1651
1655
  const p = (0, __tremolo_ui_functions.toFixed)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) * 100);
@@ -1665,9 +1669,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1665
1669
  const onDrag = (0, react.useCallback)((nx, ny) => {
1666
1670
  if (!onChange || readonly) return;
1667
1671
  const n = vertical ? ny : nx;
1668
- const v = (0, __tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew);
1669
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
1670
- 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));
1671
1673
  }, [
1672
1674
  displayReversed,
1673
1675
  max,
@@ -1680,10 +1682,8 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1680
1682
  ]);
1681
1683
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
1682
1684
  let newValue;
1683
- if (eventType == "normalized") {
1684
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
1685
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
1686
- } 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;
1687
1687
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
1688
1688
  }, [
1689
1689
  max,
@@ -1746,7 +1746,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1746
1746
  onChange,
1747
1747
  updateValueByEvent
1748
1748
  ]);
1749
- (0, react.useImperativeHandle)(ref, () => {
1749
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1750
1750
  return {
1751
1751
  focus() {
1752
1752
  thumbRef.current?.focus();
@@ -1830,11 +1830,10 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1830
1830
  /** @category WheelObserver */
1831
1831
  function WheelObserver(props) {
1832
1832
  const { children, onWheel, as: Component = "div",...attributes } = props;
1833
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
1834
- if (onWheel) onWheel(event);
1835
- }, { passive: false }, [onWheel]);
1836
1833
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
1837
- ref: wheelRefCallback,
1834
+ ref: useRefCallbackEvent("wheel", (event) => {
1835
+ if (onWheel) onWheel(event);
1836
+ }, { passive: false }, [onWheel]),
1838
1837
  ...attributes,
1839
1838
  children
1840
1839
  });
@@ -1907,7 +1906,7 @@ const defaultValueOptions = {
1907
1906
  * Simple XYPad
1908
1907
  * @category XYPad
1909
1908
  */
1910
- 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) => {
1911
1910
  const x = (0, react.useMemo)(() => {
1912
1911
  return {
1913
1912
  ...defaultValueOptions,
@@ -1938,9 +1937,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1938
1937
  if (!onChange || readonly) return;
1939
1938
  const vx = (0, __tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx$1 : nx$1, x.min, x.max, x.skew);
1940
1939
  const vy = (0, __tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny$1 : ny$1, y.min, y.max, y.skew);
1941
- const newX = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max);
1942
- const newY = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max);
1943
- 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));
1944
1941
  }, [
1945
1942
  onChange,
1946
1943
  readonly,
@@ -1957,10 +1954,8 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1957
1954
  ]);
1958
1955
  const updateValueByEvent = (0, react.useCallback)((eventType, target, x$1) => {
1959
1956
  let v;
1960
- if (eventType == "normalized") {
1961
- const n = (0, __tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew);
1962
- v = (0, __tremolo_ui_functions.rawValue)(n + x$1, target.min, target.max, target.skew);
1963
- } 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;
1964
1959
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
1965
1960
  }, []);
1966
1961
  const handleKeyDown = (0, react.useCallback)((event) => {
@@ -2027,7 +2022,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2027
2022
  readonly,
2028
2023
  updateValueByEvent
2029
2024
  ]);
2030
- (0, react.useImperativeHandle)(ref, () => {
2025
+ (0, react.useImperativeHandle)(forwardedRef, () => {
2031
2026
  return {
2032
2027
  focus() {
2033
2028
  thumbRef.current?.focus();
@@ -2090,6 +2085,9 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2090
2085
 
2091
2086
  //#endregion
2092
2087
  //#region src/hooks/useAnimationFrame.ts
2088
+ /**
2089
+ * @category hooks
2090
+ */
2093
2091
  function useAnimationFrame(callback = () => {}, deps = []) {
2094
2092
  const reqIdRef = (0, react.useRef)(-1);
2095
2093
  const loop = (0, react.useCallback)(() => {
@@ -2102,6 +2100,79 @@ function useAnimationFrame(callback = () => {}, deps = []) {
2102
2100
  }, [loop]);
2103
2101
  }
2104
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
+
2105
2176
  //#endregion
2106
2177
  exports.AnimationCanvas = AnimationCanvas;
2107
2178
  exports.AnimationKnob = AnimationKnob;
@@ -2111,7 +2182,9 @@ exports.DragObserver = DragObserver;
2111
2182
  exports.IncrementStepper = IncrementStepper;
2112
2183
  exports.KeyLabel = KeyLabel;
2113
2184
  exports.Knob = Knob;
2185
+ exports.NOT_SUPPORTED = NOT_SUPPORTED;
2114
2186
  exports.NumberInput = NumberInput;
2187
+ exports.PERMISSION_DENIED = PERMISSION_DENIED;
2115
2188
  exports.Piano = Piano;
2116
2189
  exports.SHORTCUTS = SHORTCUTS;
2117
2190
  exports.Scale = Scale;
@@ -2132,5 +2205,8 @@ exports.useDragWithElement = useDragWithElement;
2132
2205
  exports.useEventListener = useEventListener;
2133
2206
  exports.useInterval = useInterval;
2134
2207
  exports.useLongPress = useLongPress;
2208
+ exports.useMIDIAccess = useMIDIAccess;
2209
+ exports.useMIDIInput = useMIDIInput;
2210
+ exports.useMIDIMessage = useMIDIMessage;
2135
2211
  exports.useSliderContext = useSliderContext;
2136
2212
  //# sourceMappingURL=index.cjs.map