@tremolo-ui/react 0.0.11 → 0.1.0-alpha.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.
Files changed (153) hide show
  1. package/dist/cjs/index.js +711 -395
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AnimationCanvas/index.d.ts +2 -2
  4. package/dist/cjs/types/components/AnimationCanvas/index.d.ts.map +1 -1
  5. package/dist/cjs/types/components/AnimationKnob/index.d.ts +10 -3
  6. package/dist/cjs/types/components/AnimationKnob/index.d.ts.map +1 -1
  7. package/dist/cjs/types/components/Knob/index.d.ts +25 -3
  8. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
  9. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  10. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  11. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  12. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  13. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts +18 -0
  14. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  15. package/dist/cjs/types/components/NumberInput/Stepper.d.ts +2 -0
  16. package/dist/cjs/types/components/NumberInput/Stepper.d.ts.map +1 -1
  17. package/dist/cjs/types/components/NumberInput/context.d.ts +3 -2
  18. package/dist/cjs/types/components/NumberInput/context.d.ts.map +1 -1
  19. package/dist/cjs/types/components/NumberInput/index.d.ts +12 -3
  20. package/dist/cjs/types/components/NumberInput/index.d.ts.map +1 -1
  21. package/dist/cjs/types/components/Piano/KeyLabel.d.ts +1 -3
  22. package/dist/cjs/types/components/Piano/KeyLabel.d.ts.map +1 -1
  23. package/dist/cjs/types/components/Piano/context.d.ts +24 -0
  24. package/dist/cjs/types/components/Piano/context.d.ts.map +1 -0
  25. package/dist/cjs/types/components/Piano/index.d.ts +15 -12
  26. package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
  27. package/dist/cjs/types/components/Piano/key.d.ts +14 -19
  28. package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
  29. package/dist/cjs/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  30. package/dist/cjs/types/components/Slider/Scale.d.ts +1 -3
  31. package/dist/cjs/types/components/Slider/Scale.d.ts.map +1 -1
  32. package/dist/cjs/types/components/Slider/Thumb.d.ts +1 -0
  33. package/dist/cjs/types/components/Slider/Thumb.d.ts.map +1 -1
  34. package/dist/cjs/types/components/Slider/Track.d.ts +2 -2
  35. package/dist/cjs/types/components/Slider/Track.d.ts.map +1 -1
  36. package/dist/cjs/types/components/Slider/index.d.ts +5 -1
  37. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
  38. package/dist/cjs/types/components/WheelObserver/index.d.ts +3 -3
  39. package/dist/cjs/types/components/WheelObserver/index.d.ts.map +1 -1
  40. package/dist/cjs/types/components/XYPad/Area.d.ts +3 -3
  41. package/dist/cjs/types/components/XYPad/Area.d.ts.map +1 -1
  42. package/dist/cjs/types/components/XYPad/Thumb.d.ts +1 -0
  43. package/dist/cjs/types/components/XYPad/Thumb.d.ts.map +1 -1
  44. package/dist/cjs/types/components/XYPad/index.d.ts +6 -1
  45. package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
  46. package/dist/cjs/types/hooks/useDrag.d.ts.map +1 -1
  47. package/dist/cjs/types/hooks/useDragWithElement.d.ts +16 -0
  48. package/dist/cjs/types/hooks/useDragWithElement.d.ts.map +1 -0
  49. package/dist/cjs/types/hooks/useInterval.d.ts +2 -0
  50. package/dist/cjs/types/hooks/useInterval.d.ts.map +1 -0
  51. package/dist/cjs/types/hooks/useLongPress.d.ts +2 -0
  52. package/dist/cjs/types/hooks/useLongPress.d.ts.map +1 -0
  53. package/dist/cjs/types/index.d.ts +5 -0
  54. package/dist/cjs/types/index.d.ts.map +1 -1
  55. package/dist/esm/index.js +706 -396
  56. package/dist/esm/index.js.map +1 -1
  57. package/dist/esm/types/components/AnimationCanvas/index.d.ts +2 -2
  58. package/dist/esm/types/components/AnimationCanvas/index.d.ts.map +1 -1
  59. package/dist/esm/types/components/AnimationKnob/index.d.ts +10 -3
  60. package/dist/esm/types/components/AnimationKnob/index.d.ts.map +1 -1
  61. package/dist/esm/types/components/Knob/index.d.ts +25 -3
  62. package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
  63. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  64. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  65. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  66. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  67. package/dist/esm/types/components/NumberInput/InternalInput.d.ts +18 -0
  68. package/dist/esm/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  69. package/dist/esm/types/components/NumberInput/Stepper.d.ts +2 -0
  70. package/dist/esm/types/components/NumberInput/Stepper.d.ts.map +1 -1
  71. package/dist/esm/types/components/NumberInput/context.d.ts +3 -2
  72. package/dist/esm/types/components/NumberInput/context.d.ts.map +1 -1
  73. package/dist/esm/types/components/NumberInput/index.d.ts +12 -3
  74. package/dist/esm/types/components/NumberInput/index.d.ts.map +1 -1
  75. package/dist/esm/types/components/Piano/KeyLabel.d.ts +1 -3
  76. package/dist/esm/types/components/Piano/KeyLabel.d.ts.map +1 -1
  77. package/dist/esm/types/components/Piano/context.d.ts +24 -0
  78. package/dist/esm/types/components/Piano/context.d.ts.map +1 -0
  79. package/dist/esm/types/components/Piano/index.d.ts +15 -12
  80. package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
  81. package/dist/esm/types/components/Piano/key.d.ts +14 -19
  82. package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
  83. package/dist/esm/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  84. package/dist/esm/types/components/Slider/Scale.d.ts +1 -3
  85. package/dist/esm/types/components/Slider/Scale.d.ts.map +1 -1
  86. package/dist/esm/types/components/Slider/Thumb.d.ts +1 -0
  87. package/dist/esm/types/components/Slider/Thumb.d.ts.map +1 -1
  88. package/dist/esm/types/components/Slider/Track.d.ts +2 -2
  89. package/dist/esm/types/components/Slider/Track.d.ts.map +1 -1
  90. package/dist/esm/types/components/Slider/index.d.ts +5 -1
  91. package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
  92. package/dist/esm/types/components/WheelObserver/index.d.ts +3 -3
  93. package/dist/esm/types/components/WheelObserver/index.d.ts.map +1 -1
  94. package/dist/esm/types/components/XYPad/Area.d.ts +3 -3
  95. package/dist/esm/types/components/XYPad/Area.d.ts.map +1 -1
  96. package/dist/esm/types/components/XYPad/Thumb.d.ts +1 -0
  97. package/dist/esm/types/components/XYPad/Thumb.d.ts.map +1 -1
  98. package/dist/esm/types/components/XYPad/index.d.ts +6 -1
  99. package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
  100. package/dist/esm/types/hooks/useDrag.d.ts.map +1 -1
  101. package/dist/esm/types/hooks/useDragWithElement.d.ts +16 -0
  102. package/dist/esm/types/hooks/useDragWithElement.d.ts.map +1 -0
  103. package/dist/esm/types/hooks/useInterval.d.ts +2 -0
  104. package/dist/esm/types/hooks/useInterval.d.ts.map +1 -0
  105. package/dist/esm/types/hooks/useLongPress.d.ts +2 -0
  106. package/dist/esm/types/hooks/useLongPress.d.ts.map +1 -0
  107. package/dist/esm/types/index.d.ts +5 -0
  108. package/dist/esm/types/index.d.ts.map +1 -1
  109. package/dist/styles/index.css +117 -33
  110. package/package.json +3 -2
  111. package/src/components/AnimationCanvas/index.tsx +14 -6
  112. package/src/components/AnimationKnob/index.tsx +216 -187
  113. package/src/components/Knob/index.tsx +244 -171
  114. package/src/components/NumberInput/DecrementStepper.tsx +9 -2
  115. package/src/components/NumberInput/IncrementStepper.tsx +9 -2
  116. package/src/components/NumberInput/InternalInput.tsx +219 -0
  117. package/src/components/NumberInput/Stepper.tsx +2 -0
  118. package/src/components/NumberInput/context.tsx +20 -8
  119. package/src/components/NumberInput/index.css +76 -22
  120. package/src/components/NumberInput/index.tsx +94 -162
  121. package/src/components/Piano/KeyLabel.tsx +9 -5
  122. package/src/components/Piano/context.tsx +108 -0
  123. package/src/components/Piano/index.css +24 -6
  124. package/src/components/Piano/index.tsx +192 -122
  125. package/src/components/Piano/key.tsx +215 -0
  126. package/src/components/Piano/keyboardShortcuts.ts +2 -2
  127. package/src/components/Slider/Scale.tsx +1 -4
  128. package/src/components/Slider/Thumb.tsx +4 -0
  129. package/src/components/Slider/Track.tsx +5 -2
  130. package/src/components/Slider/index.css +5 -2
  131. package/src/components/Slider/index.tsx +84 -71
  132. package/src/components/WheelObserver/index.tsx +4 -2
  133. package/src/components/XYPad/Area.tsx +7 -6
  134. package/src/components/XYPad/Thumb.tsx +6 -0
  135. package/src/components/XYPad/index.css +12 -2
  136. package/src/components/XYPad/index.tsx +116 -90
  137. package/src/hooks/useDrag.ts +6 -7
  138. package/src/hooks/useDragWithElement.ts +76 -0
  139. package/src/hooks/useInterval.ts +27 -0
  140. package/src/hooks/useLongPress.ts +31 -0
  141. package/src/index.ts +6 -0
  142. package/src/styles/index.css +0 -1
  143. package/dist/cjs/types/components/Piano/BlackKey.d.ts +0 -5
  144. package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +0 -1
  145. package/dist/cjs/types/components/Piano/WhiteKey.d.ts +0 -7
  146. package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +0 -1
  147. package/dist/esm/types/components/Piano/BlackKey.d.ts +0 -5
  148. package/dist/esm/types/components/Piano/BlackKey.d.ts.map +0 -1
  149. package/dist/esm/types/components/Piano/WhiteKey.d.ts +0 -7
  150. package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +0 -1
  151. package/src/components/Piano/BlackKey.tsx +0 -118
  152. package/src/components/Piano/WhiteKey.tsx +0 -120
  153. package/src/components/Piano/key.ts +0 -46
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
- import React, { useRef, useCallback, useEffect, useInsertionEffect, useMemo, createContext, useContext, useState, forwardRef, useImperativeHandle, createRef } from 'react';
2
+ import React, { useRef, useCallback, useEffect, useInsertionEffect, useMemo, forwardRef, useImperativeHandle, createContext, useContext, useState, createRef } from 'react';
3
3
 
