aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2021-12-20 15:22:57 +0000
committers-ol <s+removethis@s-ol.nu>2021-12-20 15:22:57 +0000
commit930c3a7c40e1b1e5fa834167d4fc79a067c322f4 (patch)
treef7e850b53c7690d521951ae4b2909f6f1ce52b7f
parentadd blink function (diff)
downloadisomorphic-kb-explorer-930c3a7c40e1b1e5fa834167d4fc79a067c322f4.tar.gz
isomorphic-kb-explorer-930c3a7c40e1b1e5fa834167d4fc79a067c322f4.zip
add chord display
-rw-r--r--app/app.js189
-rw-r--r--app/notes.js54
-rw-r--r--app/style.js59
3 files changed, 222 insertions, 80 deletions
diff --git a/app/app.js b/app/app.js
index 5fa04b4..172a908 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { pos, height } from './style';
-import notes from './notes';
+import { pos, width, height } from './style';
+import * as notes from './notes';
const statbyte = (stat, chan) => (stat << 4) | chan;
const NOTE_ON = 0b1001;
@@ -78,61 +78,74 @@ const layouts = {
const range = i => new Array(i).fill(true);
-class Keyboard extends React.Component {
- state = {};
+const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => (
+ <div
+ className={`keyboard ${major}-major`}
+ style={{
+ height: height(h, major),
+ width: width(w, major),
+ }}
+ >
+ {range(w).map((_, x) => (
+ range(h).map((_, y) => {
+ const note = map([x, y], [w, h]);
+ return (
+ <Hexagon
+ key={x + ',' + y}
+ x={x} y={y}
+ note={note}
+ state={state[note]}
+ send={send}
+ major={major}
+ >
+ {showMidi ? note : notes.music[note % 12]}
+ </Hexagon>
+ );
+ })
+ ))}
+ </div>
+);
+
+const ChordView = ({ chord }) => {
+ const min = chord[0];
- onmessage = (message) => {
- switch (message.cmd) {
- case NOTE_ON:
- this.setState(old => ({ ...old, [message.note]: true }));
- break;
-
- case NOTE_OFF:
- this.setState(old => ({ ...old, [message.note]: false }));
- break;
- }
- }
-
- render() {
- const { w, h, send, layout, showMidi } = this.props;
- const { major, map } = layouts[layout];
-
- return (
- <div
- className={`keyboard ${major}-major`}
- style={{
- height: height(h, major),
- }}
- >
- {range(w).map((_, x) => (
- range(h).map((_, y) => {
- const note = map([x, y], [w, h]);
- return (
- <Hexagon
- key={x + ',' + y}
- x={x} y={y}
- note={note}
- state={this.state[note]}
- send={send}
- major={major}
- >
- {showMidi ? notes[note % 12] : note}
- </Hexagon>
- );
- })
- ))}
- </div>
- );
- }
-}
+ 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>
+ );
+};
export default class App extends React.Component {
- keyboardRef = React.createRef();
-
state = {
layout: 'wicki_hayden',
+ notes: {},
};
+ componentDidMount() {
+ document.addEventListener("keydown", this.onkeydown);
+ document.addEventListener("keyup", this.onkeyup);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onkeydown);
+ document.removeEventListener("keyup", this.onkeyup);
+ }
+
componentDidUpdate(prevProps) {
if (prevProps.midiin)
prevProps.midiin.onmidimessage = null;
@@ -142,41 +155,62 @@ export default class App extends React.Component {
}
blink = (note) => {
- if (this.keyboardRef.current) {
- const { current } = this.keyboardRef;
- current.onmessage({ cmd: NOTE_ON, note });
- setTimeout(() => current.onmessage({ cmd: NOTE_OFF, note }), 250);
- }
+ this.onmessage({ cmd: NOTE_ON, note });
+ setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250);
+ }
+
+ send = (command, note) => {
+ if (!this.props.midiout)
+ return;
+
+ const msg = [statbyte(command, 0), note, 127];
+ this.props.midiout.send(msg);
+ }
+
+ onlayout = (e) => {
+ this.setState({ layout: e.target.value });
}
onmessage = (e) => {
const message = parse(e);
- if (this.keyboardRef.current)
- this.keyboardRef.current.onmessage(message);
-
switch (message.cmd) {
case NOTE_ON:
console.info(message.note);
break;
}
- }
- send = (command, note) => {
- if (!this.props.midiout)
- return;
+ switch (message.cmd) {
+ case NOTE_ON:
+ this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } }));
+ break;
- const msg = [statbyte(command, 0), note, 127];
- this.props.midiout.send(msg);
+ case NOTE_OFF:
+ this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } }));
+ break;
+ }
}
- onlayout = (e) => {
- this.setState({ layout: e.target.value });
+ onkeydown = (e) => {
+ const note = notes.key2midi[e.code];
+ if (!note) return;
+ e.preventDefault();
+ this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } }));
+ }
+
+ onkeyup = (e) => {
+ const note = notes.key2midi[e.code];
+ if (!note) return;
+ e.preventDefault();
+ this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } }));
}
render() {
const { configure, showMidi } = this.props;
- const { layout } = this.state;
+ const { layout, notes } = this.state;
+
+ const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k);
+ chord.sort();
return (
<div className="app">
@@ -189,14 +223,19 @@ export default class App extends React.Component {
</select>
</label>
- <Keyboard
- ref={this.keyboardRef}
- w={12}
- h={4}
- send={this.send}
- layout={layout}
- showMidi={showMidi}
- />
+ <div className="main">
+ <Keyboard
+ w={12}
+ h={4}
+ send={this.send}
+ layout={layouts[layout]}
+ showMidi={showMidi}
+ state={notes}
+ />
+ <ChordView
+ chord={chord}
+ />
+ </div>
</div>
);
}
diff --git a/app/notes.js b/app/notes.js
index 3d0e659..0e3e92d 100644
--- a/app/notes.js
+++ b/app/notes.js
@@ -1,4 +1,4 @@
-export default {
+export const music = {
11: 'B',
10: 'A#',
9: 'A',
@@ -12,3 +12,55 @@ export default {
1: 'C#',
0: 'C',
};
+
+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': 61,
+ 'KeyW': 63,
+ 'KeyE': 65,
+ 'KeyR': 67,
+ 'KeyT': 69,
+ 'KeyY': 71,
+ 'KeyU': 73,
+ 'KeyI': 75,
+ 'KeyO': 77,
+ 'KeyP': 79,
+ 'BracketLeft': 81,
+ 'BracketRight': 83,
+
+ 'Digit1': 66,
+ 'Digit2': 68,
+ 'Digit3': 70,
+ 'Digit4': 72,
+ 'Digit5': 74,
+ 'Digit6': 76,
+ 'Digit7': 78,
+ 'Digit8': 80,
+ 'Digit9': 82,
+ 'Digit0': 84,
+ 'Minus': 86,
+ 'Equal': 88,
+};
diff --git a/app/style.js b/app/style.js
index f2e1b7c..ea59e75 100644
--- a/app/style.js
+++ b/app/style.js
@@ -14,7 +14,7 @@ export const css = (code) => {
const longSize = 3;
const sizeA = longSize * Math.sqrt(3);
-const sizeB = longSize * 2;
+const sizeB = longSize * 1.5;
const strideA = longSize * Math.sqrt(3);
const strideB = longSize * 1.5;
const rem = (n) => `${n}rem`;
@@ -24,14 +24,21 @@ export const pos = ([x, y], major) => {
? [strideA, strideB]
: [strideB, strideA];
return {
- bottom: rem(y * sy),
+ 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 * strideA);
+ return rem((h - 0.5) * strideA);
return rem((h + 1.5) * strideB);
};
@@ -45,10 +52,15 @@ body {
font-family: sans-serif;
}
-.app, .keyboard {
+.app, .keyboard, .chord {
position: relative;
}
+.main {
+ display: flex;
+ justify-content: space-evenly;
+}
+
.hexagon {
position: absolute;
width: ${rem(sizeA)};
@@ -83,4 +95,43 @@ body {
background: #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;
+}
`);