From cfa7b877f60202a45696a992d9b914b22d77a77f Mon Sep 17 00:00:00 2001 From: vadimwit Date: Thu, 5 Mar 2026 22:58:32 +0000 Subject: [PATCH] add piano view with instrument toggle --- src/App.jsx | 33 ++++++++-- src/components/Piano.jsx | 133 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 src/components/Piano.jsx diff --git a/src/App.jsx b/src/App.jsx index 2b353c2..90e340c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import SafeNotes from './components/SafeNotes' import Fretboard from './components/Fretboard' import ProgressionSuggestions from './components/ProgressionSuggestions' import Tuner from './components/Tuner' +import Piano from './components/Piano' import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression } from './lib/theory' // Key detection tuning @@ -22,6 +23,9 @@ export default function App() { // ── Listening state ────────────────────────────────────────────────────── const [isListening, setIsListening] = useState(false) + // ── Instrument view ─────────────────────────────────────────────────────── + const [instrument, setInstrument] = useState('guitar') // 'guitar' | 'piano' + // ── Key: auto-detected + optional lock ─────────────────────────────────── const [keyInfo, setKeyInfo] = useState(null) // auto-detected const [lockedKey, setLockedKey] = useState(null) // { root, mode } or null @@ -182,6 +186,21 @@ export default function App() { {/* ── Controls bar ── */}
+ {/* Instrument toggle */} +
+ {['guitar', 'piano'].map(v => ( + + ))} +
+ {/* Key lock */} {lockedKey ? (
@@ -256,11 +275,15 @@ export default function App() {
- + {instrument === 'guitar' ? ( + + ) : ( + + )}
diff --git a/src/components/Piano.jsx b/src/components/Piano.jsx new file mode 100644 index 0000000..024c5c7 --- /dev/null +++ b/src/components/Piano.jsx @@ -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 ( +
+

+ Piano — {root} {mode} + {currentChord && / {currentChord}} +

+ +
+ + + {/* 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 ( + + + + {k.label}{oct + 3} + + + ) + }) + )} + + {/* 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 ( + + + + {NOTES[k.pc]} + + + ) + }) + )} + + +
+ +
+ Chord tone + Scale note +
+
+ ) +}