import { OPEN_NOTES, getNoteAt, noteDisplay } from './music.js'; export const FRET_COUNT = 12; const STRING_SPACING = 28; // Width of each playable fret slot (index = fret number, index 0 unused) const FRET_WIDTHS = [0, 52, 50, 47, 45, 43, 41, 39, 37, 35, 34, 32, 31]; const MARKER_FRETS = [3, 5, 7, 9, 12]; // Returns cumulative x positions of fret wires 0..FRET_COUNT. // fretXs[f] is the right edge of the f-th fret slot (= left edge of slot f+1). function computeFretXs() { const xs = [0]; for (let i = 1; i <= FRET_COUNT; i++) xs.push(xs[i - 1] + FRET_WIDTHS[i]); return xs; } const FRET_XS = computeFretXs(); /** * Builds an SVG fretboard string. * * opts: * numStrings {number} 6 or 7 * highlightPos {stringIdx, fret} | null — mode 1 position dot * mode2Sel {Set<"s,f">} — user's current selections * mode2Correct {Set<"s,f">} | null — revealed after submission * showLabels {boolean} — show note names on dots * interactive {boolean} — add data-* click targets * fretRange [start, end] | null — highlighted fret window * sharpsOnly {boolean} */ export function buildFretboard({ numStrings = 6, highlightPos = null, mode2Sel = new Set(), mode2Correct = null, showLabels = false, interactive = false, fretRange = null, sharpsOnly = false, } = {}) { const ns = numStrings; const PAD_LEFT = 30; const PAD_RIGHT = 16; const PAD_TOP = 20; const PAD_BOTTOM = 20; const totalW = FRET_XS[FRET_COUNT]; const svgW = totalW + PAD_LEFT + PAD_RIGHT; const svgH = (ns - 1) * STRING_SPACING + PAD_TOP + PAD_BOTTOM; const parts = []; const p = s => parts.push(s); p(``); // Fret-range highlight band if (fretRange) { const [fr1, fr2] = fretRange; const x1 = PAD_LEFT + (fr1 > 0 ? FRET_XS[fr1 - 1] : 0); const x2 = PAD_LEFT + FRET_XS[fr2]; p(``); } // Nut p(``); // Fret wires for (let f = 1; f <= FRET_COUNT; f++) { const x = PAD_LEFT + FRET_XS[f]; p(``); } // Fret numbers for (let f = 1; f <= FRET_COUNT; f++) { const x = PAD_LEFT + (FRET_XS[f - 1] + FRET_XS[f]) / 2; const inRange = !fretRange || (f >= fretRange[0] && f <= fretRange[1]); p(`${f}`); } // Position marker dots for (const mf of MARKER_FRETS) { const x = PAD_LEFT + (FRET_XS[mf - 1] + FRET_XS[mf]) / 2; const midY = PAD_TOP + (ns - 1) * STRING_SPACING / 2; if (mf === 12) { p(``); p(``); } else { p(``); } } // Strings const opens = OPEN_NOTES[ns]; for (let s = 0; s < ns; s++) { const y = PAD_TOP + (ns - 1 - s) * STRING_SPACING; const thickness = 1 + (ns - 1 - s) * 0.22; p(``); p(`${opens[s]}`); } // Dots and click targets for (let s = 0; s < ns; s++) { const y = PAD_TOP + (ns - 1 - s) * STRING_SPACING; for (let f = 0; f <= FRET_COUNT; f++) { const x = f === 0 ? PAD_LEFT - 22 : PAD_LEFT + (FRET_XS[f - 1] + FRET_XS[f]) / 2; const key = `${s},${f}`; const inRange = !fretRange || (f >= fretRange[0] && f <= fretRange[1]); const dotColor = resolveDotColor({ key, s, f, highlightPos, mode2Sel, mode2Correct, interactive, inRange }); if (dotColor) { p(``); if (showLabels) { const label = noteDisplay(getNoteAt(s, f, ns), sharpsOnly); p(`${label}`); } } if (interactive && inRange) { const cellX = f === 0 ? PAD_LEFT - 36 : PAD_LEFT + FRET_XS[f - 1]; const cellW = f === 0 ? 30 : FRET_XS[f] - FRET_XS[f - 1]; p(``); } } } p(''); return parts.join(''); } function resolveDotColor({ key, s, f, highlightPos, mode2Sel, mode2Correct, interactive, inRange }) { // Mode 1 highlight if (highlightPos && highlightPos.stringIdx === s && highlightPos.fret === f) { return { fill: '#e94560', stroke: 'none' }; } if (!interactive || !inRange) return null; if (mode2Correct) { // Post-submission state const isCorrect = mode2Correct.has(key); const isSelected = mode2Sel.has(key); if (isCorrect && isSelected) return { fill: '#4caf50', stroke: '#4caf50' }; if (isCorrect && !isSelected) return { fill: 'rgba(76,175,80,0.5)', stroke: '#4caf50' }; if (!isCorrect && isSelected) return { fill: 'rgba(233,69,96,0.7)', stroke: '#e94560' }; return null; } // Pre-submission: show selected positions if (mode2Sel.has(key)) return { fill: 'var(--accent2)', stroke: 'none' }; return null; }