aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2023-05-05 19:54:08 +0000
committers-ol <s+removethis@s-ol.nu>2023-05-05 19:54:08 +0000
commite447f2d9ca1de1a33d0d25c0a8395d72a4035553 (patch)
tree3cf0650cdefcd08fabe7992ad8d02a6b3b40d2bd /app
parentupgrade dependencies (diff)
downloadisomorphic-kb-explorer-e447f2d9ca1de1a33d0d25c0a8395d72a4035553.tar.gz
isomorphic-kb-explorer-e447f2d9ca1de1a33d0d25c0a8395d72a4035553.zip
restart with v2
Diffstat (limited to 'app')
-rw-r--r--app/app.js364
-rw-r--r--app/config.js168
-rw-r--r--app/css.js1
-rw-r--r--app/index.js9
-rw-r--r--app/notes.js57
-rw-r--r--app/style.js162
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;
-}
-`);