215 lines
5.7 KiB
JavaScript
215 lines
5.7 KiB
JavaScript
/* eslint-disable react/prop-types */
|
|
import React /*, { useRef, useEffect, useState }*/ from "react";
|
|
const NOTE_NAMES = [
|
|
"C",
|
|
"C♯",
|
|
"D",
|
|
"D♯",
|
|
"E",
|
|
"F",
|
|
"F♯",
|
|
"G",
|
|
"G♯",
|
|
"A",
|
|
"A♯",
|
|
"B",
|
|
];
|
|
const WHITE_KEYS = [0, 2, 4, 5, 7, 9, 11];
|
|
|
|
// C2 = 36, C5 = 72
|
|
const LOWEST_MIDI = 24;
|
|
const HIGHEST_MIDI = 84;
|
|
|
|
function getNoteName(midi) {
|
|
const octave = Math.floor(midi / 12) - 1;
|
|
const note = NOTE_NAMES[midi % 12];
|
|
return `${note}${octave}`;
|
|
}
|
|
|
|
function isWhiteKey(midi) {
|
|
return WHITE_KEYS.includes(midi % 12);
|
|
}
|
|
|
|
export default function PianoKeyboard({ heldNotes }) {
|
|
const heldMap = {};
|
|
heldNotes.forEach((n) => (heldMap[n.midi] = n.voice));
|
|
|
|
const keys = [];
|
|
for (let midi = LOWEST_MIDI; midi <= HIGHEST_MIDI; midi++) {
|
|
const white = isWhiteKey(midi);
|
|
const held = heldMap[midi] !== undefined;
|
|
keys.push({
|
|
midi,
|
|
white,
|
|
held,
|
|
voice: heldMap[midi],
|
|
noteName: getNoteName(midi),
|
|
});
|
|
}
|
|
|
|
const whiteKeys = keys.filter((k) => k.white);
|
|
const blackKeys = keys.filter((k) => !k.white);
|
|
|
|
// For responsive black key positioning
|
|
const numWhite = whiteKeys.length;
|
|
|
|
// Map midi to white key index for black key positioning
|
|
const midiToWhiteIndex = {};
|
|
let whiteIdx = 0;
|
|
for (let midi = LOWEST_MIDI; midi <= HIGHEST_MIDI; midi++) {
|
|
if (isWhiteKey(midi)) {
|
|
midiToWhiteIndex[midi] = whiteIdx++;
|
|
}
|
|
}
|
|
|
|
// For each black key, find its position between white keys
|
|
function getBlackKeyPercent(midi) {
|
|
// Black keys are always after a white key except for the first key
|
|
// For example, C# is between C and D
|
|
// So, find the previous white key index, then add ~0.65 of a white key width
|
|
const prevWhite = midi - 1;
|
|
const idx = midiToWhiteIndex[prevWhite];
|
|
if (idx === undefined) return 0;
|
|
// Offset: (idx + 0.65) / numWhite * 100%
|
|
return ((idx + 1) / numWhite) * 100;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "relative",
|
|
height: 80,
|
|
userSelect: "none",
|
|
width: "100%",
|
|
minWidth: 200,
|
|
maxWidth: 900,
|
|
margin: "0 auto",
|
|
}}
|
|
>
|
|
{/* White keys */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
position: "relative",
|
|
zIndex: 1,
|
|
height: "100%",
|
|
}}
|
|
>
|
|
{whiteKeys.map((k, i) => (
|
|
<div
|
|
key={k.midi}
|
|
className={
|
|
k.held
|
|
? "bg-primary shadow-primary/30 shadow-[0_0_16px,inset_0_1px_2px_rgba(255,255,255,0.2)]"
|
|
: "bg-[#222]"
|
|
}
|
|
style={{
|
|
flex: "1 1 0",
|
|
height: "100%",
|
|
border: "1px solid #2a2a2a",
|
|
position: "relative",
|
|
boxSizing: "border-box",
|
|
marginRight: -1,
|
|
display: "flex",
|
|
flexDirection: "column-reverse",
|
|
alignItems: "center",
|
|
justifyContent: "flex-start",
|
|
fontSize: 10,
|
|
fontFamily: "monospace",
|
|
overflow: "hidden",
|
|
borderTopLeftRadius: i === 0 ? 6 : 0, // round left edge of first key
|
|
borderBottomLeftRadius: i === 0 ? 6 : 0,
|
|
borderTopRightRadius: i === whiteKeys.length - 1 ? 6 : 0, // round right edge of last key
|
|
borderBottomRightRadius: i === whiteKeys.length - 1 ? 6 : 0,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column-reverse",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<span style={{ color: "#555" }}>{k.noteName}</span>
|
|
{k.held && (
|
|
<span
|
|
style={{
|
|
color: "#666666",
|
|
fontWeight: "bold",
|
|
fontSize: 14,
|
|
lineHeight: "14px",
|
|
marginBottom: 2,
|
|
}}
|
|
>
|
|
{k.voice}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{/* Black keys */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
height: "62%",
|
|
width: "100%",
|
|
zIndex: 2,
|
|
pointerEvents: "none",
|
|
}}
|
|
>
|
|
{blackKeys.map((k) => (
|
|
<div
|
|
key={k.midi}
|
|
className={
|
|
k.held
|
|
? "bg-primary shadow-primary/30 shadow-[0_0_16px,inset_0_1px_2px_rgba(255,255,255,0.2)]"
|
|
: "bg-[#111]"
|
|
}
|
|
style={{
|
|
position: "absolute",
|
|
left: `${getBlackKeyPercent(k.midi)}%`,
|
|
width: `${(100 / numWhite) * 0.65}%`,
|
|
height: "100%",
|
|
border: "1px solid #333",
|
|
borderRadius: 3,
|
|
display: "flex",
|
|
flexDirection: "column-reverse",
|
|
alignItems: "center",
|
|
justifyContent: "flex-start",
|
|
fontSize: 10,
|
|
fontFamily: "monospace",
|
|
boxSizing: "border-box",
|
|
transform: "translateX(-50%)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column-reverse",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
{k.held && (
|
|
<span
|
|
style={{
|
|
color: "#666666",
|
|
fontWeight: "bold",
|
|
fontSize: 14,
|
|
lineHeight: "14px",
|
|
marginBottom: 2,
|
|
}}
|
|
>
|
|
{k.voice}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|