mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
antigravity one shot
This commit is contained in:
75
src/components/Controls.tsx
Normal file
75
src/components/Controls.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Settings, HelpCircle, Guitar } from 'lucide-react';
|
||||
import { TUNINGS } from '../music/Tunings';
|
||||
|
||||
export type Difficulty = 'all' | 'first_pos' | 'open' | 'e_string';
|
||||
|
||||
export interface AppSettings {
|
||||
difficulty: Difficulty;
|
||||
showHint: boolean;
|
||||
tuningId: string;
|
||||
}
|
||||
|
||||
interface ControlsProps {
|
||||
settings: AppSettings;
|
||||
onUpdateSettings: (s: AppSettings) => void;
|
||||
}
|
||||
|
||||
export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }) => {
|
||||
const handleDifficultyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onUpdateSettings({ ...settings, difficulty: e.target.value as Difficulty });
|
||||
};
|
||||
|
||||
const handleTuningChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onUpdateSettings({ ...settings, tuningId: e.target.value });
|
||||
};
|
||||
|
||||
const toggleHint = () => {
|
||||
onUpdateSettings({ ...settings, showHint: !settings.showHint });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="controls-container">
|
||||
<div className="control-group">
|
||||
<label className="control-label">
|
||||
<Guitar size={18} />
|
||||
<span>Tuning</span>
|
||||
</label>
|
||||
<select
|
||||
value={settings.tuningId}
|
||||
onChange={handleTuningChange}
|
||||
className="control-select"
|
||||
>
|
||||
{Object.entries(TUNINGS).map(([id, tuning]) => (
|
||||
<option key={id} value={id}>{tuning.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="control-group">
|
||||
<label className="control-label">
|
||||
<Settings size={18} />
|
||||
<span>Range</span>
|
||||
</label>
|
||||
<select
|
||||
value={settings.difficulty}
|
||||
onChange={handleDifficultyChange}
|
||||
className="control-select"
|
||||
>
|
||||
<option value="open">Open Strings (Beginner)</option>
|
||||
<option value="first_pos">First Position</option>
|
||||
<option value="e_string">E String Only</option>
|
||||
<option value="all">All Notes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`control-button ${settings.showHint ? 'active' : ''}`}
|
||||
onClick={toggleHint}
|
||||
title="Show Hint"
|
||||
>
|
||||
<HelpCircle size={20} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
97
src/components/SheetMusic.tsx
Normal file
97
src/components/SheetMusic.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter } from 'vexflow';
|
||||
import { getNoteDetails } from '../music/NoteUtils';
|
||||
|
||||
interface SheetMusicProps {
|
||||
targetMidi: number;
|
||||
playedMidi?: number | null;
|
||||
clef?: 'treble' | 'bass';
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
targetMidi,
|
||||
playedMidi,
|
||||
clef = 'treble',
|
||||
width = 300,
|
||||
height = 200
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Clear previous render
|
||||
containerRef.current.innerHTML = '';
|
||||
|
||||
const renderer = new Renderer(containerRef.current, Renderer.Backends.SVG);
|
||||
|
||||
renderer.resize(width, height);
|
||||
const context = renderer.getContext();
|
||||
|
||||
// Create Stave
|
||||
const stave = new Stave(10, 40, width - 20);
|
||||
stave.addClef(clef);
|
||||
stave.setContext(context).draw();
|
||||
|
||||
// Create Target Note
|
||||
const targetDetails = getNoteDetails(targetMidi);
|
||||
const targetKey = `${targetDetails.name.toLowerCase()}/${targetDetails.octave}`;
|
||||
|
||||
const targetStaveNote = new StaveNote({
|
||||
keys: [targetKey],
|
||||
duration: "w",
|
||||
clef: clef
|
||||
});
|
||||
|
||||
if (targetDetails.name.includes("#")) {
|
||||
targetStaveNote.addModifier(new Accidental("#"));
|
||||
}
|
||||
|
||||
if (playedMidi) {
|
||||
const playedDetails = getNoteDetails(playedMidi);
|
||||
const playedKey = `${playedDetails.name.toLowerCase()}/${playedDetails.octave}`;
|
||||
|
||||
const playedStaveNote = new StaveNote({
|
||||
keys: [playedKey],
|
||||
duration: "h",
|
||||
clef: clef,
|
||||
});
|
||||
|
||||
// Target note as half note to match measure
|
||||
const targetNoteHalf = new StaveNote({
|
||||
keys: [targetKey],
|
||||
duration: "h",
|
||||
clef: clef
|
||||
});
|
||||
if (targetDetails.name.includes("#")) targetNoteHalf.addModifier(new Accidental("#"));
|
||||
|
||||
|
||||
if (playedMidi === targetMidi) {
|
||||
playedStaveNote.setStyle({ fillStyle: "var(--color-success)", strokeStyle: "var(--color-success)" });
|
||||
} else {
|
||||
playedStaveNote.setStyle({ fillStyle: "var(--color-error)", strokeStyle: "var(--color-error)" });
|
||||
}
|
||||
|
||||
if (playedDetails.name.includes("#")) {
|
||||
playedStaveNote.addModifier(new Accidental("#"));
|
||||
}
|
||||
|
||||
const voiceCombined = new Voice({ numBeats: 4, beatValue: 4 });
|
||||
voiceCombined.addTickables([targetNoteHalf, playedStaveNote]);
|
||||
|
||||
new Formatter().joinVoices([voiceCombined]).format([voiceCombined], width - 50);
|
||||
voiceCombined.draw(context, stave);
|
||||
|
||||
} else {
|
||||
const voice = new Voice({ numBeats: 4, beatValue: 4 });
|
||||
voice.addTickables([targetStaveNote]);
|
||||
new Formatter().joinVoices([voice]).format([voice], width - 50);
|
||||
voice.draw(context, stave);
|
||||
}
|
||||
|
||||
}, [targetMidi, playedMidi, clef, width, height]);
|
||||
|
||||
return <div ref={containerRef} className="sheet-music-container" />;
|
||||
};
|
||||
Reference in New Issue
Block a user