diff options
| author | s-ol <s+removethis@s-ol.nu> | 2023-05-06 22:07:32 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2023-05-07 09:53:29 +0000 |
| commit | 1c981aea4dbecb3ca77efbafac1be7f12012327d (patch) | |
| tree | 1a02a0f607ca9a095b80760beef5c9529ecd1eb7 | |
| parent | restart with v2 (diff) | |
| download | isomorphic-kb-explorer-1c981aea4dbecb3ca77efbafac1be7f12012327d.tar.gz isomorphic-kb-explorer-1c981aea4dbecb3ca77efbafac1be7f12012327d.zip | |
add basic pattern highlighting
| -rw-r--r-- | index.html | 47 | ||||
| -rw-r--r-- | layout.js | 163 | ||||
| -rw-r--r-- | main.js | 265 | ||||
| -rw-r--r-- | pattern.js | 81 | ||||
| -rw-r--r-- | style.css | 48 |
5 files changed, 381 insertions, 223 deletions
@@ -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; @@ -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; @@ -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; } |
