antigravity one shot

This commit is contained in:
2025-12-21 12:20:40 +01:00
parent 5fd7786ba3
commit f510de47d1
23 changed files with 4462 additions and 0 deletions

View 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>
);
};

View 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" />;
};