4
4
  /******************************************************************************
5
5
  Copyright (c) Microsoft Corporation.
@@ -67,7 +67,7 @@ function setDprConfig(canvas, context, width, height, dpr) {
67
67
  canvas.style.height = `${height}px`;
68
68
  }
69
69
  function AnimationCanvas(_a) {
70
- var { width = 100, height = 100, relativeSize = false, reduceFlickering = true, options, init, draw, className } = _a, props = __rest(_a, ["width", "height", "relativeSize", "reduceFlickering", "options", "init", "draw", "className"]);
70
+ var { width = 100, height = 100, relativeSize = false, reduceFlickering = true, options, init, draw, className, onContextMenu = (event) => event.preventDefault() } = _a, props = __rest(_a, ["width", "height", "relativeSize", "reduceFlickering", "options", "init", "draw", "className", "onContextMenu"]);
71
71
  const canvasRef = useRef(null);
72
72
  const memoCanvasRef = useRef(null);
73
73
  const reqIdRef = useRef(-1);
@@ -116,7 +116,11 @@ function AnimationCanvas(_a) {
116
116
  for (const prop of drawingState) {
117
117
  context[prop] = contextMemo[prop];
118
118
  }
119
- if (reduceFlickering && memoContext) {
119
+ if (reduceFlickering &&
120
+ memoContext &&
121
+ memoCanvas &&
122
+ memoCanvas.width > 0 &&
123
+ memoCanvas.height > 0) {
120
124
  context.drawImage(memoContext.canvas, 0, 0);
121
125
  }
122
126
  }
@@ -148,7 +152,7 @@ function AnimationCanvas(_a) {
148
152
  };
149
153
  }
150
154
  }, [loop, init, options, reduceFlickering, relativeSize]);
151
- return (jsxs(Fragment, { children: [jsx("canvas", Object.assign({ className: clsx('tremolo-animation-canvas', className), width: relativeSize ? 0 : width, height: relativeSize ? 0 : height, ref: canvasRef }, props)), relativeSize && reduceFlickering && (jsx("canvas", { style: { display: 'none' }, ref: memoCanvasRef }))] }));
155
+ return (jsxs(Fragment, { children: [jsx("canvas", Object.assign({ className: clsx('tremolo-animation-canvas', className), width: relativeSize ? 0 : width, height: relativeSize ? 0 : height, ref: canvasRef, onContextMenu: onContextMenu }, props)), relativeSize && reduceFlickering && (jsx("canvas", { style: { display: 'none' }, ref: memoCanvasRef }))] }));
152
156
  }
153
157
 
154
158
  /**
@@ -256,7 +260,7 @@ function xor(a = false, b = false) {
256
260
  /**
257
261
  * @category MIDI
258
262
  */
259
- const noteNames = [
263
+ const noteKeys = [
260
264
  'C',
261
265
  'C#',
262
266
  'D',
@@ -290,18 +294,16 @@ function parseNoteName(noteName) {
290
294
  */
291
295
  function noteNumber(noteName) {
292
296
  const { letter, accidental, octave } = parseNoteName(noteName);
293
- const noteIndex = noteNames.indexOf(letter.toLocaleUpperCase());
297
+ const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase());
294
298
  const accidentalValue = (accidental[0] == 'b' ? -1 : 1) * accidental.length;
295
299
  return noteIndex + 12 * (Number(octave) + 1) + accidentalValue;
296
300
  }
297
301
  /**
298
- * Convert noteNumber to noteName
302
+ * Convert noteNumber to noteKey
299
303
  * @category MIDI
300
304
  */
301
- function noteName(noteNumber) {
302
- const noteIndex = mod(noteNumber, 12);
303
- const octave = Math.floor(noteNumber / 12) - 1;
304
- return noteNames[noteIndex] + octave;
305
+ function noteKey(noteNumber) {
306
+ return noteKeys[mod(noteNumber, 12)];
305
307
  }
306
308
  /**
307
309
  * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
@@ -317,6 +319,13 @@ function isWhiteKey(note) {
317
319
  mod(n, 12) == 9 ||
318
320
  mod(n, 12) == 11);
319
321
  }
322
+ /**
323
+ * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
324
+ * @category MIDI
325
+ */
326
+ function isBlackKey(note) {
327
+ return !isWhiteKey(note);
328
+ }
320
329
 
321
330
  /**
322
331
  * This hook is user-land implementation of the experimental `useEffectEvent` hook.
@@ -397,8 +406,7 @@ function useDrag({ threshold = 1, onDrag, onDragStart, onDragEnd, }) {
397
406
  }
398
407
  if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold)
399
408
  return;
400
- if (onDrag)
401
- onDrag(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
409
+ onDrag(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
402
410
  }, [threshold, onDrag]);
403
411
  const refHandler = useRefCallbackEvent('touchmove', handleDrag, { passive: false }, [onDrag]);
404
412
  const pointerDownHandler = useCallback((event) => {
@@ -424,13 +432,14 @@ function removeNoSelect() {
424
432
  }
425
433
 
426
434
  /**
427
- * simple rotary Knob.
435
+ * Animatable rotary knob.
428
436
  * @category AnimationKnob
429
437
  */
430
- function AnimationKnob(_a) {
438
+ const AnimationKnob = forwardRef((_a, ref) => {
431
439
  var { value, min, max, defaultValue = min, startValue = min, step = 1, skew = 1, size, width = 50, height = 50, activeColor = '#7998ec', inactiveColor = '#eee', bg = '#ccc', thumb = '#4e76e6', lineWeight = 6, thumbWeight = 4, draw, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], enableDoubleClickDefault = true, style, className, onChange } = _a, props = __rest(_a, ["value", "min", "max", "defaultValue", "startValue", "step", "skew", "size", "width", "height", "activeColor", "inactiveColor", "bg", "thumb", "lineWeight", "thumbWeight", "draw", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "style", "className", "onChange"]);
432
440
  // -- state and ref ---
433
441
  const valueRef = useRef(0);
442
+ const elmRef = useRef(null);
434
443
  // --- interpret props ---
435
444
  const isRelativeSize = typeof (size !== null && size !== void 0 ? size : width) == 'string' || typeof height == 'string';
436
445
  const onDrag = useCallback((_x, y) => {
@@ -487,14 +496,27 @@ function AnimationKnob(_a) {
487
496
  if (onChange)
488
497
  onChange(clamp(stepValue(v, step), min, max));
489
498
  }, { passive: false }, [wheel, value, min, max, skew, step, onChange]);
499
+ useImperativeHandle(ref, () => {
500
+ return {
501
+ focus() {
502
+ var _a;
503
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
504
+ },
505
+ blur() {
506
+ var _a;
507
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
508
+ },
509
+ };
510
+ }, []);
490
511
  return (jsx("div", Object.assign({ className: clsx('tremolo-animation-knob', className), ref: (div) => {
512
+ elmRef.current = div;
491
513
  wheelRefCallback(div);
492
514
  touchMoveRefCallback(div);
493
515
  }, role: "slider", tabIndex: 0, "aria-valuenow": value, "aria-valuemax": max, "aria-valuemin": min, style: Object.assign({ display: 'inline-block', width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height, cursor: 'pointer', WebkitTapHighlightColor: 'transparent' }, style), onPointerDown: pointerDownHandler, onDoubleClick: () => {
494
516
  if (enableDoubleClickDefault && onChange) {
495
517
  onChange(defaultValue);
496
518
  }
497
- }, onKeyDown: handleKeyDown, onContextMenu: (e) => e.preventDefault() }, props, { children: jsx(AnimationCanvas, Object.assign({}, (isRelativeSize
519
+ }, onKeyDown: handleKeyDown }, props, { children: jsx(AnimationCanvas, Object.assign({}, (isRelativeSize
498
520
  ? { relativeSize: true }
499
521
  : {
500
522
  width: size !== null && size !== void 0 ? size : width,
@@ -541,7 +563,7 @@ function AnimationKnob(_a) {
541
563
  ctx.lineWidth = thumbWeight;
542
564
  ctx.stroke();
543
565
  } })) })));
544
- }
566
+ });
545
567
 
546
568
  /** @category DragObserver */
547
569
  function DragObserver(props) {
@@ -555,15 +577,20 @@ function DragObserver(props) {
555
577
  return (jsx(Component, Object.assign({ ref: refCallback, onPointerDown: pointerDownHandler }, attributes, { children: children })));
556
578
  }
557
579
 
558
- /** @category Knob */
559
- function Knob(_a) {
560
- var { 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, className, classes } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "defaultValue", "startValue", "size", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "disabled", "readonly", "activeLine", "inactiveLine", "thumb", "thumbLine", "onChange", "className", "classes"]);
580
+ /**
581
+ * Interactive rotary knob component implemented in SVG.
582
+ *
583
+ * @category Knob
584
+ */
585
+ const Knob = forwardRef((_a, ref) => {
586
+ var { 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 } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "defaultValue", "startValue", "size", "bodyNoSelect", "wheel", "keyboard", "enableDoubleClickDefault", "disabled", "readonly", "activeLine", "inactiveLine", "thumb", "thumbLine", "onChange", "onKeyDown", "onPointerDown", "onDoubleClick", "className", "classes"]);
561
587
  const valueRef = useRef(0);
588
+ const elmRef = useRef(null);
562
589
  // --- interpret props ---
563
590
  const padding = 8; // %
564
591
  const thumbLineWeight = 6; // %
565
- const thumbLength = 35; // %
566
- const lineWeight = 3; // px
592
+ const thumbLineLength = 35; // %
593
+ const lineWeight = size * 0.06; // px
567
594
  const p = normalizeValue(value, min, max, skew);
568
595
  const s = normalizeValue(startValue, min, max, skew);
569
596
  // --- internal functions ---
@@ -574,6 +601,17 @@ function Knob(_a) {
574
601
  const v2 = clamp(stepValue(v, step), min, max);
575
602
  onChange(v2);
576
603
  }, [max, min, onChange, readonly, skew, step]);
604
+ const updateValueByEvent = useCallback((eventType, x) => {
605
+ let newValue;
606
+ if (eventType == 'normalized') {
607
+ const n = normalizeValue(value, min, max, skew);
608
+ newValue = rawValue(n + x, min, max, skew);
609
+ }
610
+ else {
611
+ newValue = value + x;
612
+ }
613
+ return clamp(stepValue(newValue, step), min, max);
614
+ }, [max, min, skew, step, value]);
577
615
  const handleKeyDown = useCallback((event) => {
578
616
  if (!keyboard || !onChange || readonly)
579
617
  return;
@@ -581,17 +619,9 @@ function Knob(_a) {
581
619
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
582
620
  event.preventDefault();
583
621
  const x = key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
584
- let v;
585
- if (keyboard[0] == 'normalized') {
586
- const n = normalizeValue(value, min, max, skew);
587
- v = rawValue(n + x, min, max, skew);
588
- }
589
- else {
590
- v = value + x;
591
- }
592
- onChange(clamp(stepValue(v, step), min, max));
622
+ onChange(updateValueByEvent(keyboard[0], x));
593
623
  }
594
- }, [value, min, max, step, skew, keyboard, readonly, onChange]);
624
+ }, [keyboard, onChange, readonly, updateValueByEvent]);
595
625
  // --- hooks ---
596
626
  const [touchMoveRefCallback, pointerDownHandler] = useDrag({
597
627
  onDrag: onDrag,
@@ -606,21 +636,26 @@ function Knob(_a) {
606
636
  },
607
637
  });
608
638
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
609
- if (!wheel)
639
+ if (!wheel || readonly)
610
640
  return;
611
641
  event.preventDefault();
612
- const x = event.deltaY > 0 ? -wheel[1] : wheel[1];
613
- let v;
614
- if (wheel[0] == 'normalized') {
615
- const n = normalizeValue(value, min, max, skew);
616
- v = rawValue(n + x, min, max, skew);
617
- }
618
- else {
619
- v = value + x;
620
- }
621
- if (onChange)
622
- onChange(clamp(stepValue(v, step), min, max));
623
- }, { passive: false }, [wheel, value, min, max, skew, step, onChange]);
642
+ if (!onChange || event.deltaY == 0)
643
+ return;
644
+ const x = Math.sign(event.deltaY) * -wheel[1];
645
+ onChange(updateValueByEvent(wheel[0], x));
646
+ }, { passive: false }, [wheel, onChange, readonly, updateValueByEvent]);
647
+ useImperativeHandle(ref, () => {
648
+ return {
649
+ focus() {
650
+ var _a;
651
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
652
+ },
653
+ blur() {
654
+ var _a;
655
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
656
+ },
657
+ };
658
+ }, []);
624
659
  const center = size / 2;
625
660
  const r1 = -135;
626
661
  const r2 = -135 + Math.min(p, s) * 270;
