summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Cully <bjc@spork.org>2025-03-08 12:05:53 -0500
committerBrian Cully <bjc@spork.org>2025-03-08 12:05:53 -0500
commitddc2818c6a096db5ff4db91f6538bf829043e563 (patch)
tree7db75e099280a9a86ce86337a703dcc974847e86
parentdaeace070ee8687fceb77ee5e9f7bce507669639 (diff)
downloadchords-ddc2818c6a096db5ff4db91f6538bf829043e563.tar.gz
chords-ddc2818c6a096db5ff4db91f6538bf829043e563.zip
js: add bare bones js stuff
-rw-r--r--fretboard.mjs32
-rw-r--r--main.mjs13
-rw-r--r--scale.mjs75
3 files changed, 120 insertions, 0 deletions
diff --git a/fretboard.mjs b/fretboard.mjs
new file mode 100644
index 0000000..c58543c
--- /dev/null
+++ b/fretboard.mjs
@@ -0,0 +1,32 @@
+function fretChanged(e) {
+ console.log('fret changed', e);
+}
+
+let mousedownVal = undefined;
+
+function mousedown(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 click(e) {
+ const elt = e.target;
+ // if this element was selected at mousedown time, deselect it
+ // now.
+ if (mousedownVal) {
+ elt.checked = false;
+ }
+}
+
+export default function Fretboard(form) {
+ console.debug('Fretboard()', form);
+ form.onchange = fretChanged;
+ form.querySelectorAll('input[type="radio"]').forEach(elt => {
+ elt.onmousedown = mousedown;
+ elt.onclick = click;
+ });
+}
diff --git a/main.mjs b/main.mjs
new file mode 100644
index 0000000..97ba06f
--- /dev/null
+++ b/main.mjs
@@ -0,0 +1,13 @@
+import Fretboard from './fretboard.mjs'
+import { MajorScale, MinorScale } from './scale.mjs';
+
+Fretboard(document.getElementById('fretboard'));
+const cMaj = MajorScale('C');
+console.log('test major scale', cMaj.length);
+console.log(`test fifth ${cMaj.root}`, cMaj.fifth);
+const cMin = MinorScale('C');
+console.log('test major scale', cMin.length);
+console.log(`test fifth ${cMin.root}`, cMin.fifth);
+const bMaj = MajorScale('B');
+console.log('test major scale', bMaj.length);
+console.log(`test fifth ${bMaj.root}`, bMaj.fifth);
diff --git a/scale.mjs b/scale.mjs
new file mode 100644
index 0000000..61319df
--- /dev/null
+++ b/scale.mjs
@@ -0,0 +1,75 @@
+const ringHandler = {
+ get(target, prop) {
+ return target[prop % target.length] || Reflect.get(...arguments);
+ }
+};
+
+const chromaticScale = new Proxy(
+ ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'],
+ ringHandler);
+
+function scaleFromIntervals(root, intervals) {
+ const scaleIndex = chromaticScale.indexOf(root);
+ if (scaleIndex < 0) {
+ return undefined;
+ }
+
+ let scale = [root];
+ let steps = 0;
+ let lastBase = root[0];
+ for (let i = 0; i < intervals.length; i++) {
+ steps += intervals[i];
+ const note = chromaticScale[scaleIndex + steps];
+ // don't display two base notes in a row by changing
+ // accidentals.
+ if (note[0] === lastBase) {
+ const nextBase = chromaticScale[scaleIndex + steps + 1][0];
+ scale.push(`${nextBase}b`)
+ lastBase = nextBase[0];
+ } else {
+ scale.push(note);
+ lastBase = note[0];
+ }
+ }
+ return new Proxy(scale, ringHandler);
+}
+
+class Scale {
+ constructor(root, intervals) {
+ this.scale = scaleFromIntervals(root, intervals);
+ return new Proxy(this, {
+ get(target, prop) {
+ return target.scale[prop] || Reflect.get(...arguments);
+ }
+ });
+ }
+
+ get root() {
+ return this[0];
+ }
+
+ get third() {
+ return this[2];
+ }
+
+ get fifth() {
+ return this[4];
+ }
+
+ get length() {
+ console.debug(`get length`, this.scale.length);
+ return this.intervals.length;
+ }
+}
+
+export function MajorScale(root) {
+ console.debug(`MajorScale(${root})`);
+ const intervals = [2, 2, 1, 2, 2, 2];
+ return new Scale(root, intervals);
+}
+
+export function MinorScale(root) {
+ console.debug(`MinorScale(${root})`);
+ const intervals = [2, 1, 2, 2, 1, 2];
+ return new Scale(root, intervals);
+}