commit 387dc6c50d6563929262461b7e534ef08bca4ef1
Author: massi <git@massi.world>
Date: Sun, 11 Jan 2026 23:08:37 -0800
triads
Diffstat:
5 files changed, 105 insertions(+), 0 deletions(-)
diff --git a/index.html b/index.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <ul>
+ <li><a href="https://scratch.massi.world/triads">triad exercise</a></li>
+ </ul>
+ </body>
+</html>
diff --git a/triads/index.html b/triads/index.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <script src="./main.js"></script>
+ <link rel="stylesheet" href="./style.css" />
+ </head>
+ <body>
+ <h1>triad voice leading exercise</h1>
+ <h2>from mick goodrick's Advancing Guitarist</h2>
+ <div id="desc">
+ Find the minimum difference between each triad in order to play a good
+ voice leading that goes through every possible triad.
+ </div>
+ <div id="chart"></div>
+ </body>
+</html>
diff --git a/triads/main.js b/triads/main.js
@@ -0,0 +1,28 @@
+var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
+ if (ar || !(i in from)) {
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
+ ar[i] = from[i];
+ }
+ }
+ return to.concat(ar || Array.prototype.slice.call(from));
+};
+var NOTES = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"];
+var QUALITIES = ["", "-", "°", "+"];
+var POSSIBILITIES = NOTES.reduce(function (accum, note) {
+ return __spreadArray(__spreadArray([], accum, true), QUALITIES.map(function (q) { return note + q; }), true);
+}, []);
+var bag = new Array(POSSIBILITIES.length).fill(0).map(function (_, i) { return i; });
+var result = [];
+while (bag.length > 0) {
+ var bagIdx = Math.floor(Math.random() * bag.length);
+ var possibilitiesIdx = bag[bagIdx];
+ result.push(POSSIBILITIES[possibilitiesIdx]);
+ bag.splice(bagIdx, 1);
+}
+document.addEventListener("DOMContentLoaded", function () {
+ document.getElementById("chart").innerHTML = result
+ .map(function (note) { return "<code>".concat(note, "</code>"); })
+ .join("");
+});
+// the other mode would be 1-7 in a given KEY, starting at a given INVERSION.
diff --git a/triads/main.ts b/triads/main.ts
@@ -0,0 +1,25 @@
+const NOTES = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"];
+
+const QUALITIES = ["", "-", "°", "+"];
+
+const POSSIBILITIES = NOTES.reduce<string[]>((accum, note) => {
+ return [...accum, ...QUALITIES.map((q) => note + q)];
+}, []);
+
+const bag = new Array(POSSIBILITIES.length).fill(0).map((_, i) => i);
+
+let result: string[] = [];
+while (bag.length > 0) {
+ const bagIdx = Math.floor(Math.random() * bag.length);
+ const possibilitiesIdx = bag[bagIdx];
+ result.push(POSSIBILITIES[possibilitiesIdx]);
+ bag.splice(bagIdx, 1);
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("chart")!.innerHTML = result
+ .map((note) => `<code>${note}</code>`)
+ .join("");
+});
+
+// the other mode would be 1-7 in a given KEY, starting at a given INVERSION.
diff --git a/triads/style.css b/triads/style.css
@@ -0,0 +1,29 @@
+#chart {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 1rem;
+}
+
+h1,
+h2 {
+ text-align: center;
+}
+
+#desc {
+ max-width: 40rem;
+ margin: auto;
+}
+
+code {
+ font-size: 1.5rem;
+ width: 12.5%;
+ box-sizing: border-box;
+ text-align: center;
+ padding: 1rem;
+}
+
+@media (max-aspect-ratio: 1/1) {
+ code {
+ width: 25%;
+ }
+}