@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
@@ -6,25 +6,33 @@ import React, {
6
6
  ReactElement,
7
7
  ReactNode,
8
8
  RefObject,
9
+ useCallback,
9
10
  useEffect,
11
+ useMemo,
10
12
  useRef,
11
13
  useState,
12
14
  } from 'react'
13
15
 
14
16
  import {
17
+ isBlackKey,
15
18
  isWhiteKey,
16
- NoteName,
17
- noteName,
18
- noteNames,
19
- parseNoteName,
19
+ noteKey,
20
+ NoteKey,
21
+ noteKeys,
20
22
  } from '@tremolo-ui/functions'
21
23
 
24
+ import { useDragWithElement } from '../../hooks/useDragWithElement'
22
25
  import { useEventListener } from '../../hooks/useEventListener'
23
26
 
24
- import { BlackKey } from './BlackKey'
25
- import { KeyMethods, KeyProps } from './key'
26
- import { KeyBoardShortcuts } from './keyboardShortcuts'
27
- import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
27
+ import { NoteRange, PianoProvider } from './context'
28
+ import {
29
+ BlackKey,
30
+ defaultBlackKeyWidth,
31
+ defaultWhiteKeyWidth,
32
+ KeyMethods,
33
+ WhiteKey,
34
+ } from './key'
35
+ import { KeyboardShortcuts } from './keyboardShortcuts'
28
36
 
29
37
  /**
30
38
  * Piano component
@@ -33,18 +41,27 @@ import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
33
41
  * - add scale highlight
34
42
  */
35
43
 
44
+ /** @category Piano */
45
+ export function getNoteRangeArray(noteRange: NoteRange) {
46
+ return Array.from(
47
+ { length: noteRange.last - noteRange.first + 1 },
48
+ (_, i) => i + noteRange.first,
49
+ )
50
+ }
51
+
36
52
  /** @category Piano */
