← banjotuna

Move chord inversions to fretboard with visual overlays

7377799210fa16ac7b0156da25b088710940342f · Claude · Feb 6, 2026 15:50

- Remove chord inversion display from chord diagram section
- Add UI controls on fretboard page (toggle + root note selector)
- Implement calculateMajorInversions() for computing inversion positions
- Add renderInversionsOnFretboard() with:
  - Colored polygon overlays (blue=root, red=1st, yellow=2nd)
  - Position dots on chord frets
  - Annotation blocks with chord name, inversion type, bass note, intervals
  - 5-fret rule connector arrows between consecutive inversions
  - Cycling indicator at bottom of fretboard
- Add arrowhead marker definition in SVG defs

https://claude.ai/code/session_01VSHtwaMJKL1ZgH33JDcwbY

+236 −56 · 1 file changed

  • index.html +236 −56
diff --git a/index.html b/index.html
index 0f4e863..c5a1af0 100644
--- a/index.html
+++ b/index.html
@@ -1167,6 +1167,30 @@
                 </div>
             </div>
 
+            <div class="control-group">
+                <label>Chord Inversions</label>
+                <div class="checkbox-container">
+                    <input type="checkbox" id="showInversionsToggle">
+                    <span>Show inversions on fretboard</span>
+                </div>
+                <div style="margin-top: 8px;">
+                    <select id="inversionRootNote" style="width: 100%;">
+                        <option value="C">C Major</option>
+                        <option value="C#">C#/Db Major</option>
+                        <option value="D">D Major</option>
+                        <option value="D#">D#/Eb Major</option>
+                        <option value="E">E Major</option>
+                        <option value="F">F Major</option>
+                        <option value="F#">F#/Gb Major</option>
+                        <option value="G" selected>G Major</option>
+                        <option value="G#">G#/Ab Major</option>
+                        <option value="A">A Major</option>
+                        <option value="A#">A#/Bb Major</option>
+                        <option value="B">B Major</option>
+                    </select>
+                </div>
+            </div>
+
             <div class="control-group">
                 <label>Actions</label>
                 <div style="display: flex; gap: 10px;">
@@ -1752,40 +1776,14 @@
             return null;
         }
 
-        // Determine chord inversion based on bass note (lowest non-muted string)
-        function getChordInversion(positions) {
-            // Find the lowest (first non-muted) string's degree
-            // Positions are ordered: string 4 (bass), 3, 2, 1
-            for (const pos of positions) {
-                if (!pos.muted && pos.degree) {
-                    const degree = pos.degree;
-                    // Root position: bass is root (1)
-                    if (degree === '1') {
-                        return { type: 'root', label: 'Root', color: null, pattern: 'solid' };
-                    }
-                    // First inversion: bass is third (3 or ♭3)
-                    if (degree === '3' || degree === '♭3') {
-                        return { type: 'first', label: '1st Inv.', color: '#cc0000', pattern: 'diagonal' };
-                    }
-                    // Second inversion: bass is fifth (5, ♭5, or #5)
-                    if (degree === '5' || degree === '♭5' || degree === '#5') {
-                        return { type: 'second', label: '2nd Inv.', color: '#cc9900', pattern: 'crosshatch' };
-                    }
-                    // Other degrees (7th, etc.) - treat as extended inversion
-                    return { type: 'extended', label: degree + ' bass', color: '#666', pattern: 'dots' };
-                }
-            }
-            return { type: 'unknown', label: '', color: null, pattern: null };
-        }
-
         // Generate SVG chord diagram for 4 main strings
         function generateChordDiagramSVG(voicing, chordName, tuningName, fingerNotation = 'numbers') {
             const width = 140;
-            const height = 220;  // Increased height for inversion label
+            const height = 200;
             const marginLeft = 35;
             const marginTop = 35;
             const marginRight = 15;
-            const marginBottom = 45;  // More space for inversion label
+            const marginBottom = 25;
 
             const positions = voicing.positions;
             const numStrings = 4; // Always 4 main strings for chord diagrams
@@ -1795,30 +1793,11 @@
             const stringSpacing = (width - marginLeft - marginRight) / (numStrings - 1);
             const fretSpacing = (height - marginTop - marginBottom) / numFrets;
 
-            // Determine chord inversion
-            const inversion = getChordInversion(positions);
-
             let svg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">`;
 
-            // Pattern definitions for B&W printing
-            svg += `<defs>
-                <pattern id="diagonal-${chordName.replace(/[^a-zA-Z0-9]/g, '')}" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
-                    <line x1="0" y1="0" x2="0" y2="6" stroke="#cc0000" stroke-width="2"/>
-                </pattern>
-                <pattern id="crosshatch-${chordName.replace(/[^a-zA-Z0-9]/g, '')}" width="6" height="6" patternUnits="userSpaceOnUse">
-                    <line x1="0" y1="0" x2="6" y2="6" stroke="#cc9900" stroke-width="1"/>
-                    <line x1="6" y1="0" x2="0" y2="6" stroke="#cc9900" stroke-width="1"/>
-                </pattern>
-            </defs>`;
-
-            // Background with inversion color indicator
+            // Background
             svg += `<rect width="${width}" height="${height}" fill="white"/>`;
 
-            // Inversion color border/indicator at top
-            if (inversion.color) {
-                svg += `<rect x="0" y="0" width="${width}" height="4" fill="${inversion.color}"/>`;
-            }
-
             // Nut (thick line at top) - only show if baseFret is 1
             if (baseFret === 1) {
                 svg += `<rect x="${marginLeft - 3}" y="${marginTop - 3}" width="${(numStrings - 1) * stringSpacing + 6}" height="5" fill="#1a1a1a"/>`;
@@ -1913,13 +1892,6 @@
                 }
             });
 
