@tremolo-ui/react 0.1.5 → 0.2.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.
- package/dist/index.cjs +325 -239
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +149 -64
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +229 -199
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +229 -199
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +314 -223
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Knob/index.css +50 -25
- package/src/components/Knob/index.tsx +37 -32
- package/src/components/NumberInput/index.css +31 -15
- package/src/components/NumberInput/index.tsx +22 -14
- package/src/components/Piano/context.tsx +1 -1
- package/src/components/Piano/index.css +17 -8
- package/src/components/Piano/index.tsx +259 -220
- package/src/components/Piano/key.tsx +3 -3
- package/src/components/Slider/Thumb.tsx +1 -1
- package/src/components/Slider/Track.tsx +3 -3
- package/src/components/Slider/index.css +33 -8
- package/src/components/Slider/index.tsx +25 -25
- package/src/components/XYPad/Area.tsx +4 -3
- package/src/components/XYPad/Thumb.tsx +8 -4
- package/src/components/XYPad/index.css +18 -4
- package/src/components/XYPad/index.tsx +25 -19
- package/src/hooks/useAnimationFrame.ts +3 -0
- package/src/hooks/useCallbackRef.ts +2 -2
- package/src/hooks/useDrag.ts +2 -1
- package/src/hooks/useDragWithElement.ts +2 -1
- package/src/hooks/useEventListener.ts +3 -0
- package/src/hooks/useInterval.ts +3 -0
- package/src/hooks/useLongPress.ts +3 -0
- package/src/hooks/useMIDIAccess.ts +40 -0
- package/src/hooks/useMIDIInput.ts +50 -0
- package/src/hooks/useMIDIMessage.ts +24 -0
- package/src/hooks/useRefCallbackEvent.ts +4 -0
- package/src/index.ts +30 -35
- package/src/styles/global.css +0 -4
|
@@ -3,11 +3,13 @@ import React, {
|
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
createRef,
|
|
5
5
|
CSSProperties,
|
|
6
|
+
forwardRef,
|
|
6
7
|
ReactElement,
|
|
7
8
|
ReactNode,
|
|
8
9
|
RefObject,
|
|
9
10
|
useCallback,
|
|
10
11
|
useEffect,
|
|
12
|
+
useImperativeHandle,
|
|
11
13
|
useMemo,
|
|
12
14
|
useRef,
|
|
13
15
|
useState,
|
|
@@ -33,15 +35,12 @@ import {
|
|
|
33
35
|
WhiteKey,
|
|
34
36
|
} from './key'
|
|
35
37
|
import { KeyboardShortcuts } from './keyboardShortcuts'
|
|
38
|
+
import { KeyLabel } from './KeyLabel'
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* TODO:
|
|
41
|
-
* - add scale highlight
|
|
41
|
+
* [noteRange.first, noteRange.first + 1 ..., noteRange.last]
|
|
42
|
+
* @category Piano
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
-
/** @category Piano */
|
|
45
44
|
export function getNoteRangeArray(noteRange: NoteRange) {
|
|
46
45
|
return Array.from(
|
|
47
46
|
{ length: noteRange.last - noteRange.first + 1 },
|
|
@@ -51,10 +50,8 @@ export function getNoteRangeArray(noteRange: NoteRange) {
|
|
|
51
50
|
|
|
52
51
|
/** @category Piano */
|
|
53
52
|
export interface PianoProps {
|
|
54
|
-
// required
|
|
55
53
|
noteRange: NoteRange
|
|
56
54
|
|
|
57
|
-
// optional
|
|
58
55
|
glissando?: boolean
|
|
59
56
|
midiMax?: number
|
|
60
57
|
keyboardShortcuts?: KeyboardShortcuts
|
|
@@ -62,11 +59,10 @@ export interface PianoProps {
|
|
|
62
59
|
whiteNoteWidth?: number
|
|
63
60
|
blackNoteWidth?: number
|
|
64
61
|
height?: number | string
|
|
65
|
-
|
|
66
62
|
style?: CSSProperties
|
|
67
63
|
|
|
68
|
-
onPlayNote?: (
|
|
69
|
-
onStopNote?: (
|
|
64
|
+
onPlayNote?: (note: number, velocity?: number) => void
|
|
65
|
+
onStopNote?: (note: number) => void
|
|
70
66
|
|
|
71
67
|
label?: (note: number, index: number) => ReactNode
|
|
72
68
|
|
|
@@ -79,236 +75,279 @@ export interface PianoProps {
|
|
|
79
75
|
const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth // 0.65
|
|
80
76
|
|
|
81
77
|
/**
|
|
82
|
-
* Customizable piano component.
|
|
83
78
|
* @category Piano
|
|
84
79
|
*/
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
keyboardShortcuts,
|
|
90
|
-
fill = false,
|
|
91
|
-
height = fill ? '100%' : 160,
|
|
92
|
-
whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
|
|
93
|
-
style,
|
|
94
|
-
className,
|
|
95
|
-
onPlayNote,
|
|
96
|
-
onStopNote,
|
|
97
|
-
label,
|
|
98
|
-
children,
|
|
99
|
-
onPointerDown,
|
|
100
|
-
...props
|
|
101
|
-
}: PianoProps & Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>) {
|
|
102
|
-
// -- state and ref ---
|
|
103
|
-
const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
|
|
104
|
-
const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
|
|
105
|
-
for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
|
|
106
|
-
keyRefs.current[i] = createRef<KeyMethods>()
|
|
107
|
-
}
|
|
108
|
-
const pianoRef = useRef<HTMLDivElement>(null)
|
|
109
|
-
const hitKeyIndex = useRef(-1)
|
|
80
|
+
export interface PianoMethods {
|
|
81
|
+
playNote: (note: number, velocity?: number) => void
|
|
82
|
+
stopNote: (note: number) => void
|
|
83
|
+
}
|
|
110
84
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
|
|
114
|
-
const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
|
|
115
|
-
const whiteNoteCount = whiteNotes.length
|
|
116
|
-
const padding = 1
|
|
117
|
-
const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
|
|
118
|
-
// const blackNoteShiftPercent = 0.3
|
|
85
|
+
type Props = PianoProps &
|
|
86
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
88
|
+
const PianoImpl = forwardRef<PianoMethods, Props>(
|
|
89
|
+
(
|
|
90
|
+
{
|
|
91
|
+
noteRange,
|
|
92
|
+
glissando = true,
|
|
93
|
+
midiMax = 127,
|
|
94
|
+
keyboardShortcuts,
|
|
95
|
+
fill = false,
|
|
96
|
+
height = fill ? '100%' : 160,
|
|
97
|
+
whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
|
|
98
|
+
style,
|
|
99
|
+
className,
|
|
100
|
+
onPlayNote,
|
|
101
|
+
onStopNote,
|
|
102
|
+
label,
|
|
103
|
+
children,
|
|
104
|
+
onPointerDown,
|
|
105
|
+
...props
|
|
106
|
+
},
|
|
107
|
+
forwardedRef,
|
|
108
|
+
) => {
|
|
109
|
+
// -- state and ref ---
|
|
110
|
+
const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
|
|
111
|
+
const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
|
|
112
|
+
for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
|
|
113
|
+
keyRefs.current[i] = createRef<KeyMethods>()
|
|
114
|
+
}
|
|
115
|
+
const pianoRef = useRef<HTMLDivElement>(null)
|
|
116
|
+
const hitKeyIndex = useRef(-1)
|
|
117
|
+
|
|
118
|
+
// --- interpret props ---
|
|
119
|
+
const noteRangeArray = getNoteRangeArray(noteRange)
|
|
120
|
+
const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
|
|
121
|
+
const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
|
|
122
|
+
const whiteNoteCount = whiteNotes.length
|
|
123
|
+
const padding = 1
|
|
124
|
+
const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
|
|
125
|
+
// const blackNoteShiftPercent = 0.3
|
|
126
|
+
|
|
127
|
+
const childrenWithProps = useMemo(() => {
|
|
128
|
+
return (
|
|
129
|
+
children &&
|
|
130
|
+
React.Children.map(children, (child, index) => {
|
|
131
|
+
if (React.isValidElement(child)) {
|
|
132
|
+
const __width =
|
|
133
|
+
child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * 0.65
|
|
134
|
+
const props = { ref: keyRefs.current[index], __width }
|
|
135
|
+
return React.cloneElement(child, props)
|
|
136
|
+
}
|
|
137
|
+
return child
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
}, [children, whiteNoteWidth])
|
|
141
|
+
|
|
142
|
+
// --- internal functions ---
|
|
143
|
+
const notePosition = useCallback(
|
|
144
|
+
(note: number) => {
|
|
145
|
+
const pitchPositions: Record<NoteKey, number> = {
|
|
146
|
+
C: 0,
|
|
147
|
+
'C#': 1,
|
|
148
|
+
D: 1,
|
|
149
|
+
'D#': 2,
|
|
150
|
+
E: 2,
|
|
151
|
+
F: 3,
|
|
152
|
+
'F#': 4,
|
|
153
|
+
G: 4,
|
|
154
|
+
'G#': 5,
|
|
155
|
+
A: 5,
|
|
156
|
+
'A#': 6,
|
|
157
|
+
B: 6,
|
|
129
158
|
}
|
|
130
|
-
return child
|
|
131
|
-
})
|
|
132
|
-
)
|
|
133
|
-
}, [children, whiteNoteWidth])
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
160
|
+
const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth
|
|
161
|
+
const padding = 1
|
|
162
|
+
const targetNoteKey = noteKey(note)
|
|
163
|
+
const firstNoteKey = noteKey(noteRange.first)
|
|
164
|
+
const octave = Math.floor((note - noteRange.first) / 12)
|
|
165
|
+
const octaveOffset =
|
|
166
|
+
noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey)
|
|
167
|
+
? 1
|
|
168
|
+
: 0
|
|
169
|
+
const w = whiteNoteWidth + padding
|
|
170
|
+
const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
|
|
171
|
+
const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0
|
|
172
|
+
return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset
|
|
173
|
+
},
|
|
174
|
+
[noteRange.first, whiteNoteWidth],
|
|
175
|
+
)
|
|
152
176
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
const getHitKeyIndex = useCallback(
|
|
178
|
+
(x: number, y: number) => {
|
|
179
|
+
if (!pianoRef.current) return -1
|
|
180
|
+
const containerHeight = pianoRef.current.clientHeight
|
|
181
|
+
const notes = [...blackNotes, ...whiteNotes]
|
|
182
|
+
for (let i = 0; i < notes.length; i++) {
|
|
183
|
+
const note = notes[i]
|
|
184
|
+
const pos = notePosition(note)
|
|
185
|
+
const w = isWhiteKey(note)
|
|
186
|
+
? whiteNoteWidth
|
|
187
|
+
: whiteNoteWidth * blackPerWhiteWidth
|
|
188
|
+
const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6
|
|
189
|
+
if (pos <= x && x < pos + w && 0 <= y && y < h) {
|
|
190
|
+
return note
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return -1
|
|
194
|
+
},
|
|
195
|
+
[blackNotes, notePosition, whiteNoteWidth, whiteNotes],
|
|
196
|
+
)
|
|
167
197
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
198
|
+
// TODO: 単一のポインターに対しては、useDragで対応可能だが、
|
|
199
|
+
// マルチタッチに対しては、TouchEventを使う必要がありそう
|
|
200
|
+
// 取り敢えず、シングルタッチだけ対応
|
|
201
|
+
const onDrag = useCallback(
|
|
202
|
+
(perX: number, perY: number) => {
|
|
203
|
+
if (!pianoRef.current) return
|
|
204
|
+
const x = perX * staticWidth
|
|
205
|
+
const y = perY * pianoRef.current.clientHeight
|
|
206
|
+
const note = getHitKeyIndex(x, y)
|
|
207
|
+
const index = noteRangeArray.indexOf(note)
|
|
208
|
+
if (index == -1) return
|
|
209
|
+
if (hitKeyIndex.current != index) {
|
|
210
|
+
keyRefs.current[hitKeyIndex.current]?.current?.stop()
|
|
211
|
+
keyRefs.current[index]?.current?.play()
|
|
212
|
+
hitKeyIndex.current = index
|
|
182
213
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
[blackNotes, notePosition, whiteNoteWidth, whiteNotes],
|
|
187
|
-
)
|
|
214
|
+
},
|
|
215
|
+
[getHitKeyIndex, noteRangeArray, staticWidth],
|
|
216
|
+
)
|
|
188
217
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// 取り敢えず、シングルタッチだけ対応
|
|
192
|
-
const onDrag = useCallback(
|
|
193
|
-
(perX: number, perY: number) => {
|
|
194
|
-
if (!pianoRef.current) return
|
|
195
|
-
const x = perX * staticWidth
|
|
196
|
-
const y = perY * pianoRef.current.clientHeight
|
|
197
|
-
const note = getHitKeyIndex(x, y)
|
|
198
|
-
const index = noteRangeArray.indexOf(note)
|
|
199
|
-
if (index == -1) return
|
|
200
|
-
if (hitKeyIndex.current != index) {
|
|
218
|
+
const onDragEnd = useCallback(() => {
|
|
219
|
+
if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
|
|
201
220
|
keyRefs.current[hitKeyIndex.current]?.current?.stop()
|
|
202
|
-
keyRefs.current[index]?.current?.play()
|
|
203
|
-
hitKeyIndex.current = index
|
|
204
221
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
222
|
+
hitKeyIndex.current = -1
|
|
223
|
+
}, [keyRefs])
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
// --- hooks ---
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (fill && pianoRef.current) {
|
|
228
|
+
const parent = pianoRef.current.parentElement
|
|
229
|
+
if (!parent) throw new Error("doesn't have a parent element.")
|
|
230
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
231
|
+
const w = pianoRef.current!.clientWidth
|
|
232
|
+
setWhiteNoteWidth(w / whiteNoteCount - padding)
|
|
233
|
+
})
|
|
234
|
+
resizeObserver.observe(parent)
|
|
235
|
+
return () => {
|
|
236
|
+
resizeObserver.unobserve(parent)
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
setWhiteNoteWidth(_whiteNoteWidth)
|
|
240
|
+
}
|
|
241
|
+
}, [fill, _whiteNoteWidth, whiteNoteCount])
|
|
215
242
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
222
|
-
const w = pianoRef.current!.clientWidth
|
|
223
|
-
setWhiteNoteWidth(w / whiteNoteCount - padding)
|
|
243
|
+
const [touchMoveRefCallback, pointerDownHandler] =
|
|
244
|
+
useDragWithElement<HTMLDivElement>({
|
|
245
|
+
baseElementRef: pianoRef,
|
|
246
|
+
onDrag: onDrag,
|
|
247
|
+
onDragEnd: onDragEnd,
|
|
224
248
|
})
|
|
225
|
-
resizeObserver.observe(parent)
|
|
226
|
-
return () => {
|
|
227
|
-
resizeObserver.unobserve(parent)
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
setWhiteNoteWidth(_whiteNoteWidth)
|
|
231
|
-
}
|
|
232
|
-
}, [fill, _whiteNoteWidth, whiteNoteCount])
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
250
|
+
useEventListener(globalThis.window, 'keydown', (e) => {
|
|
251
|
+
if (e.repeat) return
|
|
252
|
+
if (!keyboardShortcuts) return
|
|
253
|
+
const index = keyboardShortcuts.keys.indexOf(e.key)
|
|
254
|
+
if (index != -1) {
|
|
255
|
+
if (!keyRefs.current[index]?.current?.played()) {
|
|
256
|
+
keyRefs.current[index]?.current?.play()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
239
259
|
})
|
|
240
260
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
keyRefs.current[index]?.current?.play()
|
|
261
|
+
useEventListener(globalThis.window, 'keyup', (e) => {
|
|
262
|
+
if (e.repeat) return
|
|
263
|
+
if (!keyboardShortcuts) return
|
|
264
|
+
const index = keyboardShortcuts.keys.indexOf(e.key)
|
|
265
|
+
if (index != -1) {
|
|
266
|
+
keyRefs.current[index]?.current?.stop()
|
|
248
267
|
}
|
|
249
|
-
}
|
|
250
|
-
})
|
|
268
|
+
})
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
useImperativeHandle(forwardedRef, () => {
|
|
271
|
+
return {
|
|
272
|
+
playNote(note, velocity) {
|
|
273
|
+
const index = noteRangeArray.indexOf(note)
|
|
274
|
+
if (index != -1) {
|
|
275
|
+
if (!keyRefs.current[index]?.current?.played()) {
|
|
276
|
+
keyRefs.current[index]?.current?.play(velocity)
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
onPlayNote?.(note, velocity)
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
stopNote(note: number) {
|
|
283
|
+
const index = noteRangeArray.indexOf(note)
|
|
284
|
+
if (index != -1) {
|
|
285
|
+
keyRefs.current[index]?.current?.stop()
|
|
286
|
+
} else {
|
|
287
|
+
onStopNote?.(note)
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}, [noteRangeArray, onPlayNote, onStopNote])
|
|
260
292
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
>
|
|
272
|
-
<div
|
|
273
|
-
ref={(div) => {
|
|
274
|
-
pianoRef.current = div
|
|
275
|
-
touchMoveRefCallback(div)
|
|
276
|
-
}}
|
|
277
|
-
className={clsx('tremolo-piano', className)}
|
|
278
|
-
style={{
|
|
279
|
-
width: fill ? '100%' : staticWidth,
|
|
280
|
-
height: height,
|
|
281
|
-
...style,
|
|
282
|
-
}}
|
|
283
|
-
onPointerDown={(event) => {
|
|
284
|
-
pointerDownHandler(event)
|
|
285
|
-
onPointerDown?.(event)
|
|
286
|
-
}}
|
|
287
|
-
{...props}
|
|
293
|
+
return (
|
|
294
|
+
<PianoProvider
|
|
295
|
+
notePosition={notePosition}
|
|
296
|
+
noteRange={noteRange}
|
|
297
|
+
glissando={glissando}
|
|
298
|
+
midiMax={midiMax}
|
|
299
|
+
fill={fill}
|
|
300
|
+
onPlayNote={onPlayNote}
|
|
301
|
+
onStopNote={onStopNote}
|
|
302
|
+
label={label}
|
|
288
303
|
>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
304
|
+
<div
|
|
305
|
+
ref={(div) => {
|
|
306
|
+
pianoRef.current = div
|
|
307
|
+
touchMoveRefCallback(div)
|
|
308
|
+
}}
|
|
309
|
+
className={clsx('tremolo-piano', className)}
|
|
310
|
+
style={{
|
|
311
|
+
width: fill ? '100%' : staticWidth,
|
|
312
|
+
height: height,
|
|
313
|
+
...style,
|
|
314
|
+
}}
|
|
315
|
+
onPointerDown={(event) => {
|
|
316
|
+
pointerDownHandler(event)
|
|
317
|
+
onPointerDown?.(event)
|
|
318
|
+
}}
|
|
319
|
+
{...props}
|
|
320
|
+
>
|
|
321
|
+
{childrenWithProps ||
|
|
322
|
+
noteRangeArray.map((note, index) =>
|
|
323
|
+
isWhiteKey(note) ? (
|
|
324
|
+
<WhiteKey
|
|
325
|
+
ref={keyRefs.current[index]}
|
|
326
|
+
key={note}
|
|
327
|
+
noteNumber={note}
|
|
328
|
+
__width={whiteNoteWidth}
|
|
329
|
+
/>
|
|
330
|
+
) : (
|
|
331
|
+
<BlackKey
|
|
332
|
+
ref={keyRefs.current[index]}
|
|
333
|
+
key={note}
|
|
334
|
+
noteNumber={note}
|
|
335
|
+
__width={whiteNoteWidth * blackPerWhiteWidth}
|
|
336
|
+
/>
|
|
337
|
+
),
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
</PianoProvider>
|
|
341
|
+
)
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Customizable piano component.
|
|
347
|
+
* @category Piano
|
|
348
|
+
*/
|
|
349
|
+
export const Piano = Object.assign(PianoImpl, { WhiteKey, BlackKey, KeyLabel })
|
|
311
350
|
|
|
312
351
|
export { type KeyboardShortcuts, SHORTCUTS } from './keyboardShortcuts'
|
|
313
|
-
export {
|
|
314
|
-
export {
|
|
352
|
+
export { type KeyProps, type KeyMethods } from './key'
|
|
353
|
+
export { type KeyLabelProps } from './KeyLabel'
|
|
@@ -40,7 +40,7 @@ export interface KeyProps {
|
|
|
40
40
|
* @category Piano
|
|
41
41
|
*/
|
|
42
42
|
export interface KeyMethods {
|
|
43
|
-
play: () => void
|
|
43
|
+
play: (velocity?: number) => void
|
|
44
44
|
stop: () => void
|
|
45
45
|
played: () => boolean
|
|
46
46
|
}
|
|
@@ -92,10 +92,10 @@ const KeyImpl = forwardRef<KeyMethods, ImplProps>(
|
|
|
92
92
|
|
|
93
93
|
useImperativeHandle(ref, () => {
|
|
94
94
|
return {
|
|
95
|
-
play() {
|
|
95
|
+
play(velocity) {
|
|
96
96
|
if (disabled) return
|
|
97
97
|
setPlayed(true)
|
|
98
|
-
if (onPlayNote) onPlayNote(noteNumber)
|
|
98
|
+
if (onPlayNote) onPlayNote(noteNumber, velocity)
|
|
99
99
|
},
|
|
100
100
|
stop() {
|
|
101
101
|
setPlayed(false)
|
|
@@ -35,7 +35,7 @@ export interface SliderThumbMethods {
|
|
|
35
35
|
export const defaultThumbSize = 22
|
|
36
36
|
|
|
37
37
|
/** @category Slider */
|
|
38
|
-
export const
|
|
38
|
+
export const Thumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
|
|
39
39
|
(
|
|
40
40
|
{
|
|
41
41
|
size,
|
|
@@ -29,7 +29,7 @@ export interface SliderTrackProps {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** @category Slider */
|
|
32
|
-
export function
|
|
32
|
+
export function Track({
|
|
33
33
|
length = defaultLength,
|
|
34
34
|
thickness = defaultThickness,
|
|
35
35
|
active,
|
|
@@ -64,8 +64,8 @@ export function SliderTrack({
|
|
|
64
64
|
: {
|
|
65
65
|
...colors,
|
|
66
66
|
background: xor(vertical, reverse)
|
|
67
|
-
? `linear-gradient(to ${direction}, var(--inactive
|
|
68
|
-
: `linear-gradient(to ${direction}, var(--active
|
|
67
|
+
? `linear-gradient(to ${direction}, var(--inactive) ${__percent}%, var(--active) ${__percent}%)`
|
|
68
|
+
: `linear-gradient(to ${direction}, var(--active) ${__percent}%, var(--inactive) ${__percent}%)`,
|
|
69
69
|
borderRadius: styleHelper(thickness!, '/', 2),
|
|
70
70
|
width: !vertical ? length : thickness,
|
|
71
71
|
height: vertical ? length : thickness,
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.tremolo-slider-thumb {
|
|
19
|
-
--color:
|
|
19
|
+
--color: oklch(59.5% 0.17548 266.373);
|
|
20
20
|
|
|
21
21
|
background: var(--color);
|
|
22
22
|
border-radius: 50%;
|
|
23
23
|
outline: none;
|
|
24
24
|
|
|
25
25
|
&[aria-disabled='true'] {
|
|
26
|
-
background:
|
|
26
|
+
background: oklch(50.462% 0.03307 270.021);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
&[aria-readonly='false']:focus {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.tremolo-slider-track {
|
|
36
|
-
--active:
|
|
37
|
-
--inactive:
|
|
36
|
+
--active: oklch(69.212% 0.12903 267.633);
|
|
37
|
+
--inactive: oklch(95% 0 0);
|
|
38
38
|
|
|
39
39
|
position: relative;
|
|
40
40
|
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
&:hover {
|
|
50
|
-
--active:
|
|
50
|
+
--active: oklch(64.276% 0.15244 266.932);
|
|
51
51
|
--inactive: #e0e0e0;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
&[aria-disabled='true'] {
|
|
55
|
-
--active:
|
|
56
|
-
--inactive:
|
|
55
|
+
--active: oklch(75.327% 0.01237 264.547);
|
|
56
|
+
--inactive: oklch(89.755% 0.0001 271.152);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
.tremolo-slider-scale-option {
|
|
66
66
|
display: flex;
|
|
67
|
-
color:
|
|
67
|
+
color: oklch(25.197% 0.00003 271.152);
|
|
68
68
|
flex-direction: column;
|
|
69
69
|
justify-content: center;
|
|
70
70
|
align-items: center;
|
|
@@ -85,3 +85,28 @@
|
|
|
85
85
|
.tremolo-slider-scale-option-label {
|
|
86
86
|
text-align: right;
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
:where(.dark, [data-theme='dark']) {
|
|
90
|
+
.tremolo-slider-thumb {
|
|
91
|
+
--color: oklch(64.5% 0.17548 266.373);
|
|
92
|
+
|
|
93
|
+
&:hover {
|
|
94
|
+
--color: oklch(67.5% 0.17548 266.373);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.tremolo-slider-track {
|
|
99
|
+
--active: oklch(59.5% 0.17548 266.373);
|
|
100
|
+
--inactive: oklch(41.7% 0 0);
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
--active: oklch(62.5% 0.17548 266.373);
|
|
104
|
+
--inactive: oklch(44.7% 0 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&[aria-disabled='true'] {
|
|
108
|
+
--active: oklch(75.327% 0.01237 264.547);
|
|
109
|
+
--inactive: oklch(89.755% 0.0001 271.152);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|