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