@tremolo-ui/react 0.2.0 → 0.2.1

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 (64) hide show
  1. package/dist/index.cjs +1203 -1036
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +176 -159
  4. package/dist/index.d.cts +241 -246
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +241 -246
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1187 -1021
  9. package/dist/index.js.map +1 -1
  10. package/package.json +17 -19
  11. package/src/components/AnimationCanvas/index.tsx +0 -7
  12. package/src/components/DragObserver/index.tsx +9 -7
  13. package/src/components/Knob/ActiveLine.tsx +29 -0
  14. package/src/components/Knob/InactiveLine.tsx +53 -0
  15. package/src/components/Knob/SVGRoot.tsx +39 -0
  16. package/src/components/Knob/Thumb.tsx +64 -0
  17. package/src/components/Knob/context.tsx +120 -0
  18. package/src/components/Knob/index.css +5 -1
  19. package/src/components/Knob/index.tsx +110 -164
  20. package/src/components/NumberInput/DecrementStepper.tsx +0 -2
  21. package/src/components/NumberInput/IncrementStepper.tsx +0 -2
  22. package/src/components/NumberInput/InternalInput.tsx +3 -1
  23. package/src/components/NumberInput/Stepper.tsx +0 -2
  24. package/src/components/NumberInput/context.tsx +0 -1
  25. package/src/components/NumberInput/index.tsx +6 -8
  26. package/src/components/Piano/KeyLabel.tsx +0 -2
  27. package/src/components/Piano/context.tsx +0 -2
  28. package/src/components/Piano/index.tsx +7 -11
  29. package/src/components/Piano/key.tsx +0 -10
  30. package/src/components/Piano/keyboardShortcuts.ts +0 -2
  31. package/src/components/PointsEditor/Background.tsx +17 -0
  32. package/src/components/PointsEditor/Container.tsx +26 -0
  33. package/src/components/PointsEditor/Point.tsx +137 -0
  34. package/src/components/PointsEditor/context.tsx +91 -0
  35. package/src/components/PointsEditor/index.css +30 -0
  36. package/src/components/PointsEditor/index.tsx +129 -0
  37. package/src/components/Slider/Scale.tsx +0 -2
  38. package/src/components/Slider/ScaleOption.tsx +0 -2
  39. package/src/components/Slider/Thumb.tsx +0 -4
  40. package/src/components/Slider/Track.tsx +0 -2
  41. package/src/components/Slider/context.tsx +0 -1
  42. package/src/components/Slider/index.tsx +42 -14
  43. package/src/components/WheelObserver/index.tsx +17 -19
  44. package/src/components/XYPad/Area.tsx +0 -2
  45. package/src/components/XYPad/Thumb.tsx +0 -3
  46. package/src/components/XYPad/index.tsx +52 -25
  47. package/src/components/_util/composeRefs.tsx +63 -0
  48. package/src/components/_util/index.ts +23 -6
  49. package/src/components/_util/type.ts +1 -0
  50. package/src/hooks/useAnimationFrame.ts +0 -3
  51. package/src/hooks/useCallbackRef.ts +2 -2
  52. package/src/hooks/useDrag.ts +12 -11
  53. package/src/hooks/useDragWithElement.ts +12 -17
  54. package/src/hooks/useEventListener.ts +6 -9
  55. package/src/hooks/useInterval.ts +0 -3
  56. package/src/hooks/useLongPress.ts +0 -3
  57. package/src/hooks/useMIDIAccess.ts +0 -1
  58. package/src/hooks/useMIDIInput.ts +0 -1
  59. package/src/hooks/useMIDIMessage.ts +0 -1
  60. package/src/hooks/usePianoDrag.ts +75 -0
  61. package/src/index.ts +8 -5
  62. package/src/styles/global.css +17 -1
  63. package/dist/index.css.map +0 -1
  64. package/src/components/AnimationKnob/index.tsx +0 -314
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import './styles/global.css'
3
3
  import './components/Knob/index.css'
4
4
  import './components/NumberInput/index.css'
