Enhance fretboard generator with notes at each fret position
Integrated features from Python fretboard generator script: - Added 5 more tunings (24 total): Open G Low G, Standard C, D Tuning Reno, Plectrum C, Tenor - Implemented getNoteAtFret() calculation showing notes at every fret position - Added proper display note mapping: Ab/Bb (flats) but C#/D#/F# (sharps) - Enhanced SVG fretboard with: - Notes displayed at each fret intersection (toggleable) - 5th string starts at fret 6 with proper effective fret calculation - Peg indicator for 5th string tuning point - Diagonal neck edge transition - Ghost/dotted line for 5th string from nut to fret 5 - Fret markers with symbols (star at 5, double dots at 12) - Ordinal fret labels on right side (3rd fret, 5th fret, etc.) - Added 4-string banjo support with "Other" family category - Added "Show notes on fretboard" toggle control - Footer with string position and fret marker legend https://claude.ai/code/session_01JfA4LA5EzCPiZHbWJ2kyvj
+487 −251 · 1 file changed
-
index.html+487 −251
@@ -15,6 +15,8 @@ --d-family-light: #E3F5E9; --g-family: #2E5C8A; --g-family-light: #E3EEF8; + --other-family: #7B5BA6; + --other-family-light: #F0E8F5; --text-primary: #1a1a1a; --text-secondary: #444; --text-muted: #666; @@ -216,6 +218,23 @@ min-width: 50px; } + /* Checkbox toggle for notes */ + .checkbox-container { + display: flex; + align-items: center; + gap: 8px; + } + + .checkbox-container input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + } + + .checkbox-container span { + font-size: 14px; + } + /* Buttons */ .btn { padding: 10px 20px; @@ -277,6 +296,7 @@ .family-intro.c-family-intro { border-color: var(--c-family); } .family-intro.d-family-intro { border-color: var(--d-family); } .family-intro.g-family-intro { border-color: var(--g-family); } + .family-intro.other-family-intro { border-color: var(--other-family); } /* ============================================ TUNINGS GRID @@ -320,6 +340,11 @@ background: linear-gradient(to bottom, var(--g-family-light) 0%, #F8FBFE 100%); } + .tuning-card.other-family { + border-color: var(--other-family); + background: linear-gradient(to bottom, var(--other-family-light) 0%, #FDFBFE 100%); + } + .tuning-header { display: flex; justify-content: space-between; @@ -332,6 +357,7 @@ .c-family .tuning-header { border-color: var(--c-family); } .d-family .tuning-header { border-color: var(--d-family); } .g-family .tuning-header { border-color: var(--g-family); } + .other-family .tuning-header { border-color: var(--other-family); } .tuning-name { font-size: 16px; @@ -345,6 +371,16 @@ color: var(--text-muted); } + .string-count-badge { + display: inline-block; + background: #666; + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + margin-left: 8px; + } + .note-sequence { font-family: 'Courier New', Consolas, monospace; font-size: 20px; @@ -629,8 +665,8 @@ <!-- Header --> <header class="app-header"> <h1>INTERACTIVE BANJO TUNINGS REFERENCE</h1> - <div class="subtitle">19 Essential Tunings with Octave Notation, Capo Simulation & Fretboard Diagrams</div> - <div class="subtitle">5-String Banjo | Organized by Tuning Family</div> + <div class="subtitle">24 Tunings with Fretboard Note Charts, Capo Simulation & SVG Diagrams</div> + <div class="subtitle">5-String & 4-String Banjo | Organized by Tuning Family</div> </header> <!-- Control Panel --> @@ -642,6 +678,7 @@ <option value="c-family">C-Based Tunings</option> <option value="d-family">D-Based Tunings</option> <option value="g-family">G-Based Tunings</option> + <option value="other-family">Other (4-String)</option> </select> </div> @@ -660,13 +697,10 @@ </div> <div class="control-group"> - <label>Notation</label> - <div class="toggle-container"> - <span class="toggle-label" id="notationLabel">Sharps</span> - <label class="toggle-switch"> - <input type="checkbox" id="notationToggle"> - <span class="toggle-slider"></span> - </label> + <label>Options</label> + <div class="checkbox-container"> + <input type="checkbox" id="showNotesToggle" checked> + <span>Show notes on fretboard</span> </div> </div> @@ -693,7 +727,8 @@ Notes shown with Scientific Pitch Notation (SPN) subscripts (e.g., G₄ = G above middle C)<br> String order: 5th (drone) → 4th → 3rd → 2nd → 1st<br> Δ symbol = changes required from reference tuning | ↑ = tune up | ↓ = tune down<br> - Circle Key: ○ = Standard Open String | ● = Changed String + Circle Key: ○ = Standard Open String | ● = Changed String<br> + Display uses mixed notation: Ab, Bb (flats) but C#, D#, F# (sharps) for musical convention </div> <div id="tuningsContainer"></div> @@ -702,8 +737,9 @@ <!-- Fretboards Tab --> <div id="fretboards-tab" class="tab-content"> <div class="reference-box"> - <strong>FRETBOARD DIAGRAMS:</strong> - Select a tuning below to view its fretboard diagram. The 5th string starts at fret 5 (shown with diagonal neck edge). + <strong>FRETBOARD NOTE CHARTS:</strong> + Select a tuning below to view its complete fretboard diagram with notes at every fret position.<br> + 5th string starts at fret 6 (shown with diagonal neck edge and peg indicator).<br> Fret markers: ● at frets 3, 7, 10, 15, 17 | ★ at fret 5 | ●● at fret 12 </div> @@ -840,7 +876,7 @@ </div> <div class="legend"> - Complete Banjo Tunings Reference | 19 Tunings Across 3 Families<br> + Complete Banjo Tunings Reference | 24 Tunings Across 4 Families<br> Symbols: ↑ = tune up | ↓ = tune down | Δ = change from reference | SPN = Scientific Pitch Notation<br> ⭐ = Most common | ⚠ = Requires special consideration (string tension, setup) </div> @@ -849,146 +885,229 @@ <script> // ============================================ - // TUNINGS DATA + // CHROMATIC SCALE & NOTE UTILITIES + // ============================================ + const CHROMATIC_SCALE = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + + // Normalize note names (flats to sharps, handle case) + const NOTE_MAP = { + 'c': 'C', 'c#': 'C#', 'db': 'C#', 'Db': 'C#', + 'd': 'D', 'd#': 'D#', 'eb': 'D#', 'Eb': 'D#', + 'e': 'E', 'fb': 'E', 'Fb': 'E', + 'f': 'F', 'e#': 'F', 'E#': 'F', 'f#': 'F#', 'gb': 'F#', 'Gb': 'F#', + 'g': 'G', 'g#': 'G#', 'ab': 'G#', 'Ab': 'G#', + 'a': 'A', 'a#': 'A#', 'bb': 'A#', 'Bb': 'A#', + 'b': 'B', 'cb': 'B', 'Cb': 'B', 'b#': 'C', 'B#': 'C', + 'C': 'C', 'C#': 'C#', 'D': 'D', 'D#': 'D#', 'E': 'E', + 'F': 'F', 'F#': 'F#', 'G': 'G', 'G#': 'G#', 'A': 'A', 'A#': 'A#', 'B': 'B' + }; + + // Display note mapping - prefer flats for G# and A# (as per user's Python script) + const DISPLAY_NOTE_MAP = { + 'G#': 'Ab', + 'A#': 'Bb' + // Keep D#, C#, F# as sharps + }; + + function normalizeNote(note) { + return NOTE_MAP[note] || note.toUpperCase(); + } + + function displayNote(note) { + // Convert internal sharp note to preferred display (mix of flats and sharps) + if (DISPLAY_NOTE_MAP[note]) { + return DISPLAY_NOTE_MAP[note]; + } + return note; + } + + function getNoteAtFret(openNote, fret) { + const normalized = normalizeNote(openNote); + const startIndex = CHROMATIC_SCALE.indexOf(normalized); + if (startIndex === -1) return openNote; + const newIndex = (startIndex + fret) % 12; + return CHROMATIC_SCALE[newIndex]; + } + + // ============================================ + // TUNINGS DATA (24 tunings including 4-string) // ============================================ const TUNINGS_DATA = { // C-Based Tunings "Minstrel Tuning": { - strings: ["F4", "C2", "F3", "A3", "C4"], + strings: ["f", "C", "F", "A", "C"], family: "c-family", description: "Historical", referenceTuning: "Standard F", deltaChanges: "From Standard F: 4th↓C", capoPositions: { 0: "Not commonly capoed" }, songs: "Briggs manual, Coon Hunt", - changedStrings: [true, true, true, true, true] + changedStrings: [true, true, true, true, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Naomi Weiss": { - strings: ["G4", "C2", "G3", "A3", "D4"], + strings: ["g", "C", "G", "A", "D"], family: "c-family", description: "Song-Specific", referenceTuning: "Minstrel", deltaChanges: "From Minstrel: 5th↑G, 3rd↑G, 2nd→A (same), 1st↑D", capoPositions: { 2: "Key of D" }, songs: '"Naomi Weiss"', - changedStrings: [false, false, true, false, true] + changedStrings: [false, false, true, false, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Little Birdie": { - strings: ["E4", "C2", "G3", "A3", "D4"], + strings: ["e", "C", "G", "A", "D"], family: "c-family", description: "Appalachian", referenceTuning: "Naomi Weiss", deltaChanges: "From Naomi Weiss: 5th↓E", capoPositions: { 2: "Key of F# (uncommon)" }, songs: '"Little Birdie"', - changedStrings: [true, false, false, false, false] + changedStrings: [true, false, false, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Hook-and-Line": { - strings: ["G4", "C2", "G3", "C4", "D4"], + strings: ["g", "C", "G", "C", "D"], family: "c-family", description: "Open C Variant", referenceTuning: "Little Birdie", deltaChanges: "From Little Birdie: 5th↑G, 2nd↑C", capoPositions: { 2: "Key of D", 5: "Key of F" }, songs: '"Hook and Line"', - changedStrings: [true, false, false, true, false] + changedStrings: [true, false, false, true, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, - "Cackling Hen (Open C)": { - strings: ["G4", "C2", "G3", "C4", "E4"], + "Cackling Hen": { + strings: ["g", "C", "G", "C", "E"], family: "c-family", description: "Open C Major", referenceTuning: "Hook-and-Line", deltaChanges: "From Hook-and-Line: 1st↑E", capoPositions: { 0: "Key of C", 2: "Key of D" }, songs: "Open C tuning, Irish/Celtic", - changedStrings: [false, false, false, false, true] + changedStrings: [false, false, false, false, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, - "Darling Cora (Triple C)": { - strings: ["G4", "C2", "G3", "C4", "C4"], + "Darling Cora": { + strings: ["g", "C", "G", "C", "C"], family: "c-family", - description: "Drone Heavy", + description: "Drone Heavy (Triple C)", referenceTuning: "Cackling Hen", deltaChanges: "From Cackling Hen: 1st↓C", capoPositions: { 2: "Key of D", 5: "Key of F" }, songs: '"Darling Cora"', - changedStrings: [false, false, false, false, true] + changedStrings: [false, false, false, false, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Double C": { - strings: ["G4", "C2", "G3", "C4", "D4"], + strings: ["g", "C", "G", "C", "D"], family: "c-family", description: "Old-Time Standard", referenceTuning: "Open G", deltaChanges: "From Open G: 4th↓C, 2nd↑C", capoPositions: { 2: "Key of D", 5: "Key of F" }, songs: "Wildwood Flower, Billy in the Lowground", - changedStrings: [false, true, false, true, false] + changedStrings: [false, true, false, true, false], + shortStringIndex: 0, + shortStringStartFret: 6 + }, + "Standard C": { + strings: ["g", "C", "G", "B", "D"], + family: "c-family", + description: "C Tuning for Old-Time", + referenceTuning: "Open G", + deltaChanges: "From Open G: 4th↓C", + capoPositions: { 2: "Key of D", 5: "Key of F" }, + songs: "Old-time repertoire", + changedStrings: [false, true, false, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, // D-Based Tunings "Moonshiner": { - strings: ["G4", "D3", "G3", "A3", "D4"], + strings: ["g", "D", "G", "A", "D"], family: "d-family", description: "Modal/Dark", referenceTuning: "Standard F", deltaChanges: "From Standard F: 4th↑D, 3rd↑G, 2nd→A (same), 1st↑D", capoPositions: { 2: "Key of A", 5: "Key of C" }, songs: '"Moonshiner"', - changedStrings: [false, true, false, true, true] + changedStrings: [false, true, false, true, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "German War": { - strings: ["G4", "D3", "G3", "A3", "F#4"], + strings: ["g", "D", "G", "A", "F#"], family: "d-family", description: "Historical", referenceTuning: "Moonshiner", deltaChanges: "From Moonshiner: 1st↓F#", capoPositions: { 2: "Key of A (modal)" }, songs: "German war songs", - changedStrings: [false, false, false, false, true] + changedStrings: [false, false, false, false, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Reuben": { - strings: ["G4", "D3", "F#3", "A3", "F#4"], + strings: ["g", "D", "F#", "A", "F#"], family: "d-family", description: "Song-Specific", referenceTuning: "German War", deltaChanges: "From German War: 3rd↓F#", capoPositions: { 2: "Key of A (modal)" }, songs: '"Reuben"', - changedStrings: [false, false, true, false, false] + changedStrings: [false, false, true, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, - "F Tuning (Cumberland Gap)": { - strings: ["F4", "D3", "G3", "C4", "D4"], + "F Tuning": { + strings: ["f", "D", "G", "C", "D"], family: "d-family", - description: "High Tension", + description: "Cumberland Gap", referenceTuning: "Reuben", deltaChanges: "From Reuben: 5th↓F, 3rd↑G, 2nd↑C, 1st↑D", capoPositions: { 2: "Key of G", 5: "Key of Bb" }, songs: "Cumberland Gap, Drunkard's Doom", - changedStrings: [true, false, true, true, true] + changedStrings: [true, false, true, true, true], + shortStringIndex: 0, + shortStringStartFret: 6 }, "East Virginia": { - strings: ["F4", "F3", "G3", "C4", "D4"], + strings: ["f", "F", "G", "C", "D"], family: "d-family", description: "George Gibson Style", referenceTuning: "F Tuning", deltaChanges: "From F Tuning: 4th↑F", capoPositions: { 2: "Key of G", 5: "Key of Bb" }, songs: "George Gibson style", - changedStrings: [false, true, false, false, false] + changedStrings: [false, true, false, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, // G-Based Tunings - "Standard F": { - strings: ["F4", "C3", "F3", "A3", "C4"], + "Standard (F)": { + strings: ["f", "C", "F", "A", "C"], family: "g-family", - description: "Historical Starting Point", + description: "Historical Reference", referenceTuning: null, deltaChanges: "(Reference tuning for historical context)", capoPositions: { 2: "Key of G", 5: "Key of Bb" }, songs: "General playing, historical reference", - changedStrings: [false, false, false, false, false] + changedStrings: [false, false, false, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Open G": { - strings: ["G4", "D3", "G3", "B3", "D4"], + strings: ["g", "D", "G", "B", "D"], family: "g-family", description: "Bluegrass Standard", referenceTuning: null, @@ -996,10 +1115,24 @@ capoPositions: { 2: "Key of A", 5: "Key of C", 7: "Key of D" }, songs: "Bill Cheatum, Old Joe Clark, Cripple Creek", changedStrings: [true, true, true, true, true], - featured: true + featured: true, + shortStringIndex: 0, + shortStringStartFret: 6 + }, + "Open G (Low G)": { + strings: ["G", "D", "G", "B", "D"], + family: "g-family", + description: "Open G with Low 5th", + referenceTuning: "Open G", + deltaChanges: "From Open G: 5th↓G (one octave lower)", + capoPositions: { 2: "Key of A", 5: "Key of C" }, + songs: "Low drone sound", + changedStrings: [true, false, false, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Open A": { - strings: ["A4", "E4", "A4", "C#4", "E4"], + strings: ["a", "E", "A", "C#", "E"], family: "g-family", description: "High Tension", referenceTuning: "Open G", @@ -1007,58 +1140,99 @@ capoPositions: { 2: "Key of B", 5: "Key of D" }, songs: "Tunes in A key - requires light gauge strings!", changedStrings: [true, true, true, true, true], - warning: true + warning: true, + shortStringIndex: 0, + shortStringStartFret: 6 }, "G Variant": { - strings: ["G4", "D3", "G3", "A3", "D4"], + strings: ["g", "D", "G", "A", "D"], family: "g-family", description: "Modal G", referenceTuning: "Open G", deltaChanges: "From Open G: 2nd↓A", capoPositions: { 2: "Key of A", 5: "Key of C" }, songs: "Leather Britches, Texas, Frosty Morning", - changedStrings: [false, false, false, true, false] + changedStrings: [false, false, false, true, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, "G Modal (Sawmill)": { - strings: ["G4", "D3", "G3", "C4", "D4"], + strings: ["g", "D", "G", "C", "D"], family: "g-family", description: "Mountain/Modal", referenceTuning: "Open G", deltaChanges: "From Open G: 2nd↑C", capoPositions: { 2: "Key of A", 5: "Key of C" }, songs: "Modal tunes, F key songs", - changedStrings: [false, false, false, true, false] + changedStrings: [false, false, false, true, false], + shortStringIndex: 0, + shortStringStartFret: 6 }, "Open D": { - strings: ["A4", "D4", "F#4", "A4", "D4"], + strings: ["a", "D", "F#", "A", "D"], family: "g-family", - description: "Blues/Folk", + description: "Blues/Folk (Graveyard)", referenceTuning: "Open G", - deltaChanges: "From Open G: all strings ↑2 frets (whole step)", + deltaChanges: "From Open G: all strings adjusted for D major", capoPositions: { 2: "Key of E", 3: "Key of F", 5: "Key of G" }, songs: 'Most D key tunes, "Graveyard Tuning" - use light strings!', changedStrings: [true, true, true, true, true], - warning: true + warning: true, + shortStringIndex: 0, + shortStringStartFret: 6 }, "D Modal": { - strings: ["A4", "D4", "G4", "A4", "D4"], + strings: ["a", "D", "G", "A", "D"], family: "g-family", description: "Celtic/Medieval", referenceTuning: "Open D", deltaChanges: "From Open D: 3rd↓G", capoPositions: { 2: "Key of E (modal)", 5: "Key of G (modal)" }, songs: "D modal tunes, medieval tonality", - changedStrings: [false, false, true, false, false] - } - }; + changedStrings: [false, false, true, false, false], + shortStringIndex: 0, + shortStringStartFret: 6 + }, + "D Tuning (Reno)": { + strings: ["a", "D", "G", "B", "E"], + family: "g-family", + description: "Don Reno Style", + referenceTuning: "Open G", + deltaChanges: "From Open G: 5th↑A, 1st↑E", + capoPositions: { 2: "Key of E", 5: "Key of G" }, + songs: "Don Reno repertoire", + changedStrings: [true, false, false, false, true], + shortStringIndex: 0, + shortStringStartFret: 6 + }, - // Note conversion maps - const NOTE_ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; - const SHARP_TO_FLAT = { - 'C#': 'Db', 'D#': 'Eb', 'F#': 'Gb', 'G#': 'Ab', 'A#': 'Bb' - }; - const FLAT_TO_SHARP = { - 'Db': 'C#', 'Eb': 'D#', 'Gb': 'F#', 'Ab': 'G#', 'Bb': 'A#' + // Other / 4-String Tunings + "Plectrum C": { + strings: ["C", "G", "B", "D"], + family: "other-family", + description: "4-String Plectrum", + referenceTuning: null, + deltaChanges: "(4-string plectrum standard)", + capoPositions: { 0: "Key of C" }, + songs: "Jazz, Dixieland", + changedStrings: [false, false, false, false], + shortStringIndex: null, + shortStringStartFret: null, + stringCount: 4 + }, + "Tenor (Standard)": { + strings: ["C", "G", "D", "A"], + family: "other-family", + description: "4-String Tenor CGDA", + referenceTuning: null, + deltaChanges: "(4-string tenor standard - fifths tuning)", + capoPositions: { 0: "Key of C/G" }, + songs: "Irish, Jazz, Traditional", + changedStrings: [false, false, false, false], + shortStringIndex: null, + shortStringStartFret: null, + stringCount: 4 + } }; // ============================================ @@ -1068,11 +1242,10 @@ familyFilter: 'all', capoPosition: 0, fretCount: 15, - useFlats: false, + showNotes: true, selectedFretboardTuning: 'Open G' }; - // Load from localStorage function loadState() { const saved = localStorage.getItem('banjoTuningsState'); if (saved) { @@ -1085,77 +1258,53 @@ } } - // Save to localStorage function saveState() { localStorage.setItem('banjoTuningsState', JSON.stringify(state)); } // ============================================ - // NOTE UTILITIES + // NOTE FORMATTING // ============================================ - function parseNote(noteStr) { - const match = noteStr.match(/^([A-G][#b]?)(\d)$/); + function parseNoteWithOctave(noteStr) { + // Handle format like "G4", "C#3", "g" (lowercase = high octave) + const match = noteStr.match(/^([A-Ga-g][#b]?)(\d)?$/); if (!match) return null; - return { note: match[1], octave: parseInt(match[2]) }; - } - - function formatNote(noteStr, useFlats = false) { - const parsed = parseNote(noteStr); - if (!parsed) return noteStr; - - let note = parsed.note; - // Normalize to sharps first - if (FLAT_TO_SHARP[note]) { - note = FLAT_TO_SHARP[note]; - } + let note = match[1]; + let octave = match[2] ? parseInt(match[2]) : null; - // Convert to flats if needed - if (useFlats && SHARP_TO_FLAT[note]) { - note = SHARP_TO_FLAT[note]; + // Lowercase note = high octave (convention from Python script) + if (note === note.toLowerCase() && !octave) { + octave = 4; // High octave for 5th string + } else if (!octave) { + octave = 3; // Default octave } - // Replace # with sharp symbol and b with flat symbol - note = note.replace('#', '\u266F').replace('b', '\u266D'); - - return `${note}<sub>${parsed.octave}</sub>`; + return { note: normalizeNote(note), octave }; } - function transposeNote(noteStr, semitones) { - const parsed = parseNote(noteStr); + function formatNoteForDisplay(noteStr) { + const parsed = parseNoteWithOctave(noteStr); if (!parsed) return noteStr; - let note = parsed.note; - let octave = parsed.octave; - - // Normalize to sharps - if (FLAT_TO_SHARP[note]) { - note = FLAT_TO_SHARP[note]; - } - - let noteIndex = NOTE_ORDER.indexOf(note); - if (noteIndex === -1) return noteStr; - - noteIndex += semitones; + let note = displayNote(parsed.note); + // Replace # with sharp symbol, b with flat symbol + note = note.replace('#', '\u266F').replace('b', '\u266D'); - // Handle octave changes - while (noteIndex >= 12) { - noteIndex -= 12; - octave++; - } - while (noteIndex < 0) { - noteIndex += 12; - octave--; - } + return `${note}<sub>${parsed.octave}</sub>`; + } - return NOTE_ORDER[noteIndex] + octave; + function formatSimpleNote(note) { + // Format a simple note (no octave) for fretboard display + const displayed = displayNote(normalizeNote(note)); + return displayed.replace('#', '\u266F').replace('b', '\u266D'); } // ============================================ // RENDERING FUNCTIONS // ============================================ - function renderNoteSequence(strings, useFlats) { - return strings.map(s => formatNote(s, useFlats)).join(' '); + function renderNoteSequence(strings) { + return strings.map(s => formatNoteForDisplay(s)).join(' '); } function renderCapoInfo(capoPositions) { @@ -1164,41 +1313,33 @@ .join(' | '); } - function renderEffectiveTuning(strings, capoPosition, useFlats) { - if (capoPosition === 0) return null; - - const transposed = strings.map(s => transposeNote(s, capoPosition)); - return transposed.map(s => formatNote(s, useFlats)).join(' '); - } - function renderTuningCard(name, tuning) { - const effectiveTuning = renderEffectiveTuning(tuning.strings, state.capoPosition, state.useFlats); - const warningClass = tuning.warning ? 'warning' : ''; const featuredBadge = tuning.featured ? ' ⭐' : ''; const warningBadge = tuning.warning ? ' ⚠' : ''; + const stringCount = tuning.stringCount || 5; + const stringBadge = stringCount === 4 ? '<span class="string-count-badge">4-string</span>' : ''; + + // Render nut visual + const nutVisual = tuning.changedStrings.map((changed, i) => { + const stringNum = stringCount - i; + return `<div class="string-circle ${changed ? 'changed' : ''}">${stringNum}</div>`; + }).join(''); return ` <div class="tuning-card ${tuning.family}"> <div class="tuning-header"> - <span class="tuning-name">${name}${featuredBadge}${warningBadge}</span> + <span class="tuning-name">${name}${featuredBadge}${warningBadge}${stringBadge}</span> <span class="genre-tag">${tuning.description}</span> </div> - <div class="note-sequence">${renderNoteSequence(tuning.strings, state.useFlats)}</div> + <div class="note-sequence">${renderNoteSequence(tuning.strings)}</div> <div class="nut-visual"> - ${tuning.changedStrings.map((changed, i) => - `<div class="string-circle ${changed ? 'changed' : ''}">${5 - i}</div>` - ).join('')} + ${nutVisual} </div> <div class="tuning-details"> <div class="delta-line">Δ: ${tuning.deltaChanges}</div> <div class="capo-line">${renderCapoInfo(tuning.capoPositions)}</div> <div class="songs-line">${tuning.songs}</div> </div> - ${effectiveTuning ? ` - <div class="effective-tuning"> - <strong>With Capo ${state.capoPosition}:</strong> ${effectiveTuning} - </div> - ` : ''} </div> `; } @@ -1208,7 +1349,8 @@ const families = { 'c-family': { name: 'C-Based Tunings', intro: 'These tunings center around C tonality, ranging from historical minstrel styles to modern old-time favorites. They create bright, modal sounds ideal for melodic playing.' }, 'd-family': { name: 'D-Based Tunings', intro: 'D-based tunings create darker, more mysterious tonal landscapes. These tunings are favored for modal old-time pieces and offer rich harmonic possibilities.' }, - 'g-family': { name: 'G-Based Tunings', intro: 'The most common family including the standard bluegrass tuning. These tunings range from bright major chords to modal sounds, offering the widest versatility for different playing styles.' } + 'g-family': { name: 'G-Based Tunings', intro: 'The most common family including the standard bluegrass tuning. These tunings range from bright major chords to modal sounds, offering the widest versatility for different playing styles.' }, + 'other-family': { name: 'Other Tunings (4-String)', intro: '4-string banjo tunings for plectrum and tenor banjos. These instruments are commonly used in jazz, Dixieland, and Irish traditional music.' } }; let html = ''; @@ -1236,132 +1378,238 @@ } // ============================================ - // FRETBOARD SVG GENERATOR + // FRETBOARD SVG GENERATOR (Enhanced from Python) // ============================================ function generateFretboardSVG(tuningName) { const tuning = TUNINGS_DATA[tuningName]; if (!tuning) return ''; - const fretCount = state.fretCount; + const numFrets = state.fretCount; const capo = state.capoPosition; + const showNotes = state.showNotes; - // Dimensions + const strings = tuning.strings; + const numStrings = strings.length; + const shortStringIdx = tuning.shortStringIndex; + const shortStringStart = tuning.shortStringStartFret; + + // SVG dimensions + const marginLeft = 100; + const marginRight = 150; + const marginTop = 140; + const marginBottom = 80; const stringSpacing = 55; const fretHeight = 50; - const leftMargin = 120; - const rightMargin = 80; - const topMargin = 80; - const bottomMargin = 40; - - const numStrings = 5; - const width = leftMargin + (fretCount * fretHeight) + rightMargin; - const height = topMargin + ((numStrings - 1) * stringSpacing) + bottomMargin; - // Fret marker positions - const singleMarkers = [3, 5, 7, 10, 15, 17]; - const doubleMarkers = [12]; - const starMarker = 5; - - let svg = `<svg class="fretboard-svg" width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">`; + const fretboardWidth = (numStrings - 1) * stringSpacing; + const fretboardHeight = numFrets * fretHeight; + const svgWidth = marginLeft + fretboardWidth + marginRight; + const svgHeight = marginTop + fretboardHeight + marginBottom; + + const fbLeft = marginLeft; + const fbTop = marginTop; + const fbRight = fbLeft + fretboardWidth; + const bottomY = fbTop + fretboardHeight; + + // Fret markers + const markerFrets = [3, 5, 7, 10, 12, 15, 17]; + const doubleMarkerFrets = [12]; + + let svg = `<svg class="fretboard-svg" width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg">`; + + // Styles + svg += ` + <style> + .title { font-family: Georgia, serif; font-size: 24px; font-weight: bold; fill: #000; } + .subtitle { font-family: Georgia, serif; font-size: 16px; font-style: italic; fill: #333; } + .string-label { font-family: 'Courier New', monospace; font-size: 18px; font-weight: bold; fill: #000; } + .fret-number { font-family: Arial, sans-serif; font-size: 16px; font-weight: bold; fill: #333; } + .note { font-family: Arial, sans-serif; font-size: 12px; font-weight: bold; fill: #000; } + .note-circle { fill: white; stroke: #000; stroke-width: 1.5; } + .fret-line { stroke: #000; stroke-width: 2; } + .string-line { stroke: #444; stroke-width: 1.5; } + .nut { stroke: #000; stroke-width: 6; } + .fret-marker { fill: #ddd; stroke: none; } + .peg { fill: #333; stroke: #000; stroke-width: 1; } + .ghost-string { stroke: #999; stroke-width: 1; stroke-dasharray: 3,3; } + .fret-marker-symbol { fill: #000; stroke: none; } + </style>`; // Background - svg += `<rect width="${width}" height="${height}" fill="#f5f0e6"/>`; - - // Fretboard wood grain - svg += `<rect x="${leftMargin}" y="${topMargin - 10}" width="${fretCount * fretHeight}" height="${(numStrings - 1) * stringSpacing + 20}" fill="#d4a574" rx="3"/>`; - - // Draw fret markers - for (let fret = 1; fret <= fretCount; fret++) { - const x = leftMargin + (fret - 0.5) * fretHeight; - const centerY = topMargin + ((numStrings - 1) * stringSpacing) / 2; - - if (doubleMarkers.includes(fret)) { - svg += `<circle cx="${x}" cy="${centerY - 20}" r="8" fill="#ddd" stroke="#bbb" stroke-width="1"/>`; - svg += `<circle cx="${x}" cy="${centerY + 20}" r="8" fill="#ddd" stroke="#bbb" stroke-width="1"/>`; - } else if (fret === starMarker) { - svg += `<text x="${x}" y="${centerY + 5}" text-anchor="middle" font-size="20" fill="#c9a86c">★</text>`; - } else if (singleMarkers.includes(fret) && fret !== starMarker) { - svg += `<circle cx="${x}" cy="${centerY}" r="8" fill="#ddd" stroke="#bbb" stroke-width="1"/>`; + svg += `<rect width="100%" height="100%" fill="white"/>`; + + // Title + const tuningStr = strings.map(s => normalizeNote(s)).join(''); + svg += `<text x="${svgWidth / 2}" y="45" class="title" text-anchor="middle">Banjo Fretboard: ${tuningName}</text>`; + svg += `<text x="${svgWidth / 2}" y="70" class="subtitle" text-anchor="middle">Tuning: ${tuningStr} — ${tuning.description}</text>`; + + // Fret markers (on fretboard face) + for (const fret of markerFrets) { + if (fret <= numFrets) { + const y = fbTop + (fret - 0.5) * fretHeight; + if (doubleMarkerFrets.includes(fret)) { + svg += `<circle cx="${fbLeft + fretboardWidth/2 - 15}" cy="${y}" r="7" class="fret-marker"/>`; + svg += `<circle cx="${fbLeft + fretboardWidth/2 + 15}" cy="${y}" r="7" class="fret-marker"/>`; + } else { + svg += `<circle cx="${fbLeft + fretboardWidth/2}" cy="${y}" r="7" class="fret-marker"/>`; + } } } - // Draw nut - svg += `<rect x="${leftMargin - 8}" y="${topMargin - 10}" width="8" height="${(numStrings - 1) * stringSpacing + 20}" fill="#f5f5dc" stroke="#333" stroke-width="2"/>`; - - // Draw frets - for (let fret = 1; fret <= fretCount; fret++) { - const x = leftMargin + fret * fretHeight; - // 5th string only starts at fret 5 - const startY = fret <= 5 ? topMargin + stringSpacing : topMargin; - svg += `<line x1="${x}" y1="${startY}" x2="${x}" y2="${topMargin + (numStrings - 1) * stringSpacing}" stroke="#666" stroke-width="2"/>`; + // Nut + if (shortStringIdx !== null && shortStringStart !== null) { + const nutStartX = fbLeft + stringSpacing - 10; + svg += `<line x1="${nutStartX}" y1="${fbTop}" x2="${fbRight + 10}" y2="${fbTop}" class="nut"/>`; + } else { + svg += `<line x1="${fbLeft - 10}" y1="${fbTop}" x2="${fbRight + 10}" y2="${fbTop}" class="nut"/>`; } - // Draw 5th string diagonal transition - const fret5X = leftMargin + 5 * fretHeight; - svg += `<path d="M ${fret5X} ${topMargin} L ${fret5X - 15} ${topMargin + stringSpacing * 0.3}" stroke="#8b4513" stroke-width="3" fill="none"/>`; + // Fret lines with ordinal labels on right + for (let fret = 1; fret <= numFrets; fret++) { + const y = fbTop + fret * fretHeight; + + let lineStartX, lineEndX; + if (shortStringIdx !== null && shortStringStart !== null && fret < shortStringStart) { + lineStartX = fbLeft + stringSpacing - 10; + lineEndX = fbRight + 10; + } else { + lineStartX = fbLeft - 10; + lineEndX = fbRight + 10; + } + + // Extend line for marker frets + if (markerFrets.includes(fret)) { + lineEndX += 20; + } - // 5th string peg indicator - svg += `<circle cx="${fret5X - 20}" cy="${topMargin + stringSpacing * 0.4}" r="6" fill="#c9a86c" stroke="#8b4513" stroke-width="2"/>`; + svg += `<line x1="${lineStartX}" y1="${y}" x2="${lineEndX}" y2="${y}" class="fret-line"/>`; + + // Ordinal fret labels on right + if (markerFrets.includes(fret)) { + const ordinal = fret + (fret === 1 ? 'st' : fret === 2 ? 'nd' : fret === 3 ? 'rd' : 'th') + ' fret'; + const textX = fbRight + 70; + svg += `<text x="${textX}" y="${y}" class="fret-number" text-anchor="middle" dominant-baseline="middle">${ordinal}</text>`; + + // Marker symbols + const symbolX = textX + 50; + if (fret === 5) { + // Star for 5th fret + const starPoints = `${symbolX},${y-7} ${symbolX+2},${y-2} ${symbolX+7},${y-2} ${symbolX+3},${y+1} ${symbolX+5},${y+6} ${symbolX},${y+3} ${symbolX-5},${y+6} ${symbolX-3},${y+1} ${symbolX-7},${y-2} ${symbolX-2},${y-2}`; + svg += `<polygon points="${starPoints}" class="fret-marker-symbol"/>`; + } else if (fret === 12) { + // Double circles + svg += `<circle cx="${symbolX - 8}" cy="${y}" r="6" class="fret-marker-symbol"/>`; + svg += `<circle cx="${symbolX + 8}" cy="${y}" r="6" class="fret-marker-symbol"/>`; + } else { + // Single circle + svg += `<circle cx="${symbolX}" cy="${y}" r="6" class="fret-marker-symbol"/>`; + } + } + } - // Draw strings - for (let string = 0; string < numStrings; string++) { - const y = topMargin + string * stringSpacing; - const stringWeight = 2 - (string * 0.2); + // Neck edges with diagonal for short string + if (shortStringIdx !== null && shortStringStart !== null) { + const nutLeftX = fbLeft + stringSpacing - 10; + const fret4Y = fbTop + 4 * fretHeight; + const fret5Y = fbTop + 5 * fretHeight; + const leftEdgeX = fbLeft - 10; + + // Ghost string line (dotted) + svg += `<line x1="${fbLeft}" y1="${fbTop}" x2="${fbLeft}" y2="${fret5Y - fretHeight}" class="ghost-string"/>`; + + // Polyline neck edge + svg += `<polyline points="${nutLeftX},${fbTop} ${nutLeftX},${fret4Y} ${leftEdgeX},${fret5Y} ${leftEdgeX},${bottomY}" style="stroke:#000; stroke-width:3; fill:none;"/>`; + svg += `<line x1="${fbRight + 10}" y1="${fbTop}" x2="${fbRight + 10}" y2="${bottomY}" style="stroke:#000; stroke-width:3;"/>`; + svg += `<line x1="${leftEdgeX}" y1="${bottomY}" x2="${fbRight + 10}" y2="${bottomY}" style="stroke:#000; stroke-width:3;"/>`; + } else { + // 4-string: rectangular edges + svg += `<line x1="${fbLeft - 10}" y1="${fbTop}" x2="${fbLeft - 10}" y2="${bottomY}" style="stroke:#000; stroke-width:3;"/>`; + svg += `<line x1="${fbRight + 10}" y1="${fbTop}" x2="${fbRight + 10}" y2="${bottomY}" style="stroke:#000; stroke-width:3;"/>`; + svg += `<line x1="${fbLeft - 10}" y1="${bottomY}" x2="${fbRight + 10}" y2="${bottomY}" style="stroke:#000; stroke-width:3;"/>`; + } - if (string === 0) { - // 5th string - dotted from nut to fret 5, solid after - svg += `<line x1="${leftMargin}" y1="${y}" x2="${fret5X}" y2="${y}" stroke="#888" stroke-width="${stringWeight}" stroke-dasharray="4,4" opacity="0.4"/>`; - svg += `<line x1="${fret5X}" y1="${y}" x2="${leftMargin + fretCount * fretHeight}" y2="${y}" stroke="#444" stroke-width="${stringWeight}"/>`; + // String lines + for (let i = 0; i < numStrings; i++) { + const x = fbLeft + i * stringSpacing; + + if (i === shortStringIdx && shortStringStart !== null) { + const startY = fbTop + (shortStringStart - 1) * fretHeight; + svg += `<line x1="${x}" y1="${startY}" x2="${x}" y2="${bottomY}" class="string-line" style="stroke-width:2;"/>`; + + // Peg indicator + const pegY = startY - 50; + const pegHeadX = fbLeft - 10; + const pegShaftStartX = fbLeft + 30; + svg += `<rect x="${pegHeadX}" y="${pegY - 5}" width="${pegShaftStartX - pegHeadX}" height="8" class="peg" rx="2"/>`; + svg += `<circle cx="${pegHeadX}" cy="${pegY}" r="10" class="peg"/>`; + svg += `<circle cx="${pegHeadX}" cy="${pegY}" r="4" fill="white"/>`; } else { - svg += `<line x1="${leftMargin}" y1="${y}" x2="${leftMargin + fretCount * fretHeight}" y2="${y}" stroke="#444" stroke-width="${stringWeight}"/>`; + svg += `<line x1="${x}" y1="${fbTop}" x2="${x}" y2="${bottomY}" class="string-line"/>`; } } - // Draw capo if set - if (capo > 0) { - const capoX = leftMargin + (capo - 0.5) * fretHeight; - svg += `<rect x="${capoX - 10}" y="${topMargin - 15}" width="20" height="${(numStrings - 1) * stringSpacing + 30}" fill="#333" rx="5" opacity="0.8"/>`; - svg += `<text x="${capoX}" y="${topMargin - 25}" text-anchor="middle" font-size="12" fill="#333" font-weight="bold">CAPO</text>`; + // String labels at top + for (let i = 0; i < numStrings; i++) { + const x = fbLeft + i * stringSpacing; + const stringNumber = numStrings - i; + const note = strings[i]; + const normalized = normalizeNote(note); + const displayed = formatSimpleNote(note); + + const numberY = fbTop - 35; + const labelY = fbTop - 12; + + svg += `<text x="${x}" y="${numberY}" class="fret-number" text-anchor="middle">${stringNumber}</text>`; + svg += `<text x="${x}" y="${labelY}" class="string-label" text-anchor="middle">${displayed}</text>`; } - // Draw string labels and open notes - const strings = tuning.strings; - for (let string = 0; string < numStrings; string++) { - const y = topMargin + string * stringSpacing; - const note = strings[string]; - const transposedNote = capo > 0 ? transposeNote(note, capo) : note; - const displayNote = formatNote(transposedNote, state.useFlats); - - // String number - svg += `<text x="${leftMargin - 50}" y="${y + 5}" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">${5 - string}</text>`; - - // Open note circle - const isChanged = tuning.changedStrings[string]; - svg += `<circle cx="${leftMargin - 25}" cy="${y}" r="14" fill="${isChanged ? '#333' : 'white'}" stroke="#333" stroke-width="2"/>`; - - // Note text (need to handle subscript differently in SVG) - const parsed = parseNote(transposedNote); - if (parsed) { - let noteName = parsed.note; - if (state.useFlats && SHARP_TO_FLAT[noteName]) { - noteName = SHARP_TO_FLAT[noteName]; + // Notes at each fret position + if (showNotes) { + for (let fret = 1; fret <= numFrets; fret++) { + const y = fbTop + (fret - 0.5) * fretHeight; + + for (let i = 0; i < numStrings; i++) { + const x = fbLeft + i * stringSpacing; + const openNote = strings[i]; + + // Handle short string + if (i === shortStringIdx && shortStringStart !== null) { + if (fret < shortStringStart) { + continue; // No notes in empty space + } + // Calculate effective fret for short string + const effectiveFret = fret - (shortStringStart - 1); + const note = getNoteAtFret(openNote, effectiveFret); + const displayedNote = formatSimpleNote(note); + + svg += `<circle cx="${x}" cy="${y}" r="16" class="note-circle"/>`; + svg += `<text x="${x}" y="${y + 5}" class="note" text-anchor="middle">${displayedNote}</text>`; + } else { + const note = getNoteAtFret(openNote, fret); + const displayedNote = formatSimpleNote(note); + + svg += `<circle cx="${x}" cy="${y}" r="16" class="note-circle"/>`; + svg += `<text x="${x}" y="${y + 5}" class="note" text-anchor="middle">${displayedNote}</text>`; + } } - noteName = noteName.replace('#', '\u266F').replace('b', '\u266D'); - - svg += `<text x="${leftMargin - 25}" y="${y + 2}" text-anchor="middle" font-size="10" font-weight="bold" fill="${isChanged ? 'white' : '#333'}">${noteName}</text>`; - svg += `<text x="${leftMargin - 17}" y="${y + 8}" text-anchor="middle" font-size="7" fill="${isChanged ? 'white' : '#333'}">${parsed.octave}</text>`; } } - // Fret numbers - for (let fret = 1; fret <= fretCount; fret++) { - const x = leftMargin + (fret - 0.5) * fretHeight; - svg += `<text x="${x}" y="${topMargin + (numStrings - 1) * stringSpacing + 25}" text-anchor="middle" font-size="12" fill="#666">${fret}</text>`; + // Capo indicator + if (capo > 0) { + const capoY = fbTop + (capo - 0.5) * fretHeight; + svg += `<rect x="${fbLeft - 15}" y="${capoY - 8}" width="${fretboardWidth + 30}" height="16" fill="#333" rx="4" opacity="0.9"/>`; + svg += `<text x="${fbLeft + fretboardWidth/2}" y="${capoY + 4}" text-anchor="middle" font-size="10" fill="white" font-weight="bold">CAPO</text>`; } - // Title - svg += `<text x="${width / 2}" y="30" text-anchor="middle" font-size="18" font-weight="bold" fill="#333">${tuningName}</text>`; - svg += `<text x="${width / 2}" y="50" text-anchor="middle" font-size="12" fill="#666">${tuning.description}</text>`; + // Footer + const footerY = fbTop + fretboardHeight + 30; + svg += `<rect x="${marginLeft-50}" y="${footerY}" width="${fretboardWidth+150}" height="30" rx="5" fill="#f5f5f5" stroke="#ccc"/>`; + const footerLine1 = shortStringStart + ? `String 1 (far right) = 1st string • 5th string starts at fret ${shortStringStart}` + : `String 1 (far right) = 1st string • ${numStrings}-string banjo`; + svg += `<text x="${svgWidth/2}" y="${footerY + 12}" text-anchor="middle" font-size="10" fill="#555">${footerLine1}</text>`; + svg += `<text x="${svgWidth/2}" y="${footerY + 24}" text-anchor="middle" font-size="10" fill="#555">Fret markers: 3,5,7,10,12,15,17 • Peg indicates 5th string tuning point</text>`; svg += '</svg>'; return svg; @@ -1383,14 +1631,12 @@ // EVENT HANDLERS // ============================================ function setupEventListeners() { - // Family filter document.getElementById('familyFilter').addEventListener('change', (e) => { state.familyFilter = e.target.value; saveState(); renderTunings(); }); - // Capo position document.getElementById('capoPosition').addEventListener('change', (e) => { state.capoPosition = parseInt(e.target.value) || 0; saveState(); @@ -1398,37 +1644,29 @@ renderFretboard(); }); - // Fret count document.getElementById('fretCount').addEventListener('change', (e) => { state.fretCount = parseInt(e.target.value); saveState(); renderFretboard(); }); - // Notation toggle - document.getElementById('notationToggle').addEventListener('change', (e) => { - state.useFlats = e.target.checked; - document.getElementById('notationLabel').textContent = state.useFlats ? 'Flats' : 'Sharps'; + document.getElementById('showNotesToggle').addEventListener('change', (e) => { + state.showNotes = e.target.checked; saveState(); - renderTunings(); renderFretboard(); }); - // Fretboard tuning select document.getElementById('fretboardTuningSelect').addEventListener('change', (e) => { state.selectedFretboardTuning = e.target.value; saveState(); renderFretboard(); }); - // Tabs document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', (e) => { const tabName = e.target.dataset.tab; - document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - e.target.classList.add('active'); document.getElementById(`${tabName}-tab`).classList.add('active'); }); @@ -1440,7 +1678,7 @@ familyFilter: 'all', capoPosition: 0, fretCount: 15, - useFlats: false, + showNotes: true, selectedFretboardTuning: 'Open G' }; saveState(); @@ -1453,8 +1691,7 @@ document.getElementById('familyFilter').value = state.familyFilter; document.getElementById('capoPosition').value = state.capoPosition; document.getElementById('fretCount').value = state.fretCount; - document.getElementById('notationToggle').checked = state.useFlats; - document.getElementById('notationLabel').textContent = state.useFlats ? 'Flats' : 'Sharps'; + document.getElementById('showNotesToggle').checked = state.showNotes; document.getElementById('fretboardTuningSelect').value = state.selectedFretboardTuning; } @@ -1470,7 +1707,6 @@ renderFretboard(); } - // Start the app document.addEventListener('DOMContentLoaded', init); </script> </body>