← banjotuna

Add multiple visual enhancements and print improvements

3cfb3334d78c7bf04e9de929b90197d1d7868ba1 · Claude · Feb 2, 2026 07:40

Print improvements for chord grid:
- Landscape orientation for chord grid printing
- Smart page break at F#/Gb key transition
- Table headers repeat on each printed page
- Keep key rows together

Fretboard visual enhancements:
- 8-ball styling (thick border) for natural notes (C,D,E,F,G,A,B)
- Display effective tuning when capo is set
- Calculate transposed notes based on capo position

Chord inversion display:
- Detect chord inversion based on bass note degree
- Color-coded indicators: red for 1st inversion, yellow for 2nd
- Inversion labels shown below chord diagrams
- Pattern fills defined for B&W print compatibility

https://claude.ai/code/session_01VSHtwaMJKL1ZgH33JDcwbY

+141 −11 · 1 file changed

  • index.html +141 −11
diff --git a/index.html b/index.html
index 3025910..ee65d54 100644
--- a/index.html
+++ b/index.html
@@ -46,6 +46,12 @@
             margin: 12mm;
         }
 
+        /* Landscape orientation for chord grid view */
+        @page chord-grid-landscape {
+            size: A4 landscape;
+            margin: 10mm;
+        }
+
         @media print {
             body {
                 background: white;
@@ -63,6 +69,10 @@
                 page-break-after: always;
             }
 
+            .page-break-before {
+                page-break-before: always;
+            }
+
             .tuning-card {
                 break-inside: avoid;
             }
@@ -75,6 +85,30 @@
             .tunings-grid {
                 grid-template-columns: 1fr !important;
             }
+
+            /* Chord grid landscape printing */
+            .chord-keys-grid-container {
+                page: chord-grid-landscape;
+            }
+
+            /* Table header repetition on each page */
+            .chord-keys-grid thead {
+                display: table-header-group;
+            }
+
+            .chord-keys-grid tbody {
+                display: table-row-group;
+            }
+
+            /* Force page break before F#/Gb row */
+            .chord-keys-grid tr.page-break-before {
+                page-break-before: always;
+            }
+
+            /* Keep key rows together */
+            .chord-keys-grid tr {
+                page-break-inside: avoid;
+            }
         }
 
         .print-only {
@@ -1423,6 +1457,12 @@
             return NOTE_MAP[note] || note.toUpperCase();
         }
 
+        // Check if a note is natural (no sharps or flats)
+        function isNaturalNote(note) {
+            const normalized = normalizeNote(note);
+            return ['C', 'D', 'E', 'F', 'G', 'A', 'B'].includes(normalized);
+        }
+
         function displayNote(note) {
             // Convert internal sharp note to preferred display (mix of flats and sharps)
             if (DISPLAY_NOTE_MAP[note]) {
@@ -1712,14 +1752,40 @@
             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 = 200;
+            const height = 220;  // Increased height for inversion label
             const marginLeft = 35;
             const marginTop = 35;
             const marginRight = 15;
-            const marginBottom = 25;
+            const marginBottom = 45;  // More space for inversion label
 
             const positions = voicing.positions;
             const numStrings = 4; // Always 4 main strings for chord diagrams
@@ -1729,11 +1795,30 @@
             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">`;
 
-            // Background
+            // 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
             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"/>`;
@@ -1828,6 +1913,13 @@
                 }
             });
 
+            // 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;
         }
@@ -2329,7 +2421,7 @@
             // SVG dimensions
             const marginLeft = 100;
             const marginRight = 150;
-            const marginTop = 140;
+            const marginTop = capo > 0 ? 160 : 140;  // Extra space for effective tuning display
             const marginBottom = 80;
             const stringSpacing = 55;
             const fretHeight = 50;
@@ -2359,6 +2451,7 @@
                 .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; }