5
5
  import './components/Piano/index.css'
6
+ import './components/PointsEditor/index.css'
6
7
  import './components/Slider/index.css'
7
8
  import './components/XYPad/index.css'
8
9
 
@@ -15,11 +16,6 @@ export {
15
16
  type InitFunction,
16
17
  type RelativeSizingProps,
17
18
  } from './components/AnimationCanvas'
18
- export {
19
- AnimationKnob,
20
- type AnimationKnobProps,
21
- type AnimationKnobMethods,
22
- } from './components/AnimationKnob'
23
19
  export { DragObserver, type DragObserverProps } from './components/DragObserver'
24
20
  export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
25
21
  export {
@@ -41,6 +37,13 @@ export {
41
37
  type KeyLabelProps,
42
38
  type KeyboardShortcuts,
43
39
  } from './components/Piano'
40
+ export {
41
+ PointsEditor,
42
+ clampPoint,
43
+ type PointsEditorProps,
44
+ type PointProps,
45
+ type PointBaseType,
46
+ } from './components/PointsEditor'
44
47
  export {
45
48
  Slider,
46
49
  useSliderContext,
@@ -1,3 +1,19 @@
1
- .tremolo-no-select {
1
+ .tremolo-user-select-none {
2
2
  user-select: none;
3
3
  }
4
+
5
+ .tremolo-cursor-pointer {
6
+ cursor: pointer;
7
+ }
8
+
9
+ .tremolo-cursor-move {
10
+ cursor: move;
11
+ }
12
+
13
+ .tremolo-cursor-grabbing {
14
+ cursor: grabbing;
15
+ }
16
+
17
+ .tremolo-cursor-none {
18
+ cursor: none;
19
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.css","names":[],"sources":["../src/styles/global.css","../src/components/Knob/index.css","../src/components/NumberInput/index.css","../src/components/Piano/index.css","../src/components/Slider/index.css","../src/components/XYPad/index.css"],"sourcesContent":[".tremolo-no-select {\n user-select: none;\n}\n",".tremolo-knob {\n display: inline-block;\n cursor: pointer;\n overflow: visible;\n outline: 0;\n\n &[aria-readonly='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-knob-active-line {\n overflow: visible;\n color: oklch(69.2% 0.17548 266.373);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(59.5% 0.17548 266.373);\n }\n\n :where(.tremolo-knob[aria-disabled='true']) & {\n color: oklch(50.5% 0.03307 270.021);\n }\n}\n\n.tremolo-knob-inactive-line {\n color: oklch(95% 0 0);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(90% 0 0);\n }\n}\n\n.tremolo-knob-thumb {\n color: oklch(85.7% 0 0);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(84.5% 0 0);\n }\n}\n\n.tremolo-knob-thumb-line {\n color: oklch(94.2% 0 0);\n}\n\n:where(.dark, [data-theme='dark']) {\n .tremolo-knob-active-line {\n color: oklch(59.5% 0.17548 266.373);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(65.2% 0.17548 266.373);\n }\n }\n\n .tremolo-knob-inactive-line {\n color: oklch(41.7% 0 0);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(44.59% 0 0);\n }\n }\n\n .tremolo-knob-thumb {\n color: oklch(48.6% 0 0);\n\n :where(.tremolo-knob:focus, .tremolo-knob:hover) & {\n color: oklch(51.7% 0 0);\n }\n }\n\n .tremolo-knob-thumb-line {\n color: oklch(84.2% 0 0);\n }\n}\n",".tremolo-number-input-wrapper {\n --border-radius: 4px;\n --stepper-width: 20px;\n --active-color: oklch(59.5% 0.17548 266.373);\n --border-color: oklch(81% 0 0);\n\n display: inline-flex;\n flex-direction: row;\n position: relative;\n width: 140px;\n height: min-content;\n border-style: solid;\n border-color: var(--border-color);\n border-width: 0px;\n outline: none;\n transition: all 0.1s;\n\n &:not(:focus-within):has(> .tremolo-number-input[data-error='true']) {\n border-color: oklch(64.245% 0.21266 25.499);\n }\n\n &[data-variant='outline'] {\n border-width: 1px;\n border-radius: var(--border-radius);\n\n &:hover {\n --border-color: oklch(65.335% 0 0);\n }\n\n &:focus-within {\n --border-color: var(--active-color);\n box-shadow: 0px 0px 0px 2px\n color-mix(in srgb, var(--active-color) 10%, transparent);\n }\n }\n\n &[data-variant='filled'] {\n --bg: oklch(95.88% 0 0);\n border-width: 1px;\n border-radius: var(--border-radius);\n border-color: transparent;\n background-color: var(--bg);\n\n &:hover {\n --bg: oklch(93.166% 0 0);\n }\n\n &:focus-within {\n background-color: inherit;\n --border-color: var(--active-color);\n box-shadow: 0px 0px 0px 2px\n color-mix(in srgb, var(--active-color) 10%, transparent);\n }\n }\n\n &[data-variant='flushed'] {\n border-bottom-width: 1px;\n\n &:hover {\n --border-color: oklch(65.335% 0 0);\n }\n\n &:focus-within {\n --border-color: var(--active-color);\n box-shadow: 0px 2px 0px 0px\n color-mix(in srgb, var(--active-color) 10%, transparent);\n }\n }\n\n &[data-stepper='false'] {\n --stepper-width: 0px;\n }\n}\n\n.tremolo-number-input {\n display: block;\n color: inherit;\n width: calc(100% - 20px - var(--stepper-width));\n background-color: inherit;\n font: inherit;\n padding: 6px 10px;\n\n margin: 0;\n border: none;\n outline: none;\n appearance: none;\n\n &[aria-readonly='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-number-input-stepper {\n display: flex;\n flex-direction: column;\n height: 100%;\n position: absolute;\n top: 0;\n right: 0;\n width: var(--stepper-width);\n transition: all 0.1s linear;\n\n &[data-dynamic='true'] {\n opacity: 0;\n transform: translateX(40%);\n }\n}\n\n:is(\n .tremolo-number-input-wrapper:hover,\n .tremolo-number-input-wrapper:focus-within\n )\n .tremolo-number-input-stepper {\n opacity: 1;\n transform: translateX(0);\n}\n\n.tremolo-number-input-increment-stepper {\n border-inline-start-width: 1px;\n border-inline-start-style: solid;\n border-inline-start-color: oklch(84.522% 0 0);\n font-size: calc(0.75rem);\n display: flex;\n justify-content: center;\n align-items: center;\n flex: 1 1 0%;\n max-height: 50%;\n user-select: none;\n cursor: pointer;\n border-top-right-radius: var(--border-radius);\n\n &[aria-disabled='false']:active {\n background-color: oklch(37.798% 0.03812 254.612 / 0.055);\n }\n\n &[aria-disabled='true'] {\n cursor: not-allowed;\n }\n\n &[aria-readonly='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-number-input-decrement-stepper {\n border-inline-start-width: 1px;\n border-inline-start-style: solid;\n border-inline-start-color: oklch(84.522% 0 0);\n border-top: 1px solid oklch(84.522% 0 0);\n font-size: calc(0.75rem);\n display: flex;\n justify-content: center;\n align-items: center;\n flex: 1 1 0%;\n max-height: 50%;\n user-select: none;\n cursor: pointer;\n border-bottom-right-radius: var(--border-radius);\n\n &[aria-disabled='false']:active {\n background-color: oklch(37.798% 0.03812 254.612 / 0.055);\n }\n\n &[aria-disabled='true'] {\n cursor: not-allowed;\n }\n\n &[aria-readonly='true'] {\n cursor: not-allowed;\n }\n}\n\n:where(.dark, [data-theme='dark']) {\n .tremolo-number-input-wrapper {\n --border-color: oklch(52% 0 0);\n\n &[data-variant='filled'] {\n --bg: oklch(43.5% 0 0);\n\n &:hover {\n --bg: oklch(46.6% 0 0);\n }\n }\n }\n}\n",".tremolo-piano {\n display: inline-block;\n box-sizing: border-box;\n user-select: none;\n touch-action: none;\n position: relative;\n}\n\n.tremolo-piano-white-key {\n --bg: oklch(100% 0 0);\n --color: oklch(0% 0 0);\n --active-bg: oklch(92% 0 0);\n --active-color: oklch(0% 0 0);\n\n box-sizing: border-box;\n position: absolute;\n background-color: var(--bg);\n -webkit-tap-highlight-color: transparent;\n color: var(--color);\n border: 1px solid #555;\n border-radius: 0px 0px 8px 8px;\n cursor: pointer;\n z-index: 1;\n\n &[data-active='true'] {\n background-color: var(--active-bg);\n color: var(--active-color);\n }\n\n &[aria-disabled='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-piano-black-key {\n --color: oklch(100% 0 0);\n --bg: oklch(32% 0 0);\n --active-color: oklch(100% 0 0);\n --active-bg: oklch(44% 0 0);\n\n box-sizing: border-box;\n position: absolute;\n background-color: var(--bg);\n -webkit-tap-highlight-color: transparent;\n color: var(--color);\n border: 1px solid #555;\n border-radius: 0px 0px 8px 8px;\n cursor: pointer;\n z-index: 2;\n\n &[data-active='true'] {\n background-color: var(--active-bg);\n color: var(--active-color);\n }\n\n &[aria-disabled='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-piano-key-label-wrapper {\n display: flex;\n justify-content: center;\n align-items: end;\n height: 100%;\n}\n\n.tremolo-piano-key-label {\n box-sizing: content-box;\n display: flex;\n justify-content: center;\n align-items: center;\n font-size: 0.6rem;\n height: 10px;\n margin-bottom: 10px;\n padding: 4px;\n border-radius: 4px;\n border: 1px solid #888;\n aspect-ratio: 1;\n max-width: calc(100% - 16px);\n}\n\n:where(.dark, [data-theme='dark']) {\n .tremolo-piano-white-key {\n --bg: oklch(80% 0 0);\n --color: oklch(0% 0 0);\n --active-bg: oklch(72% 0 0);\n --active-color: oklch(0% 0 0);\n }\n}\n",".tremolo-slider {\n display: inline-block;\n cursor: pointer;\n outline: 0;\n -webkit-tap-highlight-color: transparent;\n\n &[aria-readonly='true'] {\n cursor: not-allowed;\n }\n}\n\n.tremolo-slider-thumb-wrapper {\n position: absolute;\n translate: -50% -50%;\n z-index: 100;\n}\n\n.tremolo-slider-thumb {\n --color: oklch(59.5% 0.17548 266.373);\n\n background: var(--color);\n border-radius: 50%;\n outline: none;\n\n &[aria-disabled='true'] {\n background: oklch(50.462% 0.03307 270.021);\n }\n\n &[aria-readonly='false']:focus {\n box-shadow: 0px 0px 0px 3px\n color-mix(in srgb, var(--color) 20%, transparent);\n }\n}\n\n.tremolo-slider-track {\n --active: oklch(69.212% 0.12903 267.633);\n --inactive: oklch(95% 0 0);\n\n position: relative;\n\n &[data-vertical='false'] {\n min-width: 50px;\n }\n\n &[data-vertical='true'] {\n min-height: 50px;\n }\n\n &:hover {\n --active: oklch(64.276% 0.15244 266.932);\n --inactive: #e0e0e0;\n }\n\n &[aria-disabled='true'] {\n --active: oklch(75.327% 0.01237 264.547);\n --inactive: oklch(89.755% 0.0001 271.152);\n }\n}\n\n.tremolo-slider-scale {\n display: block;\n position: relative;\n}\n\n.tremolo-slider-scale-option {\n display: flex;\n color: oklch(25.197% 0.00003 271.152);\n flex-direction: column;\n justify-content: center;\n align-items: center;\n position: absolute;\n translate: -50% 0;\n z-index: 10;\n\n &[data-vertical='true'] {\n flex-direction: row;\n translate: 0 -50%;\n }\n}\n\n.tremolo-slider-scale-option-mark {\n background-color: currentColor;\n}\n\n.tremolo-slider-scale-option-label {\n text-align: right;\n}\n\n:where(.dark, [data-theme='dark']) {\n .tremolo-slider-thumb {\n --color: oklch(64.5% 0.17548 266.373);\n\n &:hover {\n --color: oklch(67.5% 0.17548 266.373);\n }\n }\n\n .tremolo-slider-track {\n --active: oklch(59.5% 0.17548 266.373);\n --inactive: oklch(41.7% 0 0);\n\n &:hover {\n --active: oklch(62.5% 0.17548 266.373);\n --inactive: oklch(44.7% 0 0);\n }\n\n &[aria-disabled='true'] {\n --active: oklch(75.327% 0.01237 264.547);\n --inactive: oklch(89.755% 0.0001 271.152);\n }\n }\n}\n",".tremolo-xy-pad {\n display: inline-block;\n cursor: pointer;\n}\n\n.tremolo-xy-pad-area {\n --color: oklch(95% 0 0);\n\n position: relative;\n background-color: var(--color);\n}\n\n.tremolo-xy-pad-thumb-wrapper {\n position: absolute;\n translate: -50% -50%;\n z-index: 100;\n}\n\n.tremolo-xy-pad-thumb {\n --color: oklch(59.5% 0.17548 266.373);\n\n border-radius: 50%;\n outline: none;\n background-color: var(--color);\n\n &[aria-disabled='true'] {\n background-color: oklch(50.462% 0.03307 270.021);\n }\n\n &[aria-readonly='false']:focus {\n box-shadow: 0px 0px 0px 3px\n color-mix(in srgb, var(--color) 20%, transparent);\n }\n}\n\n:where(.dark, [data-theme='dark']) {\n .tremolo-xy-pad-area {\n --color: oklch(41.7% 0 0);\n }\n\n .tremolo-xy-pad-thumb {\n --color: oklch(64.5% 0.17548 266.373);\n\n &:hover {\n --color: oklch(67.5% 0.17548 266.373);\n }\n }\n}\n"],"mappings":"AAAA;AACA;AACA;;ACFA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;ACxEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;ACxLA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACzFA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AC/GA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA"}
@@ -1,314 +0,0 @@
1
- import clsx from 'clsx'
2
- import React, {
3
- ComponentPropsWithoutRef,
4
- CSSProperties,
5
- forwardRef,
6
- useCallback,
7
- useImperativeHandle,
8
- useRef,
9
- } from 'react'
10
-
11
- import {
12
- clamp,
13
- normalizeValue,
14
- radian,
15
- rawValue,
16
- stepValue,
17
- InputEventOption,
18
- } from '@tremolo-ui/functions'
19
-
20
- import { useDrag } from '../../hooks/useDrag'
21
- import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
22
- import { addNoSelect, removeNoSelect } from '../_util'
23
- import { AnimationCanvas, DrawFunction } from '../AnimationCanvas'
24
-
25
- /** @category AnimationKnob */
26
- export type AnimationKnobProps = {
27
- // required
28
- value: number
29
- min: number
30
- max: number
31
-
32
- // optional
33
- step?: number
34
- skew?: number // | SkewFunction
35
- /**
36
- * value set when double-clicking
37
- * @see enableDoubleClickDefault
38
- */
39
- defaultValue?: number
40
-
41
- /**
42
- * Value to be used as the starting point of the line when drawing.
43
- * @default min
44
- */
45
- startValue?: number
46
-
47
- /** Priority over width or height */
48
- size?: number | `${number}%`
49
-
50
- width?: number | `${number}%`
51
- height?: number | `${number}%`
52
-
53
- /** active line color */
54
- activeColor?: string
55
- /** inactive line color */
56
- inactiveColor?: string
57
- /** background color */
58
- bg?: string
59
- /** thumb color */
60
- thumb?: string
61
- lineWeight?: number
62
- thumbWeight?: number
63
-
64
- draw?: DrawFunction
65
-
66
- /** Whether to apply `{use-select: none}` when dragging */
67
- bodyNoSelect?: boolean
68
- /**
69
- * wheel control option
70
- */
71
- wheel?: InputEventOption | null
72
- /**
73
- * keyboard control option
74
- */
75
- keyboard?: InputEventOption | null
76
- enableDoubleClickDefault?: boolean
77
- style?: CSSProperties
78
- onChange?: (value: number) => void
79
- }
80
-
81
- /**
82
- * @category AnimationKnob
83
- */
84
- export interface AnimationKnobMethods {
85
- focus: () => void
86
- blur: () => void
87
- }
88
-
89
- type Props = AnimationKnobProps &
90
- Omit<ComponentPropsWithoutRef<'div'>, keyof AnimationKnobProps>
91
-
92
- /**
93
- * Animatable rotary knob.
94
- * @category AnimationKnob
95
- */
96
- export const AnimationKnob = forwardRef<AnimationKnobMethods, Props>(
97
- (
98
- {
99
- value,
100
- min,
101
- max,
102
- defaultValue = min,
103
- startValue = min,
104
- step = 1,
105
- skew = 1,
106
- size,
107
- width = 50,
108
- height = 50,
109
- activeColor = '#7998ec',
110
- inactiveColor = '#eee',
111
- bg = '#ccc',
112
- thumb = '#4e76e6',
113
- lineWeight = 6,
114
- thumbWeight = 4,
115
- draw,
116
- bodyNoSelect = true,
117
- wheel = ['raw', 1],
118
- keyboard = ['raw', 1],
119
- enableDoubleClickDefault = true,
120
- style,
121
- className,
122
- onChange,
123
- ...props
124
- }: Props,
125
- ref,
126
- ) => {
127
- // -- state and ref ---
128
- const valueRef = useRef(0)
129
- const elmRef = useRef<HTMLDivElement>(null)
130
-
131
- // --- interpret props ---
132
- const isRelativeSize =
133
- typeof (size ?? width) == 'string' || typeof height == 'string'
134
-
135
- const onDrag = useCallback(
136
- (_x: number, y: number) => {
137
- if (!onChange) return
138
- const v = rawValue(valueRef.current - y / 100, min, max, skew)
139
- const v2 = clamp(stepValue(v, step), min, max)
140
- onChange(v2)
141
- },
142
- [max, min, onChange, skew, step],
143
- )
144
-
145
- const handleKeyDown = useCallback(
146
- (event: React.KeyboardEvent<HTMLDivElement>) => {
147
- if (!keyboard || !onChange) return
148
- const key = event.key
149
- if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
150
- event.preventDefault()
151
- const x =
152
- key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
153
- let v
154
- if (keyboard[0] == 'normalized') {
155
- const n = normalizeValue(value, min, max, skew)
156
- v = rawValue(n + x, min, max, skew)
157
- } else {
158
- v = value + x
159
- }
160
- onChange(clamp(stepValue(v, step), min, max))
161
- }
162
- },
163
- [value, min, max, step, skew, keyboard, onChange],
164
- )
165
-
166
- // --- hooks ---
167
- const [touchMoveRefCallback, pointerDownHandler] = useDrag<HTMLDivElement>({
168
- onDrag: onDrag,
169
- onDragStart: () => {
170
- valueRef.current = normalizeValue(value, min, max, skew)
171
- if (bodyNoSelect) addNoSelect()
172
- },
173
- onDragEnd: () => {
174
- if (bodyNoSelect) removeNoSelect()
175
- },
176
- })
177
-
178
- const wheelRefCallback = useRefCallbackEvent(
179
- 'wheel',
180
- (event) => {
181
- if (!wheel) return
182
- event.preventDefault()
183
- const x = event.deltaY > 0 ? -wheel[1] : wheel[1]
184
- let v
185
- if (wheel[0] == 'normalized') {
186
- const n = normalizeValue(value, min, max, skew)
187
- v = rawValue(n + x, min, max, skew)
188
- } else {
189
- v = value + x
190
- }
191
- if (onChange) onChange(clamp(stepValue(v, step), min, max))
192
- },
193
- { passive: false },
194
- [wheel, value, min, max, skew, step, onChange],
195
- )
196
-
197
- useImperativeHandle(ref, () => {
198
- return {
199
- focus() {
200
- elmRef.current?.focus()
201
- },
202
- blur() {
203
- elmRef.current?.blur()
204
- },
205
- }
206
- }, [])
207
-
208
- return (
209
- <div
210
- className={clsx('tremolo-animation-knob', className)}
211
- ref={(div) => {
212
- elmRef.current = div
213
- wheelRefCallback(div)
214
- touchMoveRefCallback(div)
215
- }}
216
- role="slider"
217
- tabIndex={0}
218
- aria-valuenow={value}
219
- aria-valuemax={max}
220
- aria-valuemin={min}
221
- style={{
222
- display: 'inline-block',
223
- width: size ?? width,
224
- height: size ?? height,
225
- cursor: 'pointer',
226
- WebkitTapHighlightColor: 'transparent',
227
- ...style,
228
- }}
229
- onPointerDown={pointerDownHandler}
230
- onDoubleClick={() => {
231
- if (enableDoubleClickDefault && onChange) {
232
- onChange(defaultValue)
233
- }
234
- }}
235
- onKeyDown={handleKeyDown}
236
- {...props}
237
- >
238
- <AnimationCanvas
239
- {...(isRelativeSize
240
- ? { relativeSize: true }
241
- : {
242
- width: size ?? width,
243
- height: size ?? height,
244
- })}
245
- draw={
246
- draw
247
- ? draw
248
- : (ctx, { width, height }) => {
249
- const p = normalizeValue(value, min, max, skew)
250
- const startP = normalizeValue(startValue, min, max, skew)
251
- const cx = width / 2
252
- const cy = height / 2
253
- const r = Math.min(cx, cy)
254
- if (r <= 0) return
255
-
256
- // reset
257
- ctx.clearRect(0, 0, width, height)
258
- ctx.lineCap = 'butt'
259
-
260
- // bg
261
- ctx.beginPath()
262
- ctx.fillStyle = bg
263
- ctx.arc(cx, cy, r - 0.5, 0, 2 * Math.PI)
264
- ctx.fill()
265
-
266
- ctx.lineWidth = lineWeight
267
-
268
- // inactive rotary
269
- ctx.beginPath()
270
- ctx.strokeStyle = inactiveColor
271
- ctx.arc(
272
- cx,
273
- cy,
274
- r - 0.5 - lineWeight / 2,
275
- radian(135),
276
- radian(45),
277
- )
278
- ctx.stroke()
279
-
280
- // active rotary
281
- ctx.beginPath()
282
- ctx.strokeStyle = activeColor
283
- ctx.arc(
284
- cx,
285
- cy,
286
- r - 0.5 - lineWeight / 2,
287
- radian(135 + 270 * startP),
288
- radian(135 + 270 * p),
289
- p < startP,
290
- )
291
- ctx.stroke()
292
-
293
- // thumb
294
- ctx.lineCap = 'round'
295
- ctx.beginPath()
296
- ctx.moveTo(cx, cy)
297
- ctx.lineTo(
298
- cx +
299
- Math.cos(radian(135 + 270 * p)) *
300
- (r - 0.5 - thumbWeight / 2),
301
- cy +
302
- Math.sin(radian(135 + 270 * p)) *
303
- (r - 0.5 - thumbWeight / 2),
304
- )
305
- ctx.strokeStyle = thumb
306
- ctx.lineWidth = thumbWeight
307
- ctx.stroke()
308
- }
309
- }
310
- />
311
- </div>
312
- )
313
- },
314
- )