diff options
| author | s-ol <s+removethis@s-ol.nu> | 2023-05-05 19:54:08 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2023-05-05 19:54:08 +0000 |
| commit | e447f2d9ca1de1a33d0d25c0a8395d72a4035553 (patch) | |
| tree | 3cf0650cdefcd08fabe7992ad8d02a6b3b40d2bd /app | |
| parent | upgrade dependencies (diff) | |
| download | isomorphic-kb-explorer-e447f2d9ca1de1a33d0d25c0a8395d72a4035553.tar.gz isomorphic-kb-explorer-e447f2d9ca1de1a33d0d25c0a8395d72a4035553.zip | |
restart with v2
Diffstat (limited to 'app')
| -rw-r--r-- | app/app.js | 364 | ||||
| -rw-r--r-- | app/config.js | 168 | ||||
| -rw-r--r-- | app/css.js | 1 | ||||
| -rw-r--r-- | app/index.js | 9 | ||||
| -rw-r--r-- | app/notes.js | 57 | ||||
| -rw-r--r-- | app/style.js | 162 |
6 files changed, 0 insertions, 761 deletions
diff --git a/app/app.js b/app/app.js deleted file mode 100644 index 17cdb18..0000000 --- a/app/app.js +++ /dev/null @@ -1,364 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { css, pos, width, height } from './style'; -import * as notes from './notes'; -import { useAppState, useMIDI, Toolbar } from './config'; - -const statbyte = (stat, chan) => (stat << 4) | chan; -const CHANNEL = 0b1011; -const ALL_NOTES_OFF = 0x7B; -const ALL_SOUND_OFF = 0x78; -const NOTE_ON = 0b1001; -const NOTE_OFF = 0b1000; -const CCHANGE = 0b1011; - -const clamp = (val, min=0, max=1) => Math.max(min, Math.min(val, max)); -const ramp = i => new Array(i).fill(true).map((_, i) => i); -const mix = (i, a, b) => i * a + (1-i) * b; - -const parse = message => ({ - cmd: message.data[0] >> 4, - chan: message.data[0] & 0xf, - note: message.data[1], - val: message.data[2] / 127, -}); - -const Hexagon = ({ x, y, children, state, note, major, scale, noteon, noteoff }) => { - if (major == 'row') { - if (y % 2 == 0) - x += 0.5; - } else { - if (x % 2 == 0) - y += 0.5; - } - - return ( - <div - className={`hexagon ${state ? ' active' : ''} ${scale}`} - style={pos([x, y], major)} - > - <div - onMouseDown={() => noteon(note)} - onMouseUp={() => noteoff(note)} - > - {children} - </div> - </div> - ); -}; - -// B A -// A B A C B -// o C o o -// C - - -const layouts = { - wicki_hayden: { - major: 'row', - map: ([x, y], [w, h]) => { - // A = 5 - // B = 7 - // C = 2 = x - // A + B = 12 = y - let note = 5 + 2 * x + 6 * y; - if (y % 2 == 0) note += 1; - return note; - }, - }, - janko: { - major: 'row', - map: ([x, y], [w, h]) => { - let note = 5 + 2 * x; - if (y % 2 == 0) note += 1; - return note; - }, - }, - trad_piano: { - major: 'row', - map: ([x, y], [w, h]) => { - const pitches = [0, 1, 2, 3, 4, -1, 5, 6, 7, 8, 9, 10, 11, -1]; - - let i = 2 * x; - if (y % 2 == 1) i -= 1; - - const pitch = pitches[i % 14]; - if (pitch < 0) return pitch; - - const oct = Math.floor(i / 14) + Math.floor(y / 2); - return 10 + pitch + 12 * oct; - }, - }, - harmonic: { - // A = 3 - // B = 7 = y - // C = 4 - // C - A = 1 = x - - // A = 7 = y - // B = 4 - // C = -3 - // B + C = 1 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 0.5 + y * 7; - if (x % 2 === 1) note -= 3.5; - return note; - }, - }, - gerhard: { - // A = -3 - // B = 1 - // C = 4 - // - // A = 1 = y - // B = 4 - // C = 3 - // B + C = 7 = x - major: 'col', - map: ([x, y], [w, h]) => { - let note = 48 + x * 3.5 + y; - if (x % 2 === 1) note -= 0.5; - return note; - }, - }, -}; - -const scales = { - major: [2, 2, 1, 2, 2, 2, 1], - minor_nat: [2, 1, 2, 2, 1, 2, 2], - minor_harm: [2, 1, 2, 2, 1, 3, 1], - minor_mel: [2, 1, 2, 2, 2, 2, 1], - minor_hung: [2, 1, 3, 1, 1, 3, 1], - whole: [2, 2, 2, 2, 2, 2], - penta: [2, 2, 3, 2, 3], -}; - -const range = i => new Array(i).fill(true); -const tallyup = (steps, offset=0) => { - const notes = []; - - let sum = 0; - for (let i = 0; i < steps.length; i++) { - notes[i] = offset + sum; - sum += steps[i]; - } - - if (sum != 12) throw new Error(`scale doesn't tally up: ${steps} / ${notes}`); - - return { - notes, - modulo: notes.map(n => n % 12), - }; -}; - -const Keyboard = ({ w, h, layout: { major, map }, state, scale, transpose, labels, noteon, noteoff }) => ( - <div - className={`keyboard ${major}-major`} - style={{ - height: height(h, major), - width: width(w, major), - }} - > - {range(w).map((_, x) => ( - range(h).map((_, y) => { - const rawNote = map([x, y], [w, h]); - const disabled = rawNote < 0; - const note = disabled ? -1 : rawNote + transpose; - const onCore = !disabled && scale && scale.notes.indexOf(note) >= 0; - const onScale = !disabled && scale && scale.modulo.indexOf(note % 12) >= 0; - return ( - <Hexagon - key={x + ',' + y} - x={x} y={y} note={note} - state={state[note]} - scale={onCore ? "core" : (onScale ? "" : "disabled")} - noteon={noteon} - noteoff={noteoff} - major={major} - > - {!disabled && (labels ? labels[note % 12] : note)} - </Hexagon> - ); - }) - ))} - </div> -); - -const ChordView = ({ chord }) => { - const min = chord[0]; - - return ( - <div className="chord"> - {range(13).map((_,i) => ( - <div - key={i} - className="blip" - /> - ))} - {chord.map((n) => ( - <div - key={n} - className="note" - style={{ bottom: `${((n - min) % 12) / 12 * 100 - 3}%` }} - > - {(n - min) && (<span>+{n - min}</span>)} - </div> - ))} - </div> - ); -}; -; - -css(` -main { - position: relative; - padding: 0.5em; - margin: 1em 0; - - display: flex; - justify-content: space-evenly; -} - -main .focus-msg { - position: absolute; - inset: 0; - - display: flex; - font-size: 5em; - justify-content: space-around; - align-items: center; - text-align: center; - - border: 4px solid white; - background: #212121; - pointer-events: none; - - opacity: 0.8; - transition: opacity 0.4s; -} - -main:focus-within .focus-msg { - opacity: 0; -} -`); - -export default () => { - const [settings, setSettings] = useAppState({ - layout: 'wicki_hayden', - scale: 'major', - labels: 'english', - offset: 60, - transpose: 3*12, - w: 12, - h: 4, - }); - const [state, setState] = useState({}); - const midi = useMIDI(); - - const ref = React.createRef(); - - const midiin = midi.inputs.get(settings.midiin); - const midiout = midi.outputs.get(settings.midiout); - - const send = (command, note, vel=127) => { - if (!midiout) return; - - const msg = [statbyte(command, 0), note, vel]; - midiout.send(msg); - }; - - const noteon = (note) => { - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: note })); - } - - setState((s) => ({ ...s, [note]: true })); - send(NOTE_ON, note); - } - - const noteoff = (note) => { - setState((s) => ({ ...s, [note]: false })); - send(NOTE_OFF, note); - } - - useEffect(() => { - ref.current.onkeydown = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteon(note); - } - - ref.current.onkeyup = (e) => { - if (e.repeat) return; - - const note = notes.key2midi[e.code]; - if (!note) return; - e.preventDefault(); - noteoff(note); - } - }); - - useEffect(() => { - send(CHANNEL, ALL_SOUND_OFF, 0); - send(CHANNEL, ALL_NOTES_OFF, 0); - }, [midiout]); - - useEffect(() => { - if (!midiin) return; - - midiin.onmidimessage = (e) => { - const message = parse(e); - - switch (message.cmd) { - case NOTE_ON: - if (settings.offset === null) { - setSettings((s) => ({ ...s, offset: message.note })); - } - - setState((s) => ({ ...s, [message.note]: true })); - break; - - case NOTE_OFF: - setState((s) => ({ ...s, [message.note]: false })); - break; - } - } - - return () => { - midiin.onmidimessage = null; - }; - }, [midiin]); - - const { scale, layout, labels, offset } = settings; - const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k); - chord.sort(); - - return ( - <div className="app"> - <Toolbar - state={settings} - setState={setSettings} - midi={midi} - /> - - <main ref={ref} tabIndex="0"> - <Keyboard - {...settings} - noteon={noteon} - noteoff={noteoff} - layout={layouts[layout]} - scale={scale !== "none" && offset !== null && tallyup(scales[scale], offset)} - labels={notes.labels[labels]} - state={state} - /> - <ChordView chord={chord} /> - <div className="focus-msg"> - <span>click here to activate<br/>keyboard input</span> - </div> - </main> - </div> - ); -}; diff --git a/app/config.js b/app/config.js deleted file mode 100644 index 7bf3a1b..0000000 --- a/app/config.js +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { labels as noteLabels } from './notes'; -import { css } from './style'; - -const saveState = (key, state) => { - try { - localStorage.setItem(key, JSON.stringify(state)); - } - catch (e) { - console.error(e); - } -}; - -const loadState = (key, defaultState=null) => { - try { - const data = window.localStorage.getItem(key); - return Object.assign({}, defaultState, data && JSON.parse(data)); - } - catch (e) { - return defaultState; - } -}; - -export const useAppState = (defaultState) => { - const [state, setState] = useState(defaultState); - - useEffect(() => setState(loadState('settings', defaultState)), []); - useEffect(() => saveState('settings', state), [state]); - - return [state, setState]; -}; - -export const useMIDI = () => { - const [inputs, setInputs] = useState(new Map()); - const [outputs, setOutputs] = useState(new Map()); - - const reload = useCallback(() => { - navigator.requestMIDIAccess && - navigator.requestMIDIAccess({ sysex: true }) - .then((access) => { - setInputs(access.inputs); - setOutputs(access.outputs); - }); - }); - - useEffect(reload, []); - - return { inputs, outputs, reload }; -} - -const Dropdown = ({ list, name, onChange, value }) => ( - <select value={value} onChange={onChange} disabled={!list || !list.size}> - <option>(none)</option> - {list && ([...list.values()]).map((port) => ( - <option value={port.id} key={port.id}>{port.name}</option> - ))} - </select> -); - -css(` -nav { - display: flex; - flex-wrap: wrap; - justfiy-content: space-between; -} - -nav > .group { - flex: 1 0 auto; - - display: flex; - justify-content: center; - align-items: baseline; - padding: 0.25em 1em; - gap: 1em; - - border: 0 solid #363636; - border-width: 1px 0 1px 0; -} - -nav > .group:nth-child(2n) { - background: #363636; -} -`); - -export const Toolbar = ({ state, setState, midi }) => { - const { labels, offset, transpose } = state; - - const track = (key) => ({ - name: key, - value: state[key], - onChange: (e) => { - e.stopPropagation(); - let value = e.target.value; - if (key === 'w' || key === 'h') value = +value; - setState({ ...state, [key]: value }); - }, - }); - - return ( - <nav> - <div className="group"> - <label>layout:</label> - <select {...track('layout')}> - <option value="wicki_hayden">Wicki-Hayden</option> - <option value="harmonic">Harmonic Table</option> - <option value="gerhard">Gerhard</option> - <option value="janko">Jankó</option> - <option value="trad_piano">Traditional Piano</option> - </select> - </div> - <div className="group"> - <label>note format:</label> - <select {...track('labels')}> - <option value="english">English</option> - <option value="german">German</option> - <option value="sol">Solfège</option> - <option value="midi">MIDI no</option> - </select> - </div> - <div className="group"> - <label>scale:</label> - <button onClick={() => setState({ ...state, offset: null })}> - {offset === null - ? "listening..." - : (noteLabels[labels] ? noteLabels[labels][offset % 12] : offset)} - </button> - <select {...track('scale')}> - <option value="none">None</option> - <option value="major">Major</option> - <option value="minor_nat">Natural Minor</option> - <option value="minor_harm">Harmonic Minor</option> - <option value="minor_mel">Melodic Minor</option> - <option value="minor_hung">Hungarian Minor</option> - <option value="whole">Whole-Tone</option> - <option value="penta">Pentatonic</option> - </select> - </div> - <div className="group"> - <label>octave:</label> - <button onClick={() => setState({ ...state, transpose: transpose - 12 })}>-</button> - {Math.floor(transpose / 12)} - <button onClick={() => setState({ ...state, transpose: transpose + 12 })}>+</button> - </div> - <div className="group"> - <label>size:</label> - <input className="small" type="number" min="1" {...track('w')} /> - {'x'} - <input className="small" type="number" min="1" {...track('h')} /> - </div> - <div className="group"> - <label>midi</label> - in: - <Dropdown - name="midiin" - list={midi.inputs} - {...track('midiin')} - /> - out: - <Dropdown - name="midiout" - list={midi.outputs} - {...track('midiout')} - /> - <button onClick={midi.reload}>↻</button> - </div> - </nav> - ); -}; diff --git a/app/css.js b/app/css.js deleted file mode 100644 index 8b13789..0000000 --- a/app/css.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/index.js b/app/index.js deleted file mode 100644 index 75a2a9f..0000000 --- a/app/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './app'; - -const $App = App; - -const node = document.createElement('div'); -ReactDOM.render(<$App />, node); -document.body.appendChild(node); diff --git a/app/notes.js b/app/notes.js deleted file mode 100644 index bd124a5..0000000 --- a/app/notes.js +++ /dev/null @@ -1,57 +0,0 @@ -export const labels = { - english: 'C C# D D# E F F# G G# A A# B'.split(' '), - german: 'C C# D D# E F F# G G# A A# H'.split(' '), - sol: 'do do# re re# mi fa fa# sol sol# la la# si'.split(' '), -}; - -export const key2midi = { - 'KeyZ': 49, - 'KeyX': 51, - 'KeyC': 53, - 'KeyV': 55, - 'KeyB': 57, - 'KeyN': 59, - 'KeyM': 61, - 'Comma': 63, - 'Period': 65, - 'Slash': 67, - - 'KeyA': 54, - 'KeyS': 56, - 'KeyD': 58, - 'KeyF': 60, - 'KeyG': 62, - 'KeyH': 64, - 'KeyJ': 66, - 'KeyK': 68, - 'KeyL': 70, - 'Semicolon': 72, - 'Quote': 74, - 'Backslash': 76, - - 'KeyQ': 59, - 'KeyW': 61, - 'KeyE': 63, - 'KeyR': 65, - 'KeyT': 67, - 'KeyY': 69, - 'KeyU': 71, - 'KeyI': 73, - 'KeyO': 75, - 'KeyP': 77, - 'BracketLeft': 79, - 'BracketRight': 81, - - 'Digit1': 64, - 'Digit2': 66, - 'Digit3': 68, - 'Digit4': 70, - 'Digit5': 72, - 'Digit6': 74, - 'Digit7': 76, - 'Digit8': 78, - 'Digit9': 80, - 'Digit0': 82, - 'Minus': 84, - 'Equal': 86, -}; diff --git a/app/style.js b/app/style.js deleted file mode 100644 index 26a6ce6..0000000 --- a/app/style.js +++ /dev/null @@ -1,162 +0,0 @@ -export const css = (code) => { - const style = document.createElement('style'); - document.head.appendChild(style); - style.appendChild(document.createTextNode('')); - - let i = 0; - for (const v of code.split('}')) { - if (v.indexOf('{') < 0) continue; - style.sheet.insertRule(v + '}', i++); - } - - return style; -}; - -const longSize = 3; -const sizeA = longSize * Math.sqrt(3); -const sizeB = longSize * 1.5; -const strideA = longSize * Math.sqrt(3); -const strideB = longSize * 1.5; -const rem = (n) => `${n}rem`; - -export const pos = ([x, y], major) => { - let [sx, sy] = major === 'row' - ? [strideA, strideB] - : [strideB, strideA]; - return { - bottom: rem(y * sy), - left: rem(x * sx), - }; -}; - -export const width = (w, major) => { - if (major === 'row') - return rem((w + 0.5) * strideA); - - return rem((w + 0.5) * strideB); -}; - -export const height = (h, major) => { - if (major === 'row') - return rem((h - 0.5) * strideA); - - return rem((h + 1.5) * strideB); -}; - -css(` -body { - color: #eeeeee; - background: #212121; - margin: 2rem; - - font-family: sans-serif; -} - -button, input, select { - background: #363636; - border: 1px solid #eeeeee; - color: #eeeeee; - padding: 0.25em; - border-radius: 0.5em; -} -button:disabled, input:disabled, select:disabled { - opacity: 0.75; -} - -input.small { - width: 4em; -} - -button { - padding: 0.25em 0.5em; -} - -label { - display: inline-block; - font-weight: bold; -} - -.app, .keyboard, .chord { - position: relative; -} - -.hexagon { - position: absolute; - width: ${rem(longSize * 1.4)}; - height: ${rem(longSize * 1.4)}; - border-radius: ${rem(longSize)}; - - box-sizing: border-box; - font-size: ${rem(longSize * 0.5)}; - text-align: center; - background: #696969; - border: 3px solid #696969; - color: #eeeeee; - cursor: pointer; - - display: flex; - justify-content: center; - align-items: center; - - transition: background 300ms, border 300ms; -} - -.hexagon.disabled { - background: #363636 !important; - border-color: #363636; - color: #848484; -} -.hexagon.core { - border-color: #eeeeee; -} - -.hexagon:hover { - background: #848484; - border-color: #848484; -} -.hexagon:active, -.hexagon.active { - background: #eeeeee; - border-color: #eeeeee; - color: #696969; -} - -.chord { - display: flex; - flex-direction: column; - justify-content: space-between; - margin: 2rem 0; - width: 2rem; -} - -.chord .blip { - height: 1px; - margin: auto 0 0; - background: #eeeeee; - opacity: 0.75; -} - -.chord .blip:nth-child(2n) { - margin: auto 0.3rem 0; - opacity: 0.5; -} -.chord .blip:first-child { - margin-top: 0; -} - -.chord .note { - position: absolute; - left: 0; - right: 0; - margin: auto; - height: 6%; - aspect-ratio: 1; - background: #eeeeee; - border-radius: 100%; -} - -.chord .note span { - position: absolute; - left: 2rem; -} -`); |