@@ -635,13 +670,62 @@ function Knob(_a) {
635
670
  const x4 = center + center * Math.cos(radian(r4 - 90));
636
671
  const y4 = center + center * Math.sin(radian(r4 - 90));
637
672
  return (jsxs("svg", Object.assign({ className: clsx('tremolo-knob', className), ref: (div) => {
673
+ elmRef.current = div;
638
674
  wheelRefCallback(div);
639
675
  touchMoveRefCallback(div);
640
- }, width: size, height: size, tabIndex: 0, "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled, "aria-readonly": readonly, onPointerDown: pointerDownHandler, onDoubleClick: () => {
676
+ }, width: size, height: size, tabIndex: 0, role: "slider", "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": disabled, "aria-readonly": readonly, onPointerDown: (event) => {
677
+ pointerDownHandler(event);
678
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
679
+ }, onDoubleClick: (event) => {
641
680
  if (enableDoubleClickDefault && onChange) {
642
681
  onChange(defaultValue);
643
682
  }
644
- }, onKeyDown: handleKeyDown, onContextMenu: (e) => e.preventDefault() }, props, { children: [startValue > min && (jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsx("path", { className: clsx('tremolo-knob-active-line', classes === null || classes === void 0 ? void 0 : classes.activeLine), d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`, fill: "none", stroke: activeLine || 'currentColor', strokeWidth: lineWeight }), startValue < max && (jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxs("svg", { className: clsx('tremolo-knob-thumb', classes === null || classes === void 0 ? void 0 : classes.thumb), children: [jsx("circle", { cx: "50%", cy: "50%", r: `${50 - padding}%`, fill: thumb || 'currentColor' }), jsx("line", { className: clsx('tremolo-knob-thumb-line', classes === null || classes === void 0 ? void 0 : classes.thumbLine), x1: "50%", y1: `${padding}%`, x2: "50%", y2: `${thumbLength}%`, stroke: thumbLine || 'currentColor', strokeWidth: `${thumbLineWeight}%`, transform: `rotate(${ -135 + p * 270})`, "transform-origin": "50% 50%" })] })] })));
683
+ onDoubleClick === null || onDoubleClick === void 0 ? void 0 : onDoubleClick(event);
684
+ }, onKeyDown: (event) => {
685
+ handleKeyDown(event);
686
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
687
+ } }, props, { children: [startValue > min && (jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsx("path", { className: clsx('tremolo-knob-active-line', classes === null || classes === void 0 ? void 0 : classes.activeLine), d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`, fill: "none", stroke: activeLine || 'currentColor', strokeWidth: lineWeight }), startValue < max && (jsx("path", { className: clsx('tremolo-knob-inactive-line', classes === null || classes === void 0 ? void 0 : classes.inactiveLine), d: `M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`, fill: "none", stroke: inactiveLine || 'currentColor', strokeWidth: lineWeight })), jsxs("svg", { className: clsx('tremolo-knob-thumb', classes === null || classes === void 0 ? void 0 : classes.thumb), children: [jsx("circle", { cx: "50%", cy: "50%", r: `${50 - padding}%`, fill: thumb || 'currentColor' }), jsx("line", { className: clsx('tremolo-knob-thumb-line', classes === null || classes === void 0 ? void 0 : classes.thumbLine), x1: "50%", y1: `${padding}%`, x2: "50%", y2: `${thumbLineLength}%`, stroke: thumbLine || 'currentColor', strokeWidth: `${thumbLineWeight}%`,
688
+ // NOTE
689
+ // https://bugs.webkit.org/show_bug.cgi?id=201854
690
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
691
+ style: {
692
+ transform: `rotate(${ -135 + p * 270}deg)`,
693
+ transformOrigin: '50% 50%',
694
+ } })] })] })));
695
+ });
696
+
697
+ const createStoreImpl = (createState) => {
698
+ let state;
699
+ const listeners = /* @__PURE__ */ new Set();
700
+ const setState = (partial, replace) => {
701
+ const nextState = typeof partial === "function" ? partial(state) : partial;
702
+ if (!Object.is(nextState, state)) {
703
+ const previousState = state;
704
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
705
+ listeners.forEach((listener) => listener(state, previousState));
706
+ }
707
+ };
708
+ const getState = () => state;
709
+ const getInitialState = () => initialState;
710
+ const subscribe = (listener) => {
711
+ listeners.add(listener);
712
+ return () => listeners.delete(listener);
713
+ };
714
+ const api = { setState, getState, getInitialState, subscribe };
715
+ const initialState = state = createState(setState, getState, api);
716
+ return api;
717
+ };
718
+ const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
719
+
720
+ const identity = (arg) => arg;
721
+ function useStore(api, selector = identity) {
722
+ const slice = React.useSyncExternalStore(
723
+ api.subscribe,
724
+ () => selector(api.getState()),
725
+ () => selector(api.getInitialState())
726
+ );
727
+ React.useDebugValue(slice);
728
+ return slice;
645
729
  }
646
730
 
647
731
  /**
@@ -690,40 +774,6 @@ function parseValue(inputString, units, digit) {
690
774
  };
691
775
  }
692
776
 
693
- const createStoreImpl = (createState) => {
694
- let state;
695
- const listeners = /* @__PURE__ */ new Set();
696
- const setState = (partial, replace) => {
697
- const nextState = typeof partial === "function" ? partial(state) : partial;
698
- if (!Object.is(nextState, state)) {
699
- const previousState = state;
700
- state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
701
- listeners.forEach((listener) => listener(state, previousState));
702
- }
703
- };
704
- const getState = () => state;
705
- const getInitialState = () => initialState;
706
- const subscribe = (listener) => {
707
- listeners.add(listener);
708
- return () => listeners.delete(listener);
709
- };
710
- const api = { setState, getState, getInitialState, subscribe };
711
- const initialState = state = createState(setState, getState, api);
712
- return api;
713
- };
714
- const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
715
-
716
- const identity = (arg) => arg;
717
- function useStore(api, selector = identity) {
718
- const slice = React.useSyncExternalStore(
719
- api.subscribe,
720
- () => selector(api.getState()),
721
- () => selector(api.getInitialState())
722
- );
723
- React.useDebugValue(slice);
724
- return slice;
725
- }
726
-
727
777
  function safeClamp(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
728
778
  return clamp(value, min, max);
729
779
  }
@@ -734,6 +784,7 @@ const createNumberInputStore = (initProps) => {
734
784
  min: 0,
735
785
  max: 0,
736
786
  step: 1,
787
+ readonly: false,
737
788
  keepWithinRange: true,
738
789
  onChange: (_v, _t) => { },
739
790
  };