-            // Inversion label at bottom
-            if (inversion.label) {
-                const invLabelY = marginTop + numFrets * fretSpacing + 32;
-                const labelColor = inversion.color || '#333';
-                svg += `<text x="${width / 2}" y="${invLabelY}" font-size="10" font-weight="bold" text-anchor="middle" fill="${labelColor}">${inversion.label}</text>`;
-            }
-
             svg += '</svg>';
             return svg;
         }
@@ -2241,6 +2213,9 @@
             selectedFretboardTuning: 'Open G',
             fretboardFamilyFilter: 'all',
             useSolfege: false,
+            // Chord inversion state
+            showInversions: false,
+            inversionRootNote: 'G',
             // Chord tab state
             chordTuning: 'Open G',
             chordKey: 'G',
@@ -2388,6 +2363,184 @@
             container.innerHTML = html;
         }
 
+        // ============================================
+        // CHORD INVERSION CALCULATION
+        // ============================================
+
+        // Calculate all major chord inversions for a root note on strings 4-3-2-1
+        // Returns array of inversion objects with fret positions
+        function calculateMajorInversions(rootNote, tuningStrings, maxFret = 15) {
+            // Major chord intervals: Root=0, 3rd=4, 5th=7
+            const ROOT = 0, THIRD = 4, FIFTH = 7;
+
+            // Get pitch class of root note
+            const rootPitch = noteToPitchClass(rootNote);
+            const thirdPitch = (rootPitch + THIRD) % 12;
+            const fifthPitch = (rootPitch + FIFTH) % 12;
+
+            // For Open G tuning, strings 4-3-2-1 are indices 1,2,3,4 (D,G,B,D)
+            // We use the 4 main strings for chord shapes
+            const mainStrings = tuningStrings.slice(1, 5); // D, G, B, D
+            const stringOpenPitches = mainStrings.map(s => noteToPitchClass(normalizeNote(s)));
+
+            // Find fret for a target pitch on a given string
+            function findFret(stringPitch, targetPitch, startFret = 0) {
+                const diff = (targetPitch - stringPitch + 12) % 12;
+                let fret = diff;
+                if (fret < startFret) fret += 12;
+                return fret;
+            }
+
+            // Three inversion patterns (intervals on strings 4-3-2-1):
+            // Root Form: 1-3-5-1 (root on string 4)
+            // 1st Inversion: 3-5-1-3 (3rd on string 4)
+            // 2nd Inversion: 5-1-3-5 (5th on string 4)
+            const inversionPatterns = [
+                { name: 'Root Form', type: 'root', intervals: [ROOT, THIRD, FIFTH, ROOT], bassNote: 'Root', color: '#3366cc', label: '1 - 3 - 5 - 1' },
+                { name: '1st Inversion', type: 'first', intervals: [THIRD, FIFTH, ROOT, THIRD], bassNote: '3rd', color: '#cc3333', label: '3 - 5 - 1 - 3' },
+                { name: '2nd Inversion', type: 'second', intervals: [FIFTH, ROOT, THIRD, FIFTH], bassNote: '5th', color: '#cc9900', label: '5 - 1 - 3 - 5' }
+            ];
+
+            const inversions = [];
+
+            // Find all occurrences of each inversion pattern
+            for (const pattern of inversionPatterns) {
+                // Calculate target pitches for each string
+                const targetPitches = pattern.intervals.map(interval => (rootPitch + interval) % 12);
+
+                // Find base fret positions (lowest occurrence)
+                const baseFrets = [];
+                for (let i = 0; i < 4; i++) {
+                    baseFrets.push(findFret(stringOpenPitches[i], targetPitches[i], 0));
+                }
+
+                // Find the minimum fret to normalize the shape
+                const minFret = Math.min(...baseFrets.filter(f => f > 0));
+                const maxFretInShape = Math.max(...baseFrets);
+
+                // Find all occurrences within the fret range
+                for (let offset = 0; offset <= maxFret; offset += 12) {
+                    const frets = baseFrets.map(f => f === 0 ? 0 : f + offset - (minFret > 0 ? minFret - baseFrets.find(bf => bf === minFret) : 0));
+                    const adjustedFrets = baseFrets.map(f => {
+                        let adjusted = f + offset;
+                        // Handle open strings (fret 0)
+                        if (f === 0 && offset === 0) return 0;
+                        if (f === 0 && offset > 0) adjusted = 12 + offset;
+                        while (adjusted > maxFret + 5) adjusted -= 12;
+                        return adjusted;
+                    });
+
+                    // Check if this shape is within the displayable range
+                    const shapeFrets = adjustedFrets.filter(f => f >= 0);
+                    if (shapeFrets.length > 0 && Math.min(...shapeFrets) <= maxFret) {
+                        inversions.push({
+                            ...pattern,
+                            frets: adjustedFrets,
+                            baseFret: Math.min(...adjustedFrets.filter(f => f > 0)) || 0
+                        });
+                    }
+                }
+            }
+
+            // Sort by base fret position
+            inversions.sort((a, b) => a.baseFret - b.baseFret);
+
+            return inversions;
+        }
+
+        // Render chord inversions on fretboard SVG
+        function renderInversionsOnFretboard(svg, rootNote, tuning, fbLeft, fbTop, stringSpacing, fretHeight, numFrets, svgWidth) {
+            const inversions = calculateMajorInversions(rootNote, tuning.strings, numFrets);
+            const displayedNote = displayNote(rootNote);
+
+            // Filter inversions that are fully visible
+            const visibleInversions = inversions.filter(inv => {
+                const maxFretInShape = Math.max(...inv.frets);
+                return maxFretInShape <= numFrets && inv.baseFret >= 1;
+            });
+
+            // Render each inversion
+            visibleInversions.forEach((inv, idx) => {
+                // Calculate positions (strings 4-3-2-1 are at indices 1,2,3,4 in the display)
+                const positions = [];
+                for (let i = 0; i < 4; i++) {
+                    const stringIndex = i + 1; // Skip 5th string (index 0)
+                    const x = fbLeft + stringIndex * stringSpacing;
+                    const fret = inv.frets[i];
+                    const y = fbTop + (fret - 0.5) * fretHeight;
+                    positions.push({ x, y, fret, stringIndex });
+                }
+
+                // Draw colored overlay connecting the notes (triangle or polygon)
+                const validPositions = positions.filter(p => p.fret > 0 && p.fret <= numFrets);
+                if (validPositions.length >= 3) {
+                    // Create polygon points
+                    const points = validPositions.map(p => `${p.x},${p.y}`).join(' ');
+                    svg += `<polygon points="${points}" fill="${inv.color}" fill-opacity="0.25" stroke="${inv.color}" stroke-width="2"/>`;
+                }
+
+                // Draw dots on chord positions
+                positions.forEach(pos => {
+                    if (pos.fret > 0 && pos.fret <= numFrets) {
+                        svg += `<circle cx="${pos.x}" cy="${pos.y}" r="12" fill="${inv.color}" fill-opacity="0.8" stroke="white" stroke-width="2"/>`;
+                    }
+                });
+
+                // Annotation box on the right side
+                const annotationX = fbLeft + 5 * stringSpacing + 30;
+                const annotationY = fbTop + (inv.baseFret - 0.5) * fretHeight;
+
+                if (inv.baseFret <= numFrets - 2) {
+                    // Background for annotation
+                    svg += `<rect x="${annotationX}" y="${annotationY - 30}" width="140" height="70" fill="white" stroke="${inv.color}" stroke-width="2" rx="5"/>`;
+
+                    // Chord name and inversion type
+                    svg += `<text x="${annotationX + 70}" y="${annotationY - 12}" text-anchor="middle" font-size="12" font-weight="bold" fill="${inv.color}">"${displayedNote}" CHORD</text>`;
+                    svg += `<text x="${annotationX + 70}" y="${annotationY + 5}" text-anchor="middle" font-size="11" font-weight="bold" fill="#333">${inv.name.toUpperCase()}</text>`;
+
+                    // Bass note info
+                    svg += `<text x="${annotationX + 70}" y="${annotationY + 20}" text-anchor="middle" font-size="10" fill="#666">${inv.bassNote} is bass note</text>`;
+
+                    // Interval structure
+                    svg += `<text x="${annotationX + 70}" y="${annotationY + 35}" text-anchor="middle" font-size="10" fill="#666">[${inv.label}]</text>`;
+                }
+            });
+
+            // Draw 5-fret rule connectors between consecutive inversions
+            for (let i = 0; i < visibleInversions.length - 1; i++) {
+                const current = visibleInversions[i];
+                const next = visibleInversions[i + 1];
+
+                // The rule: 3rd string of current -> 4th string of next (5 frets higher)
+                // String 3 is at index 2 (0-indexed in our positions), String 4 is at index 1
+                const fromStringIdx = 2; // 3rd string (G string)
+                const toStringIdx = 1;   // 4th string (D string)
+
+                const fromX = fbLeft + (fromStringIdx + 1) * stringSpacing;
+                const fromY = fbTop + (current.frets[fromStringIdx] - 0.5) * fretHeight;
+                const toX = fbLeft + (toStringIdx + 1) * stringSpacing;
+                const toY = fbTop + (next.frets[toStringIdx] - 0.5) * fretHeight;
+
+                if (current.frets[fromStringIdx] <= numFrets && next.frets[toStringIdx] <= numFrets) {
+                    // Draw arrow
+                    svg += `<line x1="${fromX}" y1="${fromY}" x2="${toX}" y2="${toY}" stroke="#cc0000" stroke-width="3" marker-end="url(#arrowhead)"/>`;
+
+                    // Label showing "5 frets"
+                    const midX = (fromX + toX) / 2 - 25;
+                    const midY = (fromY + toY) / 2;
+                    svg += `<text x="${midX}" y="${midY}" font-size="10" font-weight="bold" fill="#cc0000">5 frets</text>`;
+                }
+            }
+
+            // Add cycling indicator at bottom
+            if (visibleInversions.length > 0) {
+                const bottomY = fbTop + numFrets * fretHeight + 10;
+                svg += `<text x="${fbLeft + 2.5 * stringSpacing}" y="${bottomY}" text-anchor="middle" font-size="11" fill="#666">↓ Inversions keep cycling ↓</text>`;
+            }
+
+            return svg;
+        }
+
         // ============================================
         // FRETBOARD SVG GENERATOR (Enhanced from Python)
         // ============================================
