summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--fretboard.mjs38
-rw-r--r--history.mjs2
-rw-r--r--index.html3
-rw-r--r--key-picker.mjs11
-rw-r--r--main.mjs29
-rw-r--r--scale.mjs30
-rw-r--r--string.mjs53
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);
}
}
diff --git a/index.html b/index.html
index 9300ab9..9488e0c 100644
--- a/index.html
+++ b/index.html
@@ -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();
diff --git a/main.mjs b/main.mjs
index 4108f5d..80e6524 100644
--- a/main.mjs
+++ b/main.mjs
@@ -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);
diff --git a/scale.mjs b/scale.mjs
index 5b04831..fbd3617 100644
--- a/scale.mjs
+++ b/scale.mjs
@@ -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 : '');
}
diff --git a/string.mjs b/string.mjs
index 36f15ac..d2f0461 100644
--- a/string.mjs
+++ b/string.mjs
@@ -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;
})
}
}