diff options
| author | Brian Cully <bjc@spork.org> | 2025-07-26 14:55:53 -0400 |
|---|---|---|
| committer | Brian Cully <bjc@spork.org> | 2025-07-26 14:55:53 -0400 |
| commit | 2a9b6ef0be04b5b5cacc47922e19c05d190d6ade (patch) | |
| tree | e07e9648891b065cb8c12cea563386a9b92264c7 | |
| parent | 18b63657328a619a638688e5a85c9c1da2b196c1 (diff) | |
| download | chords-2a9b6ef0be04b5b5cacc47922e19c05d190d6ade.tar.gz chords-2a9b6ef0be04b5b5cacc47922e19c05d190d6ade.zip | |
first stab at fretboard player.
need to handle actual chord positions so everything isn't in 4, but
otherwise works?
| -rw-r--r-- | fretboard.mjs | 38 | ||||
| -rw-r--r-- | history.mjs | 2 | ||||
| -rw-r--r-- | index.html | 3 | ||||
| -rw-r--r-- | key-picker.mjs | 11 | ||||
| -rw-r--r-- | main.mjs | 29 | ||||
| -rw-r--r-- | scale.mjs | 30 | ||||
| -rw-r--r-- | string.mjs | 53 |
7 files changed, 93 insertions, 73 deletions
diff --git a/fretboard.mjs b/fretboard.mjs index 7eba9f5..8a7fe36 100644 --- a/fretboard.mjs +++ b/fretboard.mjs @@ -28,6 +28,10 @@ export default class extends HTMLElement { } saveEvent = 'x-fretboard-save'; + playEvent = 'x-fretboard-play'; + stopEvent = 'x-fretboard-stop'; + + isPlaying = false; constructor() { super(); @@ -43,30 +47,38 @@ export default class extends HTMLElement { connectedCallback() { console.debug('Fretboard#connectedCallback', this); - this.querySelectorAll('.save').forEach(elt => { - elt.onclick = (e) => { - console.log('Fretboard#onSave'); + + const postEvent = (elt, eventName) => { + const res = (e) => { e.preventDefault(); - const event = new CustomEvent(this.saveEvent, { + const event = new CustomEvent(eventName, { bubbles: true, cancelable: true, detail: this }); this.dispatchEvent(event); }; - }) + return res.bind(this); + } + this.querySelectorAll('.save').forEach(elt => elt.onclick = postEvent(elt, this.saveEvent)); + this.querySelectorAll('.play').forEach(elt => elt.onclick = e => { + if (this.isPlaying) { + (postEvent(elt, this.stopEvent))(e); + elt.innerText = 'play'; + this.isPlaying = false; + } else { + (postEvent(elt, this.playEvent))(e); + elt.innerText = 'stop'; + this.isPlaying = true; + } + }); this.formChanged(); } get notes() { - console.debug('Fretboard#notes', this); - return []; - // const formData = new FormData(this.form); - // return Array.from(this.form.querySelectorAll('tbody .selected')).map(elt => { - // const string = Array.from(elt.parentNode.classList).filter(x => x.startsWith('string'))[0]; - // const val = formData.get(string); - // return fretToNote(this.form, string, val); - // }) + return Array.from(this.querySelectorAll('x-string')).map(elt => { + return elt.getAttribute('value'); + }); } updateSelected(formData) { diff --git a/history.mjs b/history.mjs index f056ac7..b7d49ed 100644 --- a/history.mjs +++ b/history.mjs @@ -40,7 +40,7 @@ export default class extends HTMLElement { // TODO: i don't think this is how slots are supposed to work, // but maybe they're not really for this at all? item.querySelectorAll('[name="fretboard"]') - .forEach(s => s.innerText = fretboard.notes.join(' ')); + .forEach(s => s.textContent = fretboard.notes.join(' ')); this.list.appendChild(item); } } @@ -24,6 +24,9 @@ </x-string> </template> + <x-player> + <button class='play'>play ▶️</button> + </x-player> <button class='save'>+</button> </x-fretboard> diff --git a/key-picker.mjs b/key-picker.mjs index b24422d..b3f871c 100644 --- a/key-picker.mjs +++ b/key-picker.mjs @@ -17,7 +17,7 @@ function scaleFrom(tonic, scale) { function handleNoteEnter(e) { const elt = e.target; - const n = Note.fromString(elt.innerText).toString(); + const n = Note.fromString(elt.textContent).toString(); // todo: this should be delegated. the key selector shouldn't know // about the fretboard at all. elt.classList.add('hover'); @@ -28,7 +28,7 @@ function handleNoteEnter(e) { function handleNoteLeave(e) { const elt = e.target; - const n = Note.fromString(elt.innerText).toString(); + const n = Note.fromString(elt.textContent).toString(); // ibid. elt.classList.remove('hover'); document.querySelectorAll(`#fretboard [x-data-note="${n}"]`).forEach(elt => { @@ -37,7 +37,7 @@ function handleNoteLeave(e) { } function noteClicked(elt, justClear=false) { - const n = Note.fromString(elt.innerText).toString(); + const n = Note.fromString(elt.textContent).toString(); const count = Number(elt.getAttribute('x-data-clicked')) || 0; const next = (count + 1) % 4; elt.classList.remove(`click${count}`); @@ -71,7 +71,8 @@ function formChanged(form) { 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]); + Array.from(form.getElementsByClassName(c)).forEach(elt => + elt.textContent = scale[i]); }); function suffixFromIndex(i) { @@ -94,7 +95,7 @@ function formChanged(form) { Array.from(form.getElementsByClassName('chords')).forEach(list => { const items = availableScales.map(s => { const elt = document.createElement('li'); - elt.innerText = s; + elt.textContent = s; return elt; }); list.replaceChildren(); @@ -2,22 +2,47 @@ import Fretboard from './fretboard.mjs'; import History from './history.mjs'; import KeyPicker from './key-picker.mjs'; import String from './string.mjs'; +import Player from './player.mjs'; +import { Note, toCents } from './scale.mjs'; function init() { console.debug('init()', this); String.register(); Fretboard.register(); - // KeyPicker.register(); - // History.register(); + KeyPicker.register(); + History.register(); + + let player = undefined; // todo: maybe just attach the listener to document? document.querySelectorAll(Fretboard.name).forEach(f => { f.addEventListener(f.saveEvent, e => { document.querySelectorAll(History.name).forEach(h => { + console.debug('h is', h, 'e is', e.detail); h.add(e.detail); }); }); + + f.addEventListener(f.playEvent, e => { + console.debug('got playEvent', e, e.detail.notes); + const a = Note.fromString('A'); + if (player) { + player.stop(); + } + player = new Player(e.detail.notes.filter(n => n !== 'x').map(n => { + // smoosh everything into 4 right now + return toCents([a, 4], [Note.fromString(n), 4]); + })); + player.start(); + }); + + f.addEventListener(f.stopEvent, e => { + console.debug('got stopEvent', e); + if (player) { + player.stop(); + } + }); }); } document.addEventListener('DOMContentLoaded', init); @@ -8,12 +8,22 @@ function notesBetween(a, b) { } } +export function toCents([aNote, aChord], [bNote, bChord]) { + console.debug('- a', [aNote, aChord]); + console.debug('- b', [bNote, bChord]); + const offset = (aNote.distanceTo(bNote)) * 100; + console.debug('- offset', offset); + const scale = (bChord - aChord) * 1200; + console.debug('- scale', scale); + return scale + offset; +} + export class Note { static natural = '♮'; static sharp = '♯'; static flat = '♭'; - static range = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + static range = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; static get first() { return this.range.at(0); } @@ -26,6 +36,16 @@ export class Note { return this.noSharps.map(n => String.fromCharCode(n.charCodeAt()+1)) } + static get noteRange() { + return this.range.flatMap(n => { + if (this.noSharps.includes(n)) { + return new Note(n); + } else { + return [new Note(n, false), new Note(n, true)]; + } + }) + } + static fromString(string) { const [root, accidental] = [string[0], string[1]]; switch (accidental) { @@ -62,6 +82,14 @@ export class Note { this.isSharp = isSharp; } + distanceTo(dest) { + const srcFromC = Note.noteRange.findIndex(t => + t.toString() === this.toString()); + const destFromC = Note.noteRange.findIndex(t => + t.toString() === dest.toString()); + return destFromC - srcFromC; + } + toString() { return this.root + (this.isSharp ? Note.sharp : ''); } @@ -12,55 +12,6 @@ function dots(i) { } } -/* - this.querySelectorAll('.open').forEach(elt => { - elt.onclick = this.openClicked.bind(this); - }); - this.querySelectorAll('input[type="radio"]').forEach(elt => { - elt.onmousedown = this.fretMousedown.bind(this); - elt.onclick = this.fretClicked.bind(this); - }); - - - #mousedownVal; - 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. - this.#mousedownVal = e.target.checked; - } - - openClicked(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; - }) - } - this.formChanged(); - } - - fretClicked(e) { - const elt = e.target; - // if this element was selected at mousedown time, deselect it - // now. - if (this.#mousedownVal) { - elt.checked = false; - // doesn't get called automatically. - this.formChanged(); - } - } - */ - export default class extends HTMLElement { // TODO: probably not worth observing frets since everything just // gets re-rendered when they change. @@ -135,7 +86,7 @@ export default class extends HTMLElement { this.querySelectorAll('slot[name="open"]').forEach(s => { const note = chromaticScale[chromIndex] s.setAttribute('value', note); - s.innerText = note; + s.textContent = note; s.parentNode.onclick = this.fretClicked.bind(this); }) @@ -160,7 +111,7 @@ export default class extends HTMLElement { this.querySelectorAll('slot[name="selected"]').forEach(s => { const note = this.getAttribute('value'); console.debug(' -- setting selected', note); - s.innerText = note; + s.textContent = note; }) } } |