@@ -2442,8 +2595,13 @@
 
             let svg = `<svg class="fretboard-svg" width="${svgWidth}" height="${svgHeight}" xmlns="http://www.w3.org/2000/svg">`;
 
-            // Styles
+            // Styles and definitions
             svg += `
+            <defs>
+                <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
+                    <polygon points="0 0, 10 3.5, 0 7" fill="#cc0000"/>
+                </marker>
+            </defs>
             <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; }
@@ -2695,6 +2853,11 @@
                 svg += `<text x="${fbLeft + fretboardWidth/2}" y="${capoY + 4}" text-anchor="middle" font-size="10" fill="white" font-weight="bold">CAPO ${capo}</text>`;
             }
 
+            // Chord inversions overlay (if enabled)
+            if (state.showInversions && numStrings >= 5) {
+                svg = renderInversionsOnFretboard(svg, state.inversionRootNote, tuning, fbLeft, fbTop, stringSpacing, fretHeight, numFrets, svgWidth);
+            }
+
             // Footer
             const footerY = fbTop + fretboardHeight + 30;
             svg += `<rect x="${marginLeft-50}" y="${footerY}" width="${fretboardWidth+150}" height="30" rx="5" fill="#f5f5f5" stroke="#ccc"/>`;
@@ -2824,6 +2987,19 @@
                 populateTuningList();
             });
 
+            // Chord inversion controls
+            document.getElementById('showInversionsToggle').addEventListener('change', (e) => {
+                state.showInversions = e.target.checked;
+                saveState();
+                renderFretboard();
+            });
+
+            document.getElementById('inversionRootNote').addEventListener('change', (e) => {
+                state.inversionRootNote = e.target.value;
+                saveState();
+                renderFretboard();
+            });
+
             // Fretboard family filter
             document.getElementById('fretboardFamilyFilter').addEventListener('change', (e) => {
                 state.fretboardFamilyFilter = e.target.value;
@@ -2851,6 +3027,8 @@
                 selectedFretboardTuning: 'Open G',
                 fretboardFamilyFilter: 'all',
                 useSolfege: false,
+                showInversions: false,
+                inversionRootNote: 'G',
                 chordTuning: 'Open G',
                 chordKey: 'G',
                 chordTypeFilter: 'all',
@@ -2872,6 +3050,8 @@
             document.getElementById('showNotesToggle').checked = state.showNotes;
             document.getElementById('solfegeToggle').checked = state.useSolfege || false;
             document.getElementById('fretboardFamilyFilter').value = state.fretboardFamilyFilter || 'all';
+            document.getElementById('showInversionsToggle').checked = state.showInversions || false;
+            document.getElementById('inversionRootNote').value = state.inversionRootNote || 'G';
 
             // Chord tab UI
             const chordTuningSelect = document.getElementById('chordTuningSelect');