summaryrefslogtreecommitdiffstats
path: root/key-picker.mjs
blob: fe344ae8fe1a034f7e379ce91364267cde788027 (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
import { MajorScale, MinorScale, Note, chromaticScale } from "./scale.mjs";

function scaleFrom(tonic, scale) {
    switch (scale) {
    case 'major':
        return MajorScale(tonic);
    case 'minor':
        return MinorScale(tonic);
    default:
        throw new Error('how this happen')
    }
}

function handleNoteEnter(e) {
    const elt = e.target;
    const n = Note.fromString(elt.innerText).toString();
    // todo: this should be delegated. the key selector shouldn't know
    // about the fretboard at all.
    elt.classList.add('hover');
    document.querySelectorAll(`#fretboard [x-data-note="${n}"]`).forEach(elt => {
        elt.classList.add('hover');
    })
}

function handleNoteLeave(e) {
    const elt = e.target;
    const n = Note.fromString(elt.innerText).toString();
    // ibid.
    elt.classList.remove('hover');
    document.querySelectorAll(`#fretboard [x-data-note="${n}"]`).forEach(elt => {
        elt.classList.remove('hover');
    })
}

function noteClicked(elt, justClear=false) {
    const n = Note.fromString(elt.innerText).toString();
    const count = Number(elt.getAttribute('x-data-clicked')) || 0;
    const next = (count + 1) % 4;
    elt.classList.remove(`click${count}`);
    if (!justClear) {
        elt.classList.add(`click${next}`);
    }
    // ibid.
    document.querySelectorAll(`#fretboard [x-data-note="${n}"]`).forEach(elt => {
        elt.classList.remove(`click${count}`);
        if (!justClear) {
            elt.classList.add(`click${next}`);
        }
    })
    if (justClear) {
        elt.setAttribute('x-data-clicked', '0');
    } else {
        elt.setAttribute('x-data-clicked', next.toString());
    }
}

function handleNoteClick(e) {
    noteClicked(e.target, false);
}

function formChanged(form) {
    // clear the selection first, since it relies on state in the dom.
    form.querySelectorAll('.notes li').forEach(elt => {
        noteClicked(elt, true);
    });

    const formData = new FormData(form);
    const scale = scaleFrom(formData.get('tonic'), formData.get('scale'));
    ['tonic', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh'].forEach((c, i) => {
        Array.from(form.getElementsByClassName(c)).forEach(elt => elt.innerText = scale[i]);
    });

    // todo: memoize this or put it in the scales module.
    const allScales = chromaticScale.flatMap(tonic => {
        return [MajorScale(tonic), MinorScale(tonic)];
    })

    const availableScales =
        allScales.reduce((acc, s, i) => {
            const suffix = i % 2 == 0 ? '' : 'm';
            if (scale.includes(s.tonic) && scale.includes(s.third) && scale.includes(s.fifth)) {
                return acc.concat(`${s.tonic}${suffix}`);
            }
            return acc;
        }, []);
    Array.from(form.getElementsByClassName('chords')).forEach(list => {
        const items = availableScales.map(s => {
            const elt = document.createElement('li');
            elt.innerText = s;
            return elt;
        });
        list.replaceChildren();
        items.forEach(item => list.appendChild(item));
    });
}

function handleFormChanged(e) {
    formChanged(e.target.form);
}

export default function KeyPicker(form) {
    console.debug('KeyPicker()', form);
    form.onchange = handleFormChanged;
    form.querySelectorAll('.notes li').forEach(elt => {
        elt.onmouseenter = handleNoteEnter;
        elt.onmouseleave = handleNoteLeave;
        elt.onclick = handleNoteClick;
    });
    formChanged(form);
}