1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
import { chromaticScale } from "./scale.mjs";
// open string notes, starting from the deepest string.
const strings = {
string1: 'E',
string2: 'A',
string3: 'D',
string4: 'G',
string5: 'B',
string6: 'E'
};
// true if not a natural note
function isAccidental(note) {
return note[1] === '#' || note[1] === 'b';
}
// convert ‘Eb’ to ‘D#’ and vice versa.
function alternateAccidental(note) {
const root = chromaticScale.indexOf(note[0]);
switch (note[1]) {
case '#':
return `${chromaticScale[root+2]}b`;
case 'b':
return `${chromaticScale[root-1]}#`;
default:
return note;
}
}
// convert ‘fret2’ to Number(2)
function fretToNote(stringName, fretName) {
const string = strings[stringName];
if (!string) {
return null;
}
if (!fretName?.startsWith('fret')) {
return string;
}
const root = chromaticScale.indexOf(string)
const fret = Number(fretName.substring(4));
return chromaticScale[root+fret];
}
function formChanged(form) {
const formData = new FormData(form);
form.querySelectorAll('tbody .selected').forEach(elt => {
const val = formData.get(elt.parentNode.className)
const note = fretToNote(elt.parentNode.className, val) || '';
if (isAccidental(note)) {
elt.innerText = `${note} / ${alternateAccidental(note)}`;
} else {
elt.innerText = note;
}
});
}
function handleFormChanged(e) {
formChanged(e.target.form);
}
let mousedownVal = undefined;
function fretMousedown(e) {
// at mousedown time, the form hasn't yet been changed. to support
// deselecting elements, we need to know what the previous
// selection was, so store it here.
//
// it'd be nice to be able to do this just in the click handler.
mousedownVal = e.target.checked;
}
function fretClick(e) {
const elt = e.target;
// if this element was selected at mousedown time, deselect it
// now.
if (mousedownVal) {
elt.checked = false;
// doesn't get called automatically.
formChanged(elt.form);
}
}
function openClick(e) {
const elt = e.target.parentNode;
if (elt.classList.contains('muted')) {
elt.classList.remove('muted');
elt.querySelectorAll('input[type="radio"]').forEach(input => {
input.disabled = false;
})
} else {
elt.classList.add('muted');
elt.querySelectorAll('input[type="radio"]').forEach(input => {
input.checked = false;
input.disabled = true;
})
}
formChanged(elt.closest('form'));
}
export default function Fretboard(form) {
console.debug('Fretboard()', form);
form.onchange = handleFormChanged;
form.querySelectorAll('.open').forEach(elt => {
elt.onclick = openClick;
});
form.querySelectorAll('input[type="radio"]').forEach(elt => {
elt.onmousedown = fretMousedown;
elt.onclick = fretClick;
});
formChanged(form);
}
|