@tremolo-ui/react 0.4.0 → 0.6.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 (90) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
@@ -1,24 +1,44 @@
1
- import clsx from 'clsx'
2
1
  import { SVGProps } from 'react'
3
2
 
4
3
  import { clamp } from '@tremolo-ui/functions'
5
4
 
5
+ import { useCheckPlacement } from '../_util/Placement'
6
+
6
7
  import { useKnobContext, viewBoxSize } from './context'
7
8
 
8
- interface Props {
9
- /** color */
9
+ export interface KnobThumbProps {
10
+ /**
11
+ * Fill colour of the circle.
12
+ * @default 'currentColor'
13
+ */
10
14
  thumb?: string
11
- /** color */
15
+ /**
16
+ * Colour of the line that points at the value.
17
+ * @default 'currentColor'
18
+ */
12
19
  thumbLine?: string
13
- /** percent (0-100) */
20
+ /**
21
+ * Diameter of the circle, as a percentage of the knob.
22
+ * @default 84
23
+ */
14
24
  thumbSize?: number
15
- /** percent (0-100) */
25
+ /**
26
+ * Thickness of the line, as a percentage of the knob.
27
+ * @default 6
28
+ */
16
29
  thumbLineWeight?: number
17
- /** percent (0-100) */
30
+ /**
31
+ * How far down the line reaches, as a percentage of the knob from its top.
32
+ * The line starts at the edge of the circle.
33
+ * @default 35
34
+ */
18
35
  thumbLineLength?: number
19
36
 
37
+ /**
38
+ * Classes for what the thumb draws inside itself: `thumbLine` is the line
39
+ * that points at the value.
40
+ */
20
41
  classes?: {
21
- thumb?: string
22
42
  thumbLine?: string
23
43
  }
24
44
  }
@@ -34,17 +54,19 @@ export function Thumb({
34
54
  classes,
35
55
 
36
56
  ...props
37
- }: Props & Omit<SVGProps<SVGSVGElement>, 'd' | keyof Props>) {
57
+ }: KnobThumbProps & Omit<SVGProps<SVGSVGElement>, 'd' | keyof KnobThumbProps>) {
58
+ useCheckPlacement('Knob.Thumb', 'Knob.SVGRoot')
59
+
38
60
  const angleRange = useKnobContext((s) => s.angleRange)
39
61
 
40
62
  const p = useKnobContext((s) => s.p)
41
63
  const r1 = useKnobContext((s) => s.r1)
42
64
 
43
65
  return (
44
- <svg className={clsx('tremolo-knob-thumb', classes?.thumb)} {...props}>
66
+ <svg className={className} {...props}>
45
67
  <circle cx="50%" cy="50%" r={`${thumbSize / 2}%`} fill={thumb} />
46
68
  <line
47
- className={clsx('tremolo-knob-thumb-line', classes?.thumbLine)}
69
+ className={classes?.thumbLine}
48
70
  x1="50%"
49
71
  y1={`${(viewBoxSize - clamp(thumbSize, 0, 100)) / 2}%`}
50
72
  x2="50%"
@@ -1,120 +1,104 @@
1
- import {
2
- createContext,
3
- PropsWithChildren,
4
- useContext,
5
- useEffect,
6
- useRef,
7
- } from 'react'
8
- import { createStore, useStore } from 'zustand'
1
+ import { createContext, useContext } from 'react'
9
2
 
10
- import { normalizeValue, radian } from '@tremolo-ui/functions'
3
+ import { radian, type Scale } from '@tremolo-ui/functions'
11
4
 
12
5
  export const viewBoxSize = 100
13
6
  export const center = viewBoxSize / 2
14
7
 
15
- type State = {
8
+ export type KnobConfig = {
16
9
  value: number
17
10
  min: number
18
11
  max: number
19
12
  step: number
20
- skew: number // | SkewFunction // TODO
13
+ scale: Scale
21
14
  startValue: number
15
+ /** angle range [degree] */
22
16
  angleRange: number
23
17
  }
24
18
 
25
- type DrawingState = {
26
- p: number // normalized value
27
- r1: number // ロータリー開始位置
28
- r2: number // activeLineの開始位置
29
- r3: number // activeLineの終了位置
30
- r4: number // ロータリー終了位置
31
- x1: number
32
- y1: number
33
- x2: number
34
- y2: number
35
- x3: number
36
- y3: number
37
- x4: number
38
- y4: number
19
+ export type KnobContextValue = KnobConfig & {
20
+ /** normalized value */
21
+ p: number
22
+ /** ロータリー開始位置 */
23
+ r1: number
24
+ /** activeLine の開始位置 */
25
+ r2: number
26
+ /** activeLine の終了位置 */
27
+ r3: number
28
+ /** ロータリー終了位置 */
29
+ r4: number
39
30
  }
40
31
 
41
- function calcDrawingState({
32
+ /**
33
+ * 角度から円弧上の点を求める。
34
+ *
35
+ * 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
36
+ * それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
37
+ */
38
+ export function pointOnArc(angle: number, radius: number) {
39
+ return {
40
+ x: center + radius * Math.cos(radian(angle - 90)),
41
+ y: center + radius * Math.sin(radian(angle - 90)),
42
+ }
43
+ }
44
+
45
+ /** 線の太さが viewBox からはみ出さないようにした半径 */
46
+ export function arcRadius(strokeWidth: number | string | undefined) {
47
+ const width =
48
+ typeof strokeWidth === 'number'
49
+ ? strokeWidth
50
+ : Number.parseFloat(String(strokeWidth))
51
+ return Number.isFinite(width) ? center - width / 2 : center
52
+ }
53
+
54
+ /** Build an SVG path for an arc, splitting full turns into drawable segments. */
55
+ export function arcPath(startAngle: number, endAngle: number, radius: number) {
56
+ const start = pointOnArc(startAngle, radius)
57
+ const sweep = endAngle - startAngle
58
+ const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180))
59
+ const segmentSweep = sweep / segmentCount
60
+ let path = `M ${start.x} ${start.y}`
61
+
62
+ for (let i = 1; i <= segmentCount; i += 1) {
63
+ const end = pointOnArc(startAngle + segmentSweep * i, radius)
64
+ path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`
65
+ }
66
+
67
+ return path
68
+ }
69
+
70
+ /** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
71
+ export function calcAngles({
42
72
  value,
43
73
  min,
44
74
  max,
45
- skew,
75
+ scale,
46
76
  startValue,
47
77
  angleRange,
48
- }: State) {
49
- const p = normalizeValue(value, min, max, skew)
50
- const s = normalizeValue(startValue, min, max, skew)
78
+ }: KnobConfig) {
79
+ const p = scale.normalize(value, min, max)
80
+ const s = scale.normalize(startValue, min, max)
51
81
 
52
82
  const r1 = -angleRange / 2
53
83
  const r2 = r1 + Math.min(p, s) * angleRange
54
84
  const r3 = r1 + Math.max(p, s) * angleRange
55
85
  const r4 = angleRange / 2
56
- const x1 = center + center * Math.cos(radian(r1 - 90))
57
- const y1 = center + center * Math.sin(radian(r1 - 90))
58
- const x2 = center + center * Math.cos(radian(r2 - 90))
59
- const y2 = center + center * Math.sin(radian(r2 - 90))
60
- const x3 = center + center * Math.cos(radian(r3 - 90))
61
- const y3 = center + center * Math.sin(radian(r3 - 90))
62
- const x4 = center + center * Math.cos(radian(r4 - 90))
63
- const y4 = center + center * Math.sin(radian(r4 - 90))
64
-
65
- return {
66
- p,
67
- r1,
68
- r2,
69
- r3,
70
- r4,
71
- x1,
72
- y1,
73
- x2,
74
- y2,
75
- x3,
76
- y3,
77
- x4,
78
- y4,
79
- }
80
- }
81
86
 
82
- type KnobStore = ReturnType<typeof createKnobStore>
83
-
84
- const createKnobStore = (initProps: State) => {
85
- return createStore<State & DrawingState>()(() => ({
86
- ...initProps,
87
- ...calcDrawingState(initProps),
88
- }))
87
+ return { p, r1, r2, r3, r4 }
89
88
  }
90
89
 
91
- const KnobContext = createContext<KnobStore | null>(null)
92
-
93
- export function KnobProvider({ children, ...props }: PropsWithChildren<State>) {
94
- const storeRef = useRef<KnobStore>(null)
95
- if (!storeRef.current) {
96
- storeRef.current = createKnobStore(props)
97
- }
90
+ const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
98
91
 
99
- useEffect(() => {
100
- if (storeRef.current) {
101
- storeRef.current.setState({ ...props, ...calcDrawingState(props) })
102
- } else {
103
- storeRef.current = createKnobStore(props)
104
- }
105
- }, [props])
106
-
107
- return (
108
- <KnobContext.Provider value={storeRef.current}>
109
- {children}
110
- </KnobContext.Provider>
111
- )
112
- }
92
+ export const KnobProvider = KnobContext.Provider
113
93
 
114
- export function useKnobContext<T>(
115
- selector: (state: State & DrawingState) => T,
116
- ): T {
117
- const store = useContext(KnobContext)
118
- if (!store) throw new Error('Missing KnobContext.Provider in the tree')
119
- return useStore(store, selector)
94
+ /**
95
+ * Everything here is derived during render, so there is no state to keep in
96
+ * sync: `value` comes from the props of `Root` and the rest follows from it.
97
+ */
98
+ export function useKnobContext(): KnobContextValue
99
+ export function useKnobContext<T>(selector: (state: KnobContextValue) => T): T
100
+ export function useKnobContext<T>(selector?: (state: KnobContextValue) => T) {
101
+ const context = useContext(KnobContext)
102
+ if (!context) throw new Error('Missing KnobContext.Provider in the tree')
103
+ return selector ? selector(context) : context
120
104
  }