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