aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2023-05-06 22:07:32 +0000
committers-ol <s+removethis@s-ol.nu>2023-05-07 09:53:29 +0000
commit1c981aea4dbecb3ca77efbafac1be7f12012327d (patch)
tree1a02a0f607ca9a095b80760beef5c9529ecd1eb7
parentrestart with v2 (diff)
downloadisomorphic-kb-explorer-1c981aea4dbecb3ca77efbafac1be7f12012327d.tar.gz
isomorphic-kb-explorer-1c981aea4dbecb3ca77efbafac1be7f12012327d.zip
add basic pattern highlighting
-rw-r--r--index.html47
-rw-r--r--layout.js163
-rw-r--r--main.js265
-rw-r--r--pattern.js81
-rw-r--r--style.css48
5 files changed, 381 insertions, 223 deletions
diff --git a/index.html b/index.html
index dc67486..17fa2f1 100644
--- a/index.html
+++ b/index.html
@@ -10,7 +10,40 @@
<canvas id="canvas-bg"></canvas>
<canvas id="canvas-fg"></canvas>
</main>
- <aside class="controls">
+ <aside class="pattern controls">
+ <div class="control control--preset">
+ <label for="pattern-preset">preset</label>
+ <select id="pattern-preset">
+ <option value="custom">(custom)</option>
+ <optgroup label="scales">
+ <option value="major-7" >major</option>
+ <option value="minor-7" >minor</option>
+ <option value="minor-harm-7">harmonic minor</option>
+ <option value="minor-mel-7" >melodic minor</option>
+ <option value="minor-hung-7">hungarian minor</option>
+ <option value="penta" >pentatonic</option>
+ </optgroup>
+ <optgroup label="triads">
+ <option value="major-3" >major</option>
+ <option value="major-3+">augmented</option>
+ <option value="minor-3" >minor</option>
+ <option value="minor-3-">diminished</option>
+ </optgroup>
+ </select>
+ </label>
+ </div>
+ <div class="control">
+ <label for="pattern-len">(semi)tones</label>
+ <input id="pattern-len" type="number" min="4" value="12" />
+ </div>
+ <div class="steps">
+ <input type="checkbox" checked disabled />
+ <input type="checkbox" />
+ <input type="checkbox" />
+ <input type="checkbox" />
+ </div>
+ </aside>
+ <aside class="layout">
<svg
id="diagram"
viewBox="-120 -145 240 260"
@@ -65,8 +98,8 @@
id="turn-cw" class="turn" transform="rotate(1.5)"
d="M 63.875 -112.84961 L 59.068359 -104.08203 C 69.570355 -98.323686 70.491716 -97.891327 78.669922 -89.982422 L 74.099609 -85.28125 L 96.498047 -79.628906 L 95.910156 -81.712891 L 90.216797 -101.85938 L 85.628906 -97.140625 C 76.875667 -105.51066 74.458477 -107.04659 63.875 -112.84961 z " />
</svg>
- <div>
- <div class="preset-control">
+ <div class="controls">
+ <div class="control control--preset">
<label for="layout-preset">preset</label>
<select id="layout-preset">
<option value="wicki-hayden">Wicki/Hayden</option>
@@ -77,7 +110,7 @@
</select>
</label>
</div>
- <div class="axis-control">
+ <div class="control control--axis">
<label for="step-1">up</label>
<input id="step-1" type="number" min="1" />
<label class="dir">
@@ -85,7 +118,7 @@
invert
</label>
</div>
- <div class="axis-control">
+ <div class="control control--axis">
<label for="step-2">hi right</label>
<input id="step-2" type="number" min="1" />
<label class="dir">
@@ -93,7 +126,7 @@
invert
</label>
</div>
- <div class="axis-control">
+ <div class="control control--axis">
<label for="step-3">low right</label>
<input id="step-3" type="number" min="1" />
<label class="dir">
@@ -103,6 +136,6 @@
</div>
</div>
</aside>
- <script src="main.js"></script>
+ <script type="module" src="main.js"></script>
</body>
</html>
diff --git a/layout.js b/layout.js
new file mode 100644
index 0000000..e4eab79
--- /dev/null
+++ b/layout.js
@@ -0,0 +1,163 @@
+const panel = document.querySelector('aside.layout');
+
+const svg = document.getElementById('diagram');
+const arrows = Array.from(svg.querySelectorAll('.arrow'));
+
+const preset = panel.querySelector('.control--preset');
+const controls = Array.from(panel.querySelectorAll('.control--axis'));
+const steps = Array.from(panel.querySelectorAll('.control--axis > input'));
+const dirs = Array.from(panel.querySelectorAll('.control--axis .dir input'));
+const turnCCW = document.getElementById('turn-ccw');
+const turnCW = document.getElementById('turn-cw');
+
+let state = [ 7, 2, null ];
+let last = 0;
+let selected = null;
+
+let rot = 0;
+let targetRot = 30;
+
+const completeState = ([a, b, c]) => {
+ if (a == null) {
+ a = b - c;
+ } else if (b == null) {
+ b = c + a;
+ } else {
+ c = b - a;
+ }
+ return [a, b, c, -a, -b, -c];
+};
+
+const updateValues = () => {
+ const full = completeState(state);
+
+ full.forEach((val, i) => {
+ arrows[i].querySelector('text').textContent = val;
+
+ if (i < steps.length) steps[i].value = Math.abs(val);
+ if (i < dirs.length) {
+ dirs[i].checked = val < 0;
+ dirs[i].disabled = val == 0;
+ }
+ });
+ updateFocus();
+};
+
+const updateFocus = () => {
+ const full = completeState(state);
+
+ controls.forEach((control, i) => {
+ let className = 'control control--axis';
+ if (state[i] == null) className += ' driven';
+ else if (i == selected) className += ' selected';
+ if (state[i] == 0) className += ' dir-disabled';
+ control.className = className;
+ });
+
+ full.forEach((val, i) => {
+ const positive = val > 0 || (val == 0 && i < 3);
+
+ let className = 'arrow';
+ if (state[i%3] == null) className += ' driven';
+ if (!positive) className += ' negative';
+ if (i%3 == selected) className += ' selected';
+ arrows[i].className.baseVal = className;
+ });
+};
+
+const select = (i) => {
+ if (state[i] != null) return false;
+
+ const newState = [null, null, null];
+ newState[i] = completeState(state)[i];
+ newState[last] = state[last];
+ state = newState;
+ last = i;
+
+ return true;
+};
+
+export const turn = (dir) => {
+ targetRot += dir * 30;
+}
+
+arrows.forEach((arrow, i) => {
+ arrow.onclick = () => steps[i%3].focus();
+});
+
+controls.forEach((control, i) => {
+ control.addEventListener('focusin', () => { selected = i; select(i); updateFocus(); });
+ control.addEventListener('focusout', () => { selected = null; updateFocus(); });
+});
+
+steps.forEach((input, i) => {
+ input.onchange = () => {
+ select(i);
+ state[i] = +input.value;
+ if (dirs[i].checked) state[i] = -state[i];
+ preset.value = 'custom';
+ updateValues();
+ };
+});
+
+dirs.forEach((input, i) => {
+ input.onchange = () => {
+ select(i);
+ state[i] = state[i] * -1;
+ preset.value = 'custom';
+ updateValues();
+ };
+});
+
+preset.onchange = () => {
+ switch (preset.value) {
+ case 'custom': return;
+ case 'wicki-hayden': state = [ 7, 2, null ]; break;
+ case 'janko': state = [ 1, 2, null ]; break;
+ case 'harmonic': state = [ 7, 4, null ]; break;
+ case 'gerhard': state = [ 4, 3, null ]; break;
+ }
+
+ last = state.findIndex(s => s != null);
+ updateValues();
+};
+preset.value = 'wicki-hayden';
+preset.onchange();
+
+turnCCW.onclick = () => turn(-1);
+turnCW.onclick = () => turn(1);
+
+export const update = () => {
+ const delta = targetRot - rot;
+ if (Math.abs(delta) < 1) rot = targetRot;
+ else rot += delta * 0.1;
+
+ if (rot < 60 && rot > -60) return;
+
+ const full = completeState(state);
+ let driven = state.indexOf(null);
+
+ // cw rotation
+ while (rot >= 60) {
+ rot -= 60;
+ targetRot -= 60;
+ full.unshift(full.pop());
+ driven++;
+ }
+
+ // cw rotation
+ while (rot <= -60) {
+ rot += 60;
+ targetRot += 60;
+ full.push(full.shift());
+ driven = (driven-1+3) % 3;
+ }
+
+ state = full.slice(0, 3);
+ state[driven%3] = null;
+
+ updateValues();
+};
+
+export const getSteps = () => completeState(state);
+export const getRot = () => rot / 180 * Math.PI;
diff --git a/main.js b/main.js
index 85c34b9..4aa2009 100644
--- a/main.js
+++ b/main.js
@@ -1,182 +1,62 @@
-const svg = document.getElementById('diagram');
-const arrows = Array.from(svg.querySelectorAll('.arrow'));
-const preset = document.getElementById('layout-preset');
-const controls = Array.from(document.querySelectorAll('.axis-control'));
-const steps = Array.from(document.querySelectorAll('.axis-control > input'));
-const dirs = Array.from(document.querySelectorAll('.axis-control .dir input'));
-
-let state = [ 7, 2, null ];
-let last = 0;
-let selected = null;
-
-const completeState = ([a, b, c]) => {
- if (a == null) {
- a = b - c;
- } else if (b == null) {
- b = c + a;
- } else {
- c = b - a;
- }
- return [a, b, c, -a, -b, -c];
-};
-
-const updateValues = () => {
- const full = completeState(state);
-
- full.forEach((val, i) => {
- arrows[i].querySelector('text').textContent = val;
-
- if (i < steps.length) steps[i].value = Math.abs(val);
- if (i < dirs.length) {
- dirs[i].checked = val < 0;
- dirs[i].disabled = val == 0;
- }
- });
- updateFocus();
-};
-
-const updateFocus = () => {
- const full = completeState(state);
-
- controls.forEach((control, i) => {
- let className = 'axis-control';
- if (state[i] == null) className += ' driven';
- else if (i == selected) className += ' selected';
- if (state[i] == 0) className += ' dir-disabled';
- control.className = className;
- });
-
- full.forEach((val, i) => {
- const positive = val > 0 || (val == 0 && i < 3);
-
- let className = 'arrow';
- if (state[i%3] == null) className += ' driven';
- if (!positive) className += ' negative';
- if (i%3 == selected) className += ' selected';
- arrows[i].className.baseVal = className;
- });
-};
-
-const select = (i) => {
- if (state[i] != null) return false;
-
- const newState = [null, null, null];
- newState[i] = completeState(state)[i];
- newState[last] = state[last];
- state = newState;
- last = i;
-
- return true;
-};
-
-arrows.forEach((arrow, i) => {
- arrow.onclick = () => steps[i%3].focus();
-});
-
-controls.forEach((control, i) => {
- control.addEventListener('focusin', () => { selected = i; select(i); updateFocus(); });
- control.addEventListener('focusout', () => { selected = null; updateFocus(); });
-});
-
-steps.forEach((input, i) => {
- input.onchange = () => {
- select(i);
- state[i] = +input.value;
- if (dirs[i].checked) state[i] = -state[i];
- preset.value = 'custom';
- updateValues();
- };
-});
-
-dirs.forEach((input, i) => {
- input.onchange = () => {
- select(i);
- state[i] = state[i] * -1;
- preset.value = 'custom';
- updateValues();
- };
-});
-
-let rot = 0;
-let targetRot = 0;
-
-document.getElementById('turn-ccw').onclick = () => {
- targetRot -= 30;
-};
-document.getElementById('turn-cw').onclick = () => {
- targetRot += 30;
-};
-
-preset.onchange = () => {
- switch (preset.value) {
- case 'custom': return;
- case 'wicki-hayden': state = [ 7, 2, null ]; break;
- case 'janko': state = [ 1, 2, null ]; break;
- case 'harmonic': state = [ 7, 4, null ]; break;
- case 'gerhard': state = [ 4, 3, null ]; break;
- }
-
- last = state.findIndex(s => s != null);
- updateValues();
-};
-preset.value = 'wicki-hayden';
-preset.onchange();
-
-const bg = document.getElementById('canvas-bg').getContext('2d');
-const fg = document.getElementById('canvas-fg').getContext('2d');
+import * as layout from './layout.js';
+import * as pattern from './pattern.js';
const sqrt3 = Math.sqrt(3);
const sqrt32 = sqrt3 / 2;
-const size = 80;
+const size = 50;
const hs = size / 2;
const hh = sqrt32 * size;
-const hex2px = (q, r) => {
+const hex2px = ([q, r]) => {
const x = size * (3/2 * r);
const y = -size * (sqrt32 * r + sqrt3 * q);
return [x, y];
};
+const px2hex = ([x, y]) => {
+ const t1 = x / size;
+ const t2 = -y / hh / 2;
-const mul = (x, y, mat) => {
+ const q = Math.floor( (Math.floor(-y / hh) + Math.floor(t2 - t1) + 2 ) / 3);
+ const r = Math.floor( (Math.floor(t1 - t2) + Math.floor(t1 + t2) + 2 ) / 3);
+
+ return [q, r];
+}
+const rotate = ([x, y], a) => {
return [
- x * mat[0] + y * mat[1] + mat[2],
- y * mat[3] + y * mat[4] + mat[5],
+ Math.cos(a) * x + Math.sin(a) * y,
+ -Math.sin(a) * x + Math.cos(a) * y,
];
};
+const hexagon = (ctx) => {
+ ctx.beginPath();
+ ctx.moveTo(-hs, hh);
+ ctx.lineTo(hs, hh);
+ ctx.lineTo(size, 0);
+ ctx.lineTo(hs, -hh);
+ ctx.lineTo(-hs, -hh);
+ ctx.lineTo(-size, 0);
+ ctx.closePath();
+};
-const updateRotation = () => {
- const delta = targetRot - rot;
- if (Math.abs(delta) < 1) rot = targetRot;
- else rot += delta * 0.1;
-
- if (rot < 60 && rot > -60) return;
-
- const full = completeState(state);
- let driven = state.indexOf(null);
+const main = document.querySelector('main');
+const bg = document.getElementById('canvas-bg').getContext('2d');
+const fg = document.getElementById('canvas-fg').getContext('2d');
- // cw rotation
- while (rot >= 60) {
- rot -= 60;
- targetRot -= 60;
- full.unshift(full.pop());
- driven++;
- }
+let mousePos = [0, 0];
+main.onmousemove = (e) => {
+ mousePos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
+};
- // cw rotation
- while (rot <= -60) {
- rot += 60;
- targetRot += 60;
- full.push(full.shift());
- driven = (driven-1+3) % 3;
+document.body.onkeydown = (e) => {
+ switch (e.key) {
+ case 'q': layout.turn(-1); break;
+ case 'e': layout.turn(1); break;
}
-
- state = full.slice(0, 3);
- state[driven%3] = null;
-
- updateValues();
};
+let lastCanvasSize = 0;
+
const updateBackground = (canvasSize) => {
bg.strokeStyle = '#b9bdc1';
bg.strokeWidth = 1.5;
@@ -190,16 +70,9 @@ const updateBackground = (canvasSize) => {
const rMax = Math.min(rad, rad-q);
for (let r = rMin; r <= rMax; r++) {
bg.save();
- bg.translate(...hex2px(q, r));
+ bg.translate(...hex2px([q, r]));
- bg.beginPath();
- bg.moveTo(-hs, hh);
- bg.lineTo(hs, hh);
- bg.lineTo(size, 0);
- bg.lineTo(hs, -hh);
- bg.lineTo(-hs, -hh);
- bg.lineTo(-size, 0);
- bg.closePath();
+ hexagon(bg);
bg.stroke();
bg.restore();
@@ -207,17 +80,6 @@ const updateBackground = (canvasSize) => {
}
};
-const main = document.querySelector('main');
-
-let mousePos = [0, 0];
-main.onmousemove = (e) => {
- mousePos = [
- e.clientX - main.clientWidth/2,
- e.clientY - main.clientHeight/2,
- ];
-};
-
-let lastCanvasSize = 0;
const draw = () => {
const width = window.innerWidth;
const height = window.innerHeight;
@@ -234,42 +96,53 @@ const draw = () => {
fg.canvas.width = fg.canvas.width;
- updateRotation();
+ layout.update();
fg.translate(canvasSize/2, canvasSize/2);
- fg.fillStyle = '#303336';
- fg.font = `${size*0.8}px sans-serif`;
+ fg.font = `${size*0.5}px sans-serif`;
fg.textAlign = 'center';
+
+ fg.strokeStyle = '#ff0000';
+ fg.strokeWidth = 5;
+
+
+ const rot = layout.getRot();
+ const [qq, rr] = layout.getSteps();
- const rotRad = rot / 180 * Math.PI;
+ const steps = pattern.getSteps();
+ const length = pattern.getLength();
- const [qq, rr] = completeState(state);
const rad = Math.ceil(canvasSize / size / 3);
for (let q = -rad; q <= rad; q++) {
const rMin = Math.max(-rad, -q-rad);
const rMax = Math.min(rad, rad-q);
for (let r = rMin; r <= rMax; r++) {
fg.save();
- fg.translate(...hex2px(q, r));
-
- const val = q*qq + r*rr;
- fg.rotate(-rotRad);
- fg.fillText(val, 0, size/3);
+ fg.translate(...hex2px([q, r]));
+
+ const note = q*qq + r*rr;
+ let step = note % length;
+ step = (step + length) % length;
+
+ if (steps.includes(note)) {
+ fg.fillStyle = '#eeeeee';
+ hexagon(fg);
+ fg.fill();
+ }
+
+ fg.rotate(-rot);
+ fg.fillStyle = '#303336';
+ fg.fillText(step, 0, size/3);
fg.restore();
}
}
- const mouse = [
- Math.cos(rotRad) * mousePos[0] + Math.sin(rotRad) * mousePos[1],
- -Math.sin(rotRad) * mousePos[0] + Math.cos(rotRad) * mousePos[1],
- ];
-
- fg.arc(mouse[0], mouse[1], 5, 0, 2*Math.PI);
- fg.fill()
+ // const mouse = px2hex(rotate(mousePos, rot));
+ // fg.arc(...hex2px(mouse), 10, 0, 2*Math.PI);
- document.body.style.setProperty('--global-rot', `${rot}deg`);
+ document.body.style.setProperty('--global-rot', `${rot}rad`);
requestAnimationFrame(draw);
};
diff --git a/pattern.js b/pattern.js
new file mode 100644
index 0000000..731139f
--- /dev/null
+++ b/pattern.js
@@ -0,0 +1,81 @@
+const panel = document.querySelector('aside.pattern');
+
+const preset = panel.querySelector('.control--preset');
+const patternLength = document.getElementById('pattern-len');
+const steps = panel.querySelector('.steps');
+
+let length = 12;
+let pattern = [0, 2, 4, 5, 7, 9, 11];
+
+const update = () => {
+ while (steps.childElementCount > length) {
+ steps.lastElementChild.remove();
+ }
+
+ while (steps.childElementCount < length) {
+ const input = document.createElement('input');
+ input.type = 'checkbox';
+ steps.append(input);
+ }
+
+ Array.from(steps.children).forEach((toggle, i) => {
+ toggle.checked = pattern.includes(i);
+ });
+};
+
+patternLength.onchange = () => {
+ length = +patternLength.value;
+ pattern = pattern.filter(n => n < length);
+ update();
+};
+
+steps.onchange = () => {
+ pattern = [];
+ Array.from(steps.children).forEach((toggle, i) => {
+ if (toggle.checked) pattern.push(i);
+ });
+};
+
+preset.onchange = (e) => {
+ const value = e.target.value;
+ switch (value) {
+ case 'custom': return;
+ case 'major-7':
+ pattern = [0, 2, 4, 5, 7, 9, 11];
+ break;
+ case 'minor-7':
+ pattern = [0, 2, 3, 5, 7, 8, 10];
+ break;
+ case 'minor-harm-7':
+ pattern = [0, 2, 3, 5, 7, 8, 11];
+ break;
+ case 'minor-mel-7':
+ pattern = [0, 2, 3, 5, 7, 9, 11];
+ break;
+ case 'minor-hung-7':
+ pattern = [0, 2, 3, 6, 7, 8, 11];
+ break;
+ case 'penta':
+ pattern = [0, 2, 4, 7, 9];
+ break;
+ case 'major-3':
+ pattern = [0, 4, 7];
+ break;
+ case 'major-3+':
+ pattern = [0, 4, 8];
+ break;
+ case 'minor-3':
+ pattern = [0, 3, 7];
+ break;
+ case 'minor-3-':
+ pattern = [0, 3, 6];
+ break;
+ }
+ update();
+ preset.value = value;
+};
+preset.value = 'major-7';
+preset.onchange({ target: preset });
+
+export const getLength = () => length;
+export const getSteps = () => pattern;
diff --git a/style.css b/style.css
index 26b820e..84425d5 100644
--- a/style.css
+++ b/style.css
@@ -22,23 +22,29 @@ canvas {
transform: translate(-50%, -50%) rotate(var(--global-rot));
}
-aside.controls {
+aside {
position: fixed;
- bottom: 0;
right: 0;
-
display: flex;
- height: 12rem;
-
filter: drop-shadow(0 0 4px rgba(0,0,0,0.5));
}
-aside.controls > svg {
+aside.pattern {
+ top: 0;
+}
+
+aside.layout {
+ bottom: 0;
+
+ height: 12rem;
+}
+
+aside.layout > svg {
width: auto;
height: 100%;
}
-aside.controls > div {
+.controls {
display: flex;
flex-direction: column;
justify-content: center;
@@ -48,8 +54,7 @@ aside.controls > div {
}
/* controls */
-.axis-control,
-.preset-control {
+.control {
display: flex;
align-items: center;
transition: background 0.3s;
@@ -57,36 +62,39 @@ aside.controls > div {
gap: 0.5rem;
}
-.preset-control {
+.control label:first-child {
+ width: 6rem;
+}
+
+
+.control--preset {
padding-bottom: 0.5rem;
border-bottom: 1px solid #b9bdc1;
margin-bottom: 0.25rem;
}
-.preset-control select {
+
+.control--preset select {
flex: 1;
}
-.preset-control label,
-.axis-control label:first-child {
- width: 6rem;
-}
-.axis-control label:first-child {
+
+.control--axis label:first-child {
flex: 1;
}
-.axis-control > input {
+.control--axis > input {
width: 5em;
}
-.axis-control.dir-disabled label.dir {
+.control--axis.dir-disabled label.dir {
color: #b9bdc1;
}
-.axis-control.driven {
+.control--axis.driven {
opacity: 0.5;
}
-.axis-control.selected {
+.control--axis.selected {
background: #eeeeee;
}