aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-01-04 19:47:01 +0000
committers-ol <s+removethis@s-ol.nu>2022-01-04 19:48:30 +0000
commit582452e79ab43e174ff33641cf7deb81fe25fcb4 (patch)
treebaeeafae6561426e995d18573766915ac7c757e4
parentrename to "isomorphic-kb-explorer" (diff)
downloadisomorphic-kb-explorer-582452e79ab43e174ff33641cf7deb81fe25fcb4.tar.gz
isomorphic-kb-explorer-582452e79ab43e174ff33641cf7deb81fe25fcb4.zip
add scale highlighting
-rw-r--r--app/app.js97
-rw-r--r--app/style.js15
2 files changed, 93 insertions, 19 deletions
diff --git a/app/app.js b/app/app.js
index 00b18bb..522cb99 100644
--- a/app/app.js
+++ b/app/app.js
@@ -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;
}