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);
}
|