@tremolo-ui/react 0.0.12 → 0.1.0-alpha.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.
- package/dist/cjs/index.js +320 -233
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/Knob/index.d.ts +12 -0
- package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
- package/dist/cjs/types/components/Piano/KeyLabel.d.ts +1 -3
- package/dist/cjs/types/components/Piano/KeyLabel.d.ts.map +1 -1
- package/dist/cjs/types/components/Piano/context.d.ts +24 -0
- package/dist/cjs/types/components/Piano/context.d.ts.map +1 -0
- package/dist/cjs/types/components/Piano/index.d.ts +9 -9
- package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
- package/dist/cjs/types/components/Piano/key.d.ts +10 -19
- package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
- package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
- package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
- package/dist/cjs/types/hooks/useDrag.d.ts.map +1 -1
- package/dist/cjs/types/hooks/useDragWithElement.d.ts +16 -0
- package/dist/cjs/types/hooks/useDragWithElement.d.ts.map +1 -0
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/esm/index.js +319 -234
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/Knob/index.d.ts +12 -0
- package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
- package/dist/esm/types/components/Piano/KeyLabel.d.ts +1 -3
- package/dist/esm/types/components/Piano/KeyLabel.d.ts.map +1 -1
- package/dist/esm/types/components/Piano/context.d.ts +24 -0
- package/dist/esm/types/components/Piano/context.d.ts.map +1 -0
- package/dist/esm/types/components/Piano/index.d.ts +9 -9
- package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
- package/dist/esm/types/components/Piano/key.d.ts +10 -19
- package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
- package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
- package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
- package/dist/esm/types/hooks/useDrag.d.ts.map +1 -1
- package/dist/esm/types/hooks/useDragWithElement.d.ts +16 -0
- package/dist/esm/types/hooks/useDragWithElement.d.ts.map +1 -0
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/styles/index.css +2 -0
- package/package.json +2 -1
- package/src/components/AnimationCanvas/index.tsx +3 -1
- package/src/components/Knob/index.tsx +14 -0
- package/src/components/Piano/KeyLabel.tsx +9 -5
- package/src/components/Piano/context.tsx +108 -0
- package/src/components/Piano/index.css +2 -0
- package/src/components/Piano/index.tsx +186 -114
- package/src/components/Piano/key.tsx +215 -0
- package/src/components/Slider/index.tsx +40 -52
- package/src/components/XYPad/index.tsx +64 -54
- package/src/hooks/useDrag.ts +6 -7
- package/src/hooks/useDragWithElement.ts +76 -0
- package/src/index.ts +1 -0
- package/dist/cjs/types/components/Piano/BlackKey.d.ts +0 -5
- package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +0 -1
- package/dist/cjs/types/components/Piano/WhiteKey.d.ts +0 -7
- package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +0 -1
- package/dist/esm/types/components/Piano/BlackKey.d.ts +0 -5
- package/dist/esm/types/components/Piano/BlackKey.d.ts.map +0 -1
- package/dist/esm/types/components/Piano/WhiteKey.d.ts +0 -7
- package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +0 -1
- package/src/components/Piano/BlackKey.tsx +0 -132
- package/src/components/Piano/WhiteKey.tsx +0 -134
- package/src/components/Piano/key.ts +0 -51
|
@@ -6,19 +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
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
isBlackKey,
|
|
18
|
+
isWhiteKey,
|
|
19
|
+
noteKey,
|
|
20
|
+
NoteKey,
|
|
21
|
+
noteKeys,
|
|
22
|
+
} from '@tremolo-ui/functions'
|
|
15
23
|
|
|
24
|
+
import { useDragWithElement } from '../../hooks/useDragWithElement'
|
|
16
25
|
import { useEventListener } from '../../hooks/useEventListener'
|
|
17
26
|
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
27
|
+
import { NoteRange, PianoProvider } from './context'
|
|
28
|
+
import {
|
|
29
|
+
BlackKey,
|
|
30
|
+
defaultBlackKeyWidth,
|
|
31
|
+
defaultWhiteKeyWidth,
|
|
32
|
+
KeyMethods,
|
|
33
|
+
WhiteKey,
|
|
34
|
+
} from './key'
|
|
20
35
|
import { KeyboardShortcuts } from './keyboardShortcuts'
|
|
21
|
-
import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
|
|
22
36
|
|
|
23
37
|
/**
|
|
24
38
|
* Piano component
|
|
@@ -27,18 +41,27 @@ import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
|
|
|
27
41
|
* - add scale highlight
|
|
28
42
|
*/
|
|
29
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
|
+
|
|
30
52
|
/** @category Piano */
|
|
31
53
|
export interface PianoProps {
|
|
32
54
|
// required
|
|
33
|
-
noteRange:
|
|
55
|
+
noteRange: NoteRange
|
|
34
56
|
|
|
35
57
|
// optional
|
|
36
|
-
height?: number | string
|
|
37
|
-
|
|
38
58
|
glissando?: boolean
|
|
39
59
|
midiMax?: number
|
|
40
60
|
keyboardShortcuts?: KeyboardShortcuts
|
|
41
61
|
fill?: boolean
|
|
62
|
+
whiteNoteWidth?: number
|
|
63
|
+
blackNoteWidth?: number
|
|
64
|
+
height?: number | string
|
|
42
65
|
|
|
43
66
|
style?: CSSProperties
|
|
44
67
|
|
|
@@ -64,76 +87,130 @@ export function Piano({
|
|
|
64
87
|
keyboardShortcuts,
|
|
65
88
|
fill = false,
|
|
66
89
|
height = fill ? '100%' : 160,
|
|
90
|
+
whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
|
|
67
91
|
style,
|
|
68
92
|
className,
|
|
69
93
|
onPlayNote,
|
|
70
94
|
onStopNote,
|
|
71
95
|
label,
|
|
72
96
|
children,
|
|
73
|
-
onPointerUp,
|
|
74
97
|
onPointerDown,
|
|
75
98
|
...props
|
|
76
99
|
}: PianoProps & Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>) {
|
|
77
100
|
// -- state and ref ---
|
|
78
|
-
const [
|
|
79
|
-
const [whiteNoteWidth, setWhiteNoteWidth] = useState(-1)
|
|
101
|
+
const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
|
|
80
102
|
const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
|
|
81
103
|
for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
|
|
82
104
|
keyRefs.current[i] = createRef<KeyMethods>()
|
|
83
105
|
}
|
|
84
106
|
const pianoRef = useRef<HTMLDivElement>(null)
|
|
107
|
+
const hitKeyIndex = useRef(-1)
|
|
85
108
|
|
|
86
109
|
// --- interpret props ---
|
|
87
|
-
const noteRangeArray =
|
|
88
|
-
|
|
89
|
-
|
|
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],
|
|
90
167
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
181
|
}
|
|
105
|
-
} else {
|
|
106
|
-
throw new Error('children is an invalid element.')
|
|
107
182
|
}
|
|
108
|
-
|
|
109
|
-
|
|
183
|
+
return -1
|
|
184
|
+
},
|
|
185
|
+
[blackNotes, notePosition, whiteNotes],
|
|
186
|
+
)
|
|
110
187
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
)
|
|
127
207
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const length = whiteNoteWidth + 3
|
|
135
|
-
return pos * length + (octave + octaveOffset) * 7 * length
|
|
136
|
-
}
|
|
208
|
+
const onDragEnd = useCallback(() => {
|
|
209
|
+
if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
|
|
210
|
+
keyRefs.current[hitKeyIndex.current]?.current?.stop()
|
|
211
|
+
}
|
|
212
|
+
hitKeyIndex.current = -1
|
|
213
|
+
}, [keyRefs])
|
|
137
214
|
|
|
138
215
|
// --- hooks ---
|
|
139
216
|
useEffect(() => {
|
|
@@ -142,16 +219,23 @@ export function Piano({
|
|
|
142
219
|
if (!parent) throw new Error("doesn't have a parent element.")
|
|
143
220
|
const resizeObserver = new ResizeObserver(() => {
|
|
144
221
|
const w = pianoRef.current!.clientWidth
|
|
145
|
-
setWhiteNoteWidth(w / whiteNoteCount -
|
|
222
|
+
setWhiteNoteWidth(w / whiteNoteCount - padding)
|
|
146
223
|
})
|
|
147
224
|
resizeObserver.observe(parent)
|
|
148
225
|
return () => {
|
|
149
226
|
resizeObserver.unobserve(parent)
|
|
150
227
|
}
|
|
151
228
|
} else {
|
|
152
|
-
setWhiteNoteWidth(
|
|
229
|
+
setWhiteNoteWidth(_whiteNoteWidth)
|
|
153
230
|
}
|
|
154
|
-
}, [fill,
|
|
231
|
+
}, [fill, _whiteNoteWidth, whiteNoteCount])
|
|
232
|
+
|
|
233
|
+
const [touchMoveRefCallback, pointerDownHandler] =
|
|
234
|
+
useDragWithElement<HTMLDivElement>({
|
|
235
|
+
baseElementRef: pianoRef,
|
|
236
|
+
onDrag: onDrag,
|
|
237
|
+
onDragEnd: onDragEnd,
|
|
238
|
+
})
|
|
155
239
|
|
|
156
240
|
useEventListener(globalThis.window, 'keydown', (e) => {
|
|
157
241
|
if (e.repeat) return
|
|
@@ -174,66 +258,54 @@ export function Piano({
|
|
|
174
258
|
})
|
|
175
259
|
|
|
176
260
|
return (
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
onPointerDown={(e) => {
|
|
186
|
-
setPressed(true)
|
|
187
|
-
onPointerDown?.(e)
|
|
188
|
-
}}
|
|
189
|
-
onPointerUp={(e) => {
|
|
190
|
-
setPressed(false)
|
|
191
|
-
onPointerUp?.(e)
|
|
192
|
-
}}
|
|
193
|
-
{...props}
|
|
261
|
+
<PianoProvider
|
|
262
|
+
noteRange={noteRange}
|
|
263
|
+
glissando={glissando}
|
|
264
|
+
midiMax={midiMax}
|
|
265
|
+
fill={fill}
|
|
266
|
+
onPlayNote={onPlayNote}
|
|
267
|
+
onStopNote={onStopNote}
|
|
268
|
+
label={label}
|
|
194
269
|
>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
</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>
|
|
231
305
|
)
|
|
232
306
|
}
|
|
233
307
|
|
|
234
|
-
export * from './key'
|
|
235
308
|
export * from './keyboardShortcuts'
|
|
236
309
|
|
|
237
|
-
export { WhiteKey } from './
|
|
238
|
-
export { BlackKey } from './BlackKey'
|
|
310
|
+
export { WhiteKey, BlackKey, type KeyProps, type KeyMethods } from './key'
|
|
239
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
|
+
)
|