git.s-ol.nu isomorphic-kb-explorer / 1c981ae
add basic pattern highlighting s-ol 24 days ago
5 changed file(s) with 380 addition(s) and 222 deletion(s). Raw diff Collapse all Expand all
99 <canvas id="canvas-bg"></canvas>
1010 <canvas id="canvas-fg"></canvas>
1111 </main>
12 <aside class="controls">
12 <aside class="pattern controls">
13 <div class="control control--preset">
14 <label for="pattern-preset">preset</label>
15 <select id="pattern-preset">
16 <option value="custom">(custom)</option>
17 <optgroup label="scales">
18 <option value="major-7" >major</option>
19 <option value="minor-7" >minor</option>
20 <option value="minor-harm-7">harmonic minor</option>
21 <option value="minor-mel-7" >melodic minor</option>
22 <option value="minor-hung-7">hungarian minor</option>
23 <option value="penta" >pentatonic</option>
24 </optgroup>
25 <optgroup label="triads">
26 <option value="major-3" >major</option>
27 <option value="major-3+">augmented</option>
28 <option value="minor-3" >minor</option>
29 <option value="minor-3-">diminished</option>
30 </optgroup>
31 </select>
32 </label>
33 </div>
34 <div class="control">
35 <label for="pattern-len">(semi)tones</label>
36 <input id="pattern-len" type="number" min="4" value="12" />
37 </div>
38 <div class="steps">
39 <input type="checkbox" checked disabled />
40 <input type="checkbox" />
41 <input type="checkbox" />
42 <input type="checkbox" />
43 </div>
44 </aside>
45 <aside class="layout">
1346 <svg
1447 id="diagram"
1548 viewBox="-120 -145 240 260"
6497 id="turn-cw" class="turn" transform="rotate(1.5)"
6598 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 " />
6699 </svg>
67 <div>
68 <div class="preset-control">
100 <div class="controls">
101 <div class="control control--preset">
69102 <label for="layout-preset">preset</label>
70103 <select id="layout-preset">
71104 <option value="wicki-hayden">Wicki/Hayden</option>
76109 </select>
77110 </label>
78111 </div>
79 <div class="axis-control">
112 <div class="control control--axis">
80113 <label for="step-1">up</label>
81114 <input id="step-1" type="number" min="1" />
82115 <label class="dir">
84117 invert
85118 </label>
86119 </div>
87 <div class="axis-control">
120 <div class="control control--axis">
88121 <label for="step-2">hi right</label>
89122 <input id="step-2" type="number" min="1" />
90123 <label class="dir">
92125 invert
93126 </label>
94127 </div>
95 <div class="axis-control">
128 <div class="control control--axis">
96129 <label for="step-3">low right</label>
97130 <input id="step-3" type="number" min="1" />
98131 <label class="dir">
102135 </div>
103136 </div>
104137 </aside>
105 <script src="main.js"></script>
138 <script type="module" src="main.js"></script>
106139 </body>
107140 </html>
0 const panel = document.querySelector('aside.layout');
1
2 const svg = document.getElementById('diagram');
3 const arrows = Array.from(svg.querySelectorAll('.arrow'));
4
5 const preset = panel.querySelector('.control--preset');
6 const controls = Array.from(panel.querySelectorAll('.control--axis'));
7 const steps = Array.from(panel.querySelectorAll('.control--axis > input'));
8 const dirs = Array.from(panel.querySelectorAll('.control--axis .dir input'));
9 const turnCCW = document.getElementById('turn-ccw');
10 const turnCW = document.getElementById('turn-cw');
11
12 let state = [ 7, 2, null ];
13 let last = 0;
14 let selected = null;
15
16 let rot = 0;
17 let targetRot = 30;
18
19 const completeState = ([a, b, c]) => {
20 if (a == null) {
21 a = b - c;
22 } else if (b == null) {
23 b = c + a;
24 } else {
25 c = b - a;
26 }
27 return [a, b, c, -a, -b, -c];
28 };
29
30 const updateValues = () => {
31 const full = completeState(state);
32
33 full.forEach((val, i) => {
34 arrows[i].querySelector('text').textContent = val;
35
36 if (i < steps.length) steps[i].value = Math.abs(val);
37 if (i < dirs.length) {
38 dirs[i].checked = val < 0;
39 dirs[i].disabled = val == 0;
40 }
41 });
42 updateFocus();
43 };
44
45 const updateFocus = () => {
46 const full = completeState(state);
47
48 controls.forEach((control, i) => {
49 let className = 'control control--axis';
50 if (state[i] == null) className += ' driven';
51 else if (i == selected) className += ' selected';
52 if (state[i] == 0) className += ' dir-disabled';
53 control.className = className;
54 });
55
56 full.forEach((val, i) => {
57 const positive = val > 0 || (val == 0 && i < 3);
58
59 let className = 'arrow';
60 if (state[i%3] == null) className += ' driven';
61 if (!positive) className += ' negative';
62 if (i%3 == selected) className += ' selected';
63 arrows[i].className.baseVal = className;
64 });
65 };
66
67 const select = (i) => {
68 if (state[i] != null) return false;
69
70 const newState = [null, null, null];
71 newState[i] = completeState(state)[i];
72 newState[last] = state[last];
73 state = newState;
74 last = i;
75
76 return true;
77 };
78
79 export const turn = (dir) => {
80 targetRot += dir * 30;
81 }
82
83 arrows.forEach((arrow, i) => {
84 arrow.onclick = () => steps[i%3].focus();
85 });
86
87 controls.forEach((control, i) => {
88 control.addEventListener('focusin', () => { selected = i; select(i); updateFocus(); });
89 control.addEventListener('focusout', () => { selected = null; updateFocus(); });
90 });
91
92 steps.forEach((input, i) => {
93 input.onchange = () => {
94 select(i);
95 state[i] = +input.value;
96 if (dirs[i].checked) state[i] = -state[i];
97 preset.value = 'custom';
98 updateValues();
99 };
100 });
101
102 dirs.forEach((input, i) => {
103 input.onchange = () => {
104 select(i);
105 state[i] = state[i] * -1;
106 preset.value = 'custom';
107 updateValues();
108 };
109 });
110
111 preset.onchange = () => {
112 switch (preset.value) {
113 case 'custom': return;
114 case 'wicki-hayden': state = [ 7, 2, null ]; break;
115 case 'janko': state = [ 1, 2, null ]; break;
116 case 'harmonic': state = [ 7, 4, null ]; break;
117 case 'gerhard': state = [ 4, 3, null ]; break;
118 }
119
120 last = state.findIndex(s => s != null);
121 updateValues();
122 };
123 preset.value = 'wicki-hayden';
124 preset.onchange();
125
126 turnCCW.onclick = () => turn(-1);
127 turnCW.onclick = () => turn(1);
128
129 export const update = () => {
130 const delta = targetRot - rot;
131 if (Math.abs(delta) < 1) rot = targetRot;
132 else rot += delta * 0.1;
133
134 if (rot < 60 && rot > -60) return;
135
136 const full = completeState(state);
137 let driven = state.indexOf(null);
138
139 // cw rotation
140 while (rot >= 60) {
141 rot -= 60;
142 targetRot -= 60;
143 full.unshift(full.pop());
144 driven++;
145 }
146
147 // cw rotation
148 while (rot <= -60) {
149 rot += 60;
150 targetRot += 60;
151 full.push(full.shift());
152 driven = (driven-1+3) % 3;
153 }
154
155 state = full.slice(0, 3);
156 state[driven%3] = null;
157
158 updateValues();
159 };
160
161 export const getSteps = () => completeState(state);
162 export const getRot = () => rot / 180 * Math.PI;
0 const svg = document.getElementById('diagram');
1 const arrows = Array.from(svg.querySelectorAll('.arrow'));
2 const preset = document.getElementById('layout-preset');
3 const controls = Array.from(document.querySelectorAll('.axis-control'));
4 const steps = Array.from(document.querySelectorAll('.axis-control > input'));
5 const dirs = Array.from(document.querySelectorAll('.axis-control .dir input'));
6
7 let state = [ 7, 2, null ];
8 let last = 0;
9 let selected = null;
10
11 const completeState = ([a, b, c]) => {
12 if (a == null) {
13 a = b - c;
14 } else if (b == null) {
15 b = c + a;
16 } else {
17 c = b - a;
18 }
19 return [a, b, c, -a, -b, -c];
20 };
21
22 const updateValues = () => {
23 const full = completeState(state);
24
25 full.forEach((val, i) => {
26 arrows[i].querySelector('text').textContent = val;
27
28 if (i < steps.length) steps[i].value = Math.abs(val);
29 if (i < dirs.length) {
30 dirs[i].checked = val < 0;
31 dirs[i].disabled = val == 0;
32 }
33 });
34 updateFocus();
35 };
36
37 const updateFocus = () => {
38 const full = completeState(state);
39
40 controls.forEach((control, i) => {
41 let className = 'axis-control';
42 if (state[i] == null) className += ' driven';
43 else if (i == selected) className += ' selected';
44 if (state[i] == 0) className += ' dir-disabled';
45 control.className = className;
46 });
47
48 full.forEach((val, i) => {
49 const positive = val > 0 || (val == 0 && i < 3);
50
51 let className = 'arrow';
52 if (state[i%3] == null) className += ' driven';
53 if (!positive) className += ' negative';
54 if (i%3 == selected) className += ' selected';
55 arrows[i].className.baseVal = className;
56 });
57 };
58
59 const select = (i) => {
60 if (state[i] != null) return false;
61
62 const newState = [null, null, null];
63 newState[i] = completeState(state)[i];
64 newState[last] = state[last];
65 state = newState;
66 last = i;
67
68 return true;
69 };
70
71 arrows.forEach((arrow, i) => {
72 arrow.onclick = () => steps[i%3].focus();
73 });
74
75 controls.forEach((control, i) => {
76 control.addEventListener('focusin', () => { selected = i; select(i); updateFocus(); });
77 control.addEventListener('focusout', () => { selected = null; updateFocus(); });
78 });
79
80 steps.forEach((input, i) => {
81 input.onchange = () => {
82 select(i);
83 state[i] = +input.value;
84 if (dirs[i].checked) state[i] = -state[i];
85 preset.value = 'custom';
86 updateValues();
87 };
88 });
89
90 dirs.forEach((input, i) => {
91 input.onchange = () => {
92 select(i);
93 state[i] = state[i] * -1;
94 preset.value = 'custom';
95 updateValues();
96 };
97 });
98
99 let rot = 0;
100 let targetRot = 0;
101
102 document.getElementById('turn-ccw').onclick = () => {
103 targetRot -= 30;
104 };
105 document.getElementById('turn-cw').onclick = () => {
106 targetRot += 30;
107 };
108
109 preset.onchange = () => {
110 switch (preset.value) {
111 case 'custom': return;
112 case 'wicki-hayden': state = [ 7, 2, null ]; break;
113 case 'janko': state = [ 1, 2, null ]; break;
114 case 'harmonic': state = [ 7, 4, null ]; break;
115 case 'gerhard': state = [ 4, 3, null ]; break;
116 }
117
118 last = state.findIndex(s => s != null);
119 updateValues();
120 };
121 preset.value = 'wicki-hayden';
122 preset.onchange();
123
124 const bg = document.getElementById('canvas-bg').getContext('2d');
125 const fg = document.getElementById('canvas-fg').getContext('2d');
0 import * as layout from './layout.js';
1 import * as pattern from './pattern.js';
1262
1273 const sqrt3 = Math.sqrt(3);
1284 const sqrt32 = sqrt3 / 2;
1295
130 const size = 80;
6 const size = 50;
1317 const hs = size / 2;
1328 const hh = sqrt32 * size;
1339
134 const hex2px = (q, r) => {
10 const hex2px = ([q, r]) => {
13511 const x = size * (3/2 * r);
13612 const y = -size * (sqrt32 * r + sqrt3 * q);
13713 return [x, y];
13814 };
15 const px2hex = ([x, y]) => {
16 const t1 = x / size;
17 const t2 = -y / hh / 2;
13918
140 const mul = (x, y, mat) => {
19 const q = Math.floor( (Math.floor(-y / hh) + Math.floor(t2 - t1) + 2 ) / 3);
20 const r = Math.floor( (Math.floor(t1 - t2) + Math.floor(t1 + t2) + 2 ) / 3);
21
22 return [q, r];
23 }
24 const rotate = ([x, y], a) => {
14125 return [
142 x * mat[0] + y * mat[1] + mat[2],
143 y * mat[3] + y * mat[4] + mat[5],
26 Math.cos(a) * x + Math.sin(a) * y,
27 -Math.sin(a) * x + Math.cos(a) * y,
14428 ];
14529 };
30 const hexagon = (ctx) => {
31 ctx.beginPath();
32 ctx.moveTo(-hs, hh);
33 ctx.lineTo(hs, hh);
34 ctx.lineTo(size, 0);
35 ctx.lineTo(hs, -hh);
36 ctx.lineTo(-hs, -hh);
37 ctx.lineTo(-size, 0);
38 ctx.closePath();
39 };
14640
147 const updateRotation = () => {
148 const delta = targetRot - rot;
149 if (Math.abs(delta) < 1) rot = targetRot;
150 else rot += delta * 0.1;
151
152 if (rot < 60 && rot > -60) return;
41 const main = document.querySelector('main');
42 const bg = document.getElementById('canvas-bg').getContext('2d');
43 const fg = document.getElementById('canvas-fg').getContext('2d');
15344
154 const full = completeState(state);
155 let driven = state.indexOf(null);
45 let mousePos = [0, 0];
46 main.onmousemove = (e) => {
47 mousePos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
48 };
15649
157 // cw rotation
158 while (rot >= 60) {
159 rot -= 60;
160 targetRot -= 60;
161 full.unshift(full.pop());
162 driven++;
50 document.body.onkeydown = (e) => {
51 switch (e.key) {
52 case 'q': layout.turn(-1); break;
53 case 'e': layout.turn(1); break;
16354 }
55 };
16456
165 // cw rotation
166 while (rot <= -60) {
167 rot += 60;
168 targetRot += 60;
169 full.push(full.shift());
170 driven = (driven-1+3) % 3;
171 }
172
173 state = full.slice(0, 3);
174 state[driven%3] = null;
175
176 updateValues();
177 };
57 let lastCanvasSize = 0;
17858
17959 const updateBackground = (canvasSize) => {
18060 bg.strokeStyle = '#b9bdc1';
18969 const rMax = Math.min(rad, rad-q);
19070 for (let r = rMin; r <= rMax; r++) {
19171 bg.save();
192 bg.translate(...hex2px(q, r));
72 bg.translate(...hex2px([q, r]));
19373
194 bg.beginPath();
195 bg.moveTo(-hs, hh);
196 bg.lineTo(hs, hh);
197 bg.lineTo(size, 0);
198 bg.lineTo(hs, -hh);
199 bg.lineTo(-hs, -hh);
200 bg.lineTo(-size, 0);
201 bg.closePath();
74 hexagon(bg);
20275 bg.stroke();
20376
20477 bg.restore();
20679 }
20780 };
20881
209 const main = document.querySelector('main');
210
211 let mousePos = [0, 0];
212 main.onmousemove = (e) => {
213 mousePos = [
214 e.clientX - main.clientWidth/2,
215 e.clientY - main.clientHeight/2,
216 ];
217 };
218
219 let lastCanvasSize = 0;
22082 const draw = () => {
22183 const width = window.innerWidth;
22284 const height = window.innerHeight;
23395
23496 fg.canvas.width = fg.canvas.width;
23597
236 updateRotation();
98 layout.update();
23799
238100 fg.translate(canvasSize/2, canvasSize/2);
239101
240 fg.fillStyle = '#303336';
241 fg.font = `${size*0.8}px sans-serif`;
102 fg.font = `${size*0.5}px sans-serif`;
242103 fg.textAlign = 'center';
104
105 fg.strokeStyle = '#ff0000';
106 fg.strokeWidth = 5;
243107
244 const rotRad = rot / 180 * Math.PI;
108
109 const rot = layout.getRot();
110 const [qq, rr] = layout.getSteps();
111
112 const steps = pattern.getSteps();
113 const length = pattern.getLength();
245114
246 const [qq, rr] = completeState(state);
247115 const rad = Math.ceil(canvasSize / size / 3);
248116 for (let q = -rad; q <= rad; q++) {
249117 const rMin = Math.max(-rad, -q-rad);
250118 const rMax = Math.min(rad, rad-q);
251119 for (let r = rMin; r <= rMax; r++) {
252120 fg.save();
253 fg.translate(...hex2px(q, r));
121 fg.translate(...hex2px([q, r]));
254122
255 const val = q*qq + r*rr;
256 fg.rotate(-rotRad);
257 fg.fillText(val, 0, size/3);
123 const note = q*qq + r*rr;
124 let step = note % length;
125 step = (step + length) % length;
126
127 if (steps.includes(note)) {
128 fg.fillStyle = '#eeeeee';
129 hexagon(fg);
130 fg.fill();
131 }
132
133 fg.rotate(-rot);
134 fg.fillStyle = '#303336';
135 fg.fillText(step, 0, size/3);
258136
259137 fg.restore();
260138 }
261139 }
262140
263 const mouse = [
264 Math.cos(rotRad) * mousePos[0] + Math.sin(rotRad) * mousePos[1],
265 -Math.sin(rotRad) * mousePos[0] + Math.cos(rotRad) * mousePos[1],
266 ];
141 // const mouse = px2hex(rotate(mousePos, rot));
142 // fg.arc(...hex2px(mouse), 10, 0, 2*Math.PI);
267143
268 fg.arc(mouse[0], mouse[1], 5, 0, 2*Math.PI);
269 fg.fill()
270
271 document.body.style.setProperty('--global-rot', `${rot}deg`);
144 document.body.style.setProperty('--global-rot', `${rot}rad`);
272145 requestAnimationFrame(draw);
273146 };
274147
0 const panel = document.querySelector('aside.pattern');
1
2 const preset = panel.querySelector('.control--preset');
3 const patternLength = document.getElementById('pattern-len');
4 const steps = panel.querySelector('.steps');
5
6 let length = 12;
7 let pattern = [0, 2, 4, 5, 7, 9, 11];
8
9 const update = () => {
10 while (steps.childElementCount > length) {
11 steps.lastElementChild.remove();
12 }
13
14 while (steps.childElementCount < length) {
15 const input = document.createElement('input');
16 input.type = 'checkbox';
17 steps.append(input);
18 }
19
20 Array.from(steps.children).forEach((toggle, i) => {
21 toggle.checked = pattern.includes(i);
22 });
23 };
24
25 patternLength.onchange = () => {
26 length = +patternLength.value;
27 pattern = pattern.filter(n => n < length);
28 update();
29 };
30
31 steps.onchange = () => {
32 pattern = [];
33 Array.from(steps.children).forEach((toggle, i) => {
34 if (toggle.checked) pattern.push(i);
35 });
36 };
37
38 preset.onchange = (e) => {
39 const value = e.target.value;
40 switch (value) {
41 case 'custom': return;
42 case 'major-7':
43 pattern = [0, 2, 4, 5, 7, 9, 11];
44 break;
45 case 'minor-7':
46 pattern = [0, 2, 3, 5, 7, 8, 10];
47 break;
48 case 'minor-harm-7':
49 pattern = [0, 2, 3, 5, 7, 8, 11];
50 break;
51 case 'minor-mel-7':
52 pattern = [0, 2, 3, 5, 7, 9, 11];
53 break;
54 case 'minor-hung-7':
55 pattern = [0, 2, 3, 6, 7, 8, 11];
56 break;
57 case 'penta':
58 pattern = [0, 2, 4, 7, 9];
59 break;
60 case 'major-3':
61 pattern = [0, 4, 7];
62 break;
63 case 'major-3+':
64 pattern = [0, 4, 8];
65 break;
66 case 'minor-3':
67 pattern = [0, 3, 7];
68 break;
69 case 'minor-3-':
70 pattern = [0, 3, 6];
71 break;
72 }
73 update();
74 preset.value = value;
75 };
76 preset.value = 'major-7';
77 preset.onchange({ target: preset });
78
79 export const getLength = () => length;
80 export const getSteps = () => pattern;
2121 transform: translate(-50%, -50%) rotate(var(--global-rot));
2222 }
2323
24 aside.controls {
24 aside {
2525 position: fixed;
26 bottom: 0;
2726 right: 0;
28
2927 display: flex;
30 height: 12rem;
31
3228 filter: drop-shadow(0 0 4px rgba(0,0,0,0.5));
3329 }
3430
35 aside.controls > svg {
31 aside.pattern {
32 top: 0;
33 }
34
35 aside.layout {
36 bottom: 0;
37
38 height: 12rem;
39 }
40
41 aside.layout > svg {
3642 width: auto;
3743 height: 100%;
3844 }
3945
40 aside.controls > div {
46 .controls {
4147 display: flex;
4248 flex-direction: column;
4349 justify-content: center;
4753 }
4854
4955 /* controls */
50 .axis-control,
51 .preset-control {
56 .control {
5257 display: flex;
5358 align-items: center;
5459 transition: background 0.3s;
5661 gap: 0.5rem;
5762 }
5863
59 .preset-control {
64 .control label:first-child {
65 width: 6rem;
66 }
67
68
69 .control--preset {
6070 padding-bottom: 0.5rem;
6171 border-bottom: 1px solid #b9bdc1;
6272 margin-bottom: 0.25rem;
6373 }
64 .preset-control select {
74
75 .control--preset select {
6576 flex: 1;
6677 }
6778
68 .preset-control label,
69 .axis-control label:first-child {
70 width: 6rem;
71 }
72 .axis-control label:first-child {
79
80 .control--axis label:first-child {
7381 flex: 1;
7482 }
7583
76 .axis-control > input {
84 .control--axis > input {
7785 width: 5em;
7886 }
7987
80 .axis-control.dir-disabled label.dir {
88 .control--axis.dir-disabled label.dir {
8189 color: #b9bdc1;
8290 }
8391
84 .axis-control.driven {
92 .control--axis.driven {
8593 opacity: 0.5;
8694 }
8795
88 .axis-control.selected {
96 .control--axis.selected {
8997 background: #eeeeee;
9098 }
9199