Skip to content

Commit 41ebea0

Browse files
cagedclaude
andcommitted
fix(chords): keep retuned shapes inside the diagram, carry tuning in links
Addresses review feedback on #23. Retuning could stretch a shape past the four frets Chord.svelte draws, so fingers rendered off the end of the neck. Uneven string shifts pull a grip apart, and a voicing that leaned on an open string high up the neck has to fret it once the tuning drops - chords-db's open G spans frets 2-7 in Drop C, and a C sus voicing spans 1-9 in Half Step Down. Those are unplayable as a single grip rather than merely badly framed, so drop them the way shapes running off the end of the neck are already dropped. Standard tuning is unaffected (0 of 2818 positions). Half Step Down loses 8.3%, Drop D 7.9%, Drop C 15.5% - all redundant alternates, leaving no chord in any tuning without a diagram. FRET_WINDOW is exported and Chord.svelte takes its frets default from it, so the window the chart draws cannot drift from the one shapes are fitted into. Chord links also dropped the selected tuning, so clicking a Drop D chord landed on the chord page in Standard and drew different shapes than the ones just played. Thread the tuning name through ScaleChords to Chord and encode it into the href. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1651071 commit 41ebea0

6 files changed

Lines changed: 71 additions & 8 deletions

File tree

src/frets/chordFingerings.enharmonics.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
22
import { Mode, Note, Scale } from "tonal";
33
import {
44
CHORDS_DB_TUNING,
5+
FRET_WINDOW,
56
getChordVariations,
67
} from "./chordFingerings.js";
78

@@ -200,6 +201,43 @@ describe("tuning-aware fingerings", () => {
200201
}
201202
});
202203

204+
// Retuning can stretch a shape past the window Chord.svelte draws, which
205+
// renders fingers off the end of the neck. Worst observed before the fix was
206+
// a C sus voicing spanning frets 1-9 in Half Step Down, because the open G
207+
// string it leaned on had to be fretted once the tuning dropped.
208+
it("never emits a fret past the diagram window", () => {
209+
for (const [, tuning] of Object.entries(TUNINGS)) {
210+
for (const [, , chordName] of allTriads()) {
211+
for (const position of getChordVariations(chordName, tuning)
212+
.positions) {
213+
for (const [, fret] of position.fingers) {
214+
if (typeof fret === "number") {
215+
expect(fret).toBeLessThanOrEqual(FRET_WINDOW);
216+
}
217+
}
218+
}
219+
}
220+
}
221+
});
222+
223+
it("keeps every retuned shape within a hand's span", () => {
224+
// Open strings ride above the nut and cost no reach, so only fretted
225+
// notes count toward the span.
226+
for (const [, tuning] of Object.entries(TUNINGS)) {
227+
for (const [, , chordName] of allTriads()) {
228+
for (const position of getChordVariations(chordName, tuning)
229+
.positions) {
230+
const fretted = position.fingers
231+
.map(([, fret]) => fret)
232+
.filter((fret) => typeof fret === "number" && fret > 0);
233+
if (fretted.length < 2) continue;
234+
const span = Math.max(...fretted) - Math.min(...fretted) + 1;
235+
expect(span).toBeLessThanOrEqual(FRET_WINDOW);
236+
}
237+
}
238+
}
239+
});
240+
203241
// Seven-string support is a known gap: convertToSVGuitarFormat hardcodes a
204242
// six-string reversal (6 - i). Rather than emit wrong shapes we fall back to
205243
// the standard-tuning fingerings.

