add piano view with instrument toggle

This commit is contained in:
vadimwit
2026-03-05 22:58:32 +00:00
parent 4ed382393e
commit cfa7b877f6
2 changed files with 161 additions and 5 deletions
+133
View File
@@ -0,0 +1,133 @@
import { getFullScale, getChordTones, NOTES } from '../lib/theory'
// White keys in order within an octave, mapped to pitch class
const WHITE_KEYS = [
{ pc: 0, label: 'C' },
{ pc: 2, label: 'D' },
{ pc: 4, label: 'E' },
{ pc: 5, label: 'F' },
{ pc: 7, label: 'G' },
{ pc: 9, label: 'A' },
{ pc: 11, label: 'B' },
]
// Black keys: position (in white-key units from left of octave) and pitch class
const BLACK_KEYS = [
{ pc: 1, offset: 0.7 }, // C#
{ pc: 3, offset: 1.7 }, // D#
{ pc: 6, offset: 3.7 }, // F#
{ pc: 8, offset: 4.7 }, // G#
{ pc: 10, offset: 5.7 }, // A#
]
const OCTAVES = 2 // number of octaves shown
const KEY_W = 40 // white key width
const KEY_H = 130 // white key height
const BLACK_W = 26 // black key width
const BLACK_H = 82 // black key height
const LABEL_Y = KEY_H - 10 // y of note label on white key
const BLACK_LABEL_Y = BLACK_H - 8
function keyColor(isChordTone, isScale, isBlack) {
if (isChordTone) return { fill: '#f59e0b', text: '#000' }
if (isScale) return { fill: '#a855f7', text: '#fff' }
return isBlack
? { fill: '#1f1f1f', text: '#6b7280' }
: { fill: '#f5f5f5', text: '#6b7280' }
}
export default function Piano({ keyInfo, currentChord }) {
const { root, mode } = keyInfo ?? {}
if (!root) return null
const scaleSet = new Set(getFullScale(root, mode).map(n => NOTES.indexOf(n)))
const chordSet = currentChord
? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n)))
: new Set()
const totalWhite = WHITE_KEYS.length * OCTAVES
const svgW = totalWhite * KEY_W + 2
const svgH = KEY_H + 20 // +20 for octave labels
return (
<div className="bg-panel border border-border rounded-2xl p-6">
<p className="text-sm text-gray-500 uppercase tracking-widest mb-4">
Piano {root} {mode}
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</span>}
</p>
<div className="overflow-x-auto">
<svg width={svgW} height={svgH} style={{ display: 'block', minWidth: svgW }}>
{/* White keys */}
{Array.from({ length: OCTAVES }, (_, oct) =>
WHITE_KEYS.map((k, wi) => {
const x = (oct * WHITE_KEYS.length + wi) * KEY_W + 1
const isChordTone = chordSet.has(k.pc)
const isScale = scaleSet.has(k.pc)
const { fill, text } = keyColor(isChordTone, isScale, false)
return (
<g key={`w-${oct}-${wi}`}>
<rect
x={x} y={0}
width={KEY_W - 1} height={KEY_H}
fill={fill}
rx={3}
stroke="#2a2a2a"
strokeWidth={1}
/>
<text
x={x + KEY_W / 2} y={LABEL_Y}
textAnchor="middle"
fontSize={9}
fontWeight="600"
fill={text}
>
{k.label}{oct + 3}
</text>
</g>
)
})
)}
{/* Black keys (drawn on top) */}
{Array.from({ length: OCTAVES }, (_, oct) =>
BLACK_KEYS.map((k, bi) => {
const x = (oct * WHITE_KEYS.length + k.offset) * KEY_W + 1
const isChordTone = chordSet.has(k.pc)
const isScale = scaleSet.has(k.pc)
const { fill, text } = keyColor(isChordTone, isScale, true)
return (
<g key={`b-${oct}-${bi}`}>
<rect
x={x} y={0}
width={BLACK_W} height={BLACK_H}
fill={fill}
rx={2}
stroke="#111"
strokeWidth={1}
/>
<text
x={x + BLACK_W / 2} y={BLACK_LABEL_Y}
textAnchor="middle"
fontSize={8}
fontWeight="600"
fill={text}
>
{NOTES[k.pc]}
</text>
</g>
)
})
)}
</svg>
</div>
<div className="mt-3 flex gap-5 text-xs text-gray-500">
<span><span className="text-amber-400"></span> Chord tone</span>
<span><span className="text-accent"></span> Scale note</span>
</div>
</div>
)
}