feat: added highlighted chord guide on piano grid

This commit is contained in:
Xiaohan-Tian
2025-12-15 20:14:30 -08:00
parent 8512fb4204
commit bdc7fc899a
7 changed files with 206 additions and 4 deletions
+87 -3
View File
@@ -3,7 +3,7 @@ import type { MutableRefObject } from 'react';
import { Playhead } from '../common';
import SelectionBox from './SelectionBox';
import { isModifierKeyPressed } from '../../util/osUtil';
import { generatePianoGridBackground } from '../../util/scaleUtil';
import { generatePianoGridBackground, getMatchingChordsForPitch } from '../../util/scaleUtil';
import type { KeySignature } from '../../core/KGProject';
interface PianoGridProps {
@@ -22,6 +22,7 @@ interface PianoGridProps {
regionStartBeat?: number;
selectedMode: string;
keySignature: KeySignature;
chordGuide: string;
}
interface CursorPosition {
@@ -41,10 +42,12 @@ const PianoGrid: React.FC<PianoGridProps> = ({
selectionBox,
regionStartBeat = 0,
selectedMode,
keySignature
keySignature,
chordGuide
}) => {
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
const [isModifierPressed, setIsModifierPressed] = useState(false);
const [selectedChordIndex, setSelectedChordIndex] = useState(0);
// Generate background with scale highlighting - only regenerate when mode or key changes
const backgroundImage = useMemo(() => {
@@ -125,6 +128,67 @@ const PianoGrid: React.FC<PianoGridProps> = ({
setCursorPosition(null);
};
// Get all matching chords for the current hover position
const matchingChords = useMemo(() => {
if (!cursorPosition) return [];
// If chord guide is disabled, return empty array
if (chordGuide === 'N') return [];
// Use the utility function to get matching chords
const functionType = chordGuide as 'T' | 'S' | 'D';
return getMatchingChordsForPitch(cursorPosition.pitch, keySignature, selectedMode, functionType);
}, [cursorPosition, chordGuide, keySignature, selectedMode]);
// Calculate chord highlights based on selected chord index
const chordHighlights = useMemo(() => {
if (matchingChords.length === 0) return [];
// Use the selected chord index (wrap around if needed)
const chordIndex = selectedChordIndex % matchingChords.length;
const matchedChordPitches = matchingChords[chordIndex];
// Convert pitch classes to actual pitches in the same octave as cursor
const highlights: Array<{ pitch: number; beat: number }> = [];
const cursorOctave = Math.floor(cursorPosition!.pitch / 12);
// For each pitch class in the chord, create highlights
for (const pitchClass of matchedChordPitches) {
// Calculate the actual pitch in the cursor's octave
const actualPitch = cursorOctave * 12 + pitchClass;
// Ensure the pitch is in valid MIDI range (0-127)
if (actualPitch >= 0 && actualPitch <= 127) {
highlights.push({ pitch: actualPitch, beat: cursorPosition!.beat });
}
}
return highlights;
}, [cursorPosition, matchingChords, selectedChordIndex]);
// Reset selected chord index when cursor moves to a different pitch or beat
useEffect(() => {
setSelectedChordIndex(0);
}, [cursorPosition?.pitch, cursorPosition?.beat]);
// Expose switchChord function via window for hotkey handler
useEffect(() => {
const switchChord = () => {
if (matchingChords.length > 1) {
setSelectedChordIndex(prev => (prev + 1) % matchingChords.length);
}
};
// Store the function on window object so PianoRoll can call it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__pianoGridSwitchChord = switchChord;
return () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).__pianoGridSwitchChord;
};
}, [matchingChords.length]);
return (
<div className="piano-grid-container">
<div
@@ -150,13 +214,33 @@ const PianoGrid: React.FC<PianoGridProps> = ({
/>
{/* Vertical beat column highlight */}
<div
<div
className="piano-grid-beat-highlight"
style={{
left: cursorPosition.beat * (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40),
width: parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40
}}
/>
{/* Chord highlights - render red boxes for each note in the matched chord */}
{chordHighlights.map((highlight, index) => {
const noteHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
const beatWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40;
const yPosition = (107 - highlight.pitch) * noteHeight; // B7 = 107, reverse for display
return (
<div
key={`chord-highlight-${index}`}
className="piano-grid-chord-highlight"
style={{
top: yPosition,
left: highlight.beat * beatWidth,
width: beatWidth,
height: noteHeight
}}
/>
);
})}
</>
)}