git.s-ol.nu isomorphic-kb-explorer / 930c3a7
add chord display s-ol 4 months ago
3 changed file(s) with 227 addition(s) and 85 deletion(s). Raw diff Collapse all Expand all
00 import React from 'react';
1 import { pos, height } from './style';
2 import notes from './notes';
1 import { pos, width, height } from './style';
2 import * as notes from './notes';
33
44 const statbyte = (stat, chan) => (stat << 4) | chan;
55 const NOTE_ON = 0b1001;
7777
7878
7979 const range = i => new Array(i).fill(true);
80 class Keyboard extends React.Component {
81 state = {};
82
83 onmessage = (message) => {
84 switch (message.cmd) {
85 case NOTE_ON:
86 this.setState(old => ({ ...old, [message.note]: true }));
87 break;
88
89 case NOTE_OFF:
90 this.setState(old => ({ ...old, [message.note]: false }));
91 break;
92 }
93 }
94
95 render() {
96 const { w, h, send, layout, showMidi } = this.props;
97 const { major, map } = layouts[layout];
98
99 return (
100 <div
101 className={`keyboard ${major}-major`}
102 style={{
103 height: height(h, major),
104 }}
105 >
106 {range(w).map((_, x) => (
107 range(h).map((_, y) => {
108 const note = map([x, y], [w, h]);
109 return (
110 <Hexagon
111 key={x + ',' + y}
112 x={x} y={y}
113 note={note}
114 state={this.state[note]}
115 send={send}
116 major={major}
117 >
118 {showMidi ? notes[note % 12] : note}
119 </Hexagon>
120 );
121 })
122 ))}
123 </div>
124 );
125 }
126 }
80 const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => (
81 <div
82 className={`keyboard ${major}-major`}
83 style={{
84 height: height(h, major),
85 width: width(w, major),
86 }}
87 >
88 {range(w).map((_, x) => (
89 range(h).map((_, y) => {
90 const note = map([x, y], [w, h]);
91 return (
92 <Hexagon
93 key={x + ',' + y}
94 x={x} y={y}
95 note={note}
96 state={state[note]}
97 send={send}
98 major={major}
99 >
100 {showMidi ? note : notes.music[note % 12]}
101 </Hexagon>
102 );
103 })
104 ))}
105 </div>
106 );
107
108 const ChordView = ({ chord }) => {
109 const min = chord[0];
110
111 return (
112 <div className="chord">
113 {range(13).map((_,i) => (
114 <div
115 key={i}
116 className="blip"
117 />
118 ))}
119 {chord.map((n) => (
120 <div
121 key={n}
122 className="note"
123 style={{ bottom: `${((n - min) % 12) / 12 * 100 - 3}%` }}
124 >
125 {(n - min) && (<span>+{n - min}</span>)}
126 </div>
127 ))}
128 </div>
129 );
130 };
127131
128132 export default class App extends React.Component {
129 keyboardRef = React.createRef();
130
131133 state = {
132134 layout: 'wicki_hayden',
135 notes: {},
133136 };
137
138 componentDidMount() {
139 document.addEventListener("keydown", this.onkeydown);
140 document.addEventListener("keyup", this.onkeyup);
141 }
142
143 componentWillUnmount() {
144 document.removeEventListener("keydown", this.onkeydown);
145 document.removeEventListener("keyup", this.onkeyup);
146 }
134147
135148 componentDidUpdate(prevProps) {
136149 if (prevProps.midiin)
141154 }
142155
143156 blink = (note) => {
144 if (this.keyboardRef.current) {
145 const { current } = this.keyboardRef;
146 current.onmessage({ cmd: NOTE_ON, note });
147 setTimeout(() => current.onmessage({ cmd: NOTE_OFF, note }), 250);
148 }
157 this.onmessage({ cmd: NOTE_ON, note });
158 setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250);
159 }
160
161 send = (command, note) => {
162 if (!this.props.midiout)
163 return;
164
165 const msg = [statbyte(command, 0), note, 127];
166 this.props.midiout.send(msg);
167 }
168
169 onlayout = (e) => {
170 this.setState({ layout: e.target.value });
149171 }
150172
151173 onmessage = (e) => {
152174 const message = parse(e);
153
154 if (this.keyboardRef.current)
155 this.keyboardRef.current.onmessage(message);
156175
157176 switch (message.cmd) {
158177 case NOTE_ON:
159178 console.info(message.note);
160179 break;
161180 }
162 }
163
164 send = (command, note) => {
165 if (!this.props.midiout)
166 return;
167
168 const msg = [statbyte(command, 0), note, 127];
169 this.props.midiout.send(msg);
170 }
171
172 onlayout = (e) => {
173 this.setState({ layout: e.target.value });
181
182 switch (message.cmd) {
183 case NOTE_ON:
184 this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } }));
185 break;
186
187 case NOTE_OFF:
188 this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } }));
189 break;
190 }
191 }
192
193 onkeydown = (e) => {
194 const note = notes.key2midi[e.code];
195 if (!note) return;
196 e.preventDefault();
197 this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } }));
198 }
199
200 onkeyup = (e) => {
201 const note = notes.key2midi[e.code];
202 if (!note) return;
203 e.preventDefault();
204 this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } }));
174205 }
175206
176207 render() {
177208 const { configure, showMidi } = this.props;
178 const { layout } = this.state;
209 const { layout, notes } = this.state;
210
211 const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k);
212 chord.sort();
179213
180214 return (
181215 <div className="app">
188222 </select>
189223 </label>
190224
191 <Keyboard
192 ref={this.keyboardRef}
193 w={12}
194 h={4}
195 send={this.send}
196 layout={layout}
197 showMidi={showMidi}
198 />
225 <div className="main">
226 <Keyboard
227 w={12}
228 h={4}
229 send={this.send}
230 layout={layouts[layout]}
231 showMidi={showMidi}
232 state={notes}
233 />
234 <ChordView
235 chord={chord}
236 />
237 </div>
199238 </div>
200239 );
201240 }
0 export default {
0 export const music = {
11 11: 'B',
22 10: 'A#',
33 9: 'A',
1111 1: 'C#',
1212 0: 'C',
1313 };
14
15 export const key2midi = {
16 'KeyZ': 49,
17 'KeyX': 51,
18 'KeyC': 53,
19 'KeyV': 55,
20 'KeyB': 57,
21 'KeyN': 59,
22 'KeyM': 61,
23 'Comma': 63,
24 'Period': 65,
25 'Slash': 67,
26
27 'KeyA': 54,
28 'KeyS': 56,
29 'KeyD': 58,
30 'KeyF': 60,
31 'KeyG': 62,
32 'KeyH': 64,
33 'KeyJ': 66,
34 'KeyK': 68,
35 'KeyL': 70,
36 'Semicolon': 72,
37 'Quote': 74,
38 'Backslash': 76,
39
40 'KeyQ': 61,
41 'KeyW': 63,
42 'KeyE': 65,
43 'KeyR': 67,
44 'KeyT': 69,
45 'KeyY': 71,
46 'KeyU': 73,
47 'KeyI': 75,
48 'KeyO': 77,
49 'KeyP': 79,
50 'BracketLeft': 81,
51 'BracketRight': 83,
52
53 'Digit1': 66,
54 'Digit2': 68,
55 'Digit3': 70,
56 'Digit4': 72,
57 'Digit5': 74,
58 'Digit6': 76,
59 'Digit7': 78,
60 'Digit8': 80,
61 'Digit9': 82,
62 'Digit0': 84,
63 'Minus': 86,
64 'Equal': 88,
65 };
1313
1414 const longSize = 3;
1515 const sizeA = longSize * Math.sqrt(3);
16 const sizeB = longSize * 2;
16 const sizeB = longSize * 1.5;
1717 const strideA = longSize * Math.sqrt(3);
1818 const strideB = longSize * 1.5;
1919 const rem = (n) => `${n}rem`;
2323 ? [strideA, strideB]
2424 : [strideB, strideA];
2525 return {
26 bottom: rem(y * sy),
26 bottom: rem(y * sy),
2727 left: rem(x * sx),
2828 };
2929 };
3030
31 export const width = (w, major) => {
32 if (major === 'row')
33 return rem((w + 0.5) * strideA);
34
35 return rem((w + 0.5) * strideB);
36 };
37
3138 export const height = (h, major) => {
3239 if (major === 'row')
33 return rem(h * strideA);
40 return rem((h - 0.5) * strideA);
3441
3542 return rem((h + 1.5) * strideB);
3643 };
4451 font-family: sans-serif;
4552 }
4653
47 .app, .keyboard {
54 .app, .keyboard, .chord {
4855 position: relative;
56 }
57
58 .main {
59 display: flex;
60 justify-content: space-evenly;
4961 }
5062
5163 .hexagon {
8294 background: #eeeeee;
8395 color: #696969;
8496 }
97
98 .chord {
99 display: flex;
100 flex-direction: column;
101 justify-content: space-between;
102 margin: 2rem 0;
103 width: 2rem;
104 }
105
106 .chord .blip {
107 height: 1px;
108 margin: auto 0 0;
109 background: #eeeeee;
110 opacity: 0.75;
111 }
112
113 .chord .blip:nth-child(2n) {
114 margin: auto 0.3rem 0;
115 opacity: 0.5;
116 }
117 .chord .blip:first-child {
118 margin-top: 0;
119 }
120
121 .chord .note {
122 position: absolute;
123 left: 0;
124 right: 0;
125 margin: auto;
126 height: 6%;
127 aspect-ratio: 1;
128 background: #eeeeee;
129 border-radius: 100%;
130 }
131
132 .chord .note span {
133 position: absolute;
134 left: 2rem;
135 }
85136 `);