From 7653933f46050092b6f1d708a332fdc7a4012304 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 9 Apr 2026 00:38:14 +0200 Subject: [PATCH] feat: PianoKeys component --- .../components/chord-diagram/piano-keys.tsx | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 app/app/components/chord-diagram/piano-keys.tsx diff --git a/app/app/components/chord-diagram/piano-keys.tsx b/app/app/components/chord-diagram/piano-keys.tsx new file mode 100644 index 0000000..94aa85c --- /dev/null +++ b/app/app/components/chord-diagram/piano-keys.tsx @@ -0,0 +1,117 @@ +/** Chroma value (0=C … 11=B) for every note name tonal might return */ +const NOTE_CHROMA: Record = { + 'C': 0, 'C#': 1, 'Db': 1, + 'D': 2, 'D#': 3, 'Eb': 3, + 'E': 4, 'Fb': 4, + 'F': 5, 'E#': 5, 'F#': 6, 'Gb': 6, + 'G': 7, 'G#': 8, 'Ab': 8, + 'A': 9, 'A#': 10, 'Bb': 10, + 'B': 11, 'Cb': 11, 'B#': 0, +}; + +const WHITE_KEYS = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; +const WHITE_KEY_W = 14; // px +const WHITE_KEY_H = 56; // px +const BLACK_KEY_W = 9; // px +const BLACK_KEY_H = 34; // px + +/** Left offset (px) of each black key from the left edge of the keyboard */ +const BLACK_KEY_LEFT: Record = { + 'C#': 1 * WHITE_KEY_W - BLACK_KEY_W / 2, + 'D#': 2 * WHITE_KEY_W - BLACK_KEY_W / 2, + 'F#': 4 * WHITE_KEY_W - BLACK_KEY_W / 2, + 'G#': 5 * WHITE_KEY_W - BLACK_KEY_W / 2, + 'A#': 6 * WHITE_KEY_W - BLACK_KEY_W / 2, +}; + +const BLACK_KEY_NAMES = Object.keys(BLACK_KEY_LEFT); + +interface Props { + /** Note names from tonal, e.g. ["C","E","G"] or ["Ab","C","Eb"] */ + notes: string[]; +} + +export function PianoKeys({ notes }: Props) { + const activeChroma = new Set( + notes.map((n) => NOTE_CHROMA[n]).filter((c) => c !== undefined) + ); + const totalWidth = WHITE_KEY_W * 7; + + if (notes.length === 0) { + return ( +
+ ? +
+ ); + } + + return ( +
+ {/* White keys */} + {WHITE_KEYS.map((note, i) => { + const active = activeChroma.has(NOTE_CHROMA[note]); + return ( +
+ {active && ( +
+ )} +
+ ); + })} + + {/* Black keys (rendered on top) */} + {BLACK_KEY_NAMES.map((note) => { + const chroma = NOTE_CHROMA[note]; + const active = activeChroma.has(chroma); + return ( +
+ {active && ( +
+ )} +
+ ); + })} +
+ ); +}