summaryrefslogtreecommitdiffstats
path: root/fretboard.mjs
blob: 4f5344e5f7e183d43b80caad7beee50b5e3f1f6c (plain)
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] === '♭';
}

// convert ‘E♭’ to ‘D♯’ and vice versa.
function alternateAccidental(note) {
    const root = chromaticScale.indexOf(note[0]);
    switch (note[1]) {
    case '♯':
        return `${chromaticScale[root+2]}♭`;
    case '♭':
        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);
}