37
53
  export interface PianoProps {
38
54
  // required
39
- noteRange: { first: number; last: number }
55
+ noteRange: NoteRange
40
56
 
41
57
  // optional
42
- height?: number | string
43
-
44
58
  glissando?: boolean
45
59
  midiMax?: number
46
- keyboardShortcuts?: KeyBoardShortcuts
60
+ keyboardShortcuts?: KeyboardShortcuts
47
61
  fill?: boolean
62
+ whiteNoteWidth?: number
63
+ blackNoteWidth?: number
64
+ height?: number | string
48
65
 
49
66
  style?: CSSProperties
50
67
 
@@ -59,7 +76,10 @@ export interface PianoProps {
59
76
  children?: ReactElement | ReactElement[]
60
77
  }
61
78
 
62
- /** @category Piano */
79
+ /**
80
+ * Customizable piano component.
81
+ * @category Piano
82
+ */
63
83
  export function Piano({
64
84
  noteRange,
65
85
  glissando = true,
@@ -67,78 +87,130 @@ export function Piano({
67
87
  keyboardShortcuts,
68
88
  fill = false,
69
89
  height = fill ? '100%' : 160,
90
+ whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
70
91
  style,
71
92
  className,
72
93
  onPlayNote,
73
94
  onStopNote,
74
95
  label,
75
96
  children,
97
+ onPointerDown,
76
98
  ...props
77
99
  }: PianoProps & Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>) {
78
100
  // -- state and ref ---
79
- const [pressed, setPressed] = useState(false)
80
- const [whiteNoteWidth, setWhiteNoteWidth] = useState(-1)
101
+ const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
81
102
  const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
82
103
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
83
104
  keyRefs.current[i] = createRef<KeyMethods>()
84
105
  }
85
106
  const pianoRef = useRef<HTMLDivElement>(null)
107
+ const hitKeyIndex = useRef(-1)
86
108
 
87
109
  // --- interpret props ---
88
- const noteRangeArray = Array.from(
89
- { length: noteRange.last - noteRange.first + 1 },
90
- (_, i) => i + noteRange.first,
110
+ const noteRangeArray = getNoteRangeArray(noteRange)
111
+ const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
112
+ const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
113
+ const whiteNoteCount = whiteNotes.length
114
+ const padding = 1
115
+ const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
116
+ // const blackNoteShiftPercent = 0.3
117
+
118
+ // TODO: if fill setWhiteNoteWidth
119
+ // useMemo
120
+ // __width
121
+ const childrenWithProps = useMemo(() => {
122
+ return (
123
+ children &&
124
+ React.Children.map(children, (child, index) => {
125
+ if (React.isValidElement(child)) {
126
+ const props = { ref: keyRefs.current[index] }
127
+ return React.cloneElement(child, props)
128
+ }
129
+ return child
130
+ })
131
+ )
132
+ }, [children])
133
+
134
+ // --- internal functions ---
135
+ const notePosition = useCallback(
136
+ (note: number) => {
137
+ const pitchPositions: Record<NoteKey, number> = {
138
+ C: 0,
139
+ 'C#': 1,
140
+ D: 1,
141
+ 'D#': 2,
142
+ E: 2,
143
+ F: 3,
144
+ 'F#': 4,
145
+ G: 4,
146
+ 'G#': 5,
147
+ A: 5,
148
+ 'A#': 6,
149
+ B: 6,
150
+ }
151
+
152
+ const whiteNoteWidth = defaultWhiteKeyWidth
153
+ const blackNoteWidth = defaultBlackKeyWidth
154
+ const padding = 1
155
+
156
+ const targetNoteKey = noteKey(note)
157
+ const firstNoteKey = noteKey(noteRange.first)
158
+ const octave = Math.floor((note - noteRange.first) / 12)
159
+ const octaveOffset =
160
+ noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0
161
+ const w = whiteNoteWidth + padding
162
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
163
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0
164
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset
165
+ },
166
+ [noteRange.first],
91
167
  )
92
- const whiteNoteCount = noteRangeArray.filter((v) => isWhiteKey(v)).length
93
-
94
- let whiteKeyProps: KeyProps = {},
95
- blackKeyProps: KeyProps = {}
96
- if (children != undefined) {
97
- React.Children.forEach(children, (child) => {
98
- if (React.isValidElement(child)) {
99
- if (child.type == WhiteKey) {
100
- whiteKeyProps = child.props as KeyProps
101
- } else if (child.type == BlackKey) {
102
- blackKeyProps = child.props as KeyProps
103
- } else {
104
- throw new Error('only <WhiteKey> or <BlackKey>')
168
+
169
+ const getHitKeyIndex = useCallback(
170
+ (x: number, y: number) => {
171
+ if (!pianoRef.current) return -1
172
+ const containerHeight = pianoRef.current.clientHeight
173
+ const notes = [...blackNotes, ...whiteNotes]
174
+ for (let i = 0; i < notes.length; i++) {
175
+ const note = notes[i]
176
+ const pos = notePosition(note)
177
+ const w = isWhiteKey(note) ? defaultWhiteKeyWidth : defaultBlackKeyWidth
178
+ const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6
179
+ if (pos <= x && x < pos + w && 0 <= y && y < h) {
180
+ return note
105
181
  }
106
- } else {
107
- throw new Error('children is an invalid element.')
108
182
  }
109
- })
110
- }
183
+ return -1
184
+ },
185
+ [blackNotes, notePosition, whiteNotes],
186
+ )
111
187
 
112
- // --- internal functions ---
113
- function notePosition(note: number) {
114
- const pitchPositions: Record<NoteName, number> = {
115
- C: 0,
116
- 'C#': 0.55,
117
- D: 1,
118
- 'D#': 1.8,
119
- E: 2,
120
- F: 3,
121
- 'F#': 3.5,
122
- G: 4,
123
- 'G#': 4.7,
124
- A: 5,
125
- 'A#': 5.85,
126
- B: 6,
127
- }
188
+ // TODO: 単一のポインターに対しては、useDragで対応可能だが、
189
+ // マルチタッチに対しては、TouchEventを使う必要がありそう
190
+ // 取り敢えず、シングルタッチだけ対応
191
+ const onDrag = useCallback(
192
+ (perX: number, perY: number) => {
193
+ if (!pianoRef.current) return
194
+ const x = perX * staticWidth
195
+ const y = perY * pianoRef.current.clientHeight
196
+ const note = getHitKeyIndex(x, y)
197
+ const index = noteRangeArray.indexOf(note)
198
+ if (index == -1) return
199
+ if (hitKeyIndex.current != index) {
200
+ keyRefs.current[hitKeyIndex.current]?.current?.stop()
201
+ keyRefs.current[index]?.current?.play()
202
+ hitKeyIndex.current = index
203
+ }
204
+ },
205
+ [getHitKeyIndex, noteRangeArray, staticWidth],
206
+ )
128
207
 
129
- const toNoteName = (n: number) => {
130
- const { letter, accidental } = parseNoteName(noteName(n))
131
- return (letter + accidental) as NoteName
208
+ const onDragEnd = useCallback(() => {
209
+ if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
210
+ keyRefs.current[hitKeyIndex.current]?.current?.stop()
132
211
  }
133
- const _noteName = toNoteName(note)
134
- const firstNoteName = toNoteName(noteRange.first)
135
- const pos = pitchPositions[_noteName] - pitchPositions[firstNoteName]
136
- const octave = Math.floor((note - noteRange.first) / 12)
137
- const octaveOffset =
138
- noteNames.indexOf(firstNoteName) > noteNames.indexOf(_noteName) ? 1 : 0
139
- const length = whiteNoteWidth + 3
140
- return pos * length + (octave + octaveOffset) * 7 * length
141
- }
212
+ hitKeyIndex.current = -1
213
+ }, [keyRefs])
142
214
 
143
215
  // --- hooks ---
144
216
  useEffect(() => {
@@ -147,13 +219,23 @@ export function Piano({
147
219
  if (!parent) throw new Error("doesn't have a parent element.")
148
220
  const resizeObserver = new ResizeObserver(() => {
149
221
  const w = pianoRef.current!.clientWidth
150
- setWhiteNoteWidth(w / whiteNoteCount - 3)
222
+ setWhiteNoteWidth(w / whiteNoteCount - padding)
151
223
  })
152
224
  resizeObserver.observe(parent)
225
+ return () => {
226
+ resizeObserver.unobserve(parent)
227
+ }
153
228
  } else {
154
- setWhiteNoteWidth(whiteKeyProps?.width || defaultWhiteKeyWidth)
229
+ setWhiteNoteWidth(_whiteNoteWidth)
155
230
  }
156
- }, [fill, whiteKeyProps?.width, whiteNoteCount])
231
+ }, [fill, _whiteNoteWidth, whiteNoteCount])
232
+
233
+ const [touchMoveRefCallback, pointerDownHandler] =
234
+ useDragWithElement<HTMLDivElement>({
235
+ baseElementRef: pianoRef,
236
+ onDrag: onDrag,
237
+ onDragEnd: onDragEnd,
238
+ })
157
239
 
158
240
  useEventListener(globalThis.window, 'keydown', (e) => {
159
241
  if (e.repeat) return
@@ -176,66 +258,54 @@ export function Piano({
176
258
  })
177
259
 
178
260
  return (
179
- <div
180
- ref={pianoRef}
181
- className={clsx('tremolo-piano', className)}
182
- style={{
183
- width: fill ? '100%' : (whiteNoteWidth + 3) * whiteNoteCount,
184
- height: height,
185
- ...style,
186
- }}
187
- onPointerDown={(e) => {
188
- if (props.onPointerDown) props.onPointerDown(e)
189
- setPressed(true)
190
- }}
191
- onPointerUp={(e) => {
192
- if (props.onPointerUp) props.onPointerUp(e)
193
- setPressed(false)
194
- }}
195
- {...props}
261
+ <PianoProvider
262
+ noteRange={noteRange}
263
+ glissando={glissando}
264
+ midiMax={midiMax}
265
+ fill={fill}
266
+ onPlayNote={onPlayNote}
267
+ onStopNote={onStopNote}
268
+ label={label}
196
269
  >
197
- {noteRangeArray.map((note, index) => {
198
- return isWhiteKey(note) ? (
199
- <WhiteKey
200
- key={note}
201
- ref={keyRefs.current[index]}
202
- __glissando={glissando && pressed}
203
- __position={notePosition(note)}
204
- __note={note}
205
- __disabled={note > midiMax}
206
- __index={index}
207
- __fill={fill}
208
- __width={whiteNoteWidth}
209
- __onPlayNote={onPlayNote}
210
- __onStopNote={onStopNote}
211
- __label={label}
212
- {...whiteKeyProps}
213
- />
214
- ) : (
215
- <BlackKey
216
- key={note}
217
- ref={keyRefs.current[index]}
218
- __glissando={glissando && pressed}
219
- __position={notePosition(note)}
220
- __note={note}
221
- __disabled={note > midiMax}
222
- __index={index}
223
- __fill={fill}
224
- __width={whiteNoteWidth * 0.65}
225
- __onPlayNote={onPlayNote}
226
- __onStopNote={onStopNote}
227
- __label={label}
228
- {...blackKeyProps}
229
- />
230
- )
231
- })}
232
- </div>
270
+ <div
271
+ ref={(div) => {
272
+ pianoRef.current = div
273
+ touchMoveRefCallback(div)
274
+ }}
275
+ className={clsx('tremolo-piano', className)}
276
+ style={{
277
+ width: fill ? '100%' : staticWidth,
278
+ height: height,
279
+ ...style,
280
+ }}
281
+ onPointerDown={(event) => {
282
+ pointerDownHandler(event)
283
+ onPointerDown?.(event)
284
+ }}
285
+ {...props}
286
+ >
287
+ {childrenWithProps ||
288
+ getNoteRangeArray(noteRange).map((note, index) =>
289
+ isWhiteKey(note) ? (
290
+ <WhiteKey
291
+ ref={keyRefs.current[index]}
292
+ key={note}
293
+ noteNumber={note}
294
+ />
295
+ ) : (
296
+ <BlackKey
297
+ ref={keyRefs.current[index]}
298
+ key={note}
299
+ noteNumber={note}
300
+ />
301
+ ),
302
+ )}
303
+ </div>
304
+ </PianoProvider>
233
305
  )
234
306
  }
235
307
 
236
- export * from './key'
237
308
  export * from './keyboardShortcuts'
238
309
 
239
- export { WhiteKey } from './WhiteKey'
240
- export { BlackKey } from './BlackKey'
310
+ export { WhiteKey, BlackKey, type KeyProps, type KeyMethods } from './key'
241
311
  export { KeyLabel, type KeyLabelProps } from './KeyLabel'
@@ -0,0 +1,215 @@
1
+ import clsx from 'clsx'
2
+ import React, {
3
+ CSSProperties,
4
+ ComponentPropsWithoutRef,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useState,
8
+ ReactElement,
9
+ } from 'react'
10
+
11
+ import { usePianoContext } from './context'
12
+ import { KeyLabel, KeyLabelProps } from './KeyLabel'
13
+
14
+ /**
15
+ * @category Piano
16
+ */
17
+ export interface KeyProps {
18
+ noteNumber: number
19
+
20
+ width?: number
21
+ height?: number | `${number}%`
22
+
23
+ bg?: string
24
+ color?: string
25
+ activeBg?: string
26
+ activeColor?: string
27
+
28
+ style?: CSSProperties
29
+
30
+ /**
31
+ * \<KeyLabel />
32
+ */
33
+ children?: ReactElement
34
+
35
+ /** @internal */
36
+ __width?: number
37
+ }
38
+
39
+ /**
40
+ * @category Piano
41
+ */
42
+ export interface KeyMethods {
43
+ play: () => void
44
+ stop: () => void
45
+ played: () => boolean
46
+ }
47
+
48
+ type Props = KeyProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyProps>
49
+ type ImplProps = Props & { keyType: 'black' | 'white' }
50
+
51
+ /** @category Piano */
52
+ export const defaultWhiteKeyWidth = 40
53
+ /** @category Piano */
54
+ export const defaultBlackKeyWidth = 26
55
+
56
+ const KeyImpl = forwardRef<KeyMethods, ImplProps>(
57
+ (
58
+ {
59
+ keyType,
60
+ noteNumber,
61
+ width,
62
+ height,
63
+ bg,
64
+ color,
65
+ activeBg,
66
+ activeColor,
67
+ className,
68
+ style,
69
+ children,
70
+ __width,
71
+ ...props
72
+ }: ImplProps,
73
+ ref,
74
+ ) => {
75
+ const [played, setPlayed] = useState(false)
76
+ const midiMax = usePianoContext((s) => s.midiMax)
77
+ const fill = usePianoContext((s) => s.fill)
78
+ // const glissando = usePianoContext(s => s.glissando)
79
+ const onPlayNote = usePianoContext((s) => s.onPlayNote)
80
+ const onStopNote = usePianoContext((s) => s.onStopNote)
81
+ const label = usePianoContext((s) => s.label)
82
+ const notePosition = usePianoContext((s) => s.notePosition)
83
+ const position = notePosition(noteNumber)
84
+
85
+ const disabled = noteNumber > midiMax
86
+ const colors = {
87
+ '--bg': bg,
88
+ '--color': color,
89
+ '--active-bg': activeBg,
90
+ '--active-color': activeColor,
91
+ }
92
+
93
+ useImperativeHandle(ref, () => {
94
+ return {
95
+ play() {
96
+ if (disabled) return
97
+ setPlayed(true)
98
+ if (onPlayNote) onPlayNote(noteNumber)
99
+ },
100
+ stop() {
101
+ setPlayed(false)
102
+ if (onStopNote) onStopNote(noteNumber)
103
+ },
104
+ played() {
105
+ return played
106
+ },
107
+ }
108
+ }, [disabled, noteNumber, onPlayNote, onStopNote, played])
109
+
110
+ let keyLabelProps: KeyLabelProps = {}
111
+ if (children != undefined) {
112
+ React.Children.forEach(children, (child) => {
113
+ if (React.isValidElement(child)) {
114
+ if (child.type == KeyLabel) {
115
+ keyLabelProps = child.props as KeyLabelProps
116
+ } else {
117
+ throw new Error('only <KeyLabel>')
118
+ }
119
+ } else {
120
+ throw new Error('children is an invalid element.')
121
+ }
122
+ })
123
+ }
124
+
125
+ return (
126
+ <div
127
+ className={clsx(`tremolo-piano-${keyType}-key`, className)}
128
+ aria-disabled={disabled}
129
+ data-active={played}
130
+ style={{
131
+ ...colors,
132
+ left: position,
133
+ width: fill ? __width : width,
134
+ height: height,
135
+ ...style,
136
+ }}
137
+ // onPointerDown={() => {
138
+ // if (disabled) return
139
+ // setPlayed(true)
140
+ // if (onPlayNote) onPlayNote(noteNumber)
141
+ // }}
142
+ // onPointerEnter={() => {
143
+ // if (disabled) return
144
+ // if (glissando) {
145
+ // setPlayed(true)
146
+ // if (onPlayNote) onPlayNote(noteNumber)
147
+ // }
148
+ // }}
149
+ // onPointerUp={() => {
150
+ // if (disabled) return
151
+ // setPlayed(false)
152
+ // if (onStopNote) onStopNote(noteNumber)
153
+ // }}
154
+ // onPointerLeave={() => {
155
+ // if (disabled) return
156
+ // if (glissando) {
157
+ // setPlayed(false)
158
+ // if (onStopNote) onStopNote(noteNumber)
159
+ // }
160
+ // }}
161
+ {...props}
162
+ >
163
+ <KeyLabel label={label} __note={noteNumber} {...keyLabelProps} />
164
+ </div>
165
+ )
166
+ },
167
+ )
168
+
169
+ /** @category Piano */
170
+ export const WhiteKey = forwardRef<KeyMethods, Props>(
171
+ (
172
+ {
173
+ width = defaultWhiteKeyWidth,
174
+ height = '100%',
175
+ onContextMenu = (e) => e.preventDefault(),
176
+ ...props
177
+ }: Props,
178
+ ref,
179
+ ) => {
180
+ return (
181
+ <KeyImpl
182
+ ref={ref}
183
+ keyType="white"
184
+ width={width}
185
+ height={height}
186
+ onContextMenu={onContextMenu}
187
+ {...props}
188
+ />
189
+ )
190
+ },
191
+ )
192
+
193
+ /** @category Piano */
194
+ export const BlackKey = forwardRef<KeyMethods, Props>(
195
+ (
196
+ {
197
+ width = defaultBlackKeyWidth,
198
+ height = '60%',
199
+ onContextMenu = (e) => e.preventDefault(),
200
+ ...props
201
+ }: Props,
202
+ ref,
203
+ ) => {
204
+ return (
205
+ <KeyImpl
206
+ ref={ref}
207
+ keyType="black"
208
+ width={width}
209
+ height={height}
210
+ onContextMenu={onContextMenu}
211
+ {...props}
212
+ />
213
+ )
214
+ },
215
+ )
@@ -1,5 +1,5 @@
1
1
  /** @category Piano */
2
- export type KeyBoardShortcuts = {
2
+ export type KeyboardShortcuts = {
3
3
  keys: string[]
4
4
  // TODO
5
5
  flags?: {
@@ -8,7 +8,7 @@ export type KeyBoardShortcuts = {
8
8
  }
9
9
 
10
10
  /** @category Piano */
11
- export const Shortcuts = {
11
+ export const SHORTCUTS = {
12
12
  HOME_ROW: {
13
13
  keys: [
14
14
  'a',
@@ -14,9 +14,6 @@ export interface ScaleProps {
14
14
  children?: ReactNode
15
15
  }
16
16
 
17
- type Props = ScaleProps &
18
- Omit<ComponentPropsWithoutRef<'div'>, keyof ScaleProps>
19
-
20
17
  /** @category Slider */
21
18
  export function Scale({
22
19
  gap = 6,
@@ -25,7 +22,7 @@ export function Scale({
25
22
  className,
26
23
  style,
27
24
  ...props
28
- }: Props) {
25
+ }: ScaleProps & Omit<ComponentPropsWithoutRef<'div'>, keyof ScaleProps>) {
29
26
  const min = useSliderContext((s) => s.min)
30
27
  const max = useSliderContext((s) => s.max)
31
28
  const step = useSliderContext((s) => s.step)
@@ -15,6 +15,8 @@ export interface SliderThumbProps {
15
15
  width?: number | string
16
16
  height?: number | string
17
17
 
18
+ color?: string
19
+
18
20
  className?: string
19
21
  style?: CSSProperties
20
22
  children?: ReactNode
@@ -39,6 +41,7 @@ export const SliderThumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
39
41
  size,
40
42
  width = defaultThumbSize,
41
43
  height = defaultThumbSize,
44
+ color,
42
45
  children,
43
46
  className,
44
47
  style,
@@ -82,6 +85,7 @@ export const SliderThumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
82
85
  aria-disabled={disabled}
83
86
  aria-readonly={readonly}
84
87
  style={{
88
+ ...{ '--color': color },
85
89
  width: size ?? width,
86
90
  height: size ?? height,
87
91
  ...style,