diff options
| author | s-ol <s+removethis@s-ol.nu> | 2022-01-04 19:47:01 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2022-01-04 19:48:30 +0000 |
| commit | 582452e79ab43e174ff33641cf7deb81fe25fcb4 (patch) | |
| tree | baeeafae6561426e995d18573766915ac7c757e4 | |
| parent | rename to "isomorphic-kb-explorer" (diff) | |
| download | isomorphic-kb-explorer-582452e79ab43e174ff33641cf7deb81fe25fcb4.tar.gz isomorphic-kb-explorer-582452e79ab43e174ff33641cf7deb81fe25fcb4.zip | |
add scale highlighting
| -rw-r--r-- | app/app.js | 97 | ||||
| -rw-r--r-- | app/style.js | 15 |
2 files changed, 93 insertions, 19 deletions
@@ -3,6 +3,9 @@ import { pos, width, height } from './style'; import * as notes from './notes'; 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; @@ -24,7 +27,7 @@ const parse = message => ({ val: message.data[2] / 127, }); -const Hexagon = ({ x, y, children, state, note, send, major }) => { +const Hexagon = ({ x, y, children, state, note, send, major, scale }) => { if (major == 'row') { if (y % 2 == 0) x += 0.5; @@ -35,7 +38,7 @@ const Hexagon = ({ x, y, children, state, note, send, major }) => { return ( <div - className={'hexagon' + (state ? ' active' : '')} + className={`hexagon ${state ? ' active' : ''} ${scale}`} style={pos([x, y], major)} > <div @@ -76,9 +79,35 @@ const layouts = { }, }; +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 Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => ( +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, send, layout: { major, map }, showMidi, state, scale }) => ( <div className={`keyboard ${major}-major`} style={{ @@ -89,6 +118,8 @@ const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => ( {range(w).map((_, x) => ( range(h).map((_, y) => { const note = map([x, y], [w, h]); + const onCore = scale && scale.notes.indexOf(note) >= 0; + const onScale = scale && scale.modulo.indexOf(note % 12) >= 0; return ( <Hexagon key={x + ',' + y} @@ -97,6 +128,7 @@ const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => ( state={state[note]} send={send} major={major} + scale={onCore ? "core" : (onScale ? "" : "disabled")} > {showMidi ? note : notes.music[note % 12]} </Hexagon> @@ -133,7 +165,9 @@ const ChordView = ({ chord }) => { export default class App extends React.Component { state = { layout: 'wicki_hayden', - notes: {}, + scale: 'major', + offset: 60, + state: {}, }; componentDidMount() { @@ -147,6 +181,9 @@ export default class App extends React.Component { } componentDidUpdate(prevProps) { + this.send(CHANNEL, ALL_SOUND_OFF, 0); + this.send(CHANNEL, ALL_NOTES_OFF, 0); + if (prevProps.midiin) prevProps.midiin.onmidimessage = null; @@ -159,17 +196,21 @@ export default class App extends React.Component { setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250); } - send = (command, note) => { + send = (command, note, vel=127) => { + if (command === NOTE_ON && this.state.offset === null) + this.setState({ offset: note }); + if (!this.props.midiout) return; - const msg = [statbyte(command, 0), note, 127]; + const msg = [statbyte(command, 0), note, vel]; this.props.midiout.send(msg); } - onlayout = (e) => { - this.setState({ layout: e.target.value }); - } + onlayout = (e) => this.setState({ layout: e.target.value }) + onscale = (e) => this.setState({ scale: e.target.value }) + + onoffset = (e) => this.setState({ offset: null }); onmessage = (e) => { const message = parse(e); @@ -182,11 +223,11 @@ export default class App extends React.Component { switch (message.cmd) { case NOTE_ON: - this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } })); + this.setState(({ state }) => ({ state: { ...state, [message.note]: true } })); break; case NOTE_OFF: - this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } })); + this.setState(({ state }) => ({ state: { ...state, [message.note]: false } })); break; } } @@ -197,7 +238,11 @@ export default class App extends React.Component { const note = notes.key2midi[e.code]; if (!note) return; e.preventDefault(); - this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } })); + + if (this.state.offset === null) + this.setState({ offset: note }); + + this.setState(({ state }) => ({ state: { ...state, [note]: true } })); this.send(NOTE_ON, note); } @@ -207,36 +252,52 @@ export default class App extends React.Component { const note = notes.key2midi[e.code]; if (!note) return; e.preventDefault(); - this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } })); + + this.setState(({ state }) => ({ state: { ...state, [note]: false } })); this.send(NOTE_OFF, note); } render() { const { configure, showMidi } = this.props; - const { layout, notes } = this.state; + const { scale, offset, layout, state } = this.state; - const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k); + const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k); chord.sort(); return ( <div className="app"> <button onClick={configure}>settings</button> - <label>layout: + <label>layout:{' '} <select name="layout" onChange={this.onlayout} value={layout}> <option value="wicki_hayden">Wicki-Hayden</option> <option value="harmonic">Harmonic Table</option> <option value="gerhard">Gerhard</option> </select> </label> + <label>scale:{' '} + <select name="layout" onChange={this.onscale} value={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> + </label> + <button onClick={this.onoffset}>set offset</button> <div className="main"> <Keyboard w={12} h={4} send={this.send} - layout={layouts[layout]} showMidi={showMidi} - state={notes} + layout={layouts[layout]} + scale={scale !== "none" && offset !== null && tallyup(scales[scale], offset)} + offset={offset} + state={state} /> <ChordView chord={chord} diff --git a/app/style.js b/app/style.js index ea59e75..0bd718e 100644 --- a/app/style.js +++ b/app/style.js @@ -82,17 +82,30 @@ body { font-size: ${rem(longSize * 0.5)}; text-align: center; background: #696969; + border: 3px solid #696969; color: #eeeeee; cursor: pointer; - transition: background 300ms; + transition: background 300ms, border 300ms; } + +.hexagon.disabled > .inner { + background: #363636 !important; + border-color: #363636; + color: #848484; +} +.hexagon.core> .inner { + border-color: #eeeeee; +} + .hexagon > .inner:hover { background: #848484; + border-color: #848484; } .hexagon > .inner:active, .hexagon.active > .inner { background: #eeeeee; + border-color: #eeeeee; color: #696969; } |
