aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-10-04 12:19:25 +0000
committers-ol <s-ol@users.noreply.github.com>2020-10-04 12:19:25 +0000
commit1ab9ed5b297048c5d3bc0278eddeb5a1d1c80367 (patch)
tree929cd8dd9eaa99894e4f7da9b2ad0f6e5be1abd6 /app
downloadisomorphic-kb-explorer-1ab9ed5b297048c5d3bc0278eddeb5a1d1c80367.tar.gz
isomorphic-kb-explorer-1ab9ed5b297048c5d3bc0278eddeb5a1d1c80367.zip
initial commit
Diffstat (limited to 'app')
-rw-r--r--app/app.js155
-rw-r--r--app/config.js155
-rw-r--r--app/css.js13
-rw-r--r--app/index.js10
4 files changed, 333 insertions, 0 deletions
diff --git a/app/app.js b/app/app.js
new file mode 100644
index 0000000..27c0e38
--- /dev/null
+++ b/app/app.js
@@ -0,0 +1,155 @@
+import React from 'react';
+import css from './css';
+
+const statbyte = (stat, chan) => (stat << 4) | chan;
+const noteon = (key, vel=127, chan=0) => [statbyte(0b1001, chan), key, vel];
+const noteoff = (key, vel=127, chan=0) => [statbyte(0b1000, chan), key, vel];
+const ccchange = (ctl, val, chan=0) => [statbyte(0b1011, chan), ctl, val];
+
+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;
+
+window.ccchange = ccchange;
+window.noteon = noteon;
+
+const parse = message => ({
+ cmd: message.data[0] >> 4,
+ chan: message.data[0] & 0xf,
+ note: message.data[1],
+ val: message.data[2] / 127,
+});
+
+const longSize = 3;
+const rowStaggered = true;
+const w = longSize * (rowStaggered ? Math.sqrt(3) : 2 );
+const h = longSize * (rowStaggered ? 2 : Math.sqrt(3));
+const sx = longSize * (rowStaggered ? Math.sqrt(3) : 1.5 );
+const sy = longSize * (rowStaggered ? 1.5 : Math.sqrt(3));
+const rem = (n) => `${n}rem`;
+
+css(`
+body {
+ background: #212121;
+ margin: 2rem;
+
+ font-family: sans-serif;
+}
+
+.app {
+ position: relative;
+}
+
+.hexagon {
+ position: absolute;
+ width: ${rem(w)};
+ height: ${rem(h)};
+}
+
+.hexagon > .inner {
+ margin: auto;
+ width: ${rem(Math.min(w, h) * 0.8)};
+ height: ${rem(Math.min(w, h) * 0.8)};
+ padding: ${rem(longSize * 0.4)};
+ border-radius: ${rem(longSize)};
+ box-sizing: border-box;
+ font-size: ${rem(longSize * 0.5)};
+ text-align: center;
+ background: #696969;
+ color: #eeeeee;
+ cursor: pointer;
+
+ transition: background 300ms;
+}
+.hexagon > .inner:hover {
+ background: #848484;
+}
+.hexagon > .inner:active,
+.hexagon.active > .inner {
+ background: #eeeeee;
+ color: #696969;
+}
+`);
+
+const Hexagon = ({ x, y, children, state }) => {
+ if (rowStaggered) {
+ if (y % 2 == 0)
+ x += 0.5;
+ } else {
+ if (x % 2 == 0)
+ y += 0.5;
+ }
+
+ return (
+ <div
+ className={'hexagon' + (state ? ' active' : '')}
+ style={{
+ top: rem(y * sy),
+ left: rem(x * sx),
+ }}
+ >
+ <div
+ className="inner"
+ onClick={() => console.info(x,y)}
+ >
+ {children}
+ </div>
+ </div>
+ );
+};
+
+
+const range = i => new Array(i).fill(true);
+
+export default class App extends React.Component {
+ state = {};
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.midiin)
+ prevProps.midiin.onmidimessage = null;
+
+ if (this.props.midiin)
+ this.props.midiin.onmidimessage = this.onmessage;
+ }
+
+ onmessage = (e) => {
+ const message = parse(e);
+
+ switch (message.cmd) {
+ case 9:
+ // NOTE_ON
+ this.setState(old => ({ ...old, [message.note]: true }));
+ break;
+
+ case 8:
+ // NOTE_OFF
+ this.setState(old => ({ ...old, [message.note]: false }));
+ break;
+
+ case 11:
+ // CC
+ break;
+ }
+ }
+
+ render() {
+ const xs = 10;
+ const ys = 4;
+
+ return (
+ <div className="app">
+ {range(xs).map((_, x) => (
+ range(ys).map((_, y) => {
+ let note = 36 + 2 * x + 6 * (ys - y);
+ if (y % 2 == 0) note += 1;
+ return (
+ <Hexagon key={x + ',' + y} x={x} y={y} note={note} state={this.state[note]}>
+ {note}
+ </Hexagon>
+ );
+ })
+ ))}
+ </div>
+ );
+ }
+};
diff --git a/app/config.js b/app/config.js
new file mode 100644
index 0000000..e5f2b0c
--- /dev/null
+++ b/app/config.js
@@ -0,0 +1,155 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import css from './css';
+
+const setState = (key, state) => {
+ try {
+ localStorage.setItem(key, JSON.stringify(state));
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+};
+
+const loadState = (key, defaultState=null) => {
+ try {
+ const data = window.localStorage.getItem(key);
+ return Object.assign({}, defaultState, data && JSON.parse(data));
+ }
+ catch (e) {
+ return defaultState;
+ }
+};
+
+css(`
+ .settings {
+ width: 500px;
+ margin: auto;
+ padding: 1em;
+ text-align: left;
+ }
+
+ .setting {
+ margin: 0.4em 0;
+ }
+
+ .setting > label {
+ display: inline-block;
+ width: 200px;
+ }
+
+ .setting > select, .setting > input, .settings button {
+ width: 200px;
+ margin-left: 20px;
+ }
+
+ .setting > select:disabled {
+ color: #aaa;
+ background: #444;
+ }
+
+ .settings > button {
+ text-align: center;
+ margin-right: 20px;
+ }
+`);
+
+const Dropdown = ({ name, list, set, disabled, value }) => (
+ <div className="setting">
+ <label>{name}</label>
+ <select value={value} onChange={e => set(e, e.target.value)} disabled={!list || disabled}>
+ <option>(none)</option>
+ {list && ([...list.values()]).map((port) => (
+ <option value={port.id} key={port.id}>{port.name}</option>
+ ))}
+ </select>
+ </div>
+);
+
+const Checkbox = ({ name, set, value }) => (
+ <div className="setting">
+ <label>{name}</label>
+ <input
+ type="checkbox"
+ onChange={e => set(e, !value)}
+ checked={value}
+ />
+ </div>
+);
+
+export default WrappedComponent =>
+ class SettingsStore extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = loadState('settings', { configured: false, midiSupport: false });
+ this.configure = () => this.setState({ configured: false });
+
+ this.reload = () => {
+ navigator.requestMIDIAccess({ sysex: true })
+ .then(access => {
+ this.inputs = access.inputs;
+ this.outputs = access.outputs;
+ this.setState({
+ midiSupport: true,
+ });
+ this.forceUpdate();
+ });
+ }
+ }
+
+ componentWillUpdate(props, state) {
+ setState('settings', state);
+ }
+
+ componentDidMount() {
+ if (navigator.requestMIDIAccess)
+ this.reload();
+ }
+
+ set(k, v) {
+ return (e, vv) => {
+ e.stopPropagation();
+
+ this.setState({ [k]: v === undefined
+ ? vv
+ : v });
+ }
+ }
+
+ render() {
+ const { configured, midiin, midiout } = this.state;
+
+ if (configured) {
+ return (
+ <WrappedComponent
+ {...this.props}
+ midiin={this.inputs && this.inputs.get(midiin)}
+ midiout={this.outputs && this.outputs.get(midiout)}
+ configure={this.configure}
+ />
+ );
+ }
+
+ return (
+ <div className="panel settings">
+ <h3>MIDI I/O</h3>
+ <Dropdown
+ name="MIDI Keyboard Input"
+ list={this.inputs}
+ set={this.set('midiin')}
+ value={midiin}
+ />
+ <Dropdown
+ name="MIDI Output"
+ list={this.outputs}
+ set={this.set('midiout')}
+ value={midiout}
+ />
+ <button onClick={this.reload}>reload MIDI device list</button>
+ <button onClick={this.set('configured', true)}>start</button>
+ </div>
+ );
+ };
+ };
diff --git a/app/css.js b/app/css.js
new file mode 100644
index 0000000..842214a
--- /dev/null
+++ b/app/css.js
@@ -0,0 +1,13 @@
+export default (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;
+};
diff --git a/app/index.js b/app/index.js
new file mode 100644
index 0000000..6e2a51e
--- /dev/null
+++ b/app/index.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './app';
+import config from './config';
+
+const $App = config(App);
+
+const node = document.createElement('div');
+ReactDOM.render(<$App />, node);
+document.body.appendChild(node);