add basic pattern highlighting
s-ol
24 days ago
9 | 9 | <canvas id="canvas-bg"></canvas> |
10 | 10 | <canvas id="canvas-fg"></canvas> |
11 | 11 | </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"> | |
13 | 46 | <svg |
14 | 47 | id="diagram" |
15 | 48 | viewBox="-120 -145 240 260" |
64 | 97 | id="turn-cw" class="turn" transform="rotate(1.5)" |
65 | 98 | 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 " /> |
66 | 99 | </svg> |
67 | <div> | |
68 | <div class="preset-control"> | |
100 | <div class="controls"> | |
101 | <div class="control control--preset"> | |
69 | 102 | <label for="layout-preset">preset</label> |
70 | 103 | <select id="layout-preset"> |
71 | 104 | <option value="wicki-hayden">Wicki/Hayden</option> |
76 | 109 | </select> |
77 | 110 | </label> |
78 | 111 | </div> |
79 | <div class="axis-control"> | |
112 | <div class="control control--axis"> | |
80 | 113 | <label for="step-1">up</label> |
81 | 114 | <input id="step-1" type="number" min="1" /> |
82 | 115 | <label class="dir"> |
84 | 117 | invert |
85 | 118 | </label> |
86 | 119 | </div> |
87 | <div class="axis-control"> | |
120 | <div class="control control--axis"> | |
88 | 121 | <label for="step-2">hi right</label> |
89 | 122 | <input id="step-2" type="number" min="1" /> |
90 | 123 | <label class="dir"> |
92 | 125 | invert |
93 | 126 | </label> |
94 | 127 | </div> |
95 | <div class="axis-control"> | |
128 | <div class="control control--axis"> | |
96 | 129 | <label for="step-3">low right</label> |
97 | 130 | <input id="step-3" type="number" min="1" /> |
98 | 131 | <label class="dir"> |
102 | 135 | </div> |
103 | 136 | </div> |
104 | 137 | </aside> |
105 | <script src="main.js"></script> | |
138 | <script type="module" src="main.js"></script> | |
106 | 139 | </body> |
107 | 140 | </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'; | |
126 | 2 | |
127 | 3 | const sqrt3 = Math.sqrt(3); |
128 | 4 | const sqrt32 = sqrt3 / 2; |
129 | 5 | |
130 | const size = 80; | |
6 | const size = 50; | |
131 | 7 | const hs = size / 2; |
132 | 8 | const hh = sqrt32 * size; |
133 | 9 | |
134 | const hex2px = (q, r) => { | |
10 | const hex2px = ([q, r]) => { | |
135 | 11 | const x = size * (3/2 * r); |
136 | 12 | const y = -size * (sqrt32 * r + sqrt3 * q); |
137 | 13 | return [x, y]; |
138 | 14 | }; |
15 | const px2hex = ([x, y]) => { | |
16 | const t1 = x / size; | |
17 | const t2 = -y / hh / 2; | |
139 | 18 | |
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) => { | |
141 | 25 | 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, | |
144 | 28 | ]; |
145 | 29 | }; |
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 | }; | |
146 | 40 | |
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'); | |
153 | 44 | |
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 | }; | |
156 | 49 | |
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; | |
163 | 54 | } |
55 | }; | |
164 | 56 | |
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; | |
178 | 58 | |
179 | 59 | const updateBackground = (canvasSize) => { |
180 | 60 | bg.strokeStyle = '#b9bdc1'; |
189 | 69 | const rMax = Math.min(rad, rad-q); |
190 | 70 | for (let r = rMin; r <= rMax; r++) { |
191 | 71 | bg.save(); |
192 | bg.translate(...hex2px(q, r)); | |
72 | bg.translate(...hex2px([q, r])); | |
193 | 73 | |
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); | |
202 | 75 | bg.stroke(); |
203 | 76 | |
204 | 77 | bg.restore(); |
206 | 79 | } |
207 | 80 | }; |
208 | 81 | |
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; | |
220 | 82 | const draw = () => { |
221 | 83 | const width = window.innerWidth; |
222 | 84 | const height = window.innerHeight; |
233 | 95 | |
234 | 96 | fg.canvas.width = fg.canvas.width; |
235 | 97 | |
236 | updateRotation(); | |
98 | layout.update(); | |
237 | 99 | |
238 | 100 | fg.translate(canvasSize/2, canvasSize/2); |
239 | 101 | |
240 | fg.fillStyle = '#303336'; | |
241 | fg.font = `${size*0.8}px sans-serif`; | |
102 | fg.font = `${size*0.5}px sans-serif`; | |
242 | 103 | fg.textAlign = 'center'; |
104 | ||
105 | fg.strokeStyle = '#ff0000'; | |
106 | fg.strokeWidth = 5; | |
243 | 107 | |
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(); | |
245 | 114 | |
246 | const [qq, rr] = completeState(state); | |
247 | 115 | const rad = Math.ceil(canvasSize / size / 3); |
248 | 116 | for (let q = -rad; q <= rad; q++) { |
249 | 117 | const rMin = Math.max(-rad, -q-rad); |
250 | 118 | const rMax = Math.min(rad, rad-q); |
251 | 119 | for (let r = rMin; r <= rMax; r++) { |
252 | 120 | fg.save(); |
253 | fg.translate(...hex2px(q, r)); | |
121 | fg.translate(...hex2px([q, r])); | |
254 | 122 | |
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); | |
258 | 136 | |
259 | 137 | fg.restore(); |
260 | 138 | } |
261 | 139 | } |
262 | 140 | |
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); | |
267 | 143 | |
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`); | |
272 | 145 | requestAnimationFrame(draw); |
273 | 146 | }; |
274 | 147 |
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; |
21 | 21 | transform: translate(-50%, -50%) rotate(var(--global-rot)); |
22 | 22 | } |
23 | 23 | |
24 | aside.controls { | |
24 | aside { | |
25 | 25 | position: fixed; |
26 | bottom: 0; | |
27 | 26 | right: 0; |
28 | ||
29 | 27 | display: flex; |
30 | height: 12rem; | |
31 | ||
32 | 28 | filter: drop-shadow(0 0 4px rgba(0,0,0,0.5)); |
33 | 29 | } |
34 | 30 | |
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 { | |
36 | 42 | width: auto; |
37 | 43 | height: 100%; |
38 | 44 | } |
39 | 45 | |
40 | aside.controls > div { | |
46 | .controls { | |
41 | 47 | display: flex; |
42 | 48 | flex-direction: column; |
43 | 49 | justify-content: center; |
47 | 53 | } |
48 | 54 | |
49 | 55 | /* controls */ |
50 | .axis-control, | |
51 | .preset-control { | |
56 | .control { | |
52 | 57 | display: flex; |
53 | 58 | align-items: center; |
54 | 59 | transition: background 0.3s; |
56 | 61 | gap: 0.5rem; |
57 | 62 | } |
58 | 63 | |
59 | .preset-control { | |
64 | .control label:first-child { | |
65 | width: 6rem; | |
66 | } | |
67 | ||
68 | ||
69 | .control--preset { | |
60 | 70 | padding-bottom: 0.5rem; |
61 | 71 | border-bottom: 1px solid #b9bdc1; |
62 | 72 | margin-bottom: 0.25rem; |
63 | 73 | } |
64 | .preset-control select { | |
74 | ||
75 | .control--preset select { | |
65 | 76 | flex: 1; |
66 | 77 | } |
67 | 78 | |
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 { | |
73 | 81 | flex: 1; |
74 | 82 | } |
75 | 83 | |
76 | .axis-control > input { | |
84 | .control--axis > input { | |
77 | 85 | width: 5em; |
78 | 86 | } |
79 | 87 | |
80 | .axis-control.dir-disabled label.dir { | |
88 | .control--axis.dir-disabled label.dir { | |
81 | 89 | color: #b9bdc1; |
82 | 90 | } |
83 | 91 | |
84 | .axis-control.driven { | |
92 | .control--axis.driven { | |
85 | 93 | opacity: 0.5; |
86 | 94 | } |
87 | 95 | |
88 | .axis-control.selected { | |
96 | .control--axis.selected { | |
89 | 97 | background: #eeeeee; |
90 | 98 | } |
91 | 99 |