diff options
| author | s-ol <s+removethis@s-ol.nu> | 2021-12-20 15:22:57 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2021-12-20 15:22:57 +0000 |
| commit | 930c3a7c40e1b1e5fa834167d4fc79a067c322f4 (patch) | |
| tree | f7e850b53c7690d521951ae4b2909f6f1ce52b7f | |
| parent | add blink function (diff) | |
| download | isomorphic-kb-explorer-930c3a7c40e1b1e5fa834167d4fc79a067c322f4.tar.gz isomorphic-kb-explorer-930c3a7c40e1b1e5fa834167d4fc79a067c322f4.zip | |
add chord display
| -rw-r--r-- | app/app.js | 189 | ||||
| -rw-r--r-- | app/notes.js | 54 | ||||
| -rw-r--r-- | app/style.js | 59 |
3 files changed, 222 insertions, 80 deletions
@@ -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; +} `); |