@@ -742,8 +793,10 @@ const createNumberInputStore = (initProps) => {
742
793
  valueAsNumber: parseValue(String((initProps === null || initProps === void 0 ? void 0 : initProps.value) || ''), initProps === null || initProps === void 0 ? void 0 : initProps.units, initProps === null || initProps === void 0 ? void 0 : initProps.digit).rawValue,
743
794
  }), { increment: () => set((state) => {
744
795
  var _a, _b;
745
- let next = parseValue(state.value, state.units, state.digit).rawValue +
746
- ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
796
+ if (state.readonly)
797
+ return {};
798
+ const current = parseValue(state.value, state.units, state.digit).rawValue;
799
+ let next = current + ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
747
800
  if (state.keepWithinRange) {
748
801
  next = safeClamp(next, state.min, state.max);
749
802
  }
@@ -755,8 +808,10 @@ const createNumberInputStore = (initProps) => {
755
808
  };
756
809
  }), decrement: () => set((state) => {
757
810
  var _a, _b;
758
- let next = parseValue(state.value, state.units, state.digit).rawValue -
759
- ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
811
+ if (state.readonly)
812
+ return {};
813
+ const current = parseValue(state.value, state.units, state.digit).rawValue;
814
+ let next = current - ((_a = state.step) !== null && _a !== void 0 ? _a : 1);
760
815
  if (state.keepWithinRange) {
761
816
  next = safeClamp(next, state.min, state.max);
762
817
  }
@@ -767,6 +822,8 @@ const createNumberInputStore = (initProps) => {
767
822
  valueAsNumber: next,
768
823
  };
769
824
  }), change: (value) => set((state) => {
825
+ if (state.readonly)
826
+ return {};
770
827
  const v = value !== null && value !== void 0 ? value : '';
771
828
  return {
772
829
  value: v,
@@ -803,36 +860,17 @@ function useNumberInputContext(selector) {
803
860
  return useStore(store, selector);
804
861
  }
805
862
 
806
- function Stepper(_a) {
807
- var { dynamic = true, children, className } = _a, props = __rest(_a, ["dynamic", "children", "className"]);
808
- return (jsx("div", Object.assign({ className: clsx('tremolo-number-input-stepper', className), "data-dynamic": dynamic }, props, { children: children })));
809
- }
810
-
811
- function IncrementStepper(_a) {
812
- var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
813
- const max = useNumberInputContext((s) => s.max);
814
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
815
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
816
- const increment = useNumberInputContext((s) => s.increment);
817
- // TODO: add long press event
818
- return (jsx("div", Object.assign({ className: clsx('tremolo-number-input-increment-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber >= (max !== null && max !== void 0 ? max : Number.MAX_SAFE_INTEGER), onPointerDown: increment }, props, { children: children || (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "18 15 12 9 6 15" }) })) })));
819
- }
820
-
821
- function DecrementStepper(_a) {
822
- var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
823
- const min = useNumberInputContext((s) => s.min);
824
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
825
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
826
- const decrement = useNumberInputContext((s) => s.decrement);
827
- // TODO: add long press event
828
- return (jsx("div", Object.assign({ className: clsx('tremolo-number-input-decrement-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber <= (min !== null && min !== void 0 ? min : Number.MIN_SAFE_INTEGER), onPointerDown: decrement }, props, { children: children || (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "6 9 12 15 18 9" }) })) })));
829
- }
830
-
831
- function InternalInput(_a) {
832
- var { min, max, step, defaultValue, units, disabled, digit, selectWithFocus, blurOnEnter, keepWithinRange, clampValueOnBlur, wheel, keyboard, wrapperClassName, className, onChange, onFocus, onBlur, children: _children } = _a, props = __rest(_a, ["min", "max", "step", "defaultValue", "units", "disabled", "digit", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
863
+ const InternalInput = forwardRef((_a, ref) => {
864
+ var { disabled, readonly, selectWithFocus, blurOnEnter, clampValueOnBlur, wheel, keyboard, className, onChange, onFocus, onBlur, onKeyDown, children: _children } = _a, props = __rest(_a, ["disabled", "readonly", "selectWithFocus", "blurOnEnter", "clampValueOnBlur", "wheel", "keyboard", "className", "onChange", "onFocus", "onBlur", "onKeyDown", "children"]);
865
+ const elmRef = useRef(null);
833
866
  const [editing, setEditing] = useState(false);
834
867
  const value = useNumberInputContext((s) => s.value);
835
868
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
869
+ const min = useNumberInputContext((s) => s.min);
870
+ const max = useNumberInputContext((s) => s.max);
871
+ const step = useNumberInputContext((s) => s.step);
872
+ const units = useNumberInputContext((s) => s.units);
873
+ const digit = useNumberInputContext((s) => s.digit);
836
874
  const change = useNumberInputContext((s) => s.change);
837
875
  const error = useMemo(() => {
838
876
  if (editing)
@@ -840,11 +878,71 @@ function InternalInput(_a) {
840
878
  const v = valueAsNumber;
841
879
  return (min !== null && min !== void 0 ? min : -Infinity) > v || v > (max !== null && max !== void 0 ? max : Infinity);
842
880
  }, [editing, max, min, valueAsNumber]);
843
- return (jsx("input", Object.assign({ className: clsx('tremolo-number-input', className), value: value, min: min, max: max, step: step, type: "text", disabled: disabled, tabIndex: -1, "data-error": error, onChange: (event) => {
881
+ const updateValueByEvent = useCallback((eventType, x) => {
882
+ let newValue;
883
+ if (eventType == 'normalized') {
884
+ if (!min || !max) {
885
+ throw new Error('required parameter: min, max'); // TODO
886
+ }
887
+ const n = normalizeValue(valueAsNumber, min, max);
888
+ newValue = rawValue(n + x, min, max);
889
+ return clamp(stepValue(newValue, step), min, max);
890
+ }
891
+ else {
892
+ newValue = valueAsNumber + x;
893
+ return stepValue(newValue, step);
894
+ }
895
+ }, [max, min, step, valueAsNumber]);
896
+ const handleKeyDown = useCallback((event) => {
897
+ if (!keyboard || !onChange || readonly)
898
+ return;
899
+ const key = event.key;
900
+ if (!['ArrowUp', 'ArrowDown'].includes(key))
901
+ return;
902
+ event.preventDefault();
903
+ const x = key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
904
+ const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
905
+ console.log('key control');
906
+ onChange === null || onChange === void 0 ? void 0 : onChange(parsed.rawValue, parsed.formatValue);
907
+ }, [keyboard, readonly, onChange, updateValueByEvent]);
908
+ const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
909
+ if (!wheel || readonly)
910
+ return;
911
+ event.preventDefault();
912
+ if (!onChange || event.deltaY == 0)
913
+ return;
914
+ const x = Math.sign(event.deltaY) * -wheel[1];
915
+ const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
916
+ console.log('wheel control');
917
+ onChange === null || onChange === void 0 ? void 0 : onChange(parsed.rawValue, parsed.formatValue);
918
+ }, { passive: false }, [wheel, readonly, onChange, updateValueByEvent]);
919
+ useImperativeHandle(ref, () => {
920
+ return {
921
+ focus() {
922
+ var _a;
923
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.focus();
924
+ },
925
+ blur() {
926
+ var _a;
927
+ (_a = elmRef.current) === null || _a === void 0 ? void 0 : _a.blur();
928
+ },
929
+ };
930
+ }, []);
931
+ return (jsx("input", Object.assign({ ref: (input) => {
932
+ elmRef.current = input;
933
+ wheelRefCallback(input);
934
+ }, className: clsx('tremolo-number-input', className), value: value, min: min, max: max, step: step, type: "text", "aria-disabled": disabled, "aria-readonly": readonly, tabIndex: -1, "data-error": error, onChange: (event) => {
935
+ if (readonly)
936
+ return;
844
937
  const v = event.currentTarget.value;
938
+ const cursorPos = event.target.selectionStart;
845
939
  change(v);
846
940
  const valueAsNumber = parseValue(v, units, digit).rawValue;
847
941
  onChange === null || onChange === void 0 ? void 0 : onChange(valueAsNumber, v);
942
+ if (cursorPos) {
943
+ event.target.selectionStart = cursorPos;
944
+ event.target.selectionEnd = cursorPos;
945
+ }
848
946
  }, onFocus: (event) => {
849
947
  setEditing(true);
850
948
  const v = event.currentTarget.value;
@@ -873,53 +971,190 @@ function InternalInput(_a) {
873
971
  if (blurOnEnter && event.key == 'Enter') {
874
972
  event.currentTarget.blur();
875
973
  }
974
+ handleKeyDown(event);
975
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
876
976
  } }, props)));
977
+ });
978
+
979
+ /** @category NumberInput */
980
+ function Stepper(_a) {
981
+ var { dynamic = true, children, className } = _a, props = __rest(_a, ["dynamic", "children", "className"]);
982
+ return (jsx("div", Object.assign({ className: clsx('tremolo-number-input-stepper', className), "data-dynamic": dynamic }, props, { children: children })));
983
+ }
984
+
985
+ function useInterval(callback, delay) {
986
+ const savedCallback = useRef(callback);
987
+ // Remember the latest callback if it changes.
988
+ useEffect(() => {
989
+ savedCallback.current = callback;
990
+ }, [callback]);
991
+ // Set up the interval.
992
+ useEffect(() => {
993
+ // Don't schedule if no delay is specified.
994
+ // Note: 0 is a valid value for delay.
995
+ if (delay === null) {
996
+ return;
997
+ }
998
+ const id = setInterval(() => {
999
+ savedCallback.current();
1000
+ }, delay);
1001
+ return () => {
1002
+ clearInterval(id);
1003
+ };
1004
+ }, [delay]);
877
1005
  }
1006
+
1007
+ function useLongPress(callback, initialDelay = 500, interval = 40) {
1008
+ const [pressed, setPressed] = useState(false);
1009
+ const [delay, setDelay] = useState(initialDelay);
1010
+ useInterval(() => {
1011
+ callback();
1012
+ setDelay(interval);
1013
+ }, pressed ? delay : null);
1014
+ useEventListener(globalThis.window, 'pointerup', () => {
1015
+ setPressed(false);
1016
+ setDelay(initialDelay);
1017
+ });
1018
+ return useCallback(() => {
1019
+ callback();
1020
+ setPressed(true);
1021
+ }, [callback]);
1022
+ }
1023
+
1024
+ /** @category NumberInput */
1025
+ function IncrementStepper(_a) {
1026
+ var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
1027
+ const max = useNumberInputContext((s) => s.max);
1028
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
1029
+ const readonly = useNumberInputContext((s) => s.readonly);
1030
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
1031
+ const increment = useNumberInputContext((s) => s.increment);
1032
+ const press = useLongPress(increment);
1033
+ return (
1034
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
1035
+ jsx("div", Object.assign({ className: clsx('tremolo-number-input-increment-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber >= (max !== null && max !== void 0 ? max : Number.MAX_SAFE_INTEGER), "aria-readonly": readonly, onPointerDown: press }, props, { children: children || (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "18 15 12 9 6 15" }) })) })));
1036
+ }
1037
+
1038
+ /** @category NumberInput */
1039
+ function DecrementStepper(_a) {
1040
+ var { size = 12, children, className } = _a, props = __rest(_a, ["size", "children", "className"]);
1041
+ const min = useNumberInputContext((s) => s.min);
1042
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
1043
+ const readonly = useNumberInputContext((s) => s.readonly);
1044
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
1045
+ const decrement = useNumberInputContext((s) => s.decrement);
1046
+ const press = useLongPress(decrement);
1047
+ return (
1048
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
1049
+ jsx("div", Object.assign({ className: clsx('tremolo-number-input-decrement-stepper', className), role: "button", tabIndex: -1, "aria-disabled": keepWithinRange && valueAsNumber <= (min !== null && min !== void 0 ? min : Number.MIN_SAFE_INTEGER), "aria-readonly": readonly, onPointerDown: press }, props, { children: children || (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "6 9 12 15 18 9" }) })) })));
1050
+ }
1051
+
878
1052
  /**
879
1053
  * Input with some useful functions for entering numerical values.
880
1054
  * @category NumberInput
881
1055
  */
882
- function NumberInput(_a) {
883
- var { value, min, max, step = 1, defaultValue, units, disabled = false, digit, selectWithFocus = 'none', blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ['raw', 1], keyboard = ['raw', 1], wrapperClassName, className, onChange, onFocus, onBlur, children } = _a, props = __rest(_a, ["value", "min", "max", "step", "defaultValue", "units", "disabled", "digit", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
884
- return (jsx(NumberInputProvider, { value: String(value), min: min, max: max, step: step, units: units, digit: digit, keepWithinRange: keepWithinRange, onChange: onChange, children: jsxs("div", { className: clsx('tremolo-number-input-wrapper', wrapperClassName),
1056
+ const NumberInput = forwardRef((_a, ref) => {
1057
+ var { 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 } = _a, props = __rest(_a, ["value", "min", "max", "step", "units", "readonly", "disabled", "digit", "variant", "selectWithFocus", "blurOnEnter", "keepWithinRange", "clampValueOnBlur", "wheel", "keyboard", "activeColor", "wrapperClassName", "className", "onChange", "onFocus", "onBlur", "children"]);
1058
+ const colors = {
1059
+ '--active-color': activeColor,
1060
+ };
1061
+ return (jsx(NumberInputProvider, { value: String(value), min: min, max: max, step: step, units: units, digit: digit, readonly: readonly, keepWithinRange: keepWithinRange, onChange: onChange, children: jsxs("div", { className: clsx('tremolo-number-input-wrapper', wrapperClassName),
885
1062
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
886
- tabIndex: 0, "data-stepper": !!children, children: [jsx(InternalInput, Object.assign({ min: min, max: max, step: step, defaultValue: defaultValue, units: units, disabled: disabled, digit: digit, selectWithFocus: selectWithFocus, blurOnEnter: blurOnEnter, keepWithinRange: keepWithinRange, clampValueOnBlur: clampValueOnBlur, wheel: wheel, keyboard: keyboard, wrapperClassName: wrapperClassName, className: className, onChange: onChange, onFocus: onFocus, onBlur: onBlur }, props)), children] }) }));
1063
+ tabIndex: 0, style: Object.assign({}, colors), "data-stepper": !!children, "data-variant": variant, children: [jsx(InternalInput, Object.assign({ ref: ref, readonly: readonly, disabled: disabled, selectWithFocus: selectWithFocus, blurOnEnter: blurOnEnter, clampValueOnBlur: clampValueOnBlur, wheel: wheel, keyboard: keyboard, className: className, onChange: onChange, onFocus: onFocus, onBlur: onBlur }, props)), children] }) }));
1064
+ });
1065
+
1066
+ /**
1067
+ * @returns [refCallback, pointerDownHandler]
1068
+ */
1069
+ function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd, }) {
1070
+ const dragged = useRef(false);
1071
+ const normalizedX = useRef(0);
1072
+ const normalizedY = useRef(0);
1073
+ const handleDrag = useCallback((event) => {
1074
+ if (!baseElementRef.current || !dragged.current) {
1075
+ return;
1076
+ }
1077
+ const isTouch = event instanceof TouchEvent;
1078
+ if (isTouch && event.cancelable)
1079
+ event.preventDefault();
1080
+ const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
1081
+ const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1082
+ const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1083
+ const nx = normalizeValue(mouseX, left, right);
1084
+ const ny = normalizeValue(mouseY, top, bottom);
1085
+ normalizedX.current = nx;
1086
+ normalizedY.current = ny;
1087
+ onDrag(nx, ny);
1088
+ }, [onDrag, baseElementRef]);
1089
+ const refHandler = useRefCallbackEvent('touchmove', handleDrag, { passive: false }, [onDrag]);
1090
+ const pointerDownHandler = useCallback((event) => {
1091
+ dragged.current = true;
1092
+ handleDrag(event);
1093
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(normalizedX.current, normalizedY.current);
1094
+ }, [handleDrag, onDragStart]);
1095
+ useEventListener(globalThis.window, 'pointermove', handleDrag);
1096
+ useEventListener(globalThis.window, 'pointerup', () => {
1097
+ if (!dragged.current)
1098
+ return;
1099
+ dragged.current = false;
1100
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(normalizedX.current, normalizedY.current);
1101
+ });
1102
+ return [refHandler, pointerDownHandler];
887
1103
  }
888
1104
 
889
1105
  /** @category Piano */
890
1106
  function KeyLabel(_a) {
891
- var { label, className, wrapperClassName, wrapperStyle, __note, __index, __label } = _a, props = __rest(_a, ["label", "className", "wrapperClassName", "wrapperStyle", "__note", "__index", "__label"]);
1107
+ var { label, className, wrapperClassName, wrapperStyle, __note, __label } = _a, props = __rest(_a, ["label", "className", "wrapperClassName", "wrapperStyle", "__note", "__label"]);
1108
+ const noteRange = usePianoContext((s) => s.noteRange);
1109
+ const noteRangeArray = getNoteRangeArray(noteRange);
1110
+ const index = noteRangeArray.indexOf(__note);
892
1111
  const content = label
893
- ? label(__note, __index)
894
- : __label && __label(__note, __index);
1112
+ ? label(__note, index)
1113
+ : __label && __label(__note, index);
895
1114
  return ((content != undefined || content != null) && (jsx("div", { className: clsx('tremolo-piano-key-label-wrapper', wrapperClassName), style: wrapperStyle, children: jsx("div", Object.assign({ className: clsx('tremolo-piano-key-label', className) }, props, { children: content })) })));
896
1115
  }
897
1116
 
898
1117
  /** @category Piano */
899
1118
  const defaultWhiteKeyWidth = 40;
900
1119
  /** @category Piano */
901
- const WhiteKey = forwardRef((_a, ref) => {
902
- var { width = defaultWhiteKeyWidth, height = '100%', className, style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __onPlayNote, __onStopNote, __label } = _a, props = __rest(_a, ["width", "height", "className", "style", "children", "__glissando", "__position", "__note", "__disabled", "__index", "__fill", "__width", "__onPlayNote", "__onStopNote", "__label"]);
1120
+ const defaultBlackKeyWidth = 26;
1121
+ const KeyImpl = forwardRef((_a, ref) => {
1122
+ var { keyType, noteNumber, width, height, bg, color, activeBg, activeColor, className, style, children, __width } = _a, props = __rest(_a, ["keyType", "noteNumber", "width", "height", "bg", "color", "activeBg", "activeColor", "className", "style", "children", "__width"]);
903
1123
  const [played, setPlayed] = useState(false);
1124
+ const midiMax = usePianoContext((s) => s.midiMax);
1125
+ const fill = usePianoContext((s) => s.fill);
1126
+ // const glissando = usePianoContext(s => s.glissando)
1127
+ const onPlayNote = usePianoContext((s) => s.onPlayNote);
1128
+ const onStopNote = usePianoContext((s) => s.onStopNote);
1129
+ const label = usePianoContext((s) => s.label);
1130
+ const notePosition = usePianoContext((s) => s.notePosition);
1131
+ const position = notePosition(noteNumber);
1132
+ const disabled = noteNumber > midiMax;
1133
+ const colors = {
1134
+ '--bg': bg,
1135
+ '--color': color,
1136
+ '--active-bg': activeBg,
1137
+ '--active-color': activeColor,
1138
+ };
904
1139
  useImperativeHandle(ref, () => {
905
1140
  return {
906
1141
  play() {
907
- if (__disabled)
1142
+ if (disabled)
908
1143
  return;
909
1144
  setPlayed(true);
910
- if (__onPlayNote)
911
- __onPlayNote(__note);
1145
+ if (onPlayNote)
1146
+ onPlayNote(noteNumber);
912
1147
  },
913
1148
  stop() {
914
1149
  setPlayed(false);
915
- if (__onStopNote)
916
- __onStopNote(__note);
1150
+ if (onStopNote)
1151
+ onStopNote(noteNumber);
917
1152
  },
918
1153
  played() {
919
1154
  return played;
920
1155
  },
921
1156
  };
922
- }, [__disabled, __note, __onPlayNote, __onStopNote, played]);
1157
+ }, [disabled, noteNumber, onPlayNote, onStopNote, played]);
923
1158
  let keyLabelProps = {};
924
1159
  if (children != undefined) {
925
1160
  React.Children.forEach(children, (child) => {
@@ -936,109 +1171,85 @@ const WhiteKey = forwardRef((_a, ref) => {
936
1171
  }
937
1172
  });
938
1173
  }
939
- return (jsx("div", Object.assign({ className: clsx(`tremolo-piano-white-key`, className), "aria-disabled": __disabled, "data-active": played, style: Object.assign({ left: __position, width: __fill ? __width : (width !== null && width !== void 0 ? width : __width), height: height }, style), onPointerDown: () => {
940
- if (__disabled)
941
- return;
942
- setPlayed(true);
943
- if (__onPlayNote)
944
- __onPlayNote(__note);
945
- }, onPointerEnter: () => {
946
- if (__disabled)
947
- return;
948
- if (__glissando) {
949
- setPlayed(true);
950
- if (__onPlayNote)
951
- __onPlayNote(__note);
952
- }
953
- }, onPointerUp: () => {
954
- if (__disabled)
955
- return;
956
- setPlayed(false);
957
- if (__onStopNote)
958
- __onStopNote(__note);
959
- }, onPointerLeave: () => {
960
- if (__disabled)
961
- return;
962
- if (__glissando) {
963
- setPlayed(false);
964
- if (__onStopNote)
965
- __onStopNote(__note);
966
- }
967
- } }, props, { children: jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) })));
1174
+ return (jsx("div", Object.assign({ className: clsx(`tremolo-piano-${keyType}-key`, className), "aria-disabled": disabled, "data-active": played, style: Object.assign(Object.assign(Object.assign({}, colors), { left: position, width: fill ? __width : width, height: height }), style) }, props, { children: jsx(KeyLabel, Object.assign({ label: label, __note: noteNumber }, keyLabelProps)) })));
1175
+ });
1176
+ /** @category Piano */
1177
+ const WhiteKey = forwardRef((_a, ref) => {
1178
+ var { width = defaultWhiteKeyWidth, height = '100%', onContextMenu = (e) => e.preventDefault() } = _a, props = __rest(_a, ["width", "height", "onContextMenu"]);
1179
+ return (jsx(KeyImpl, Object.assign({ ref: ref, keyType: "white", width: width, height: height, onContextMenu: onContextMenu }, props)));
968
1180
  });
969
-
970
1181
  /** @category Piano */
971
1182
  const BlackKey = forwardRef((_a, ref) => {
972
- var { width = defaultWhiteKeyWidth * 0.65, height = '60%', className, style, children, __glissando, __position, __note, __disabled, __index, __fill, __width, __onPlayNote, __onStopNote, __label } = _a, props = __rest(_a, ["width", "height", "className", "style", "children", "__glissando", "__position", "__note", "__disabled", "__index", "__fill", "__width", "__onPlayNote", "__onStopNote", "__label"]);
973
- const [played, setPlayed] = useState(false);
974
- useImperativeHandle(ref, () => {
975
- return {
976
- play() {
977
- if (__disabled)
978
- return;
979
- setPlayed(true);
980
- if (__onPlayNote)
981
- __onPlayNote(__note);
982
- },
983
- stop() {
984
- setPlayed(false);
985
- if (__onStopNote)
986
- __onStopNote(__note);
987
- },
988
- played() {
989
- return played;
990
- },
991
- };
992
- }, [__disabled, __note, __onPlayNote, __onStopNote, played]);
993
- let keyLabelProps = {};
994
- if (children != undefined) {
995
- React.Children.forEach(children, (child) => {
996
- if (React.isValidElement(child)) {
997
- if (child.type == KeyLabel) {
998
- keyLabelProps = child.props;
999
- }
1000
- else {
1001
- throw new Error('only <KeyLabel>');
1002
- }
1003
- }
1004
- else {
1005
- throw new Error('children is an invalid element.');
1006
- }
1007
- });
1008
- }
1009
- return (jsx("div", Object.assign({ className: clsx(`tremolo-piano-black-key`, className), "aria-disabled": __disabled, "data-active": played, style: Object.assign({ left: __position, width: __fill ? __width : (width !== null && width !== void 0 ? width : __width), height: height }, style), onPointerDown: () => {
1010
- if (__disabled)
1011
- return;
1012
- setPlayed(true);
1013
- if (__onPlayNote)
1014
- __onPlayNote(__note);
1015
- }, onPointerEnter: () => {
1016
- if (__disabled)
1017
- return;
1018
- if (__glissando) {
1019
- setPlayed(true);
1020
- if (__onPlayNote)
1021
- __onPlayNote(__note);
1022
- }
1023
- }, onPointerUp: () => {
1024
- if (__disabled)
1025
- return;
1026
- setPlayed(false);
1027
- if (__onStopNote)
1028
- __onStopNote(__note);
1029
- }, onPointerLeave: () => {
1030
- if (__disabled)
1031
- return;
1032
- if (__glissando) {
1033
- setPlayed(false);
1034
- if (__onStopNote)
1035
- __onStopNote(__note);
1036
- }
1037
- } }, props, { children: jsx(KeyLabel, Object.assign({ __note: __note, __label: __label, __index: __index }, keyLabelProps)) })));
1183
+ var { width = defaultBlackKeyWidth, height = '60%', onContextMenu = (e) => e.preventDefault() } = _a, props = __rest(_a, ["width", "height", "onContextMenu"]);
1184
+ return (jsx(KeyImpl, Object.assign({ ref: ref, keyType: "black", width: width, height: height, onContextMenu: onContextMenu }, props)));
1038
1185
  });
1039
1186
 
1187
+ const createPianoStore = (initProps) => {
1188
+ const noteRange = (initProps === null || initProps === void 0 ? void 0 : initProps.noteRange) || { first: 0};
1189
+ const DEFAULT_PROPS = {
1190
+ noteRange: { first: 0, last: 127 },
1191
+ glissando: true,
1192
+ midiMax: 127,
1193
+ fill: false,
1194
+ label: () => undefined,
1195
+ onPlayNote: () => { },
1196
+ onStopNote: () => { },
1197
+ };
1198
+ return createStore()(() => (Object.assign(Object.assign(Object.assign({}, DEFAULT_PROPS), initProps), { notePosition: (note) => {
1199
+ const pitchPositions = {
1200
+ C: 0,
1201
+ 'C#': 1,
1202
+ D: 1,
1203
+ 'D#': 2,
1204
+ E: 2,
1205
+ F: 3,
1206
+ 'F#': 4,
1207
+ G: 4,
1208
+ 'G#': 5,
1209
+ A: 5,
1210
+ 'A#': 6,
1211
+ B: 6,
1212
+ };
1213
+ const whiteNoteWidth = defaultWhiteKeyWidth;
1214
+ const blackNoteWidth = defaultBlackKeyWidth;
1215
+ const padding = 1;
1216
+ const targetNoteKey = noteKey(note);
1217
+ const firstNoteKey = noteKey(noteRange.first);
1218
+ const octave = Math.floor((note - noteRange.first) / 12);
1219
+ const octaveOffset = noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1220
+ const w = whiteNoteWidth + padding;
1221
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1222
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0;
1223
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1224
+ } })));
1225
+ };
1226
+ const PianoContext = createContext(null);
1227
+ function PianoProvider(_a) {
1228
+ var { children } = _a, props = __rest(_a, ["children"]);
1229
+ const storeRef = useRef(null);
1230
+ if (!storeRef.current) {
1231
+ storeRef.current = createPianoStore(props);
1232
+ }
1233
+ useEffect(() => {
1234
+ if (storeRef.current) {
1235
+ storeRef.current.setState(props);
1236
+ }
1237
+ else {
1238
+ storeRef.current = createPianoStore(props);
1239
+ }
1240
+ }, [props]);
1241
+ return (jsx(PianoContext.Provider, { value: storeRef.current, children: children }));
1242
+ }
1243
+ /** @category Piano */
1244
+ function usePianoContext(selector) {
1245
+ const store = useContext(PianoContext);
1246
+ if (!store)
1247
+ throw new Error('Missing PianoContext.Provider in the tree');
1248
+ return useStore(store, selector);
1249
+ }
1250
+
1040
1251
  /** @category Piano */
1041
- const Shortcuts = {
1252
+ const SHORTCUTS = {
1042
1253
  HOME_ROW: {
1043
1254
  keys: [
1044
1255
  'a',
@@ -1062,67 +1273,121 @@ const Shortcuts = {
1062
1273
  },
1063
1274
  };
1064
1275
 
1276
+ /**
1277
+ * Piano component
1278
+ *
1279
+ * TODO:
1280
+ * - add scale highlight
1281
+ */
1065
1282
  /** @category Piano */
1283
+ function getNoteRangeArray(noteRange) {
1284
+ return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1285
+ }
1286
+ /**
1287
+ * Customizable piano component.
1288
+ * @category Piano
1289
+ */
1066
1290
  function Piano(_a) {
1067
- var { noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? '100%' : 160, style, className, onPlayNote, onStopNote, label, children } = _a, props = __rest(_a, ["noteRange", "glissando", "midiMax", "keyboardShortcuts", "fill", "height", "style", "className", "onPlayNote", "onStopNote", "label", "children"]);
1291
+ var { noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? '100%' : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown } = _a, props = __rest(_a, ["noteRange", "glissando", "midiMax", "keyboardShortcuts", "fill", "height", "whiteNoteWidth", "style", "className", "onPlayNote", "onStopNote", "label", "children", "onPointerDown"]);
1068
1292
  // -- state and ref ---
1069
- const [pressed, setPressed] = useState(false);
1070
- const [whiteNoteWidth, setWhiteNoteWidth] = useState(-1);
1293
+ const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth);
1071
1294
  const keyRefs = useRef([]);
1072
1295
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
1073
1296
  keyRefs.current[i] = createRef();
1074
1297
  }
1075
1298
  const pianoRef = useRef(null);
1299
+ const hitKeyIndex = useRef(-1);
1076
1300
  // --- interpret props ---
1077
- const noteRangeArray = Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1078
- const whiteNoteCount = noteRangeArray.filter((v) => isWhiteKey(v)).length;
1079
- let whiteKeyProps = {}, blackKeyProps = {};
1080
- if (children != undefined) {
1081
- React.Children.forEach(children, (child) => {
1082
- if (React.isValidElement(child)) {
1083
- if (child.type == WhiteKey) {
1084
- whiteKeyProps = child.props;
1085
- }
1086
- else if (child.type == BlackKey) {
1087
- blackKeyProps = child.props;
1088
- }
1089
- else {
1090
- throw new Error('only <WhiteKey> or <BlackKey>');
1301
+ const noteRangeArray = getNoteRangeArray(noteRange);
1302
+ const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v));
1303
+ const blackNotes = noteRangeArray.filter((v) => isBlackKey(v));
1304
+ const whiteNoteCount = whiteNotes.length;
1305
+ const padding = 1;
1306
+ const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount;
1307
+ // const blackNoteShiftPercent = 0.3
1308
+ // TODO: if fill setWhiteNoteWidth
1309
+ // useMemo
1310
+ // __width
1311
+ const childrenWithProps = useMemo(() => {
1312
+ return (children &&
1313
+ React.Children.map(children, (child, index) => {
1314
+ if (React.isValidElement(child)) {
1315
+ const props = { ref: keyRefs.current[index] };
1316
+ return React.cloneElement(child, props);
1091
1317
  }
1092
- }
1093
- else {
1094
- throw new Error('children is an invalid element.');
1095
- }
1096
- });
1097
- }
1318
+ return child;
1319
+ }));
1320
+ }, [children]);
1098
1321
  // --- internal functions ---
1099
- function notePosition(note) {
1322
+ const notePosition = useCallback((note) => {
1100
1323
  const pitchPositions = {
1101
1324
  C: 0,
1102
- 'C#': 0.55,
1325
+ 'C#': 1,
1103
1326
  D: 1,
1104
- 'D#': 1.8,
1327
+ 'D#': 2,
1105
1328
  E: 2,
1106
1329
  F: 3,
1107
- 'F#': 3.5,
1330
+ 'F#': 4,
1108
1331
  G: 4,
1109
- 'G#': 4.7,
1332
+ 'G#': 5,
1110
1333
  A: 5,
1111
- 'A#': 5.85,
1334
+ 'A#': 6,
1112
1335
  B: 6,
1113
1336
  };
1114
- const toNoteName = (n) => {
1115
- const { letter, accidental } = parseNoteName(noteName(n));
1116
- return (letter + accidental);
1117
- };
1118
- const _noteName = toNoteName(note);
1119
- const firstNoteName = toNoteName(noteRange.first);
1120
- const pos = pitchPositions[_noteName] - pitchPositions[firstNoteName];
1337
+ const whiteNoteWidth = defaultWhiteKeyWidth;
1338
+ const blackNoteWidth = defaultBlackKeyWidth;
1339
+ const padding = 1;
1340
+ const targetNoteKey = noteKey(note);
1341
+ const firstNoteKey = noteKey(noteRange.first);
1121
1342
  const octave = Math.floor((note - noteRange.first) / 12);
1122
- const octaveOffset = noteNames.indexOf(firstNoteName) > noteNames.indexOf(_noteName) ? 1 : 0;
1123
- const length = whiteNoteWidth + 3;
1124
- return pos * length + (octave + octaveOffset) * 7 * length;
1125
- }
1343
+ const octaveOffset = noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1344
+ const w = whiteNoteWidth + padding;
1345
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1346
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0;
1347
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1348
+ }, [noteRange.first]);
1349
+ const getHitKeyIndex = useCallback((x, y) => {
1350
+ if (!pianoRef.current)
1351
+ return -1;
1352
+ const containerHeight = pianoRef.current.clientHeight;
1353
+ const notes = [...blackNotes, ...whiteNotes];
1354
+ for (let i = 0; i < notes.length; i++) {
1355
+ const note = notes[i];
1356
+ const pos = notePosition(note);
1357
+ const w = isWhiteKey(note) ? defaultWhiteKeyWidth : defaultBlackKeyWidth;
1358
+ const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6;
1359
+ if (pos <= x && x < pos + w && 0 <= y && y < h) {
1360
+ return note;
1361
+ }
1362
+ }
1363
+ return -1;
1364
+ }, [blackNotes, notePosition, whiteNotes]);
1365
+ // TODO: 単一のポインターに対しては、useDragで対応可能だが、
1366
+ // マルチタッチに対しては、TouchEventを使う必要がありそう
1367
+ // 取り敢えず、シングルタッチだけ対応
1368
+ const onDrag = useCallback((perX, perY) => {
1369
+ var _a, _b, _c, _d;
1370
+ if (!pianoRef.current)
1371
+ return;
1372
+ const x = perX * staticWidth;
1373
+ const y = perY * pianoRef.current.clientHeight;
1374
+ const note = getHitKeyIndex(x, y);
1375
+ const index = noteRangeArray.indexOf(note);
1376
+ if (index == -1)
1377
+ return;
1378
+ if (hitKeyIndex.current != index) {
1379
+ (_b = (_a = keyRefs.current[hitKeyIndex.current]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.stop();
1380
+ (_d = (_c = keyRefs.current[index]) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.play();
1381
+ hitKeyIndex.current = index;
1382
+ }
1383
+ }, [getHitKeyIndex, noteRangeArray, staticWidth]);
1384
+ const onDragEnd = useCallback(() => {
1385
+ var _a, _b, _c, _d;
1386
+ if ((_b = (_a = keyRefs.current[hitKeyIndex.current]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.played()) {
1387
+ (_d = (_c = keyRefs.current[hitKeyIndex.current]) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.stop();
1388
+ }
1389
+ hitKeyIndex.current = -1;
1390
+ }, [keyRefs]);
1126
1391
  // --- hooks ---
1127
1392
  useEffect(() => {
1128
1393
  if (fill && pianoRef.current) {
@@ -1131,14 +1396,22 @@ function Piano(_a) {
1131
1396
  throw new Error("doesn't have a parent element.");
1132
1397
  const resizeObserver = new ResizeObserver(() => {
1133
1398
  const w = pianoRef.current.clientWidth;
1134
- setWhiteNoteWidth(w / whiteNoteCount - 3);
1399
+ setWhiteNoteWidth(w / whiteNoteCount - padding);
1135
1400
  });
1136
1401
  resizeObserver.observe(parent);
1402
+ return () => {
1403
+ resizeObserver.unobserve(parent);
1404
+ };
1137
1405
  }
1138
1406
  else {
1139
- setWhiteNoteWidth((whiteKeyProps === null || whiteKeyProps === void 0 ? void 0 : whiteKeyProps.width) || defaultWhiteKeyWidth);
1407
+ setWhiteNoteWidth(_whiteNoteWidth);
1140
1408
  }
1141
- }, [fill, whiteKeyProps === null || whiteKeyProps === void 0 ? void 0 : whiteKeyProps.width, whiteNoteCount]);
1409
+ }, [fill, _whiteNoteWidth, whiteNoteCount]);
1410
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1411
+ baseElementRef: pianoRef,
1412
+ onDrag: onDrag,
1413
+ onDragEnd: onDragEnd,
1414
+ });
1142
1415
  useEventListener(globalThis.window, 'keydown', (e) => {
1143
1416
  var _a, _b, _c, _d;
1144
1417
  if (e.repeat)
@@ -1163,17 +1436,14 @@ function Piano(_a) {
1163
1436
  (_b = (_a = keyRefs.current[index]) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.stop();
1164
1437
  }
1165
1438
  });
1166
- return (jsx("div", Object.assign({ ref: pianoRef, className: clsx('tremolo-piano', className), style: Object.assign({ width: fill ? '100%' : (whiteNoteWidth + 3) * whiteNoteCount, height: height }, style), onPointerDown: (e) => {
1167
- if (props.onPointerDown)
1168
- props.onPointerDown(e);
1169
- setPressed(true);
1170
- }, onPointerUp: (e) => {
1171
- if (props.onPointerUp)
1172
- props.onPointerUp(e);
1173
- setPressed(false);
1174
- } }, props, { children: noteRangeArray.map((note, index) => {
1175
- return isWhiteKey(note) ? (jsx(WhiteKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth, __onPlayNote: onPlayNote, __onStopNote: onStopNote, __label: label }, whiteKeyProps), note)) : (jsx(BlackKey, Object.assign({ ref: keyRefs.current[index], __glissando: glissando && pressed, __position: notePosition(note), __note: note, __disabled: note > midiMax, __index: index, __fill: fill, __width: whiteNoteWidth * 0.65, __onPlayNote: onPlayNote, __onStopNote: onStopNote, __label: label }, blackKeyProps), note));
1176
- }) })));
1439
+ return (jsx(PianoProvider, { noteRange: noteRange, glissando: glissando, midiMax: midiMax, fill: fill, onPlayNote: onPlayNote, onStopNote: onStopNote, label: label, children: jsx("div", Object.assign({ ref: (div) => {
1440
+ pianoRef.current = div;
1441
+ touchMoveRefCallback(div);
1442
+ }, className: clsx('tremolo-piano', className), style: Object.assign({ width: fill ? '100%' : staticWidth, height: height }, style), onPointerDown: (event) => {
1443
+ pointerDownHandler(event);
1444
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1445
+ } }, props, { children: childrenWithProps ||
1446
+ getNoteRangeArray(noteRange).map((note, index) => isWhiteKey(note) ? (jsx(WhiteKey, { ref: keyRefs.current[index], noteNumber: note }, note)) : (jsx(BlackKey, { ref: keyRefs.current[index], noteNumber: note }, note))) })) }));
1177
1447
  }
1178
1448
 
1179
1449
  const createSliderStore = (initProps) => {
@@ -1287,7 +1557,7 @@ function Scale(_a) {
1287
1557
  /** @category Slider */
1288
1558
  const defaultThumbSize = 22;
1289
1559
  /** @category Slider */
1290
- const SliderThumb = forwardRef(({ size, width = defaultThumbSize, height = defaultThumbSize, children, className, style, __percent, }, ref) => {
1560
+ const SliderThumb = forwardRef(({ size, width = defaultThumbSize, height = defaultThumbSize, color, children, className, style, __percent, }, ref) => {
1291
1561
  const wrapperRef = useRef(null);
1292
1562
  const vertical = useSliderContext((s) => s.vertical);
1293
1563
  const disabled = useSliderContext((s) => s.disabled);
@@ -1311,13 +1581,14 @@ const SliderThumb = forwardRef(({ size, width = defaultThumbSize, height = defau
1311
1581
  // default slider thumb
1312
1582
  jsx("div", { ref: wrapperRef, className: clsx('tremolo-slider-thumb', className),
1313
1583
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
1314
- tabIndex: 0, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }, style) })) }));
1584
+ tabIndex: 0, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign(Object.assign({ '--color': color }, { width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }), style) })) }));
1315
1585
  });
1316
1586
 
1317
1587
  const defaultLength = 140;
1318
1588
  const defaultThickness = 10;
1319
1589
  /** @category Slider */
1320
- function SliderTrack({ length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0, }) {
1590
+ function SliderTrack(_a) {
1591
+ var { length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0 } = _a, props = __rest(_a, ["length", "thickness", "active", "inactive", "children", "className", "style", "defaultStyle", "__thumb", "__percent"]);
1321
1592
  const vertical = useSliderContext((s) => s.vertical);
1322
1593
  const reverse = useSliderContext((s) => s.reverse);
1323
1594
  const disabled = useSliderContext((s) => s.disabled);
@@ -1326,11 +1597,11 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1326
1597
  '--active': active,
1327
1598
  '--inactive': inactive,
1328
1599
  };
1329
- return (jsxs("div", { className: clsx('tremolo-slider-track', className), "aria-disabled": disabled, "data-vertical": vertical, style: !defaultStyle
1600
+ return (jsxs("div", Object.assign({ className: clsx('tremolo-slider-track', className), "aria-disabled": disabled, "data-vertical": vertical, style: !defaultStyle
1330
1601
  ? style
1331
1602
  : Object.assign(Object.assign(Object.assign({}, colors), { background: xor(vertical, reverse)
1332
1603
  ? `linear-gradient(to ${direction}, var(--inactive, #eee) ${__percent}%, var(--active, #7998ec) ${__percent}%)`
1333
- : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`, borderRadius: styleHelper(thickness, '/', 2), width: !vertical ? length : thickness, height: vertical ? length : thickness }), style), children: [children, __thumb] }));
1604
+ : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`, borderRadius: styleHelper(thickness, '/', 2), width: !vertical ? length : thickness, height: vertical ? length : thickness }), style) }, props, { children: [children, __thumb] })));
1334
1605
  }
1335
1606
 
1336
1607
  /**
@@ -1338,12 +1609,11 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1338
1609
  * @category Slider
1339
1610
  */
1340
1611
  const Slider = forwardRef((_a, ref) => {
1341
- var _b, _c, _d;
1342
- var { value, min, max, step = 1, skew = 1, vertical = false, reverse = false, className, style, bodyNoSelect = true, wheel = ['raw', 1], keyboard = ['raw', 1], disabled = false, readonly = false, onChange, children } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "vertical", "reverse", "className", "style", "bodyNoSelect", "wheel", "keyboard", "disabled", "readonly", "onChange", "children"]);
1612
+ var _b;
1613
+ var { 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 } = _a, props = __rest(_a, ["value", "min", "max", "step", "skew", "vertical", "reverse", "bodyNoSelect", "wheel", "keyboard", "disabled", "readonly", "onChange", "onDragStart", "onDragEnd", "className", "style", "children", "onFocus", "onBlur", "onPointerDown", "onKeyDown"]);
1343
1614
  // -- state and ref ---
1344
1615
  const trackElementRef = useRef(null);
1345
1616
  const thumbRef = useRef(null);
1346
- const thumbDragged = useRef(false);
1347
1617
  // --- interpret props ---
1348
1618
  const p = toFixed$1(normalizeValue(value, min, max, skew) * 100);
1349
1619
  const rev = toFixed$1(100 - p);
@@ -1379,27 +1649,25 @@ const Slider = forwardRef((_a, ref) => {
1379
1649
  });
1380
1650
  }
1381
1651
  // --- internal functions ---
1382
- const handleValue = (event) => {
1383
- if (!trackElementRef.current ||
1384
- !thumbDragged.current ||
1385
- !onChange ||
1386
- readonly) {
1652
+ const onDrag = useCallback((nx, ny) => {
1653
+ if (!onChange || readonly)
1387
1654
  return;
1388
- }
1389
- const isTouch = event instanceof TouchEvent;
1390
- if (isTouch && event.cancelable)
1391
- event.preventDefault();
1392
- if (bodyNoSelect)
1393
- addNoSelect();
1394
- const { left: x1, top: y1, right: x2, bottom: y2, } = trackElementRef.current.getBoundingClientRect();
1395
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1396
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1397
- const n = vertical
1398
- ? normalizeValue(mouseY, y1, y2)
1399
- : normalizeValue(mouseX, x1, x2);
1655
+ const n = vertical ? ny : nx;
1400
1656
  const v = rawValue(displayReversed ? 1 - n : n, min, max, skew);
1401
- onChange(clamp(stepValue(v, step), min, max));
1402
- };
1657
+ const v2 = clamp(stepValue(v, step), min, max);
1658
+ onChange(v2);
1659
+ }, [displayReversed, max, min, onChange, readonly, skew, step, vertical]);
1660
+ const updateValueByEvent = useCallback((eventType, x) => {
1661
+ let newValue;
1662
+ if (eventType == 'normalized') {
1663
+ const n = normalizeValue(value, min, max, skew);
1664
+ newValue = rawValue(n + x, min, max, skew);
1665
+ }
1666
+ else {
1667
+ newValue = value + x;
1668
+ }
1669
+ return clamp(stepValue(newValue, step), min, max);
1670
+ }, [max, min, skew, step, value]);
1403
1671
  const handleKeyDown = useCallback((event) => {
1404
1672
  if (!keyboard || !onChange || readonly)
1405
1673
  return;
@@ -1409,19 +1677,30 @@ const Slider = forwardRef((_a, ref) => {
1409
1677
  let x = key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1];
1410
1678
  if (reverse)
1411
1679
  x *= -1;
1412
- let v;
1413
- if (keyboard[0] == 'normalized') {
1414
- const n = normalizeValue(value, min, max, skew);
1415
- v = rawValue(n + x, min, max, skew);
1416
- }
1417
- else {
1418
- v = value + x;
1419
- }
1420
- onChange(clamp(stepValue(v, step), min, max));
1680
+ onChange(updateValueByEvent(keyboard[0], x));
1421
1681
  }
1422
- }, [value, min, max, step, skew, reverse, keyboard, onChange, readonly]);
1682
+ }, [keyboard, onChange, readonly, reverse, updateValueByEvent]);
1423
1683
  // --- hooks ---
1424
- const touchMoveRefCallback = useRefCallbackEvent('touchmove', handleValue, { passive: false }, [min, max, skew, step, vertical, reverse, onChange, readonly]);
1684
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1685
+ baseElementRef: trackElementRef,
1686
+ onDrag: onDrag,
1687
+ onDragStart: (nx, ny) => {
1688
+ var _a;
1689
+ if (readonly)
1690
+ return;
1691
+ if (bodyNoSelect)
1692
+ addNoSelect();
1693
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1694
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(clamp(stepValue(rawValue(vertical ? ny : nx, min, max, skew), step), min, max));
1695
+ },
1696
+ onDragEnd: (nx, ny) => {
1697
+ if (readonly)
1698
+ return;
1699
+ if (bodyNoSelect)
1700
+ removeNoSelect();
1701
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(clamp(stepValue(rawValue(vertical ? ny : nx, min, max, skew), step), min, max));
1702
+ },
1703
+ });
1425
1704
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
1426
1705
  if (!wheel || !onChange || readonly)
1427
1706
  return;
@@ -1437,26 +1716,10 @@ const Slider = forwardRef((_a, ref) => {
1437
1716
  }
1438
1717
  if (vertical && reverse)
1439
1718
  x *= -1;
1440
- let v;
1441
- if (wheel[0] == 'normalized') {
1442
- const n = normalizeValue(value, min, max, skew);
1443
- v = rawValue(n + x, min, max, skew);
1444
- }
1445
- else {
1446
- v = value + x;
1447
- }
1448
- onChange(clamp(stepValue(v, step), min, max));
1719
+ onChange(updateValueByEvent(wheel[0], x));
1449
1720
  }, {
1450
1721
  passive: false,
1451
- }, [wheel, value, min, max, skew, step]);
1452
- useEventListener(globalThis.window, 'mousemove', (event) => {
1453
- handleValue(event);
1454
- });
1455
- useEventListener(globalThis.window, 'mouseup', () => {
1456
- thumbDragged.current = false;
1457
- if (bodyNoSelect)
1458
- removeNoSelect();
1459
- });
1722
+ }, [wheel, vertical, readonly, onChange, updateValueByEvent]);
1460
1723
  useImperativeHandle(ref, () => {
1461
1724
  return {
1462
1725
  focus() {
@@ -1473,9 +1736,20 @@ const Slider = forwardRef((_a, ref) => {
1473
1736
  wheelRefCallback(div);
1474
1737
  touchMoveRefCallback(div);
1475
1738
  }, tabIndex: -1, role: "slider", "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-orientation": vertical ? 'vertical' : 'horizontal', "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ margin: `calc(${styleHelper((_b = thumbProps === null || thumbProps === void 0 ? void 0 : thumbProps.size) !== null && _b !== void 0 ? _b : 22)} / 2)` }, style), onPointerDown: (event) => {
1476
- thumbDragged.current = true;
1477
- handleValue(event);
1478
- }, onKeyDown: handleKeyDown, onFocus: (_c = thumbRef.current) === null || _c === void 0 ? void 0 : _c.focus, onBlur: (_d = thumbRef.current) === null || _d === void 0 ? void 0 : _d.blur }, props, { children: jsxs("div", { style: {
1739
+ pointerDownHandler(event);
1740
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1741
+ }, onKeyDown: (event) => {
1742
+ handleKeyDown(event);
1743
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
1744
+ }, onFocus: (event) => {
1745
+ var _a;
1746
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1747
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
1748
+ }, onBlur: (event) => {
1749
+ var _a;
1750
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.blur();
1751
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1752
+ } }, props, { children: jsxs("div", { style: {
1479
1753
  display: 'flex',
1480
1754
  flexDirection: vertical ? 'row' : 'column',
1481
1755
  width: !vertical ? '100%' : undefined,
@@ -1494,12 +1768,14 @@ function WheelObserver(props) {
1494
1768
  }
1495
1769
 
1496
1770
  /** @category XYPad */
1497
- function XYPadArea({ width = 120, height = 120, bg = '#eee', children, className, style, __thumb, }) {
1498
- return (jsxs("div", { className: clsx('tremolo-xy-pad-area', className), style: Object.assign({ position: 'relative', width: width, height: height, background: bg }, style), children: [children, __thumb] }));
1771
+ function XYPadArea(_a) {
1772
+ var { width = 120, height = 120, color, children, className, style, __thumb } = _a, props = __rest(_a, ["width", "height", "color", "children", "className", "style", "__thumb"]);
1773
+ return (jsxs("div", Object.assign({ className: clsx('tremolo-xy-pad-area', className), style: Object.assign(Object.assign({ '--color': color }, { width: width, height: height }), style) }, props, { children: [children, __thumb] })));
1499
1774
  }
1500
1775
 
1501
1776
  /** @category XYPad */
1502
- const XYPadThumb = forwardRef(({ size, width = 22, height = 22, children, wrapperClassName, wrapperStyle, className, style, __disabled, __css, }, ref) => {
1777
+ const XYPadThumb = forwardRef((_a, ref) => {
1778
+ var { size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __css } = _a, props = __rest(_a, ["size", "width", "height", "color", "children", "wrapperClassName", "wrapperStyle", "className", "style", "__disabled", "__css"]);
1503
1779
  const wrapperRef = useRef(null);
1504
1780
  useImperativeHandle(ref, () => {
1505
1781
  return {
@@ -1513,11 +1789,11 @@ const XYPadThumb = forwardRef(({ size, width = 22, height = 22, children, wrappe
1513
1789
  },
1514
1790
  };
1515
1791
  }, []);
1516
- return (jsx("div", { className: clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName), style: Object.assign(Object.assign({}, __css), wrapperStyle), children: children ? (children) : (
1792
+ return (jsx("div", Object.assign({ className: clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName), style: Object.assign(Object.assign({}, __css), wrapperStyle) }, props, { children: children ? (children) : (
1517
1793
  // default thumb
1518
1794
  jsx("div", { ref: wrapperRef, className: clsx('tremolo-xy-pad-thumb', className),
1519
1795
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
1520
- tabIndex: 0, "aria-disabled": __disabled, style: Object.assign({ width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }, style) })) }));
1796
+ tabIndex: 0, "aria-disabled": __disabled, style: Object.assign(Object.assign({ '--color': color }, { width: size !== null && size !== void 0 ? size : width, height: size !== null && size !== void 0 ? size : height }), style) })) })));
1521
1797
  });
1522
1798
 
1523
1799
  const defaultValueOptions = {
@@ -1532,8 +1808,8 @@ const defaultValueOptions = {
1532
1808
  * @category XYPad
1533
1809
  */
1534
1810
  const XYPad = forwardRef((_a, ref) => {
1535
- var _b, _c, _d;
1536
- var { x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, children } = _a, props = __rest(_a, ["x", "y", "className", "style", "bodyNoSelect", "disabled", "readonly", "onChange", "children"]);
1811
+ var _b;
1812
+ var { x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children } = _a, props = __rest(_a, ["x", "y", "className", "style", "bodyNoSelect", "disabled", "readonly", "onChange", "onDragStart", "onDragEnd", "onPointerDown", "onKeyDown", "onFocus", "onBlur", "children"]);
1537
1813
  const x = useMemo(() => {
1538
1814
  return Object.assign(Object.assign({}, defaultValueOptions), _x);
1539
1815
  }, [_x]);
@@ -1543,13 +1819,13 @@ const XYPad = forwardRef((_a, ref) => {
1543
1819
  // -- state and ref ---
1544
1820
  const areaElementRef = useRef(null);
1545
1821
  const thumbRef = useRef(null);
1546
- const thumbDragged = useRef(false);
1547
1822
  // --- interpret props ---
1548
1823
  const nx = normalizeValue(x.value, x.min, x.max, x.skew);
1549
1824
  const ny = normalizeValue(y.value, y.min, y.max, y.skew);
1550
1825
  const percentX = toFixed$1((x.reverse ? 1 - nx : nx) * 100);
1551
1826
  const percentY = toFixed$1((y.reverse ? 1 - ny : ny) * 100);
1552
- let areaProps = {}, thumbProps = {};
1827
+ let areaProps = {};
1828
+ let thumbProps = {};
1553
1829
  if (children != undefined) {
1554
1830
  React.Children.forEach(children, (child) => {
1555
1831
  if (React.isValidElement(child)) {
@@ -1569,28 +1845,39 @@ const XYPad = forwardRef((_a, ref) => {
1569
1845
  });
1570
1846
  }
1571
1847
  // --- internal functions ---
1572
- const handleValue = (event) => {
1573
- if (!areaElementRef.current ||
1574
- !thumbDragged.current ||
1575
- !onChange ||
1576
- readonly)
1848
+ const onDrag = useCallback((nx, ny) => {
1849
+ if (!onChange || readonly)
1577
1850
  return;
1578
- const isTouch = event instanceof TouchEvent;
1579
- if (isTouch && event.cancelable)
1580
- event.preventDefault();
1581
- if (bodyNoSelect)
1582
- addNoSelect();
1583
- const { left: x1, top: y1, right: x2, bottom: y2, } = areaElementRef.current.getBoundingClientRect();
1584
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1585
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1586
- const nx = normalizeValue(mouseX, x1, x2);
1587
- const ny = normalizeValue(mouseY, y1, y2);
1588
1851
  const vx = rawValue(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
1589
1852
  const vy = rawValue(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
1590
1853
  const newX = clamp(stepValue(vx, x.step), x.min, x.max);
1591
1854
  const newY = clamp(stepValue(vy, y.step), y.min, y.max);
1592
1855
  onChange(newX, newY);
1593
- };
1856
+ }, [
1857
+ onChange,
1858
+ readonly,
1859
+ x.max,
1860
+ x.min,
1861
+ x.reverse,
1862
+ x.skew,
1863
+ x.step,
1864
+ y.max,
1865
+ y.min,
1866
+ y.reverse,
1867
+ y.skew,
1868
+ y.step,
1869
+ ]);
1870
+ const updateValueByEvent = useCallback((eventType, target, x) => {
1871
+ let v;
1872
+ if (eventType == 'normalized') {
1873
+ const n = normalizeValue(target.value, target.min, target.max, target.skew);
1874
+ v = rawValue(n + x, target.min, target.max, target.skew);
1875
+ }
1876
+ else {
1877
+ v = target.value + x;
1878
+ }
1879
+ return clamp(stepValue(v, target.step), target.min, target.max);
1880
+ }, []);
1594
1881
  const handleKeyDown = useCallback((event) => {
1595
1882
  if (!onChange || readonly)
1596
1883
  return;
@@ -1606,15 +1893,7 @@ const XYPad = forwardRef((_a, ref) => {
1606
1893
  delta *= -1;
1607
1894
  if (target.reverse)
1608
1895
  delta *= -1;
1609
- let v;
1610
- if (target.keyboard[0] == 'normalized') {
1611
- const n = normalizeValue(target.value, target.min, target.max, target.skew);
1612
- v = rawValue(n + delta, target.min, target.max, target.skew);
1613
- }
1614
- else {
1615
- v = target.value + delta;
1616
- }
1617
- const newValue = clamp(stepValue(v, target.step), target.min, target.max);
1896
+ const newValue = updateValueByEvent(target.keyboard[0], target, delta);
1618
1897
  if (isHorizontal) {
1619
1898
  onChange(newValue, y.value);
1620
1899
  }
@@ -1622,9 +1901,32 @@ const XYPad = forwardRef((_a, ref) => {
1622
1901
  onChange(x.value, newValue);
1623
1902
  }
1624
1903
  }
1625
- }, [x, y, onChange, readonly]);
1904
+ }, [onChange, readonly, x, y, updateValueByEvent]);
1626
1905
  // --- hooks ---
1627
- const touchMoveRefCallback = useRefCallbackEvent('touchmove', handleValue, { passive: false }, [x, onChange, readonly]);
1906
+ const [touchMoveRefCallback, pointerDownHandler] = useDragWithElement({
1907
+ baseElementRef: areaElementRef,
1908
+ onDrag: onDrag,
1909
+ onDragStart: (nx, ny) => {
1910
+ var _a;
1911
+ if (readonly)
1912
+ return;
1913
+ if (bodyNoSelect)
1914
+ addNoSelect();
1915
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1916
+ const valueX = clamp(stepValue(rawValue(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
1917
+ const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
1918
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(valueX, valueY);
1919
+ },
1920
+ onDragEnd: (nx, ny) => {
1921
+ if (readonly)
1922
+ return;
1923
+ if (bodyNoSelect)
1924
+ removeNoSelect();
1925
+ const valueX = clamp(stepValue(rawValue(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
1926
+ const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
1927
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(valueX, valueY);
1928
+ },
1929
+ });
1628
1930
  const wheelRefCallback = useRefCallbackEvent('wheel', (event) => {
1629
1931
  if (!onChange || readonly)
1630
1932
  return;
@@ -1637,15 +1939,7 @@ const XYPad = forwardRef((_a, ref) => {
1637
1939
  delta *= -1;
1638
1940
  if (target.reverse)
1639
1941
  delta *= -1;
1640
- let v;
1641
- if (target.wheel[0] == 'normalized') {
1642
- const n = normalizeValue(target.value, target.min, target.max, target.skew);
1643
- v = rawValue(n + delta, target.min, target.max, target.skew);
1644
- }
1645
- else {
1646
- v = target.value + delta;
1647
- }
1648
- const newValue = clamp(stepValue(v, target.step), target.min, target.max);
1942
+ const newValue = updateValueByEvent(target.wheel[0], target, delta);
1649
1943
  if (event.shiftKey) {
1650
1944
  onChange(newValue, y.value);
1651
1945
  }
@@ -1654,15 +1948,7 @@ const XYPad = forwardRef((_a, ref) => {
1654
1948
  }
1655
1949
  }, {
1656
1950
  passive: false,
1657
- }, [x, y]);
1658
- useEventListener(globalThis.window, 'mousemove', (event) => {
1659
- handleValue(event);
1660
- });
1661
- useEventListener(globalThis.window, 'mouseup', () => {
1662
- thumbDragged.current = false;
1663
- if (bodyNoSelect)
1664
- removeNoSelect();
1665
- });
1951
+ }, [x, y, onChange, readonly, updateValueByEvent]);
1666
1952
  useImperativeHandle(ref, () => {
1667
1953
  return {
1668
1954
  focus() {
@@ -1681,13 +1967,37 @@ const XYPad = forwardRef((_a, ref) => {
1681
1967
  wheelRefCallback(div);
1682
1968
  touchMoveRefCallback(div);
1683
1969
  }, tabIndex: -1, "aria-disabled": disabled, "aria-readonly": readonly, style: Object.assign({ margin: `calc(${styleHelper((_b = thumbProps === null || thumbProps === void 0 ? void 0 : thumbProps.size) !== null && _b !== void 0 ? _b : 22)} / 2)`, cursor: readonly ? 'not-allowed' : 'pointer', WebkitTapHighlightColor: 'transparent' }, style), onPointerDown: (event) => {
1684
- thumbDragged.current = true;
1685
- handleValue(event);
1686
- }, onKeyDown: handleKeyDown, onFocus: (_c = thumbRef.current) === null || _c === void 0 ? void 0 : _c.focus, onBlur: (_d = thumbRef.current) === null || _d === void 0 ? void 0 : _d.blur }, props, { children: jsx("div", { className: "tremolo-xy-pad-area-wrapper", ref: areaElementRef, children: jsx(XYPadArea, Object.assign({ __thumb: jsx(XYPadThumb, Object.assign({ ref: thumbRef, __disabled: disabled, __css: {
1970
+ pointerDownHandler(event);
1971
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
1972
+ }, onKeyDown: (event) => {
1973
+ handleKeyDown(event);
1974
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
1975
+ }, onFocus: (event) => {
1976
+ var _a;
1977
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1978
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
1979
+ }, onBlur: (event) => {
1980
+ var _a;
1981
+ (_a = thumbRef.current) === null || _a === void 0 ? void 0 : _a.blur();
1982
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1983
+ } }, props, { children: jsx("div", { className: "tremolo-xy-pad-area-wrapper", ref: areaElementRef, children: jsx(XYPadArea, Object.assign({ __thumb: jsx(XYPadThumb, Object.assign({ ref: thumbRef, __disabled: disabled, __css: {
1687
1984
  top: `${percentY}%`,
1688
1985
  left: `${percentX}%`,
1689
1986
  } }, thumbProps)) }, areaProps)) }) })));
1690
1987
  });
1691
1988
 
1692
- export { AnimationCanvas, AnimationKnob, BlackKey, DecrementStepper, DragObserver, IncrementStepper, KeyLabel, Knob, NumberInput, Piano, Scale, ScaleOption, Shortcuts, Slider, SliderThumb, SliderTrack, Stepper, WheelObserver, WhiteKey, XYPad, XYPadArea, XYPadThumb, useSliderContext };
1989
+ const useAnimationFrame = (callback = () => { }, deps = []) => {
1990
+ const reqIdRef = useRef(-1);
1991
+ const loop = useCallback(() => {
1992
+ reqIdRef.current = requestAnimationFrame(loop);
1993
+ callback();
1994
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1995
+ }, [callback, ...deps]);
1996
+ useEffect(() => {
1997
+ reqIdRef.current = requestAnimationFrame(loop);
1998
+ return () => cancelAnimationFrame(reqIdRef.current);
1999
+ }, [loop]);
2000
+ };
2001
+
2002
+ export { AnimationCanvas, AnimationKnob, BlackKey, DecrementStepper, DragObserver, IncrementStepper, KeyLabel, Knob, NumberInput, Piano, SHORTCUTS, Scale, ScaleOption, Slider, SliderThumb, SliderTrack, Stepper, WheelObserver, WhiteKey, XYPad, XYPadArea, XYPadThumb, getNoteRangeArray, useAnimationFrame, useDrag, useEventListener, useInterval, useLongPress, useSliderContext };
1693
2003
  //# sourceMappingURL=index.js.map