+                .note-circle-natural { fill: white; stroke: #000; stroke-width: 4; }
                 .fret-line { stroke: #000; stroke-width: 2; }
                 .string-line { stroke: #444; stroke-width: 1.5; }
                 .nut { stroke: #000; stroke-width: 6; }
@@ -2381,8 +2474,39 @@
                 // Use lowercase if this index is marked as first occurrence of a duplicate
                 return lowercaseIndices.has(idx) ? note.toLowerCase() : note;
             }).join('');
+
+            // Calculate effective tuning with capo
+            let effectiveTuningStr = '';
+            if (capo > 0) {
+                effectiveTuningStr = strings.map((s, idx) => {
+                    // For short string, calculate from its actual starting point
+                    let effectiveCapo = capo;
+                    if (idx === shortStringIdx && shortStringStart !== null) {
+                        // Short string: capo only affects it if capo >= shortStringStart
+                        if (capo < shortStringStart) {
+                            effectiveCapo = 0; // Capo doesn't affect short string
+                        } else {
+                            effectiveCapo = capo - (shortStringStart - 1);
+                        }
+                    }
+                    const transposedNote = getNoteAtFret(s, effectiveCapo);
+                    let note = displayNote(normalizeNote(transposedNote));
+                    if (state.useSolfege) {
+                        note = toSolfege(note);
+                    }
+                    return lowercaseIndices.has(idx) ? note.toLowerCase() : note;
+                }).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>`;
+
+            // Subtitle with effective tuning when capo is applied
+            if (capo > 0) {
+                svg += `<text x="${svgWidth / 2}" y="70" class="subtitle" text-anchor="middle">Open Tuning: ${tuningStr} — ${tuning.description}</text>`;
+                svg += `<text x="${svgWidth / 2}" y="90" class="subtitle" text-anchor="middle" style="fill: #c00; font-weight: bold;">Capo ${capo}: Effective Tuning: ${effectiveTuningStr}</text>`;
+            } else {
+                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) {
@@ -2539,21 +2663,25 @@
                             const note = getNoteAtFret(openNote, effectiveFret);
                             const displayedNote = formatSimpleNote(note);
 
-                            // Gray out notes behind capo
+                            // Gray out notes behind capo, use 8-ball style for natural notes
+                            const isNatural = isNaturalNote(note);
+                            const circleClass = isNatural ? 'note-circle-natural' : 'note-circle';
                             const circleStyle = isBehindCapo ? 'fill: #e0e0e0; stroke: #bbb;' : '';
                             const textStyle = isBehindCapo ? 'fill: #aaa;' : '';
 
-                            svg += `<circle cx="${x}" cy="${y}" r="16" class="note-circle" style="${circleStyle}"/>`;
+                            svg += `<circle cx="${x}" cy="${y}" r="16" class="${circleClass}" style="${circleStyle}"/>`;
                             svg += `<text x="${x}" y="${y + 5}" class="note" text-anchor="middle" style="${textStyle}">${displayedNote}</text>`;
                         } else {
                             const note = getNoteAtFret(openNote, fret);
                             const displayedNote = formatSimpleNote(note);
 
-                            // Gray out notes behind capo
+                            // Gray out notes behind capo, use 8-ball style for natural notes
+                            const isNatural = isNaturalNote(note);
+                            const circleClass = isNatural ? 'note-circle-natural' : 'note-circle';
                             const circleStyle = isBehindCapo ? 'fill: #e0e0e0; stroke: #bbb;' : '';
                             const textStyle = isBehindCapo ? 'fill: #aaa;' : '';
 
-                            svg += `<circle cx="${x}" cy="${y}" r="16" class="note-circle" style="${circleStyle}"/>`;
+                            svg += `<circle cx="${x}" cy="${y}" r="16" class="${circleClass}" style="${circleStyle}"/>`;
                             svg += `<text x="${x}" y="${y + 5}" class="note" text-anchor="middle" style="${textStyle}">${displayedNote}</text>`;
                         }
                     }
@@ -2960,8 +3088,10 @@
 
             // Build table rows for each key
             let rowsHtml = '';
-            CHORD_KEYS.forEach(key => {
-                rowsHtml += `<tr><td class="key-cell">${key.display}</td>`;
+            CHORD_KEYS.forEach((key, keyIndex) => {
+                // Add page break class before F#/Gb (index 6) for print
+                const pageBreakClass = key.root === 'F#' ? ' class="page-break-before"' : '';
+                rowsHtml += `<tr${pageBreakClass}><td class="key-cell">${key.display}</td>`;
 
                 typesToShow.forEach(chordType => {
                     const type = CHORD_TYPES[chordType];