synthing

a waveform sequencing synth on the web
Log | Files | Refs | Submodules

commit e58f4210bcaff9b5f24db582641f271ad8829a41
parent d403e80f994b8a43a863dc3783ed65172e585741
Author: Massimo Siboldi <mdsiboldi@gmail.com>
Date:   Fri, 16 Mar 2018 21:36:12 -0700

Merge pull request #1 from mdsib/help-menu

Help menu + more
Diffstat:
Mpackage-lock.json | 5+++++
Mpackage.json | 1+
Msrc/App/index.js | 9+++++----
Asrc/ClickOutside/index.js | 27+++++++++++++++++++++++++++
Asrc/Help/Help.css | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Help/index.js | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/Param/index.js | 33+++++++++++++++++++++------------
Msrc/WaveEditor/index.js | 2+-
Msrc/WaveManager/index.js | 1-
Msrc/WaveTable/index.js | 7++++---
Msrc/helpers.js | 11++++++++++-
Msrc/store.js | 7+++++++
12 files changed, 174 insertions(+), 22 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -5837,6 +5837,11 @@ "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "dev": true }, + "keymage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/keymage/-/keymage-1.1.3.tgz", + "integrity": "sha1-JsZbT5TM7cBK4pQP+Az1K6/n7kE=" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/package.json b/package.json @@ -9,6 +9,7 @@ "dependencies": { "dsp.js": "^1.0.1", "envelope-generator": "^3.0.0", + "keymage": "^1.1.3", "preact": "^8.2.6", "preact-redux": "^2.0.3", "redux": "^3.7.2", diff --git a/src/App/index.js b/src/App/index.js @@ -6,6 +6,8 @@ import CircleButton from '../CircleButton/'; import HSlider from '../HSlider/'; import Param from '../Param/'; import Wheel from '../Wheel/'; +import Help from '../Help/'; +import Keybindings from '../Keybindings/'; import './App.css'; import '../iconfont/style.css'; import consts from '../consts.js'; @@ -21,7 +23,6 @@ const Adsr = (props) => ( name={aspect.name} minVal={0} maxVal={aspect.maxVal} - step={0.1} update={(newVal) => {props.update(aspect.name, newVal)}} > <Wheel percent={props.adsr[aspect.name] / aspect.maxVal} /> @@ -122,7 +123,7 @@ class App extends Component { }) return ( <div - className="App" + class="App" onKeyDown={this.keyHandler} > <div> @@ -155,7 +156,6 @@ class App extends Component { name="bpm" minVal={20} maxVal={600} - step={1} val={this.props.bpm} update={this.props.setBpm} /> @@ -163,7 +163,6 @@ class App extends Component { name="beats" minVal={3} maxVal={16} - step="1" val={this.props.numBeats} update={this.props.setNumBeats} /> @@ -179,6 +178,8 @@ class App extends Component { volume={this.props.volume} adsr={this.props.adsr} ></Synth> + <Help /> + <Keybindings /> </div> ); } diff --git a/src/ClickOutside/index.js b/src/ClickOutside/index.js @@ -0,0 +1,27 @@ +import { h, Component } from 'preact'; + +export default class ClickOutside extends Component { + handleClick = (ev) => { + if (!this.elRef.contains(ev.target)) { + console.log('actioning'); + this.props.action(); + } + } + componentDidMount() { + console.log('mounting') + // setTimeout ensures the click event doesn't get triggered while mounting + window.setTimeout(() => { + document.addEventListener('click', this.handleClick); + }, 0); + } + componentWillUnmount() { + document.removeEventListener('click', this.handleClick); + } + render() { + return ( + <div ref={ref => {this.elRef = ref}}> + {this.props.children} + </div> + ); + } +} diff --git a/src/Help/Help.css b/src/Help/Help.css @@ -0,0 +1,49 @@ +.help-modal { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.3); +} + +.help-container { + box-sizing: border-box; + background: aqua; + padding: 30px 60px; + margin: 30px; + overflow-y: auto; + max-height: calc(100vh - 60px); + max-width: 900px; +} + +.help-tag { + display: flex; + align-items: center; + justify-content: center; + background: aqua; + position: fixed; + bottom: 25px; + right: 25px; + border-radius: 9999px; + height: 2em; + width: 2em; + z-index: 100; + cursor: pointer; + opacity: 0.8; +} + +.help-tag:hover { + opacity: 1; +} + +.keyboard { + width: 100%; +} + +* { + text-align: left; +} diff --git a/src/Help/index.js b/src/Help/index.js @@ -0,0 +1,44 @@ +import { h } from 'preact'; +import { connect } from 'preact-redux'; +import ClickOutside from '../ClickOutside/'; +import './Help.css'; + +const setHelpOpen = (value) => ({type: 'SET_HELP_OPEN', value}); + +export default connect(state => ({open: state.helpOpen}), {setHelpOpen})((props) => { + const closeIfOpen = () => { + if (props.open) + props.setHelpOpen(false); + } + const modal = props.open ? ( + <div class="help-modal"> + <ClickOutside action={closeIfOpen}> + <div class="help-container"> + <h1>Help</h1> + <h2>Keybindings</h2> + <img class="keyboard" src="../../AudioKeys/images/audiokeys-mapping-rows1.jpg" /> + <h2>About</h2> + Synthing allows you to experiment with and visualize a waveform's effect on generated sound. Works best in Chrome. + <h2>Overview</h2> + <ul> + <li>Draw on the waveform to alter it.</li> + <li>Use the sequencer to activate different waveforms at different times. You can mute, solo, and mix waveforms.</li> + <li>Press play to start the sequencer, and feel free to change the bpm to make it faster or slower.</li> + </ul> + </div> + </ClickOutside> + </div> + ) : ''; + + return ( + <div class="help"> + <div + class="help-tag" + onClick={props.setHelpOpen.bind(null, !props.open)} + > + ? + </div> + {modal} + </div> + ); +}) diff --git a/src/Param/index.js b/src/Param/index.js @@ -4,25 +4,34 @@ import './style.css'; export default class Param extends Component { componentDidMount() { + const setUpMove = () => { + this.internalVal = this.props.val; + } const handleMove = (ev) => { - let step = this.props.step || 0.5; - let newVal = this.props.val - (ev.movementY * step); - if (typeof(this.props.minVal) === 'number' && (newVal < this.props.minVal)) { - newVal = this.props.minVal; - } else if (typeof(this.props.maxVal) === 'number' && (newVal > this.props.maxVal)) { - newVal = this.props.maxVal; - } - this.props.update(newVal); + // calculate how much to adjust the value given the + // min and max values of the param, the speed of the mouse movement, + // and the precision of the param. + const maxMov = 300 / (this.props.precision ? this.props.precision + 1 : 1); + const ratioMov = ev.movementY / maxMov; + const expRatioMov = -Math.sign(ratioMov) * Math.pow(Math.abs(ratioMov), 1.3); + const newVal = (expRatioMov * (this.props.maxVal - this.props.minVal)) + this.internalVal; + this.internalVal = helpers.bounded( + newVal, + this.props.minVal, + this.props.maxVal + ); + this.props.update(Number(this.internalVal.toFixed(this.props.precision))); } - helpers.clickNDrag(this.paramRef, null, handleMove, null); + helpers.clickNDrag(this.paramRef, setUpMove, handleMove, null); } handleChange(e) { this.props.update(e.target.value); } getNumString() { - return (this.props.precision ? - this.props.val.toFixed(this.props.precision) : - this.props.val) + (this.props.suffix || ''); + return (this.props.precision !== undefined && this.props.val.toFixed + ? this.props.val.toFixed(this.props.precision) + : this.props.val) + + (this.props.suffix || ''); } render() { const inputId = `param-${this.props.name}`; diff --git a/src/WaveEditor/index.js b/src/WaveEditor/index.js @@ -74,7 +74,7 @@ export default class waveEditor extends Component { class="wave-editor" ref={(div) => {this.divRef = div}} > - <WaveTable waveform={this.props.waveform} /> + <WaveTable resize={true} waveform={this.props.waveform} /> </div> ); } diff --git a/src/WaveManager/index.js b/src/WaveManager/index.js @@ -52,7 +52,6 @@ export default class WaveManager extends Component { <div class="beats"> <div onClick={this.props.activate}> <WaveTable - resize={true} height={40} width={75} waveform={this.props.tone.waveform.slice()} diff --git a/src/WaveTable/index.js b/src/WaveTable/index.js @@ -1,4 +1,4 @@ -import { h, Component} from 'preact'; +import { h, Component } from 'preact'; import helpers from '../helpers'; import consts from '../consts'; import './style.css'; @@ -33,8 +33,9 @@ export default class WaveTable extends Component { } if (this.props.resize) { window.addEventListener('resize', (ev) => { + drawArea(this.props.waveform, this.canvasRef); this.forceUpdate(); - }) + }); } } componentWillReceiveProps(newProps) { @@ -42,7 +43,7 @@ export default class WaveTable extends Component { } render() { const height = this.props.height || 400; - const width = this.props.width || window.innerWidth - 120; + const width = this.props.width || window.innerWidth - 100; return ( <canvas class="wave-table" diff --git a/src/helpers.js b/src/helpers.js @@ -11,7 +11,16 @@ export default { partial, oneTime, linear: (m, x, b) => (m * x) + b, - bounded: (val, min, max) => val < min ? min : (val > max ? max : val), + bounded: (val, min, max) => { + if (min !== undefined && min !== null && val < min) { + return min; + } + else if (max !== undefined && max !== null && val > max) { + return max; + } + else + return val; + }, scale: (buf, amt) => buf.map(val => val * amt), add: (arr1, arr2) => arr1.map((v, i) => v + arr2[i]), soon: (fn, ms=0) => { diff --git a/src/store.js b/src/store.js @@ -11,6 +11,7 @@ const initialState = { playing: false, numBeats, editingToneIdx: 0, + helpOpen: false, adsr: { attack: 0.3, decay: 1, @@ -133,6 +134,12 @@ const globalReducer = (state, action) => { state.tones.length - 2 ); break; + case 'SET_HELP_OPEN': + updates.helpOpen = action.value; + break; + case 'TOGGLE_HELP_OPEN': + updates.helpOpen = !state.helpOpen; + break; default: break; }