src/frets/chordFingerings.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ const CHROMA_TO_DB_KEY = [
3535
/** Highest fret we consider a fingering playable at. */
3636
const MAX_FRET = 24;
3737

38+
/**
39+
* How many frets a chord diagram spans. Exported so `Chord.svelte` draws the
40+
* same window that `retunePosition` fits shapes into — if the two drift, the
41+
* chart renders fingers past the end of the neck it drew.
42+
*/
43+
export const FRET_WINDOW = 4;
44+
3845
/**
3946
* Get fingering positions for a chord using the chords-db library
4047
*
@@ -195,7 +202,17 @@ function retunePosition(position, offsets, tuning) {
195202
const fretted = absolute.filter((fret) => fret > 0);
196203
const highest = fretted.length ? Math.max(...fretted) : 0;
197204
const lowest = fretted.length ? Math.min(...fretted) : 0;
198-
const newBaseFret = highest <= 4 || !fretted.length ? 1 : lowest;
205+
206+
// Retuning can stretch a shape past the diagram. Uneven string shifts pull
207+
// the grip apart, and a voicing that leaned on an open string high up the
208+
// neck now has to fret it — chords-db's open G in Drop C spans frets 2-7,
209+
// and C sus voicings can span nine. Those are unplayable as a single grip,
210+
// not merely badly framed, so drop them the way out-of-range shapes are
211+
// dropped above. Open strings ride above the nut and cost no reach, so only
212+
// fretted notes count toward the span.
213+
if (fretted.length && highest - lowest + 1 > FRET_WINDOW) return null;
214+
215+
const newBaseFret = highest <= FRET_WINDOW || !fretted.length ? 1 : lowest;
199216

200217
const toRelative = (fret) => (fret <= 0 ? fret : fret - newBaseFret + 1);
201218

src/lib/Chord.svelte

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
import { getChordVariations } from "../frets/chordFingerings.js";
2+
import { getChordVariations, FRET_WINDOW } from "../frets/chordFingerings.js";
33
import { tunings } from "$lib";
44
import { getContext, onMount } from "svelte";
55
import { SVGuitarChord } from "svguitar";
@@ -8,7 +8,8 @@
88
chordName,
99
position = 0,
1010
tuning = tunings.get("Standard"),
11-
frets = 4,
11+
tuningName = null,
12+
frets = FRET_WINDOW,
1213
} = $props();
1314
1415
const { player } = getContext("app");
@@ -23,6 +24,13 @@
2324
const variations = $derived(getChordVariations(chordName, tuning));
2425
const chordData = $derived(variations?.positions?.[position] ?? null);
2526
27+
// Carry the tuning into the link, or the chord page falls back to Standard
28+
// and shows different shapes than the ones just clicked.
29+
const chordHref = $derived(
30+
`/chords/${encodeURIComponent(chordName)}` +
31+
(tuningName ? `?tuning=${encodeURIComponent(tuningName)}` : ""),
32+
);
33+
2634
// Colors based on color scheme
2735
const chordColor = $derived(isDarkMode ? "#e5e7eb" : "#333");
2836
@@ -90,7 +98,7 @@
9098
bind:clientHeight={height}
9199
bind:this={el}
92100
data-chord={chordName}
93-
href="/chords/{encodeURIComponent(chordName)}"
101+
href={chordHref}
94102
title="View all positions for {chordName}"
95103
class="w-full h-full flex">
96104
</a>

src/lib/ScaleChords.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Mode } from "tonal";
33
import Chord from "$lib/Chord.svelte";
44
5-
let { scale, tuning } = $props();
5+
let { scale, tuning, tuningName = null } = $props();
66
77
// Chord names are passed through as Mode.triads spells them — enharmonic
88
// normalisation happens in the chords-db lookup, so Cb/Fb/E#/B# and the
@@ -23,7 +23,7 @@
2323
{#each chords as chord}
2424
<div
2525
class="bg-gray-50 border border-transparent hover:bg-gray-100 hover:border-gray-200 transition-all dark:bg-blue-900/20 dark:border dark:border-blue-900/50 dark:hover:bg-blue-900/40 rounded">
26-
<Chord chordName={chord} {tuning} />
26+
<Chord chordName={chord} {tuning} {tuningName} />
2727
</div>
2828
{/each}
2929
</div>

src/routes/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,6 @@
116116
117117
{#if triads.length > 0}
118118
<div class="relative px-5 dark:border-blue-900">
119-
<ScaleChords scale={scaleObj} tuning={tunings.get(tuning)} />
119+
<ScaleChords scale={scaleObj} tuning={tunings.get(tuning)} tuningName={tuning} />
120120
</div>
121121
{/if}

src/routes/chords/[chord]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<div class="absolute top-2 right-2 text-xs text-blue-500">
4141
Position {idx + 1}
4242
</div>
43-
<Chord {chordName} position={idx} tuning={tuningObj} />
43+
<Chord {chordName} position={idx} tuning={tuningObj} tuningName={tuning} />
4444
</div>
4545
{/each}
4646
</div>

0 commit comments

Comments
 (0)