Skip to content

Commit 72e65c8

Browse files
committed
draw and implement scale notes on the fretboard
1 parent 7267866 commit 72e65c8

8 files changed

Lines changed: 329 additions & 146 deletions

File tree

src/lib/FretBoard.svelte

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,25 @@
4949
class="stroke-gray-100"
5050
stroke-width={lineW(i)} />
5151
{#each notes as stringNote}
52-
<g transform="translate({fretX(stringNote.fret)}, 0)">
52+
<g
53+
data-interval={stringNote.interval}
54+
data-in-scale={!!stringNote.interval}
55+
data-fret={stringNote.fret}
56+
class:in-scale={!!stringNote.interval}
57+
transform="translate({fretX(stringNote.fret)}, 0)"
58+
class="fret-note">
5359
<g
5460
transform="translate({fretX.bandwidth() / 2 - margin.left}, 0)">
5561
{#if stringNote.fret > 0}
56-
<circle
57-
dx="0"
58-
r="12"
59-
class="fill-amber-300 stroke-amber-500 stroke-1" />
62+
<circle class="fret-note-background" dx="0" r="12" />
6063
{/if}
6164

6265
<text
6366
dy="1"
6467
font-size="10"
6568
text-anchor="middle"
66-
class="fill-amber-900"
67-
dominant-baseline="middle"
68-
>{Note.simplify(stringNote.note.pc)}</text>
69+
class="fret-note-text"
70+
dominant-baseline="middle">{stringNote.label}</text>
6971
</g>
7072
</g>
7173
{/each}
@@ -115,4 +117,28 @@
115117
.fret-line {
116118
@apply fill-gray-400 stroke-0;
117119
}
120+
121+
.fret-note-background {
122+
@apply fill-white;
123+
}
124+
125+
.fret-note-label {
126+
@apply text-sm fill-gray-800;
127+
}
128+
129+
.fret-note[data-fret="0"].in-scale .fret-note-text {
130+
@apply fill-green-500;
131+
}
132+
133+
.in-scale .fret-note-background {
134+
@apply fill-green-600;
135+
}
136+
137+
.in-scale .fret-note-text {
138+
@apply fill-white font-bold;
139+
}
140+
141+
.in-scale[data-interval="1P"] circle {
142+
@apply fill-green-800 stroke-emerald-900;
143+
}
118144
</style>

src/lib/KeySelector.svelte

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,12 @@
11
<script>
2-
import { tonic } from "./store";
3-
export let key;
4-
5-
let note = key;
6-
let modifier = null;
7-
8-
const notes = ["C", "D", "E", "F", "G", "A", "B"];
9-
10-
$: key = [note, modifier].join("");
11-
$: tonic.set(key);
12-
13-
function toggleEnabled(event) {
14-
if (modifier === event.target.value) modifier = null;
15-
}
2+
let { value = $bindable() } = $props();
3+
let notes = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
164
</script>
175

18-
<div class="flex items-center space-x-5">
19-
<div>
20-
<select name="note" bind:value={note} class=" border-gray-300">
21-
{#each notes as note}
22-
<option value={note}>{note}</option>
23-
{/each}
24-
</select>
25-
</div>
26-
<div class="flex items-center">
27-
<input
28-
type="radio"
29-
bind:group={modifier}
30-
on:click={toggleEnabled}
31-
value="b"
32-
class="mr-1" />
33-
<span class="text-2xl">♭</span>
34-
</div>
35-
<div class="flex items-center">
36-
<input
37-
type="radio"
38-
bind:group={modifier}
39-
on:click={toggleEnabled}
40-
value="#"
41-
class="mr-1" />
42-
<span>#</span>
43-
</div>
44-
</div>
6+
<select bind:value class="border-gray-300 w-full">
7+
{#each notes as note}
8+
<option value={note}>
9+
{note}
10+
</option>
11+
{/each}
12+
</select>

src/lib/KeySelectorLegacy.svelte

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script>
2+
import { tonic } from "./store";
3+
export let key;
4+
5+
let note = key;
6+
let modifier = null;
7+
8+
const notes = ["C", "D", "E", "F", "G", "A", "B"];
9+
10+
$: key = [note, modifier].join("");
11+
$: tonic.set(key);
12+
13+
function toggleEnabled(event) {
14+
if (modifier === event.target.value) modifier = null;
15+
}
16+
</script>
17+
18+
<div class="flex items-center space-x-5">
19+
<div>
20+
<select name="note" bind:value={note} class=" border-gray-300">
21+
{#each notes as note}
22+
<option value={note}>{note}</option>
23+
{/each}
24+
</select>
25+
</div>
26+
<div class="flex items-center">
27+
<input
28+
type="radio"
29+
bind:group={modifier}
30+
on:click={toggleEnabled}
31+
value="b"
32+
class="mr-1" />
33+
<span class="text-2xl">♭</span>
34+
</div>
35+
<div class="flex items-center">
36+
<input
37+
type="radio"
38+
bind:group={modifier}
39+
on:click={toggleEnabled}
40+
value="#"
41+
class="mr-1" />
42+
<span>#</span>
43+
</div>
44+
</div>

src/lib/ScaleSelector.svelte

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,32 @@
11
<script>
2-
import Select from "svelte-select";
3-
import { ScaleType } from "tonal";
2+
let { value = $bindable() } = $props();
43
5-
export let value;
6-
7-
const allScales = ScaleType.all();
8-
const commonScales = [
4+
let scales = [
95
"major",
106
"minor",
117
"major pentatonic",
128
"minor pentatonic",
9+
"dorian",
1310
"phrygian",
1411
"lydian",
1512
"mixolydian",
1613
"locrian",
17-
"dorian",
1814
"blues",
1915
"harmonic minor",
20-
"phrygian dominant",
21-
].map((name) => {
22-
return { group: "Popular", ...ScaleType.get(name) };
23-
});
24-
25-
const otherScales = allScales
26-
.filter(
27-
(a) =>
28-
!commonScales.find((b) => {
29-
return b.setNum == a.setNum;
30-
}),
31-
)
32-
.map((s) => {
33-
return { ...s, group: "Other" };
34-
});
35-
36-
const scales = [...commonScales, ...otherScales].map((s) => {
37-
return {
38-
label: `${s.name}${s.aliases.length ? " (" + s.aliases[0] + ")" : ""}`,
39-
value: s.name,
40-
group: s.group,
41-
};
42-
});
43-
44-
const groupBy = (s) => s.group;
45-
46-
function handleSelect(event) {
47-
value = event.detail.value;
48-
}
49-
50-
function handleClear() {
51-
value = "";
52-
}
53-
54-
$: selectedItem = scales.find((s) => s.value === value);
16+
"melodic minor",
17+
];
5518
</script>
5619

57-
<div class="themed w-full">
58-
<Select
59-
items={scales}
60-
placeholder="Select a scale..."
61-
on:select={handleSelect}
62-
on:clear={handleClear}
63-
containerClasses="text-sm"
64-
containerStyles="text-transform: capitalize"
65-
value={selectedItem}
66-
{groupBy}
67-
/>
68-
</div>
20+
<select bind:value class="border-gray-300 w-full">
21+
{#each scales as scale}
22+
<option value={scale}>
23+
{scale}
24+
</option>
25+
{/each}
26+
</select>
6927

7028
<style>
71-
.themed {
72-
--border: 1px solid rgb(209 213 219);
73-
--borderRadius: 0.5rem;
74-
--placeholderColor: rgb(107 114 128);
75-
--height: 2.75rem;
29+
select {
30+
text-transform: capitalize;
7631
}
7732
</style>

src/lib/ScaleSelectorLegacy.svelte

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<script>
2+
import Select from "svelte-select";
3+
import { ScaleType } from "tonal";
4+
5+
export let value;
6+
7+
const allScales = ScaleType.all();
8+
const commonScales = [
9+
"major",
10+
"minor",
11+
"major pentatonic",
12+
"minor pentatonic",
13+
"phrygian",
14+
"lydian",
15+
"mixolydian",
16+
"locrian",
17+
"dorian",
18+
"blues",
19+
"harmonic minor",
20+
"phrygian dominant",
21+
].map((name) => {
22+
return { group: "Popular", ...ScaleType.get(name) };
23+
});
24+
25+
const otherScales = allScales
26+
.filter(
27+
(a) =>
28+
!commonScales.find((b) => {
29+
return b.setNum == a.setNum;
30+
}),
31+
)
32+
.map((s) => {
33+
return { ...s, group: "Other" };
34+
});
35+
36+
const scales = [...commonScales, ...otherScales].map((s) => {
37+
return {
38+
label: `${s.name}${s.aliases.length ? " (" + s.aliases[0] + ")" : ""}`,
39+
value: s.name,
40+
group: s.group,
41+
};
42+
});
43+
44+
const groupBy = (s) => s.group;
45+
46+
function handleSelect(event) {
47+
value = event.detail.value;
48+
}
49+
50+
function handleClear() {
51+
value = "";
52+
}
53+
54+
$: selectedItem = scales.find((s) => s.value === value);
55+
</script>
56+
57+
<div class="themed w-full">
58+
<Select
59+
items={scales}
60+
placeholder="Select a scale..."
61+
on:select={handleSelect}
62+
on:clear={handleClear}
63+
containerClasses="text-sm"
64+
containerStyles="text-transform: capitalize"
65+
value={selectedItem}
66+
{groupBy}
67+
/>
68+
</div>
69+
70+
<style>
71+
.themed {
72+
--border: 1px solid rgb(209 213 219);
73+
--borderRadius: 0.5rem;
74+
--placeholderColor: rgb(107 114 128);
75+
--height: 2.75rem;
76+
}
77+
</style>

src/lib/frets/index.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1-
import { Interval, Note } from "tonal";
1+
import { Interval, Note, Scale } from "tonal";
2+
3+
function getScaleNoteIntervalMap(scale) {
4+
return scale.notes.reduce((map, note, index) => {
5+
map[note] = scale.intervals[index];
6+
return map;
7+
}, {});
8+
}
9+
10+
// Create a map of note chromas to scale note names for label normalization
11+
function getScaleNoteLabelMap(scale) {
12+
return scale.notes.reduce((map, note) => {
13+
const noteObj = Note.get(note);
14+
map[noteObj.chroma] = note;
15+
return map;
16+
}, {});
17+
}
218

319
export default function frets(
420
tuning = ["E2", "A2", "D3", "G3", "B3", "E4"],
521
count = 13,
22+
scale,
623
reverse = true
724
) {
25+
const scaleObj = Scale.get(scale);
826
const processedTuning = reverse ? [...tuning].reverse() : tuning;
27+
const intervals = getScaleNoteIntervalMap(scaleObj);
28+
const scaleNoteLabelMap = getScaleNoteLabelMap(scaleObj);
929

1030
const strings = processedTuning.map((string, stringIndex) => {
1131
const notes = [];
1232
for (let fret = 0; fret < count; fret++) {
13-
const note = Note.transpose(string, Interval.fromSemitones(fret));
33+
const noteVal = Note.transpose(string, Interval.fromSemitones(fret));
34+
const noteObject = Note.get(noteVal);
35+
36+
// Use the scale's note name if this chroma is in the scale
37+
const label = scaleNoteLabelMap[noteObject.chroma] || noteObject.pc;
38+
1439
notes.push({
15-
note: Note.get(note),
40+
note: noteObject,
1641
string: stringIndex,
17-
fret
42+
interval: intervals[label] || null,
43+
label,
44+
fret,
1845
});
1946
}
2047
return notes;

0 commit comments

Comments
 (0)