add fretboard view

This commit is contained in:
2025-12-21 22:01:51 +01:00
parent 5a3c646e5d
commit 8f84fc1acd
2 changed files with 176 additions and 8 deletions

View File

@@ -12,6 +12,7 @@ import {
getFretboardPositions
} from './music/Tunings';
import { INSTRUMENT_DEFINITIONS } from './music/InstrumentConfigs';
import { FretboardHint } from './components/FretboardHint';
import { Mic, MicOff, SkipForward, HelpCircle } from 'lucide-react';
import './App.css';
import './styles/skip-button.css';
@@ -195,18 +196,12 @@ function App() {
)}
</div>
{settings.showHint && currentInstrumentDef.showTuning && (
{settings.showHint && currentInstrumentDef.showTuning && currentTuning && (
<div className="hint-card">
<div className="hint-note">
{getNoteDetails(targetMidi + currentInstrumentDef.transpose).scientific} (Written)
</div>
<div className="hint-positions">
{hintPositions.map((p, i) => (
<span key={i} className="hint-tag">
String {p.stringIndex + 1} / Fret {p.fret}
</span>
))}
</div>
<FretboardHint tuning={currentTuning} positions={hintPositions} />
</div>
)}

View File

@@ -0,0 +1,173 @@
import type { Tuning, FretPosition } from '../music/Tunings';
interface FretboardHintProps {
tuning: Tuning;
positions: FretPosition[];
maxFrets?: number; // How many frets to display
}
export function FretboardHint({ tuning, positions, maxFrets = 15 }: FretboardHintProps) {
// Config
const numStrings = tuning.strings.length;
// Visual params
const stringSpacing = 30; // Vertical space between strings
const fretWidth = 50; // Horizontal space for one fret
const nutWidth = 6; // Width of the nut
const paddingX = 20; // Padding left/right
const paddingY = 20; // Padding top/bottom
const width = paddingX * 2 + nutWidth + (maxFrets * fretWidth);
const height = paddingY * 2 + ((numStrings - 1) * stringSpacing);
// Markers (standard guitar dots)
// 3, 5, 7, 9, 12, 15, 17, 19, 21
const markers = [3, 5, 7, 9, 12, 15, 17, 19, 21].filter(m => m <= maxFrets);
// Helper to get Y coordinate for a string index
// String 0 is lowest pitch. In tab/charts, lowest pitch is usually the BOTTOM line.
// So index 0 -> max Y.
const getStringY = (stringIndex: number) => {
// stringIndex 0 (Low E) -> bottom line
// stringIndex N (High E) -> top line
return height - paddingY - (stringIndex * stringSpacing);
};
// Helper to get X coordinate for a fret line
// Fret N line is at x = padding + nut + N * width
const getFretLineX = (fretNum: number) => {
return paddingX + nutWidth + (fretNum * fretWidth);
};
// Helper to get Center X for a note at fret N
// If fret 0 (open), place it to left of nut.
// If fret > 0, place it in middle of Fret N-1 and Fret N.
const getNoteX = (fretNum: number) => {
if (fretNum === 0) {
return paddingX + (nutWidth / 2) - 15; // To the left of nut
}
const startX = getFretLineX(fretNum - 1);
const endX = getFretLineX(fretNum);
return (startX + endX) / 2;
};
return (
<div className="fretboard-container" style={{ overflowX: 'auto', maxWidth: '100%' }}>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{ display: 'block', margin: '0 auto' }}
>
{/* Fretboard background */}
<rect
x={paddingX + nutWidth}
y={paddingY}
width={width - (paddingX * 2) - nutWidth}
height={height - (paddingY * 2)}
fill="#4a3e35" // Dark wood color
stroke="none"
/>
{/* Nut */}
<rect
x={paddingX}
y={paddingY}
width={nutWidth}
height={height - (paddingY * 2)}
fill="#ddd" // Bone/plastic color
/>
{/* Fret Markers (Dots) on fretboard */}
{markers.map(m => {
const cx = getNoteX(m);
const cy = height / 2;
const isDouble = m % 12 === 0;
if (isDouble) {
// Draw two dots
return (
<g key={`marker-${m}`}>
<circle cx={cx} cy={height / 2 - stringSpacing} r={6} fill="#aaa" opacity={0.6} />
<circle cx={cx} cy={height / 2 + stringSpacing} r={6} fill="#aaa" opacity={0.6} />
</g>
);
}
return (
<circle key={`marker-${m}`} cx={cx} cy={cy} r={6} fill="#aaa" opacity={0.6} />
);
})}
{/* Frets (Vertical lines) */}
{Array.from({ length: maxFrets + 1 }).map((_, i) => {
if (i === 0) return null; // Nut handles 0
const x = getFretLineX(i);
return (
<line
key={`fret-${i}`}
x1={x}
y1={paddingY}
x2={x}
y2={height - paddingY}
stroke="#silver"
strokeWidth={2}
/>
);
})}
{/* Strings (Horizontal lines) */}
{tuning.strings.map((_, i) => {
const y = getStringY(i);
// i=0 is LOW pitch (thickest).
const visualThickness = 3 - (i * 0.3); // 0 -> 3, 5 -> 1.5
return (
<line
key={`string-${i}`}
x1={paddingX}
y1={y}
x2={width - paddingX}
y2={y}
stroke="#d4d4d4" // String color
strokeWidth={Math.max(1, visualThickness)}
/>
);
})}
{/* Target Note Positions */}
{positions.map((p, idx) => {
// Verify if fret is within range
if (p.fret > maxFrets) return null;
const cx = getNoteX(p.fret);
const cy = getStringY(p.stringIndex);
return (
<g key={`pos-${idx}`}>
<circle
cx={cx}
cy={cy}
r={10}
fill="var(--color-primary, #e63946)"
stroke="white"
strokeWidth={2}
/>
<text
x={cx}
y={cy}
dy={4}
fill="white"
fontSize={10}
fontWeight="bold"
textAnchor="middle"
>
{p.fret}
</text>
</g>
);
})}
</svg>
</div>
);
}