Files
harmonizer_plugin/Assets/web/src/Components/PianoKeyboard.js
2025-11-11 18:33:30 -05:00

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>
);
}