From ba48fa330f9de7c983e77356eb51bc936ddfb44e Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 14:00:14 +0200 Subject: [PATCH 01/41] show looping state of plots --- app/components/ScatterPlots.css | 12 ++++++ app/components/ScatterPlotsInteractive.js | 49 +++++++++++++---------- app/components/SelectArea.css | 10 +++++ app/components/SelectArea.js | 8 ++-- app/sound/connectSoundApp.js | 3 ++ 5 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 app/components/SelectArea.css diff --git a/app/components/ScatterPlots.css b/app/components/ScatterPlots.css index 11dd3bb..c3be005 100644 --- a/app/components/ScatterPlots.css +++ b/app/components/ScatterPlots.css @@ -2,3 +2,15 @@ font-size: 2em; opacity: 0.1; } + +.pending { + stroke: rgba(249, 199, 22, 0.8); +} + +.looping { + stroke: rgba(192, 22, 249, 1); +} + +.focused { + /*stroke: rgba(193, 196, 12, 0.6);*/ +} diff --git a/app/components/ScatterPlotsInteractive.js b/app/components/ScatterPlotsInteractive.js index 7e2648e..5534760 100644 --- a/app/components/ScatterPlotsInteractive.js +++ b/app/components/ScatterPlotsInteractive.js @@ -20,6 +20,7 @@ import { // import ScatterPlotClickSurface from './ScatterPlotClickSurface'; import Axis from './Axis'; import SelectArea from './SelectArea'; +import style from './ScatterPlots.css'; const unset = {}; @@ -114,8 +115,6 @@ class ScatterPlotsInteractive extends React.Component { if (this.props.featureSideLengthScale.length > 0) { if (_.isNumber(this.props.hovering.m)) { - // does hovering really need to go through redux ? - // it is microstate const hovx = (this.props.hovering.m || 0); const hovy = (this.props.hovering.n || 0); const featx = this.props.featureSideLengthScale[hovx]; @@ -135,31 +134,39 @@ class ScatterPlotsInteractive extends React.Component { } } - const getClassName = (box) => { - const loopMode = this.props.loopMode; - const isLooping = - (_.get(loopMode, 'nowPlaying.m') === box.m) && - (_.get(loopMode, 'nowPlaying.n') === box.n); + const s = { + nowPlaying: { + m: _.get(this.props.loopMode, 'nowPlaying.m'), + n: _.get(this.props.loopMode, 'nowPlaying.n') + }, + pending: { + m: _.get(this.props.loopMode, 'pending.m'), + n: _.get(this.props.loopMode, 'pending.n') + }, + last: { + m: _.get(this.state, 'last.m'), + n: _.get(this.state, 'last.n') + } + }; - const isPending = - (_.get(loopMode, 'pending.m') === box.m) && - (_.get(loopMode, 'pending.n') === box.n); + // pending should be erased once it becomes active + console.log(s); - const isLastFocused = - (_.get(this.state, 'last.m') === box.m) && - (_.get(this.state, 'last.n') === box.n); - - if (isPending) { - return 'pending'; + const getClassName = (box) => { + if ((s.nowPlaying.m === box.m) && (s.nowPlaying.n === box.n)) { + return style.looping; + // return this.state.loopMode.looping ? style.looping : null; } - if (isLooping) { - return 'looping'; + if ((s.pending.m === box.m) && (s.pending.n === box.n)) { + return style.pending; } - if (isLastFocused) { - return 'focused'; + if ((s.last.m === box.m) && (s.last.n === box.n)) { + return style.focused; } + + return 'none'; }; layout.boxes.forEach((box) => { @@ -195,7 +202,7 @@ class ScatterPlotsInteractive extends React.Component { } }, show: isLastFocused, - className: getClassName(box) + overlayClassName: getClassName(box) }); children.push(selectedArea); diff --git a/app/components/SelectArea.css b/app/components/SelectArea.css new file mode 100644 index 0000000..04cb250 --- /dev/null +++ b/app/components/SelectArea.css @@ -0,0 +1,10 @@ + +.overlay { + +} + +.selection { + fill: #777; + fill-opacity: 0.3; + stroke: #fff; +} diff --git a/app/components/SelectArea.js b/app/components/SelectArea.js index 46a7c5c..040758e 100644 --- a/app/components/SelectArea.js +++ b/app/components/SelectArea.js @@ -1,4 +1,5 @@ import React from 'react'; +import style from './SelectArea.css'; const handleSize = 6; @@ -464,7 +465,7 @@ export default class SelectArea extends React.Component { let overlayTapHandler = (event) => this._started(event, {type: 'overlay'}); let overlay = ( this._started(event, {type: 'selection'}); let selection = ( Date: Mon, 27 Jun 2016 14:00:27 +0200 Subject: [PATCH 02/41] get basic e2e test running --- package.json | 2 +- test/e2e.js | 138 +++++++++++++++++++++++++-------------------------- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 173bf56..6c8a796 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", "test-watch": "npm test -- --watch", - "test-e2e": "cross-env NODE_ENV=test mocha --compilers js:babel-register --require ./test/setup.js --require co-mocha ./test/e2e.js", + "test-e2e": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --require ./test/setup.js --require co-mocha ./test/e2e.js", "lint": "eslint app test *.js", "hot-server": "node -r babel-register server.js", "build": "npm run build-main && npm run build-renderer", diff --git a/test/e2e.js b/test/e2e.js index 2b53232..757f043 100644 --- a/test/e2e.js +++ b/test/e2e.js @@ -3,8 +3,8 @@ import chromedriver from 'chromedriver'; import webdriver from 'selenium-webdriver'; import { expect } from 'chai'; import electronPath from 'electron-prebuilt'; -import homeStyles from '../app/components/Home.css'; -import counterStyles from '../app/components/Counter.css'; +// import homeStyles from '../app/components/Home.css'; +// import counterStyles from '../app/components/Counter.css'; chromedriver.start(); // on port 9515 process.on('exit', chromedriver.stop); @@ -32,76 +32,76 @@ describe('main window', function spec() { await this.driver.quit(); }); - const findCounter = () => this.driver.findElement(webdriver.By.className(counterStyles.counter)); - - const findButtons = () => this.driver.findElements(webdriver.By.className(counterStyles.btn)); + // const findCounter = () => this.driver.findElement(webdriver.By.className(counterStyles.counter)); + // + // const findButtons = () => this.driver.findElements(webdriver.By.className(counterStyles.btn)); it('should open window', async () => { const title = await this.driver.getTitle(); - expect(title).to.equal('Hello Electron React!'); - }); - - it('should to Counter with click "to Counter" link', async () => { - const link = await this.driver.findElement(webdriver.By.css(`.${homeStyles.container} > a`)); - link.click(); - - const counter = await findCounter(); - expect(await counter.getText()).to.equal('0'); - }); - - it('should display updated count after increment button click', async () => { - const buttons = await findButtons(); - buttons[0].click(); - - const counter = await findCounter(); - expect(await counter.getText()).to.equal('1'); - }); - - it('should display updated count after descrement button click', async () => { - const buttons = await findButtons(); - const counter = await findCounter(); - - buttons[1].click(); // - - - expect(await counter.getText()).to.equal('0'); - }); - - it('shouldnt change if even and if odd button clicked', async () => { - const buttons = await findButtons(); - const counter = await findCounter(); - buttons[2].click(); // odd - - expect(await counter.getText()).to.equal('0'); - }); - - it('should change if odd and if odd button clicked', async () => { - const buttons = await findButtons(); - const counter = await findCounter(); - - buttons[0].click(); // + - buttons[2].click(); // odd - - expect(await counter.getText()).to.equal('2'); + expect(title).to.equal('PlaySPLOM'); }); - it('should change if async button clicked and a second later', async () => { - const buttons = await findButtons(); - const counter = await findCounter(); - buttons[3].click(); // async - - expect(await counter.getText()).to.equal('2'); - - await this.driver.wait(() => - counter.getText().then(text => text === '3') - , 1000, 'count not as expected'); - }); - - it('should back to home if back button clicked', async () => { - const link = await this.driver.findElement( - webdriver.By.css(`.${counterStyles.backButton} > a`) - ); - link.click(); - - await this.driver.findElement(webdriver.By.className(homeStyles.container)); - }); + // it('should to Counter with click "to Counter" link', async () => { + // const link = await this.driver.findElement(webdriver.By.css(`.${homeStyles.container} > a`)); + // link.click(); + // + // const counter = await findCounter(); + // expect(await counter.getText()).to.equal('0'); + // }); + // + // it('should display updated count after increment button click', async () => { + // const buttons = await findButtons(); + // buttons[0].click(); + // + // const counter = await findCounter(); + // expect(await counter.getText()).to.equal('1'); + // }); + // + // it('should display updated count after descrement button click', async () => { + // const buttons = await findButtons(); + // const counter = await findCounter(); + // + // buttons[1].click(); // - + // + // expect(await counter.getText()).to.equal('0'); + // }); + // + // it('shouldnt change if even and if odd button clicked', async () => { + // const buttons = await findButtons(); + // const counter = await findCounter(); + // buttons[2].click(); // odd + // + // expect(await counter.getText()).to.equal('0'); + // }); + // + // it('should change if odd and if odd button clicked', async () => { + // const buttons = await findButtons(); + // const counter = await findCounter(); + // + // buttons[0].click(); // + + // buttons[2].click(); // odd + // + // expect(await counter.getText()).to.equal('2'); + // }); + // + // it('should change if async button clicked and a second later', async () => { + // const buttons = await findButtons(); + // const counter = await findCounter(); + // buttons[3].click(); // async + // + // expect(await counter.getText()).to.equal('2'); + // + // await this.driver.wait(() => + // counter.getText().then(text => text === '3') + // , 1000, 'count not as expected'); + // }); + // + // it('should back to home if back button clicked', async () => { + // const link = await this.driver.findElement( + // webdriver.By.css(`.${counterStyles.backButton} > a`) + // ); + // link.click(); + // + // await this.driver.findElement(webdriver.By.className(homeStyles.container)); + // }); }); From 6a3cc52c18cf5bbcf54899a23b970fd0f2ec9c11 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 14:00:49 +0200 Subject: [PATCH 03/41] update supercolliderjs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c8a796..31ebbd7 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "redux-thunk": "^2.1.0", "reselect": "^2.5.1", "source-map-support": "^0.4.0", - "supercolliderjs": "^0.11.0", + "supercolliderjs": "^0.11.1", "updeep": "^0.16.0", "winston": "^2.2.0" }, From adff3ddb1cf691a91d438bd6b2d8da56f240b171 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 15:24:27 +0200 Subject: [PATCH 04/41] Fix: brush gets stuck when you drag outside of box --- app/components/ScatterPlotsInteractive.js | 25 +++++++++++------------ app/components/SelectArea.js | 6 ++---- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/app/components/ScatterPlotsInteractive.js b/app/components/ScatterPlotsInteractive.js index 5534760..cd5a7ca 100644 --- a/app/components/ScatterPlotsInteractive.js +++ b/app/components/ScatterPlotsInteractive.js @@ -120,17 +120,18 @@ class ScatterPlotsInteractive extends React.Component { const featx = this.props.featureSideLengthScale[hovx]; const featy = this.props.featureSideLengthScale[hovy]; const box = getBox(hovx, hovy); - - children.push(h(Axis, { - xOffset: box.x, - yOffset: box.y, - sideLength: innerSideLength, - muiTheme: this.props.muiTheme, - xScale: featx.mappedScale, - yScale: featy.mappedScale, - xLabel: featx.feature.name, - yLabel: featy.feature.name - })); + if (box) { + children.push(h(Axis, { + xOffset: box.x, + yOffset: box.y, + sideLength: innerSideLength, + muiTheme: this.props.muiTheme, + xScale: featx.mappedScale, + yScale: featy.mappedScale, + xLabel: featx.feature.name, + yLabel: featy.feature.name + })); + } } } @@ -150,8 +151,6 @@ class ScatterPlotsInteractive extends React.Component { }; // pending should be erased once it becomes active - console.log(s); - const getClassName = (box) => { if ((s.nowPlaying.m === box.m) && (s.nowPlaying.n === box.n)) { return style.looping; diff --git a/app/components/SelectArea.js b/app/components/SelectArea.js index 040758e..bf5b730 100644 --- a/app/components/SelectArea.js +++ b/app/components/SelectArea.js @@ -424,12 +424,10 @@ export default class SelectArea extends React.Component { } _mouseEnter(e) { - if (this.props.onMouseEnter) { + // onHover enter + if (this.props.onMouseEnter && (!e.buttons)) { this.props.onMouseEnter(e); } - // reset drag mode in case you were still dragging from a previous - // interaction inside this box - this.mouseMode = null; } _setSelected(selected) { From d5e10c761bb5601349318aa673bbcbda52ca75c8 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 15:35:17 +0200 Subject: [PATCH 05/41] un-invert y parameter when mapping to sound --- app/selectors/sound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/selectors/sound.js b/app/selectors/sound.js index cd03521..80e9670 100644 --- a/app/selectors/sound.js +++ b/app/selectors/sound.js @@ -179,7 +179,7 @@ export function xyPointsEnteringToSynthEvents(pointsEntering, } if (mapperY) { - args[paramY] = mapperY(y); + args[paramY] = mapperY(1.0 - y); } return { From 15980c43849abaa340cc2551488b43787e213b21 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 15:45:39 +0200 Subject: [PATCH 06/41] add stereoGrain3 --- app/synthdefs/shockwave-microsound-2.json | 4 +- app/synthdefs/shockwave-microsound.json | 4 +- app/synthdefs/stereoGrain3.json | 97 ++++++++++++++++++++++ app/synthdefs/stereoGrain3.scd | 10 +++ app/synthdefs/stereoGrain3.scsyndef | Bin 0 -> 689 bytes 5 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 app/synthdefs/stereoGrain3.json create mode 100644 app/synthdefs/stereoGrain3.scd create mode 100644 app/synthdefs/stereoGrain3.scsyndef diff --git a/app/synthdefs/shockwave-microsound-2.json b/app/synthdefs/shockwave-microsound-2.json index 4351021..366cfd4 100644 --- a/app/synthdefs/shockwave-microsound-2.json +++ b/app/synthdefs/shockwave-microsound-2.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 20000, + "maxval": 10000, "class": "ControlSpec", - "minval": 20, + "minval": 40, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/shockwave-microsound.json b/app/synthdefs/shockwave-microsound.json index a4fab4b..d138dc4 100644 --- a/app/synthdefs/shockwave-microsound.json +++ b/app/synthdefs/shockwave-microsound.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 20000, + "maxval": 10000, "class": "ControlSpec", - "minval": 20, + "minval": 40, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/stereoGrain3.json b/app/synthdefs/stereoGrain3.json new file mode 100644 index 0000000..f4c7ca2 --- /dev/null +++ b/app/synthdefs/stereoGrain3.json @@ -0,0 +1,97 @@ +{ + "hasArrayArgs": false, + "inputs": [], + "hasGate": false, + "name": "stereoGrain3", + "hasVariants": false, + "output": [ + { + "rate": "audio", + "startingChannel": "out", + "numberOfChannels": 2, + "type": "OffsetOut" + } + ], + "controls": [ + { + "name": "out", + "defaultValue": 0, + "lag": 0, + "rate": "scalar", + "index": 0 + }, + { + "name": "amp", + "defaultValue": 0.10000000149012, + "lag": 0, + "rate": "scalar", + "spec": { + "default": 0, + "maxval": 1, + "class": "ControlSpec", + "minval": 0, + "warp": "amp", + "step": 0, + "units": "" + }, + "index": 1 + }, + { + "name": "freq", + "defaultValue": 440, + "lag": 0, + "rate": "scalar", + "spec": { + "default": 440, + "maxval": 10000, + "class": "ControlSpec", + "minval": 40, + "warp": "exp", + "step": 0, + "units": " Hz" + }, + "index": 2 + }, + { + "name": "freq2", + "defaultValue": 440, + "lag": 0, + "rate": "scalar", + "spec": { + "default": 440, + "maxval": 10000, + "class": "ControlSpec", + "minval": 40, + "warp": "exp", + "step": 0, + "units": " Hz" + }, + "index": 3 + }, + { + "name": "sustain", + "defaultValue": 0.0099999997764826, + "lag": 0, + "rate": "scalar", + "spec": { + "default": 1, + "maxval": 4, + "class": "ControlSpec", + "minval": 0, + "warp": "linear", + "step": 0, + "units": "" + }, + "index": 4 + } + ], + "canFreeSynth": true, + "sourceCode": "SynthDef('stereoGrain3', { |out, amp=0.1, freq=440, freq2=440, sustain=0.01|var snd = [LFSaw.ar(freq), LFSaw.ar(freq2)];var amp2 = amp * AmpComp.ir(freq.max(50)) * 0.5;var env = EnvGen.ar(Env.perc(1.0, 1.0), timeScale: sustain, levelScale: amp2, doneAction: 2);OffsetOut.ar(out, snd * env);})", + "controlNames": [ + "out", + "amp", + "freq", + "freq2", + "sustain" + ] +} \ No newline at end of file diff --git a/app/synthdefs/stereoGrain3.scd b/app/synthdefs/stereoGrain3.scd new file mode 100644 index 0000000..696c371 --- /dev/null +++ b/app/synthdefs/stereoGrain3.scd @@ -0,0 +1,10 @@ +Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\freq, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); +Spec.specs.put(\freq2, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); + +SynthDef("stereoGrain3", { |out, amp=0.1, freq=440, freq2=440, sustain=0.01| + var snd = [LFSaw.ar(freq), LFSaw.ar(freq2)]; + var amp2 = amp * AmpComp.ir(freq.max(50)) * 0.5; + var env = EnvGen.ar(Env.perc(1.0, 1.0), timeScale: sustain, levelScale: amp2, doneAction: 2); + OffsetOut.ar(out, snd * env); +}, \ir ! 5).add; diff --git a/app/synthdefs/stereoGrain3.scsyndef b/app/synthdefs/stereoGrain3.scsyndef new file mode 100644 index 0000000000000000000000000000000000000000..f3a3659dc90a25fcac380694385229f5b2399e48 GIT binary patch literal 689 zcmaJ;J5Iwu5FP(x2nFH@+yGi25DP*P$wkBgT&z(fVjFu6qC=d7ih@%h3aTK{(Qp@d zvz|3jBqPnfH#=|N{$|N#PDCy_!$zxGm6JM~7yFpnK+^b}D0%qUIeLDYj!=*8iH?D< zZ@@>Q7u1M=hr{LaJ^6wjd9eG*>jSTBHTS%1Q2`u3uhlh(8!*}9PSmuG#-89lax+TG zLf7RLxC!G87EI4(*{uu4>6ji5?l;1aRSK_H49D{#tMAh4Vxo9MvD#vauvQL5CyOd6 z&6&wVr`wJhiJ8@Uw8E56$-ztcVzf?ZE-B7Fg{Q?04#*DMNH|D~Wg*hmOgVh%hk~|m z>qzD;2G&n-AQ%hU^;XyS-G7(;{fxg~67h-F3GK^t!Z^+IM(MQGJa3+u4f+@ei#z-Q DSVEa~ literal 0 HcmV?d00001 From 8fd7a787c27c81c767f5eecf38b7eba1d51a580c Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 16:26:53 +0200 Subject: [PATCH 07/41] new styling: white theme, smaller fonts title to bottom left --- app/components/Label.js | 2 +- app/components/ScatterPlots.css | 7 +++++-- app/components/ScatterPlots.js | 2 +- app/containers/DatasetSelector.js | 5 ++++- app/containers/Sidebar.css | 7 +++++-- app/containers/SoundSelector.js | 3 +++ app/reducers/ui.js | 3 ++- app/stylesheets/skeleton.css | 2 +- 8 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/components/Label.js b/app/components/Label.js index 501f0e1..c187657 100644 --- a/app/components/Label.js +++ b/app/components/Label.js @@ -60,7 +60,7 @@ module.exports = React.createClass({ const style = { stroke: props.textColor, fill: props.textColor, - fontSize: 9 + fontSize: 10 }; return ( diff --git a/app/components/ScatterPlots.css b/app/components/ScatterPlots.css index c3be005..62e244c 100644 --- a/app/components/ScatterPlots.css +++ b/app/components/ScatterPlots.css @@ -1,6 +1,9 @@ .title { - font-size: 2em; - opacity: 0.1; + font-size: 5em; + opacity: 0.05; + text-transform: uppercase; + /*font-variant: small-caps;*/ + letter-spacing: .16em; } .pending { diff --git a/app/components/ScatterPlots.js b/app/components/ScatterPlots.js index 43d5b2d..5604cb1 100644 --- a/app/components/ScatterPlots.js +++ b/app/components/ScatterPlots.js @@ -27,7 +27,7 @@ class ScatterPlots extends React.Component { if (this.props.dataset) { const title = h('text', { x: 50, - y: 50, + y: this.props.layout.svgStyle.height - 200, className: style.title, style: { fill: this.props.muiTheme.palette.textColor diff --git a/app/containers/DatasetSelector.js b/app/containers/DatasetSelector.js index f39444b..1679f43 100644 --- a/app/containers/DatasetSelector.js +++ b/app/containers/DatasetSelector.js @@ -32,7 +32,10 @@ class DatasetSelector extends Component { return h(ListItem, { primaryText: dataset.name, selected: true, - value: dataset.path + value: dataset.path, + style: { + fontSize: '1em' + } }); }) ), diff --git a/app/containers/Sidebar.css b/app/containers/Sidebar.css index 57b57a7..235d8a3 100644 --- a/app/containers/Sidebar.css +++ b/app/containers/Sidebar.css @@ -4,16 +4,19 @@ display: flex; flex-direction: column; height: 100vh; + padding-top: 2rem; } .sidebar > div { - padding: .5rem; + padding: 1rem; + padding-left: 0; } .sidebar h6 { margin-bottom: 0; font-weight: bold; - letter-spacing: .25em; + letter-spacing: .16em; + font-variant: small-caps; } .sidebar .selectable-list { diff --git a/app/containers/SoundSelector.js b/app/containers/SoundSelector.js index f7471e4..3a889bf 100644 --- a/app/containers/SoundSelector.js +++ b/app/containers/SoundSelector.js @@ -23,6 +23,9 @@ class SoundSelector extends React.Component { return h(ListItem, { primaryText: sound.name, selected: sound.name === this.props.selectedSound, + style: { + fontSize: '1em' + }, // selected: true, value: sound.name }); diff --git a/app/reducers/ui.js b/app/reducers/ui.js index ecd5f8f..eb973cf 100644 --- a/app/reducers/ui.js +++ b/app/reducers/ui.js @@ -1,5 +1,6 @@ import getMuiTheme from 'material-ui/styles/getMuiTheme'; import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; +import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; import { FOCUS_SCATTERPLOT, @@ -23,7 +24,7 @@ const initial = { width: window.innerWidth, height: window.innerHeight }, - muiTheme: getMuiTheme(darkBaseTheme) + muiTheme: getMuiTheme(lightBaseTheme) }; /** diff --git a/app/stylesheets/skeleton.css b/app/stylesheets/skeleton.css index 831b4b3..0117711 100644 --- a/app/stylesheets/skeleton.css +++ b/app/stylesheets/skeleton.css @@ -123,7 +123,7 @@ are based on 10px sizing. So basically 1.5rem = 15px :) */ html { font-size: 62.5%; } body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + font-size: 1.3em; /* currently ems cause chrome bug misinterpreting rems on body element */ line-height: 1.6; font-weight: 400; font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; From 3f829f8ec8df9d132094be1c70d49493832964eb Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 17:01:41 +0200 Subject: [PATCH 08/41] fix(packaging): use custom playsplom icon again --- package.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/package.js b/package.js index 4eac64f..0805bb7 100644 --- a/package.js +++ b/package.js @@ -99,19 +99,20 @@ function startPack() { function pack(plat, arch, cb) { // there is no darwin ia32 electron - if (plat === 'darwin' && arch === 'ia32') return; - - const iconObj = { - icon: DEFAULT_OPTS.icon + (() => { - let extension = '.png'; - if (plat === 'darwin') { - extension = '.icns'; - } else if (plat === 'win32') { - extension = '.ico'; - } - return extension; - })() - }; + if (plat === 'darwin' && arch === 'ia32') { + return; + } + + const iconObj = {}; + switch (plat) { + case 'darwin': + iconObj.icon = 'resources/osx/icon.icns'; + break; + case 'win32': + iconObj.icon = 'resources/windows/icon.ico'; + break; + default: + } const opts = Object.assign({}, DEFAULT_OPTS, iconObj, { platform: plat, From 7efc3be65ceab7b825db21a908d4e02825f996e0 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 17:02:48 +0200 Subject: [PATCH 09/41] fix: minifier cannot do dead code removal with a const variable, needs process.env.NODE_ENV === 'production' --- app/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/index.js b/app/index.js index 1f4e476..e640c5d 100644 --- a/app/index.js +++ b/app/index.js @@ -1,3 +1,4 @@ +/* eslint global-require: 0 */ /** * The frontend application. */ @@ -16,7 +17,6 @@ import { mapXYtoParam } from './actions/mapping'; import './app.global.css'; import { join } from 'path'; -const production = process.env.NODE_ENV === 'production'; // how ironic, the path to config is different // const config = require(production ? './config/index' : '../config/index'); // console.log('dirname', __dirname); @@ -45,7 +45,7 @@ connectSoundApp(store, callActionOnMain); injectTapEventPlugin(); // add right click inspect element context menu -if (!production) { +if (process.env.NODE_ENV !== 'production') { require('debug-menu').install(); } @@ -57,7 +57,7 @@ render( ); // load an initial dataset and sound -const appRoot = production ? __dirname : join(__dirname, '../app'); +const appRoot = process.env.NODE_ENV === 'production' ? __dirname : join(__dirname, '../app'); const iris = join(appRoot, 'vendor/datasets', 'iris.csv'); store.dispatch(loadInternalDataset(iris)); From 3aa53ad5361cee486f16b8358fef41325ede4fbd Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 27 Jun 2016 17:03:11 +0200 Subject: [PATCH 10/41] lint --- app/sound/connectSoundApp.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/sound/connectSoundApp.js b/app/sound/connectSoundApp.js index 4736170..ff34e49 100644 --- a/app/sound/connectSoundApp.js +++ b/app/sound/connectSoundApp.js @@ -120,7 +120,8 @@ export default function connectSoundApp(store, callActionOnMain) { } } - observeStore(store, getLoopMode, (loopMode) => { + observeStore(store, getLoopMode, () => { + // /*loopMode*/ // console.log('loopMode changed', loopMode); if (!timer) { // console.log('start interval'); From cd3965ffdb8ee922740268d36df73c1d937ced0d Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 28 Jun 2016 12:49:06 +0200 Subject: [PATCH 11/41] fix: sizing of sidebar items on laptop sized screens --- app/app.global.css | 18 ++++++++++++++++++ app/components/XYParamTable.css | 1 + app/containers/Sidebar.css | 22 ++++++++-------------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/app.global.css b/app/app.global.css index b6ced21..04811cc 100644 --- a/app/app.global.css +++ b/app/app.global.css @@ -24,3 +24,21 @@ body { .MainLayout { height: 100vh; } + +/* sidebar heights that can't be done with css modules */ +.selectable-list { + overflow-y: auto; + margin-bottom: 3px; +} + +.sound-selector .selectable-list { + height: 150px; +} + +/*.dataset-selector { + height: 150px; +}*/ + +/*.dataset-selector .selectable-list { + height: 73px; +}*/ diff --git a/app/components/XYParamTable.css b/app/components/XYParamTable.css index 0abde26..caf35e4 100644 --- a/app/components/XYParamTable.css +++ b/app/components/XYParamTable.css @@ -29,6 +29,7 @@ .range { width: 100px; padding: 0; + padding-right: 20px; } .connected td, diff --git a/app/containers/Sidebar.css b/app/containers/Sidebar.css index 235d8a3..b9c7ce9 100644 --- a/app/containers/Sidebar.css +++ b/app/containers/Sidebar.css @@ -19,23 +19,17 @@ font-variant: small-caps; } -.sidebar .selectable-list { - overflow-y: scroll; +/*.sidebar .selectable-list { + overflow-y: auto; margin-bottom: 3px; -} +}*/ -.datasets { - height: 150px; -} -.datasets .selectable-list { - height: 73px; -} -.sounds { - height: 300px; -} +/*.sounds { + height: 100px; +}*/ .params { flex: 1; } -.help { +/*.help { height: 150px; -} +}*/ From efaf187da4da1b855e8205e130ed476f420372dd Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 16 Aug 2016 17:46:26 +0200 Subject: [PATCH 12/41] Changes loop instantly from one box to another or start/stop. Simplify UI and redux actions for changing loop. Uses supercolliderjs SynthEventList with loopTime so scheduling happens there, not in the renderer; removes the setInterval loop --- app/actions/interaction.js | 28 ++--- app/components/ScatterPlotClickSurface.js | 1 - app/components/ScatterPlotsInteractive.js | 17 +-- app/reducers/interaction.js | 85 ++++++++------- app/selectors/sound.js | 59 +++-------- app/sound/SoundApp.js | 12 +-- app/sound/connectSoundApp.js | 121 +++++++--------------- test/reducers/interaction.spec.js | 59 +---------- 8 files changed, 122 insertions(+), 260 deletions(-) diff --git a/app/actions/interaction.js b/app/actions/interaction.js index b3d2973..e5eef45 100644 --- a/app/actions/interaction.js +++ b/app/actions/interaction.js @@ -34,6 +34,14 @@ export function setPointsUnderBrush(m, n, indices) { }; } + +/** + * toggleLoopMode - turns loop on, or changes it to a different box or turns it off + * + * @param {number} m box coordinate + * @param {number} n box coordinate + * @return {Object} action + */ export function toggleLoopMode(m, n) { return { type: TOGGLE_LOOP_MODE, @@ -44,23 +52,3 @@ export function toggleLoopMode(m, n) { }; } -/** - * @param {Object} loopingState - * - * May contain these: - * - * nowPlaying: {m n} - * pending: {m n} - */ -export function setLooping(loopingState) { - return (dispatch, getState) => { - const state = getState().interaction.loopMode; - const comp = {nowPlaying: state.nowPlaying, pending: state.pending}; - if (!_.isEqual(comp, loopingState)) { - dispatch({ - type: SET_LOOPING, - payload: loopingState - }); - } - }; -} diff --git a/app/components/ScatterPlotClickSurface.js b/app/components/ScatterPlotClickSurface.js index a5b8677..7d082c7 100644 --- a/app/components/ScatterPlotClickSurface.js +++ b/app/components/ScatterPlotClickSurface.js @@ -106,7 +106,6 @@ export default class ScatterPlotClickSurface extends React.Component { onMouseDown: (e) => { if (e.buttons && e.metaKey) { - // toggle loop mode this.props.toggleLoopMode(this.props.m, this.props.n); } else { this._brush(e.clientX, e.clientY); diff --git a/app/components/ScatterPlotsInteractive.js b/app/components/ScatterPlotsInteractive.js index cd5a7ca..562bf66 100644 --- a/app/components/ScatterPlotsInteractive.js +++ b/app/components/ScatterPlotsInteractive.js @@ -136,13 +136,9 @@ class ScatterPlotsInteractive extends React.Component { } const s = { - nowPlaying: { - m: _.get(this.props.loopMode, 'nowPlaying.m'), - n: _.get(this.props.loopMode, 'nowPlaying.n') - }, - pending: { - m: _.get(this.props.loopMode, 'pending.m'), - n: _.get(this.props.loopMode, 'pending.n') + box: { + m: _.get(this.props.loopMode, 'box.m'), + n: _.get(this.props.loopMode, 'box.n') }, last: { m: _.get(this.state, 'last.m'), @@ -152,13 +148,8 @@ class ScatterPlotsInteractive extends React.Component { // pending should be erased once it becomes active const getClassName = (box) => { - if ((s.nowPlaying.m === box.m) && (s.nowPlaying.n === box.n)) { + if ((s.box.m === box.m) && (s.box.n === box.n)) { return style.looping; - // return this.state.loopMode.looping ? style.looping : null; - } - - if ((s.pending.m === box.m) && (s.pending.n === box.n)) { - return style.pending; } if ((s.last.m === box.m) && (s.last.n === box.n)) { diff --git a/app/reducers/interaction.js b/app/reducers/interaction.js index f0dd9cb..51b2508 100644 --- a/app/reducers/interaction.js +++ b/app/reducers/interaction.js @@ -1,16 +1,22 @@ import { SET_POINTS_UNDER_BRUSH, - TOGGLE_LOOP_MODE, - SET_LOOPING + TOGGLE_LOOP_MODE } from '../actionTypes'; import { calcPointsEntering } from '../selectors/index'; import u from 'updeep'; -import { xor, get, assign } from 'lodash'; +import { xor, get, now } from 'lodash'; + +const DEFAULT_LOOP_TIME = 10; /** - * this reducer only gets state.interaction + * Top level reducer that takes handles actions and calls + * the appropriate reducer. + * + * A reducer take state and action and returns a new transformed state. + * + * This reducer only gets state.interaction */ export default function interaction(state = {}, action) { switch (action.type) { @@ -18,13 +24,20 @@ export default function interaction(state = {}, action) { return setPointsUnderBrush(state, action); case TOGGLE_LOOP_MODE: return toggleLoopMode(state, action); - case SET_LOOPING: - return setLooping(state, action); default: return state; } } + +/** + * setPointsUnderBrush - Action triggered by mouse move, + * sets the current points inside the brush rectangle. + * + * @param {Object} state current state + * @param {Object} action payload is {m n indices} + * @return {Object} new state + */ function setPointsUnderBrush(state, action) { const differentBox = (state.m !== action.payload.m) || (state.n !== action.payload.n); @@ -54,40 +67,36 @@ function _sameBox(state, payload, key) { } } + +/** + * toggleLoopMode - toggles the SoundApp's loop mode on or off + * + * Updates state.interaction.loopMode + * + * If not currently looping then it starts looping that m@n box. + * If you click on a different box then it changes the loop to that. + * If you click on the currently playing box then it stops looping. + * + * @param {Object} state current state + * @param {Object} action .payload is {m n} + * @return {Object} new state + */ function toggleLoopMode(state, action) { - // console.log('toggleLoopMode', state, action); - // click on the same box again: toggle to off - if (_sameBox(state, action.payload, 'nowPlaying') - || _sameBox(state, action.payload, 'pending')) { - // toggle it to off - return u({ - loopMode: { - looping: false - } - }, state); - // TODO: but a second tap on pending should not change anything - } + const loopMode = { + loopTime: get(state, 'loopMode.loopTime') || DEFAULT_LOOP_TIME + }; - return u({ - loopMode: { - pending: action.payload, - looping: true + // clicked the same box, toggle it to off + if (_sameBox(state, action.payload, 'box')) { + loopMode.box = null; + loopMode.epoch = null; + } else { + loopMode.box = action.payload; + // if not already playing then start the loop in 50ms + if (!get(state, 'loopMode.epoch')) { + loopMode.epoch = now() + 50; } - }, state); -} + } -/** - * for UI updates to show that the loop is pending or now playing m@n - */ -function setLooping(state, action) { - // action may contain nowPlaying and pending - // updates loopMode, adding these - // const updates = { - // looping: action.payload.looping, - // nowPlaying: action.payload.nowPlaying || null, - // pending: action.payload.pending || null - // }; - const loopMode = assign({}, state.loopMode || {}, action.payload); - const newState = assign({}, state, {loopMode}); - return newState; + return u({ loopMode }, state); } diff --git a/app/selectors/sound.js b/app/selectors/sound.js index 80e9670..9fb583c 100644 --- a/app/selectors/sound.js +++ b/app/selectors/sound.js @@ -11,6 +11,7 @@ const getInteraction = (state) => state.interaction || {}; const getSoundName = (state) => state.sound; const getSounds = (state) => state.sounds; const getMapping = (state) => state.mapping || {}; +const getLoop = (state) => _.get(state, 'interaction.loopMode', {}); export const getSound = createSelector( [getSoundName, getSounds], @@ -203,32 +204,37 @@ export function makeMapper(spec) { } /** - * Builds the payload for SET_LOOP action + * loopModePayload - Builds the payload for SET_LOOP action that is sent to the main thread + * to be sent by the SoundApp and then sent using the updateStream to the + * SynthEventList dryad. */ -export function loopModePayload(m, n, state) { +export function loopModePayload(state) { const sound = getSound(state); - if (!sound) { - return; + const loopMode = getLoop(state); + if (!sound || (!loopMode.box)) { + return { + events: [] + }; } const npoints = getNormalizedPoints(state); const mapping = getMapping(state); const mappingControls = getXYMappingControls(state); - // const events = loopModeSynthEventList(loopMode, sound, npoints, mapping, mappingControls); const events = loopModeEvents( - m, - n, + loopMode.box.m, + loopMode.box.n, npoints, mapping, mappingControls, sound, - 10.0 + loopMode.loopTime ); return { events, - epoch: _.now() + 300 + loopTime: loopMode.loopTime, + epoch: loopMode.epoch }; } @@ -272,38 +278,3 @@ export function loopModeEvents(m, n, npoints, mapping, mappingControls, sound, l }; }); } - -/** - * Returns a dryadic json document - * - * ['syntheventlist', { - defaultParams: { - defName: 'blip', - args: {} - }, - events: [ - { - time: - args: {} - } - ] - }] - */ -// export function loopModeSynthEventList(loopMode, sound, npoints, mapping) { -// const loopTime = loopMode.loopTime || 10.0; -// if (loopMode.looping && sound) { -// // create list from m n npoints mapping -// return [ -// 'syntheventlist', -// { -// defaultParams: { -// defName: sound.name, -// args: {} // fixed args -// }, -// events: loopModeEvents(loopMode.m, loopMode.n, npoints, mapping, sound, loopTime) -// } -// ]; -// } else { -// return null; // delete current playing loop, replace with nothing -// } -// } diff --git a/app/sound/SoundApp.js b/app/sound/SoundApp.js index b4aff5a..dfc112d 100644 --- a/app/sound/SoundApp.js +++ b/app/sound/SoundApp.js @@ -164,10 +164,14 @@ export default class SoundApp { } /** - * events: epoch: + * Push the object to the BaconJS loopModeEventStream which is connected to the SynthEventList. + * + * @param {Object} payload - + * events: + * epoch: + * loopTime: */ setLoop(payload) { - this.clearSched(); this.loopModeEventStream.push(payload); } @@ -175,10 +179,6 @@ export default class SoundApp { this.masterControlStream.push(event); } - clearSched() { - // console.log(this.player); - } - /** * Read sounds metadata files and dispatch setSounds action to renderer process. */ diff --git a/app/sound/connectSoundApp.js b/app/sound/connectSoundApp.js index ff34e49..b063085 100644 --- a/app/sound/connectSoundApp.js +++ b/app/sound/connectSoundApp.js @@ -1,3 +1,11 @@ +/** + * Runs in the renderer process. + * + * Connects to the redux store and forwards actions + * to the background process, where they are forwarded + * to the SoundApp + */ + import { SPAWN_SYNTHS, SET_LOOP @@ -7,18 +15,19 @@ import { spawnEventsFromBrush, loopModePayload } from '../selectors/index'; -import { - setLooping -} from '../actions/interaction'; + /** - * Runs in the renderer process. + * observeStore - Call onChange whenever the state changes * - * Connects to the redux store and forwards actions - * to the background process, where they are forwarded - * to the SoundApp + * On each change in the Redux store, this derives an object + * using a Reselect selector function and compares that to the previously derived object. + * + * @param {type} store Redux store + * @param {Function} select Reselect selector function that derives an Object from state + * @param {Function} onChange Handler that you wish to be called + * @return {Function} Unsubscribe function */ - function observeStore(store, select, onChange) { let currentState; @@ -39,14 +48,27 @@ function observeStore(store, select, onChange) { const getPointsEntering = (state) => state.interaction.pointsEntering; const getLoopMode = (state) => state.interaction.loopMode; + +/** + * connectSoundApp - connect redux store to the SoundApp on main thread + * + * Observes the redux store: + * + * for pointsEntering it spawns synth events via callActionOnMain SPAWN_SYNTHS + * + * for loopMode it makes the loop payload and sends SET_LOOP to main + * + * @param {Object} store redux store + * @param {Function} callActionOnMain + * @return {undefined} + */ export default function connectSoundApp(store, callActionOnMain) { + // call handler on change of pointsEntering observeStore(store, getPointsEntering, (state) => { // pointsEntering changed: m n indices // generate synths from that // for now: send using callActionOnMain - // later: set to state.synth.spawnEvents - // and let redux-electron-store copy it over const synthEvents = spawnEventsFromBrush(state); if (synthEvents.length) { callActionOnMain({ @@ -56,78 +78,11 @@ export default function connectSoundApp(store, callActionOnMain) { } }); - /** - * This is a temporary loop system. - * It just runs a timer and resends the loop events - * to the SoundApp which pushes it through to a Dryadic component - * which does the sending. - * - * A better system will be to have a have a dryadic client - * here in the frontend app and have it communicate the changes. - * A dryad that plays a loop of events and can be live updated. - */ - let timer; - - function triggerLoop() { - const state = store.getState(); - const loopMode = state.interaction.loopMode; - // console.log('triggerLoop gets loopMode:', loopMode); - let newLoopMode = { - looping: loopMode.looping - }; - - // stop - if (!loopMode.looping) { - newLoopMode.nowPlaying = null; - newLoopMode.pending = null; - newLoopMode.looping = false; - } else { - // if pending then copy it in - if (loopMode.pending) { - newLoopMode.nowPlaying = loopMode.pending; - newLoopMode.pending = null; - } else { - // carry on playing - newLoopMode = loopMode; - } - } - - // console.log('newLoopMode:', newLoopMode); - - if (newLoopMode.nowPlaying) { - const payload = loopModePayload(newLoopMode.nowPlaying.m, newLoopMode.nowPlaying.n, state); - // console.log('payload', payload); - if (payload) { - callActionOnMain({ - type: SET_LOOP, - payload - }); - } - } else { - if (timer) { - // console.log('clear timer', timer); - clearInterval(timer); - timer = null; - // send kill loop - } - } - - // update the ui - if (newLoopMode !== loopMode) { - // console.log('update state'); - // TODO: does not unset pending - store.dispatch(setLooping(newLoopMode)); - } - } - - observeStore(store, getLoopMode, () => { - // /*loopMode*/ - // console.log('loopMode changed', loopMode); - if (!timer) { - // console.log('start interval'); - timer = setInterval(triggerLoop, 10000); - // console.log('first time trigger'); - triggerLoop(); - } + // on change of: loopMode, sound, mapping + observeStore(store, getLoopMode, (state) => { + callActionOnMain({ + type: SET_LOOP, + payload: loopModePayload(state) + }); }); } diff --git a/test/reducers/interaction.spec.js b/test/reducers/interaction.spec.js index b726d14..f1bd42f 100644 --- a/test/reducers/interaction.spec.js +++ b/test/reducers/interaction.spec.js @@ -5,8 +5,7 @@ import { import interaction from '../../app/reducers/interaction'; import { TOGGLE_LOOP_MODE, - SET_POINTS_UNDER_BRUSH, - SET_LOOPING + SET_POINTS_UNDER_BRUSH } from '../../app/actionTypes'; describe('reducers/interaction', function() { @@ -29,69 +28,19 @@ describe('reducers/interaction', function() { it('should toggle loop from null state', function() { const state = interaction({}, click1); - expect(state.loopMode.pending).to.deep.equal(click1.payload); + expect(state.loopMode.box).to.deep.equal(click1.payload); }); it('should keep loop mode when clicking other', function() { const initial = interaction({}, click1); const state = interaction(initial, click2); - expect(state.loopMode.pending).to.deep.equal(click2.payload); + expect(state.loopMode.box).to.deep.equal(click2.payload); }); it('should toogle loop back off when clicking same', function() { const initial = interaction({}, click1); const state = interaction(initial, click1); - expect(state.loopMode.looping).to.equal(false); - }); - }); - - describe('setLooping', function() { - const pending = { - type: SET_LOOPING, - payload: { - pending: { - m: 1, - n: 1 - } - } - }; - - const blank = { - type: SET_LOOPING, - payload: {} - }; - - const playing = { - type: SET_LOOPING, - payload: { - nowPlaying: { - m: 1, - n: 1 - }, - pending: null - } - }; - - it('should set pending', function() { - let s = interaction({}, pending); - expect(s.loopMode.pending.m).to.equal(1); - expect(s.loopMode.pending.n).to.equal(1); - }); - - it('should set playing', function() { - let s = interaction({}, pending); - let r = interaction(s, playing); - expect(r.loopMode.nowPlaying.m).to.equal(1); - expect(r.loopMode.nowPlaying.n).to.equal(1); - expect(r.loopMode.pending).to.be.null; - // expect(r.loopMode.pending.n).to.be.undefined; - }); - - it('should unset on blank', function() { - let s = interaction({}, playing); - let r = interaction(s, blank); - expect(r.loopMode.pending).to.be.null; - expect(r.loopMode.looping).to.be.undefined; + expect(state.loopMode.box).to.equal(null); }); }); From 2268978815a0768c92ee909295623b138f9e469d Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 16 Aug 2016 18:30:44 +0200 Subject: [PATCH 13/41] re-compute and send loop on any change of sound, mapping, looping --- app/selectors/sound.js | 60 ++++++++++++++++++++---------------- app/sound/connectSoundApp.js | 23 +++++--------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/app/selectors/sound.js b/app/selectors/sound.js index 9fb583c..0ef0522 100644 --- a/app/selectors/sound.js +++ b/app/selectors/sound.js @@ -204,39 +204,47 @@ export function makeMapper(spec) { } /** - * loopModePayload - Builds the payload for SET_LOOP action that is sent to the main thread + * getLoopModePayload - Builds the payload for SET_LOOP action that is sent to the main thread * to be sent by the SoundApp and then sent using the updateStream to the * SynthEventList dryad. + * + * This is a Reselect selector. + * + * @return {Object} events, loopTime, epoch */ -export function loopModePayload(state) { - const sound = getSound(state); - const loopMode = getLoop(state); - if (!sound || (!loopMode.box)) { +export const getLoopModePayload = createSelector( + [ + getSound, + getLoop, + getNormalizedPoints, + getMapping, + getXYMappingControls + ], + (sound, loopMode, npoints, mapping, mappingControls) => { + if (!sound || (!loopMode.box)) { + return { + events: [] + }; + } + + const events = loopModeEvents( + loopMode.box.m, + loopMode.box.n, + npoints, + mapping, + mappingControls, + sound, + loopMode.loopTime + ); + return { - events: [] + events, + loopTime: loopMode.loopTime, + epoch: loopMode.epoch }; } +); - const npoints = getNormalizedPoints(state); - const mapping = getMapping(state); - const mappingControls = getXYMappingControls(state); - - const events = loopModeEvents( - loopMode.box.m, - loopMode.box.n, - npoints, - mapping, - mappingControls, - sound, - loopMode.loopTime - ); - - return { - events, - loopTime: loopMode.loopTime, - epoch: loopMode.epoch - }; -} export function loopModeEvents(m, n, npoints, mapping, mappingControls, sound, loopTime) { diff --git a/app/sound/connectSoundApp.js b/app/sound/connectSoundApp.js index b063085..557b1c1 100644 --- a/app/sound/connectSoundApp.js +++ b/app/sound/connectSoundApp.js @@ -13,7 +13,7 @@ import { import { spawnEventsFromBrush, - loopModePayload + getLoopModePayload } from '../selectors/index'; @@ -29,14 +29,14 @@ import { * @return {Function} Unsubscribe function */ function observeStore(store, select, onChange) { - let currentState; + let currentDerivedState; function handleChange() { const state = store.getState(); - const nextState = select(state); - if (nextState !== currentState) { - currentState = nextState; - onChange(state); + const derivedState = select(state); + if (derivedState !== currentDerivedState) { + currentDerivedState = derivedState; + onChange(state, derivedState); } } @@ -46,8 +46,6 @@ function observeStore(store, select, onChange) { } const getPointsEntering = (state) => state.interaction.pointsEntering; -const getLoopMode = (state) => state.interaction.loopMode; - /** * connectSoundApp - connect redux store to the SoundApp on main thread @@ -64,11 +62,7 @@ const getLoopMode = (state) => state.interaction.loopMode; */ export default function connectSoundApp(store, callActionOnMain) { - // call handler on change of pointsEntering observeStore(store, getPointsEntering, (state) => { - // pointsEntering changed: m n indices - // generate synths from that - // for now: send using callActionOnMain const synthEvents = spawnEventsFromBrush(state); if (synthEvents.length) { callActionOnMain({ @@ -78,11 +72,10 @@ export default function connectSoundApp(store, callActionOnMain) { } }); - // on change of: loopMode, sound, mapping - observeStore(store, getLoopMode, (state) => { + observeStore(store, getLoopModePayload, (state, payload) => { callActionOnMain({ type: SET_LOOP, - payload: loopModePayload(state) + payload }); }); } From a91fb2e45dfdfb9ff88e10109ccb2431cd0a9056 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Wed, 17 Aug 2016 17:16:47 +0200 Subject: [PATCH 14/41] Show animated play head position. --- app/components/LoopPlayHead.css | 5 ++ app/components/LoopPlayHead.js | 70 +++++++++++++++++++++++++ app/components/ScatterPlots.css | 2 +- app/containers/ScatterPlotsContainer.js | 8 +-- app/selectors/sound.js | 3 +- app/selectors/ui.js | 22 ++++++++ 6 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 app/components/LoopPlayHead.css create mode 100644 app/components/LoopPlayHead.js diff --git a/app/components/LoopPlayHead.css b/app/components/LoopPlayHead.css new file mode 100644 index 0000000..20350ac --- /dev/null +++ b/app/components/LoopPlayHead.css @@ -0,0 +1,5 @@ + +.playHead { + fill: rgba(22, 249, 45, 1); + opacity: 0.5; +} diff --git a/app/components/LoopPlayHead.js b/app/components/LoopPlayHead.js new file mode 100644 index 0000000..6b39ec2 --- /dev/null +++ b/app/components/LoopPlayHead.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import connect from '../utils/reduxers'; +import { getLoop, getLoopBox } from '../selectors'; +import { now } from 'lodash'; + +import styles from './LoopPlayHead.css'; + +// animation frame rate is 16 +const SPEED = 20; + +/** + * Animates a vertical line to show where the loop is playing. + * + * There is one of these in the app, positioned over the currently playing loop box. + */ +class LoopPlayHead extends React.Component { + + static propTypes = { + loopBox: React.PropTypes.object.isRequired, + loopMode: React.PropTypes.object.isRequired + }; + + componentDidMount() { + this._interval = window.setInterval(() => this.tick(), SPEED); + } + + componentWillUnmount() { + window.clearInterval(this._interval); + } + + tick() { + if (this.props.loopMode.box) { + const delta = now() - this.props.loopMode.epoch; + const inLoop = (delta / 1000) % this.props.loopMode.loopTime; + const pos = inLoop / this.props.loopMode.loopTime; + this.setState({ + pos + }); + } + } + + render() { + if (this.props.loopBox) { + const x = (this.state.pos || 0) * this.props.loopBox.width + this.props.loopBox.x; + const y = this.props.loopBox.y; + return ( + + + + ); + } + + return null; + } +} + + +export default connect({ + loopBox: getLoopBox, + loopMode: getLoop +})(LoopPlayHead); diff --git a/app/components/ScatterPlots.css b/app/components/ScatterPlots.css index 62e244c..635e4f9 100644 --- a/app/components/ScatterPlots.css +++ b/app/components/ScatterPlots.css @@ -11,7 +11,7 @@ } .looping { - stroke: rgba(192, 22, 249, 1); + stroke: rgba(22, 249, 45, 1); } .focused { diff --git a/app/containers/ScatterPlotsContainer.js b/app/containers/ScatterPlotsContainer.js index 014fad2..41e0ee8 100644 --- a/app/containers/ScatterPlotsContainer.js +++ b/app/containers/ScatterPlotsContainer.js @@ -4,6 +4,7 @@ import connect from '../utils/reduxers'; import ScatterPlots from '../components/ScatterPlots'; import ScatterPlotsActivePoints from '../components/ScatterPlotsActivePoints'; import ScatterPlotsInteractive from '../components/ScatterPlotsInteractive'; +import LoopPlayHead from '../components/LoopPlayHead'; import * as _ from 'lodash'; import { @@ -15,9 +16,10 @@ import { /** - * Goes inside a svg, adds a g containing: + * Goes inside a svg, adds a g which layers each of these on top of each other: * - ScatterPlots * - ScatterPlotsActivePoints + * - LoopPlayHead * - ScatterPlotsInteractive */ class ScatterPlotsContainer extends Component { @@ -44,9 +46,8 @@ class ScatterPlotsContainer extends Component { props.width = this.props.width - (padding * 2); const plots = h(ScatterPlots, props); - const activePoints = h(ScatterPlotsActivePoints); - + const loopPlayHead = h(LoopPlayHead); const surface = h(ScatterPlotsInteractive, props); return h( @@ -59,6 +60,7 @@ class ScatterPlotsContainer extends Component { [ plots, activePoints, + loopPlayHead, surface ] ); diff --git a/app/selectors/sound.js b/app/selectors/sound.js index 0ef0522..25d8c07 100644 --- a/app/selectors/sound.js +++ b/app/selectors/sound.js @@ -11,7 +11,8 @@ const getInteraction = (state) => state.interaction || {}; const getSoundName = (state) => state.sound; const getSounds = (state) => state.sounds; const getMapping = (state) => state.mapping || {}; -const getLoop = (state) => _.get(state, 'interaction.loopMode', {}); + +export const getLoop = (state) => _.get(state, 'interaction.loopMode', {}); export const getSound = createSelector( [getSoundName, getSounds], diff --git a/app/selectors/ui.js b/app/selectors/ui.js index 44126fb..61a1613 100644 --- a/app/selectors/ui.js +++ b/app/selectors/ui.js @@ -1,6 +1,7 @@ import { centeredSquareWithMargin } from '../utils/layout'; import { createSelector } from 'reselect'; import { getDatasetMetadata, getFeatures } from './dataset'; +import { getLoop } from './sound'; import * as _ from 'lodash'; export const getWindowSize = (state) => state.ui.windowSize; @@ -127,3 +128,24 @@ export const getPointsForPlot = createSelector( }); } ); + + +/** + * Selector that returns the rect of the box that is currently set to play loop. + * Returns undefined if none is looping. + * {x y width height} + */ +export const getLoopBox = createSelector( + [getLayout, getNumFeatures, getLoop], + (layout, numFeatures, loopMode) => { + if (loopMode.box) { + const box = layout.boxes[loopMode.box.m * numFeatures + loopMode.box.n]; + return { + x: box.x, + y: box.y, + width: layout.sideLength - layout.margin, + height: layout.sideLength - layout.margin + }; + } + } +); From a8fc1dc62655c194d6fa40c0e64fd5fcc0153ef7 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Wed, 17 Aug 2016 20:17:22 +0200 Subject: [PATCH 15/41] class and component documentation --- app/actions/interaction.js | 11 ++++++ app/components/Axis.js | 5 +++ app/components/AxisLine.js | 7 ++-- app/components/AxisTicks.js | 8 +++-- app/components/LAxis.js | 7 +++- app/components/Label.js | 6 ++++ app/components/Line.js | 3 ++ app/components/MapButton.js | 5 +++ app/components/Notification.js | 4 +++ app/components/Points.js | 4 +++ app/components/SVGFilters.js | 12 ++++++- app/components/ScatterPlotClickSurface.js | 8 +++-- app/components/ScatterPlots.js | 4 ++- app/components/ScatterPlotsActivePoints.js | 12 +++++-- app/components/ScatterPlotsInteractive.js | 31 ++++++---------- app/components/SelectArea.js | 9 +++++ app/components/XAxis.js | 6 ++++ app/components/XYParamTable.js | 5 +++ app/components/YAxis.js | 10 +++--- app/containers/App.js | 5 +++ app/containers/DatasetSelector.js | 5 +++ app/containers/DevTools.js | 6 ++++ app/containers/Help.js | 5 +++ app/containers/Main.js | 5 +++ app/containers/MainLayout.js | 11 ++++++ app/containers/ParamMapping.js | 13 +++++++ app/containers/SVGFrame.js | 7 ++-- app/containers/ScatterPlotsContainer.js | 5 ++- app/containers/SoundSelector.js | 2 +- app/ipc/handleActionOnMain.js | 2 +- app/reducers/index.js | 15 ++++++++ app/routes.js | 13 +++++++ app/selectors/index.js | 10 ++++++ app/selectors/sound.js | 41 +++++++++++++++------- app/sound/SoundApp.js | 3 +- main.development.js | 9 ++--- 36 files changed, 253 insertions(+), 61 deletions(-) diff --git a/app/actions/interaction.js b/app/actions/interaction.js index e5eef45..d625c8c 100644 --- a/app/actions/interaction.js +++ b/app/actions/interaction.js @@ -17,6 +17,17 @@ export function showBrush(show, x, y) { }; } + +/** + * setPointsUnderBrush - called when moving the brush over points. + * + * points under brush is further processed into The sound app responds to changes in poin + * + * @param {number} m box coordinate + * @param {number} n box coordinate + * @param {Array} indices list of point indices + * @return {Object} action + */ export function setPointsUnderBrush(m, n, indices) { return (dispatch, getState) => { const s = getState().interaction; diff --git a/app/components/Axis.js b/app/components/Axis.js index 1e240e6..08d11c5 100644 --- a/app/components/Axis.js +++ b/app/components/Axis.js @@ -4,6 +4,11 @@ import h from 'react-hyperscript'; import XAxis from './XAxis'; import YAxis from './YAxis'; +/** + * Standard pair of X and Y axis with ticks and labelling. + * + * This combines the X and Y into one component. + */ export default class Axis extends React.Component { static propTypes = { diff --git a/app/components/AxisLine.js b/app/components/AxisLine.js index 49087a7..43ad772 100644 --- a/app/components/AxisLine.js +++ b/app/components/AxisLine.js @@ -1,11 +1,10 @@ +import React from 'react'; /** + * Draws a line along an axis - used by XAxis and YAxis + * * https://github.com/esbullington/react-d3 */ - -import React from 'react'; -// var d3 = require('d3'); - export default React.createClass({ displayName: 'AxisLine', diff --git a/app/components/AxisTicks.js b/app/components/AxisTicks.js index 11245b1..bf3db09 100644 --- a/app/components/AxisTicks.js +++ b/app/components/AxisTicks.js @@ -1,9 +1,11 @@ +import React from 'react'; + + /** + * Draws ticks along an axis - used by XAxis and YAxis + * * https://github.com/esbullington/react-d3 */ -import React from 'react'; -// var d3 = require('d3'); - export default React.createClass({ displayName: 'AxisTicks', diff --git a/app/components/LAxis.js b/app/components/LAxis.js index c2ec31c..069aefb 100644 --- a/app/components/LAxis.js +++ b/app/components/LAxis.js @@ -1,7 +1,12 @@ import h from 'react-hyperscript'; + +/** + * Draw a very minimal L-shaped axis along the bottom left of a plot. + * + * Each plot has one of these. + */ export default (props) => { - // draw a minimal L-shaped axis return h('polyline', { points: `0,0 0,${props.sideLength} ${props.sideLength},${props.sideLength}`, strokeWidth: 1, diff --git a/app/components/Label.js b/app/components/Label.js index c187657..ab336fa 100644 --- a/app/components/Label.js +++ b/app/components/Label.js @@ -2,6 +2,12 @@ import React from 'react'; +/** + * A string label used by the XAxis and YAxis. + * + * source: + * https://github.com/esbullington/react-d3 + */ module.exports = React.createClass({ displayName: 'Label', diff --git a/app/components/Line.js b/app/components/Line.js index 580fb81..ecff556 100644 --- a/app/components/Line.js +++ b/app/components/Line.js @@ -1,5 +1,8 @@ import React from 'react'; +/** + * Renders an SVG line + */ export default class Line extends React.Component { render() { diff --git a/app/components/MapButton.js b/app/components/MapButton.js index 5cd2beb..fc91891 100644 --- a/app/components/MapButton.js +++ b/app/components/MapButton.js @@ -7,6 +7,11 @@ import FontIcon from 'material-ui/FontIcon'; import IconButton from 'material-ui/IconButton'; import muiThemeable from 'material-ui/styles/muiThemeable'; + +/** + * A radio-button used in the XYParamTable to + * assign a mapping between the X or Y dimension and a sound parameter. + */ class MapButton extends React.Component { static propTypes = { diff --git a/app/components/Notification.js b/app/components/Notification.js index ebd0ef2..d940cf7 100644 --- a/app/components/Notification.js +++ b/app/components/Notification.js @@ -2,6 +2,10 @@ import React from 'react'; import connect from '../utils/reduxers'; /** + * Displays a text notification for events like + * errors and 'loading...' + * + * This connects to state.ui.notification and displays that message. */ class Notification extends React.Component { diff --git a/app/components/Points.js b/app/components/Points.js index 4ff0955..4d34205 100644 --- a/app/components/Points.js +++ b/app/components/Points.js @@ -1,6 +1,10 @@ import React from 'react'; import h from 'react-hyperscript'; + +/** + * Renders points as SVG circles in a g + */ export default (props) => { // move to layout const radius = props.sideLength < 100 ? 1 : 3; diff --git a/app/components/SVGFilters.js b/app/components/SVGFilters.js index 6be86b4..c0602d7 100644 --- a/app/components/SVGFilters.js +++ b/app/components/SVGFilters.js @@ -1,7 +1,17 @@ import React from 'react'; import h from 'react-hyperscript'; -export default class SVG extends React.Component { +/** + * A radial gradient SVG filter for adding to the SVG component. + * + * For SVG you can declare filters (effects) at the top of the document + * for use on individual nodes (Rect, Circle etc.) + * + * This was used for a circular Brush to show a ring with some transparency + * effects, but is now unused since the adjustable d3 style rect + * brush was added. + */ +export default class SVGFilter extends React.Component { render() { return h('defs', [ diff --git a/app/components/ScatterPlotClickSurface.js b/app/components/ScatterPlotClickSurface.js index 7d082c7..6adefe5 100644 --- a/app/components/ScatterPlotClickSurface.js +++ b/app/components/ScatterPlotClickSurface.js @@ -6,8 +6,12 @@ import styles from './ScatterPlotClickSurface.css'; const RADIUS = 10; // for now /** - * Renders a single rect to handle mouse clicks. - * Changes color when hovering. + * deprec. no longer used. ScatterPlotsInteractive does it + * now with a single component. + * + * Renders a single rect on top of each plot to handle mouse events. + * + * Usually invisible, but it changes color when hovering. */ export default class ScatterPlotClickSurface extends React.Component { diff --git a/app/components/ScatterPlots.js b/app/components/ScatterPlots.js index 5604cb1..7c00c82 100644 --- a/app/components/ScatterPlots.js +++ b/app/components/ScatterPlots.js @@ -7,7 +7,9 @@ import muiThemeable from 'material-ui/styles/muiThemeable'; import style from './ScatterPlots.css'; /** - * Adds a ScatterPlot for each feature pair. + * A single component that adds a ScatterPlot for each feature pair plot. + * + * Also paints the title in the background. */ class ScatterPlots extends React.Component { diff --git a/app/components/ScatterPlotsActivePoints.js b/app/components/ScatterPlotsActivePoints.js index 4382e72..843f5fb 100644 --- a/app/components/ScatterPlotsActivePoints.js +++ b/app/components/ScatterPlotsActivePoints.js @@ -13,8 +13,16 @@ import { } from '../selectors/index'; /** - * Renders just the points that are selected/within the brush - * on top of the normal plot. + * Renders just the points that are selected/within the brush. + * + * This adds a `ScatterPlot` for each box but gives each one + * a different style and adds only the points that are currently active. + * + * It is on top of the `ScatterPlot`s that display all of the points, + * and simply paints identical points (but just for the selected + * points) above them. This means only updating a smaller number of points + * (eg. 30) and not all points for the dataset (eg. 500) -- multiplied + * by the number of boxes (eg. 25) */ class ScatterPlotsActivePoints extends React.Component { diff --git a/app/components/ScatterPlotsInteractive.js b/app/components/ScatterPlotsInteractive.js index 562bf66..c4ec727 100644 --- a/app/components/ScatterPlotsInteractive.js +++ b/app/components/ScatterPlotsInteractive.js @@ -17,7 +17,6 @@ import { getFeatureSideLengthScale } from '../selectors/index'; -// import ScatterPlotClickSurface from './ScatterPlotClickSurface'; import Axis from './Axis'; import SelectArea from './SelectArea'; import style from './ScatterPlots.css'; @@ -37,6 +36,17 @@ const handlers = { toggleLoopMode }; + +/** + * A single component that goes on top of the plots and handles + * mouse events and interactive UI. + * + * This holds all the things that change and respond, thus allowing + * the plots and background to remain fixed without having to re-render + * or recalculate due to UI events. + * + * This adds a SelectArea on top of each ScatterPlot + */ class ScatterPlotsInteractive extends React.Component { static propTypes = { @@ -196,25 +206,6 @@ class ScatterPlotsInteractive extends React.Component { }); children.push(selectedArea); - - // const sp = h(ScatterPlotClickSurface, { - // m: box.m, - // n: box.n, - // points, - // xOffset: box.x, - // yOffset: box.y, - // // for calculating mouse down by clientX/Y - // baseClientX: box.baseClientX, - // baseClientY: box.baseClientY, - // sideLength: innerSideLength, - // setPointsUnderBrush: this.props.setPointsUnderBrush, - // setHovering: this.props.setHovering, - // toggleLoopMode: this.props.toggleLoopMode, - // muiTheme: this.props.muiTheme, - // isLooping, - // isPending - // }); - // children.push(sp); }); return h( diff --git a/app/components/SelectArea.js b/app/components/SelectArea.js index bf5b730..c40f5d8 100644 --- a/app/components/SelectArea.js +++ b/app/components/SelectArea.js @@ -1,3 +1,9 @@ +/** + * Copyright 2016 Chris Sattinger + * MIT License + * + * Adapted from https://github.com/d3/d3-brush + */ import React from 'react'; import style from './SelectArea.css'; @@ -143,6 +149,9 @@ function clip(v, min, max) { * D3 Brush - select a rectangular area * * Adapted from https://github.com/d3/d3-brush + * + * This is one of the nicer components from d3 itself, + * here ported to a simple reusable React component */ export default class SelectArea extends React.Component { diff --git a/app/components/XAxis.js b/app/components/XAxis.js index 15b25fd..e5bcaa9 100644 --- a/app/components/XAxis.js +++ b/app/components/XAxis.js @@ -9,6 +9,12 @@ import AxisTicks from './AxisTicks'; import AxisLine from './AxisLine'; import Label from './Label'; +/** + * XAxis with lines, ticks and string label + * + * source: + * https://github.com/esbullington/react-d3 + */ export default React.createClass({ displayName: 'XAxis', diff --git a/app/components/XYParamTable.js b/app/components/XYParamTable.js index f6a34de..832ce37 100644 --- a/app/components/XYParamTable.js +++ b/app/components/XYParamTable.js @@ -13,6 +13,11 @@ import { debounce } from 'lodash'; // const round = format('g'); +/** + * Editable params in the right sidebar of the app + * to assign data -> sound parameter mappings and to adjust + * the ranges of those mappings. + */ export default class XYParamTable extends React.Component { render() { diff --git a/app/components/YAxis.js b/app/components/YAxis.js index e0461e3..d6d5312 100644 --- a/app/components/YAxis.js +++ b/app/components/YAxis.js @@ -1,14 +1,16 @@ -/** - * https://github.com/esbullington/react-d3 - */ - import React from 'react'; var d3 = require('d3'); import AxisTicks from './AxisTicks'; import AxisLine from './AxisLine'; import Label from './Label'; +/** + * YAxis with lines, ticks and string label + * + * source: + * https://github.com/esbullington/react-d3 + */ export default React.createClass({ displayName: 'YAxis', diff --git a/app/containers/App.js b/app/containers/App.js index 2023167..1eade00 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -1,6 +1,11 @@ import React, { Component, PropTypes } from 'react'; import styles from './App.css'; + +/** + * App is the parent top level component which just wraps + * the route and inserts DevTools. +*/ export default class App extends Component { static propTypes = { diff --git a/app/containers/DatasetSelector.js b/app/containers/DatasetSelector.js index 1679f43..dd64e0d 100644 --- a/app/containers/DatasetSelector.js +++ b/app/containers/DatasetSelector.js @@ -10,6 +10,11 @@ import { openDatasetDialog } from '../actions/datasets'; + +/** + * Component in right sidebar to select from available + * datasets or to click to open a dataset from the filesystem. + */ class DatasetSelector extends Component { static propTypes = { diff --git a/app/containers/DevTools.js b/app/containers/DevTools.js index 460a382..5bf6bd1 100644 --- a/app/containers/DevTools.js +++ b/app/containers/DevTools.js @@ -4,6 +4,12 @@ import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; // import FilterableLogMonitor from 'redux-devtools-filterable-log-monitor'; + +/** + * For development work, this adds redux devtools to the app. + * + * It appears on the right side when you press ctrl-h + */ export default createDevTools( @@ -191,6 +199,13 @@ export function xyPointsEnteringToSynthEvents(pointsEntering, }); } + +/** + * makeMapper - return one of the mapping functions from supercollider.js + * + * @param {Object} spec Similar to supercollider's ControlSpec + * @return {Function} Function that maps a unipolar value to the Spec's range and warp curve. + */ export function makeMapper(spec) { switch (spec.warp) { case 'exp': diff --git a/app/sound/SoundApp.js b/app/sound/SoundApp.js index dfc112d..9677343 100644 --- a/app/sound/SoundApp.js +++ b/app/sound/SoundApp.js @@ -23,7 +23,7 @@ const options = _.assign({}, config.supercolliderjs.options || {}, { // so it requires that a path to an external SuperCollider.app is supplied // in config/development.json scsynth: path.join(config.appRoot, 'vendor/supercollider/osx/scsynth'), - echo: true, // wonky. this means post osc + echo: true, // wonky. this means post osc messages to console debug: true, includePaths: [], sclang_conf: null @@ -62,6 +62,7 @@ export default class SoundApp { && process.env.NODE_ENV === 'development'; // hasSclang = true; + // dryadic document const synthDef = (name) => { let opts; if (hasSclang) { diff --git a/main.development.js b/main.development.js index c21c6fa..d91780b 100644 --- a/main.development.js +++ b/main.development.js @@ -4,11 +4,12 @@ * The backend application that creates windows * and launches the frontend application app/index.js * - * All modules should use require, not import - * as this is not babel processed. + * main.development.js is transpiled to main.js * - * `let` and `const` and arrow functions are fine as - * well as all listed here: https://kangax.github.io/compat-table/es6/#node4 + * All module imports should use require, not import. + * You can import from local files. + * + * The frontend and backend communicate using electron ipc */ // process.env.NODE_ENV = process.env.NODE_ENV || 'production'; From 56df08f4dbbf5c5ee9f144bac53f293af4f47702 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Wed, 17 Aug 2016 20:34:50 +0200 Subject: [PATCH 16/41] update dependencies --- package.json | 28 +++++++++++++--------------- test/e2e.js | 10 ++++++++++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 31ebbd7..14a53ed 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "url": "http://ixdm.ch/critical-media-lab" }, "main": "main.js", - "electronVersion": "1.2.3", "scripts": { "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", "test-watch": "npm test -- --watch", @@ -46,29 +45,29 @@ "electron" ], "dependencies": { - "baconjs": "^0.7.84", + "baconjs": "^0.7.85", "d3": "^3.5.17", "electron-debug": "^1.0.1", "font-awesome": "^4.6.3", - "fs-jetpack": "^0.9.1", + "fs-jetpack": "^0.9.2", "keymirror": "^0.1.1", - "lodash": "^4.13.1", - "material-ui": "^0.15.1", + "lodash": "^4.15.0", + "material-ui": "^0.15.4", "miso.dataset": "^0.4.1", - "react": "^15.1.0", - "react-dom": "^15.1.0", + "react": "^15.3.0", + "react-dom": "^15.3.0", "react-hyperscript": "^2.4.0", "react-redux": "^4.4.5", - "react-router": "^2.4.1", + "react-router": "^2.6.1", "react-router-redux": "^4.0.5", - "react-slider": "^0.6.1", + "react-slider": "^0.7.0", "react-tap-event-plugin": "^1.0.0", "redux": "^3.5.2", "redux-thunk": "^2.1.0", - "reselect": "^2.5.1", - "source-map-support": "^0.4.0", + "reselect": "^2.5.3", + "source-map-support": "^0.4.2", "supercolliderjs": "^0.11.1", - "updeep": "^0.16.0", + "updeep": "^0.16.1", "winston": "^2.2.0" }, "devDependencies": { @@ -88,7 +87,6 @@ "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", "chai": "^3.5.0", - "chromedriver": "^2.21.2", "co-mocha": "^1.1.2", "concurrently": "^2.1.0", "copy-webpack-plugin": "^3.0.1", @@ -97,8 +95,8 @@ "css-modules-require-hook": "^4.0.1", "debug-menu": "^0.4.0", "del": "^2.2.1", - "electron-packager": "^7.0.4", - "electron-prebuilt": "^1.2.3", + "electron-packager": "^7.3.0", + "electron-prebuilt": "^1.2.8", "electron-rebuild": "^1.1.5", "eslint": "^2.13.1", "eslint-config-airbnb": "^9.0.1", diff --git a/test/e2e.js b/test/e2e.js index 757f043..6e945a0 100644 --- a/test/e2e.js +++ b/test/e2e.js @@ -1,3 +1,13 @@ +/** + * Not currently using this. + * + * chromedriver not in package file, + * was this: + * + * "chromedriver": "^2.21.2", + * + * but that package is failing to install. + */ import path from 'path'; import chromedriver from 'chromedriver'; import webdriver from 'selenium-webdriver'; From 4c49bd8ea2944c1d224c765639167db73e60ed88 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Thu, 18 Aug 2016 12:10:51 +0200 Subject: [PATCH 17/41] LoopControl: set loopTime --- app/actionTypes.js | 2 +- app/actions/interaction.js | 18 +++++++- app/actions/sounds.js | 6 ++- app/components/LoopControl.js | 78 ++++++++++++++++++++++++++++++++++ app/components/LoopPlayHead.js | 6 +-- app/components/MapButton.js | 2 + app/components/ToggleButton.js | 39 +++++++++++++++++ app/components/XYParamTable.js | 7 +-- app/containers/Sidebar.js | 2 + app/reducers/interaction.js | 14 +++++- app/selectors/sound.js | 2 +- 11 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 app/components/LoopControl.js create mode 100644 app/components/ToggleButton.js diff --git a/app/actionTypes.js b/app/actionTypes.js index dabb53a..b0c33b9 100644 --- a/app/actionTypes.js +++ b/app/actionTypes.js @@ -31,7 +31,7 @@ module.exports = keyMirror({ SET_HOVERING: null, SET_POINTS_UNDER_BRUSH: null, TOGGLE_LOOP_MODE: null, - SET_LOOPING: null, + SET_LOOP_TIME: null, // ui FOCUS_SCATTERPLOT: null, diff --git a/app/actions/interaction.js b/app/actions/interaction.js index d625c8c..4d892fc 100644 --- a/app/actions/interaction.js +++ b/app/actions/interaction.js @@ -3,7 +3,7 @@ import { SHOW_BRUSH, SET_POINTS_UNDER_BRUSH, TOGGLE_LOOP_MODE, - SET_LOOPING + SET_LOOP_TIME } from '../actionTypes'; export function showBrush(show, x, y) { @@ -63,3 +63,19 @@ export function toggleLoopMode(m, n) { }; } + + +/** + * setLoopTime + * + * @param {number} loopTime + * @return {Object} action + */ +export function setLoopTime(loopTime) { + return { + type: SET_LOOP_TIME, + payload: { + loopTime + } + }; +} diff --git a/app/actions/sounds.js b/app/actions/sounds.js index b099187..867eb54 100644 --- a/app/actions/sounds.js +++ b/app/actions/sounds.js @@ -3,8 +3,12 @@ const path = require('path'); const jetpack = require('fs-jetpack'); import { - SET_SOUNDS, SELECT_SOUND, SPAWN_SYNTH, SET_MASTER_CONTROLS + SET_SOUNDS, + SELECT_SOUND, + SPAWN_SYNTH, + SET_MASTER_CONTROLS } from '../actionTypes'; + import callActionOnMain from '../ipc/callActionOnMain'; /** diff --git a/app/components/LoopControl.js b/app/components/LoopControl.js new file mode 100644 index 0000000..cfdacc6 --- /dev/null +++ b/app/components/LoopControl.js @@ -0,0 +1,78 @@ +import React from 'react'; +import connect from '../utils/reduxers'; +import { getLoop } from '../selectors'; +import { toggleLoopMode, setLoopTime } from '../actions/interaction'; +import { Slider } from 'material-ui'; +import ToggleButton from './ToggleButton'; +import { debounce } from 'lodash'; +import d3 from 'd3'; + +const MIN = 0.1; +const MAX = 60.0; +const mapv = d3.scale.linear().domain([0, 1]).range([MIN, MAX]); +const unmapv = d3.scale.linear().domain([MIN, MAX]).range([0, 1]); + + +/** + * A toggle button to turn looping on and off, + * and a slider to adjust loopTime. + * + */ +class LoopControl extends React.Component { + + static propTypes = { + loopMode: React.PropTypes.object.isRequired, + setLoopTime: React.PropTypes.func.isRequired, + toggleLoopMode: React.PropTypes.func.isRequired + }; + + render() { + const sliderAction = (e, v) => { + this.props.setLoopTime(mapv(1 - v)); + }; + + const sliderStyle = { + marginTop: 4, + marginBottom: 4 + }; + + const slider = ( + + ); + + // has to initiate action with m,n + // and toggle off with the last m,n + // or make a different action + const button = ( + this.props.toggleLoopMode()} + iconActive="repeat" + iconInactive="repeat" + /> + ); + + return ( +
+ + {slider} + {button} +
+ ); + } +} + + +export default connect({ + loopMode: getLoop +}, { + setLoopTime, + toggleLoopMode +})(LoopControl); diff --git a/app/components/LoopPlayHead.js b/app/components/LoopPlayHead.js index 6b39ec2..25f21ba 100644 --- a/app/components/LoopPlayHead.js +++ b/app/components/LoopPlayHead.js @@ -16,7 +16,7 @@ const SPEED = 20; class LoopPlayHead extends React.Component { static propTypes = { - loopBox: React.PropTypes.object.isRequired, + loopBox: React.PropTypes.object, loopMode: React.PropTypes.object.isRequired }; @@ -29,7 +29,7 @@ class LoopPlayHead extends React.Component { } tick() { - if (this.props.loopMode.box) { + if (this.props.loopBox) { const delta = now() - this.props.loopMode.epoch; const inLoop = (delta / 1000) % this.props.loopMode.loopTime; const pos = inLoop / this.props.loopMode.loopTime; @@ -41,7 +41,7 @@ class LoopPlayHead extends React.Component { render() { if (this.props.loopBox) { - const x = (this.state.pos || 0) * this.props.loopBox.width + this.props.loopBox.x; + const x = (this.state && this.state.pos || 0) * this.props.loopBox.width + this.props.loopBox.x; const y = this.props.loopBox.y; return ( sound parameter mappings and to adjust diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index c6b636f..ad978ce 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -5,6 +5,7 @@ import DatasetSelector from './DatasetSelector'; import SoundSelector from './SoundSelector'; import ParamMapping from './ParamMapping'; import Help from './Help'; +import LoopControl from '../components/LoopControl'; import styles from './Sidebar.css'; /** @@ -17,6 +18,7 @@ export default class Sidebar extends Component { [ h(DatasetSelector, { className: styles.datasets }), h(SoundSelector, { className: styles.sounds }), + h(LoopControl, { className: styles.loopControl }), h(ParamMapping, { className: styles.params }), h(Help, { className: styles.help }) ]); diff --git a/app/reducers/interaction.js b/app/reducers/interaction.js index 51b2508..52fb5da 100644 --- a/app/reducers/interaction.js +++ b/app/reducers/interaction.js @@ -1,6 +1,7 @@ import { SET_POINTS_UNDER_BRUSH, - TOGGLE_LOOP_MODE + TOGGLE_LOOP_MODE, + SET_LOOP_TIME } from '../actionTypes'; import { calcPointsEntering @@ -24,6 +25,8 @@ export default function interaction(state = {}, action) { return setPointsUnderBrush(state, action); case TOGGLE_LOOP_MODE: return toggleLoopMode(state, action); + case SET_LOOP_TIME: + return setLoopTime(state, action); default: return state; } @@ -100,3 +103,12 @@ function toggleLoopMode(state, action) { return u({ loopMode }, state); } + + +function setLoopTime(state, action) { + const loopMode = { + loopTime: action.payload.loopTime || DEFAULT_LOOP_TIME + }; + + return u({loopMode}, state); +} diff --git a/app/selectors/sound.js b/app/selectors/sound.js index bac8d2e..9f9e217 100644 --- a/app/selectors/sound.js +++ b/app/selectors/sound.js @@ -219,6 +219,7 @@ export function makeMapper(spec) { } } + /** * getLoopModePayload - Builds the payload for SET_LOOP action that is sent to the main thread * to be sent by the SoundApp and then sent using the updateStream to the @@ -262,7 +263,6 @@ export const getLoopModePayload = createSelector( ); - export function loopModeEvents(m, n, npoints, mapping, mappingControls, sound, loopTime) { // now you have time and x doing the same movement // it will accentuate it I guess From ff686707190bd878c51c453ab038f9a1473cdeb7 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 10:01:57 +0200 Subject: [PATCH 18/41] replace MapButton with ToggleButton A more general purpose button where you specify the two icons for the two states. MapButton was specific to the XYParamTable --- app/components/LoopControl.js | 15 ++++++++++---- app/components/MapButton.js | 36 ---------------------------------- app/components/XYParamTable.js | 24 +++++++++++++++++------ 3 files changed, 29 insertions(+), 46 deletions(-) delete mode 100644 app/components/MapButton.js diff --git a/app/components/LoopControl.js b/app/components/LoopControl.js index cfdacc6..86a72ee 100644 --- a/app/components/LoopControl.js +++ b/app/components/LoopControl.js @@ -6,6 +6,7 @@ import { Slider } from 'material-ui'; import ToggleButton from './ToggleButton'; import { debounce } from 'lodash'; import d3 from 'd3'; +import style from './XYParamTable.css'; const MIN = 0.1; const MAX = 60.0; @@ -60,10 +61,16 @@ class LoopControl extends React.Component { ); return ( -
- - {slider} - {button} +
+ + + + + + + + +
Loop{button}{slider}
); } diff --git a/app/components/MapButton.js b/app/components/MapButton.js deleted file mode 100644 index 88cdf1c..0000000 --- a/app/components/MapButton.js +++ /dev/null @@ -1,36 +0,0 @@ - -import React from 'react'; -import h from 'react-hyperscript'; - -// requires the material-ui fonts in vendor -// http://www.material-ui.com/#/components/font-icon -// https://design.google.com/icons/ -import FontIcon from 'material-ui/FontIcon'; -import IconButton from 'material-ui/IconButton'; -import muiThemeable from 'material-ui/styles/muiThemeable'; - - -/** - * A radio-button used in the XYParamTable to - * assign a mapping between the X or Y dimension and a sound parameter. - */ -class MapButton extends React.Component { - - static propTypes = { - action: React.PropTypes.func.isRequired, - isActive: React.PropTypes.bool.isRequired, - muiTheme: React.PropTypes.object.isRequired - }; - - render() { - const palette = this.props.muiTheme.palette; - return h(IconButton, {onClick: this.props.action}, [ - h(FontIcon, { - className: 'material-icons', - color: this.props.isActive ? palette.primary1Color : palette.disabledColor - }, this.props.isActive ? 'radio_button_checked' : 'radio_button_unchecked') - ]); - } -} - -export default muiThemeable()(MapButton); diff --git a/app/components/XYParamTable.js b/app/components/XYParamTable.js index 87e37de..c3ea733 100644 --- a/app/components/XYParamTable.js +++ b/app/components/XYParamTable.js @@ -1,7 +1,7 @@ import React from 'react'; import h from 'react-hyperscript'; -import MapButton from './MapButton'; +import ToggleButton from './ToggleButton'; // single value slider: import { Slider } from 'material-ui'; // dual value range slider: @@ -38,6 +38,12 @@ export default class XYParamTable extends React.Component { min max */ + + const sliderStyle = { + marginTop: 4, + marginBottom: 4 + }; + const rows = this.props.xyMappingControls.map((control) => { let range; if (control.connected) { @@ -73,7 +79,8 @@ export default class XYParamTable extends React.Component { min: 0.0, max: 1.0, step: 0.001, - onChange: debounce(sliderAction, 300) + onChange: debounce(sliderAction, 300), + sliderStyle }); } @@ -84,17 +91,22 @@ export default class XYParamTable extends React.Component { h('td', {className: style.pa0}, [ - h(MapButton, { + h(ToggleButton, { isActive: control.xConnected, - action: () => this.props.mapXYtoParam('x', control.name) + // TODO: or unmap if it is already the one that is set + action: () => this.props.mapXYtoParam('x', control.name), + iconActive: 'radio_button_checked', + iconInactive: 'radio_button_unchecked' }) ]), h('td', {className: style.pa0}, [ - h(MapButton, { + h(ToggleButton, { isActive: control.yConnected, - action: () => this.props.mapXYtoParam('y', control.name) + action: () => this.props.mapXYtoParam('y', control.name), + iconActive: 'radio_button_checked', + iconInactive: 'radio_button_unchecked' }) ]), h('td', From e8ef9bf5ac6baee8edd5a1e23029978eace5cab1 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 10:03:36 +0200 Subject: [PATCH 19/41] move mui theme creation into theme.js - more obvious place to find and configure --- app/containers/MainLayout.js | 2 +- app/containers/Sidebar.css | 2 +- app/reducers/ui.js | 7 +++---- app/selectors/ui.js | 1 - app/theme.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 app/theme.js diff --git a/app/containers/MainLayout.js b/app/containers/MainLayout.js index f11531e..03c396b 100644 --- a/app/containers/MainLayout.js +++ b/app/containers/MainLayout.js @@ -21,7 +21,7 @@ const stateToProps = { * This is the layout of the application itself. * An SVG plot area on the left and a Sidebar on the right. * - * It wraps everything ina MuiThemeProvider which allows selectable + * It wraps everything in a MuiThemeProvider which allows selectable * styling themes that child components can access via the context. * * The layout sizes are all calculated in getLayout which recalculates diff --git a/app/containers/Sidebar.css b/app/containers/Sidebar.css index b9c7ce9..dc41502 100644 --- a/app/containers/Sidebar.css +++ b/app/containers/Sidebar.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; height: 100vh; - padding-top: 2rem; + /*padding-top: 2rem;*/ } .sidebar > div { diff --git a/app/reducers/ui.js b/app/reducers/ui.js index eb973cf..6c86b7f 100644 --- a/app/reducers/ui.js +++ b/app/reducers/ui.js @@ -1,6 +1,3 @@ -import getMuiTheme from 'material-ui/styles/getMuiTheme'; -import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; -import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; import { FOCUS_SCATTERPLOT, @@ -12,6 +9,8 @@ import { SET_NOTIFICATION } from '../actionTypes'; import u from 'updeep'; +import defaultTheme from '../theme'; + const initial = { focused: null, @@ -24,7 +23,7 @@ const initial = { width: window.innerWidth, height: window.innerHeight }, - muiTheme: getMuiTheme(lightBaseTheme) + muiTheme: defaultTheme() }; /** diff --git a/app/selectors/ui.js b/app/selectors/ui.js index 61a1613..bfa6b87 100644 --- a/app/selectors/ui.js +++ b/app/selectors/ui.js @@ -2,7 +2,6 @@ import { centeredSquareWithMargin } from '../utils/layout'; import { createSelector } from 'reselect'; import { getDatasetMetadata, getFeatures } from './dataset'; import { getLoop } from './sound'; -import * as _ from 'lodash'; export const getWindowSize = (state) => state.ui.windowSize; diff --git a/app/theme.js b/app/theme.js new file mode 100644 index 0000000..22b085b --- /dev/null +++ b/app/theme.js @@ -0,0 +1,28 @@ +import getMuiTheme from 'material-ui/styles/getMuiTheme'; +// import baseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; +import baseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; + +import u from 'updeep'; + +const customize = { + palette: { + textColor: '#777777', + // buttons + primary1Color: '#ff0082', + disabledColor: 'rgba(#8700ff, 0.25)' + }, + spacing: { + desktopDrawerMenuItemHeight: 12 + } +}; + +const makeMuiTheme = (base) => { + const t = u(customize, base); + return getMuiTheme(t); +}; + +export default function defaultTheme() { + const t = makeMuiTheme(baseTheme); + console.log(t); + return t; +} From 0e70ef33c6e69c33f154e1335664fb248e0e7404 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 10:42:49 +0200 Subject: [PATCH 20/41] Toggle looping with the loop button --- app/actionTypes.js | 3 +- app/actions/interaction.js | 23 +++++--- app/components/ScatterPlotClickSurface.js | 4 +- app/components/ScatterPlotsInteractive.js | 10 ++-- app/reducers/interaction.js | 66 ++++++++++++++++++++--- app/theme.js | 2 +- 6 files changed, 85 insertions(+), 23 deletions(-) diff --git a/app/actionTypes.js b/app/actionTypes.js index b0c33b9..2da3fb8 100644 --- a/app/actionTypes.js +++ b/app/actionTypes.js @@ -1,5 +1,5 @@ -var keyMirror = require('keyMirror'); +import keyMirror from 'keymirror'; module.exports = keyMirror({ SELECT_DATASET: null, @@ -32,6 +32,7 @@ module.exports = keyMirror({ SET_POINTS_UNDER_BRUSH: null, TOGGLE_LOOP_MODE: null, SET_LOOP_TIME: null, + SET_LOOP_BOX: null, // ui FOCUS_SCATTERPLOT: null, diff --git a/app/actions/interaction.js b/app/actions/interaction.js index 4d892fc..607a990 100644 --- a/app/actions/interaction.js +++ b/app/actions/interaction.js @@ -3,6 +3,7 @@ import { SHOW_BRUSH, SET_POINTS_UNDER_BRUSH, TOGGLE_LOOP_MODE, + SET_LOOP_BOX, SET_LOOP_TIME } from '../actionTypes'; @@ -19,9 +20,10 @@ export function showBrush(show, x, y) { /** - * setPointsUnderBrush - called when moving the brush over points. + * setPointsUnderBrush - Called when moving the brush over points. * - * points under brush is further processed into The sound app responds to changes in poin + * Points under brush is further processed by the reducers into points entering. + * The sound app responds to changes in point. * * @param {number} m box coordinate * @param {number} n box coordinate @@ -47,15 +49,15 @@ export function setPointsUnderBrush(m, n, indices) { /** - * toggleLoopMode - turns loop on, or changes it to a different box or turns it off + * setLoopBox - Start loop at box, or change loop to box, toggle loop off if already playing. * * @param {number} m box coordinate * @param {number} n box coordinate * @return {Object} action */ -export function toggleLoopMode(m, n) { +export function setLoopBox(m, n) { return { - type: TOGGLE_LOOP_MODE, + type: SET_LOOP_BOX, payload: { m, n @@ -64,9 +66,18 @@ export function toggleLoopMode(m, n) { } +/** + * toggleLoopMode - Turn looping on or off + */ +export function toggleLoopMode() { + return { + type: TOGGLE_LOOP_MODE + }; +} + /** - * setLoopTime + * setLoopTime - Set time of loop in seconds. * * @param {number} loopTime * @return {Object} action diff --git a/app/components/ScatterPlotClickSurface.js b/app/components/ScatterPlotClickSurface.js index 6adefe5..5f6f369 100644 --- a/app/components/ScatterPlotClickSurface.js +++ b/app/components/ScatterPlotClickSurface.js @@ -27,7 +27,7 @@ export default class ScatterPlotClickSurface extends React.Component { setPointsUnderBrush: React.PropTypes.func.isRequired, setHovering: React.PropTypes.func.isRequired, muiTheme: React.PropTypes.object.isRequired, - toggleLoopMode: React.PropTypes.func.isRequired, + setLoopBox: React.PropTypes.func.isRequired, isLooping: React.PropTypes.bool.isRequired, isPending: React.PropTypes.bool.isRequired }; @@ -110,7 +110,7 @@ export default class ScatterPlotClickSurface extends React.Component { onMouseDown: (e) => { if (e.buttons && e.metaKey) { - this.props.toggleLoopMode(this.props.m, this.props.n); + this.props.setLoopBox(this.props.m, this.props.n); } else { this._brush(e.clientX, e.clientY); } diff --git a/app/components/ScatterPlotsInteractive.js b/app/components/ScatterPlotsInteractive.js index c4ec727..9650d5f 100644 --- a/app/components/ScatterPlotsInteractive.js +++ b/app/components/ScatterPlotsInteractive.js @@ -5,7 +5,7 @@ import connect from '../utils/reduxers'; import { setPointsUnderBrush, - toggleLoopMode + setLoopBox } from '../actions/interaction'; import { @@ -33,7 +33,7 @@ const selectors = { const handlers = { setPointsUnderBrush, setHovering, - toggleLoopMode + setLoopBox }; @@ -61,7 +61,7 @@ class ScatterPlotsInteractive extends React.Component { features: React.PropTypes.array.isRequired, setPointsUnderBrush: React.PropTypes.func.isRequired, setHovering: React.PropTypes.func.isRequired, - toggleLoopMode: React.PropTypes.func.isRequired + setLoopBox: React.PropTypes.func.isRequired }; setPointsIn(area, box, points) { @@ -197,8 +197,8 @@ class ScatterPlotsInteractive extends React.Component { onChange: (area) => this.setPointsIn(area, box, points), onMouseEnter: () => this.setHoveringBox(box), onMetaClick: () => { - if (this.props.toggleLoopMode) { - this.props.toggleLoopMode(box.m, box.n); + if (this.props.setLoopBox) { + this.props.setLoopBox(box.m, box.n); } }, show: isLastFocused, diff --git a/app/reducers/interaction.js b/app/reducers/interaction.js index 52fb5da..619e127 100644 --- a/app/reducers/interaction.js +++ b/app/reducers/interaction.js @@ -1,6 +1,7 @@ import { SET_POINTS_UNDER_BRUSH, TOGGLE_LOOP_MODE, + SET_LOOP_BOX, SET_LOOP_TIME } from '../actionTypes'; import { @@ -17,7 +18,12 @@ const DEFAULT_LOOP_TIME = 10; * * A reducer take state and action and returns a new transformed state. * - * This reducer only gets state.interaction + * This reducer only gets state.interaction so state and new state here + * refer just to that part of the whole state object. + * + * @param {Object} state current state + * @param {Object} action with .type and .payload + * @return {Object} new state */ export default function interaction(state = {}, action) { switch (action.type) { @@ -25,6 +31,8 @@ export default function interaction(state = {}, action) { return setPointsUnderBrush(state, action); case TOGGLE_LOOP_MODE: return toggleLoopMode(state, action); + case SET_LOOP_BOX: + return setLoopBox(state, action); case SET_LOOP_TIME: return setLoopTime(state, action); default: @@ -37,9 +45,9 @@ export default function interaction(state = {}, action) { * setPointsUnderBrush - Action triggered by mouse move, * sets the current points inside the brush rectangle. * - * @param {Object} state current state - * @param {Object} action payload is {m n indices} - * @return {Object} new state + * @param {Object} state current state + * @param {Object} action payload is {m n indices} + * @return {Object} new state */ function setPointsUnderBrush(state, action) { const differentBox = (state.m !== action.payload.m) || @@ -62,7 +70,15 @@ function setPointsUnderBrush(state, action) { } -function _sameBox(state, payload, key) { +/** + * _sameBox - is payload.m/n the same box as state.loopMode[key].m/n ? + * + * @param {Object} state current state + * @param {Object} action payload is {m n indices} + * @param {string} key='box' which key in current state to compare it to + * @return {Boolean} the answer + */ +function _sameBox(state, payload, key = 'box') { if (get(state, `loopMode.${key}`)) { const m = get(state, `loopMode.${key}.m`); const n = get(state, `loopMode.${key}.n`); @@ -72,7 +88,7 @@ function _sameBox(state, payload, key) { /** - * toggleLoopMode - toggles the SoundApp's loop mode on or off + * setLoopBox - toggles the SoundApp's loop mode on or off * * Updates state.interaction.loopMode * @@ -84,13 +100,14 @@ function _sameBox(state, payload, key) { * @param {Object} action .payload is {m n} * @return {Object} new state */ -function toggleLoopMode(state, action) { +function setLoopBox(state, action) { const loopMode = { loopTime: get(state, 'loopMode.loopTime') || DEFAULT_LOOP_TIME }; // clicked the same box, toggle it to off if (_sameBox(state, action.payload, 'box')) { + loopMode.lastBox = get(state, 'loopMode.box'); loopMode.box = null; loopMode.epoch = null; } else { @@ -99,16 +116,49 @@ function toggleLoopMode(state, action) { if (!get(state, 'loopMode.epoch')) { loopMode.epoch = now() + 50; } + loopMode.lastBox = get(state, 'loopMode.box'); + } + + return u({ loopMode }, state); +} + + +/** + * toggleLoopMode - turn loop on or off + * + * If turning it on then it starts with the last looped box. + * + * @param {Object} state current state + * @return {Object} new state + */ +function toggleLoopMode(state) { + /* , action */ + const loopMode = {}; + + if (get(state, 'loopMode.box')) { + loopMode.box = null; + loopMode.epoch = null; + } else { + // or last hovered + const box = get(state, 'loopMode.lastBox') || {m: 0, n: 0}; + return setLoopBox(state, {payload: box}) } return u({ loopMode }, state); } +/** + * setLoopTime - set the loop time in seconds + * + * @param {Object} state current state + * @param {Object} action .payload is {loopTime: int} + * @return {Object} new state + */ function setLoopTime(state, action) { const loopMode = { loopTime: action.payload.loopTime || DEFAULT_LOOP_TIME }; - return u({loopMode}, state); + return u({ loopMode }, state); } diff --git a/app/theme.js b/app/theme.js index 22b085b..c520b20 100644 --- a/app/theme.js +++ b/app/theme.js @@ -9,7 +9,7 @@ const customize = { textColor: '#777777', // buttons primary1Color: '#ff0082', - disabledColor: 'rgba(#8700ff, 0.25)' + disabledColor: '#eeeeee' }, spacing: { desktopDrawerMenuItemHeight: 12 From 49093e2d6d9c5d447823b2eff4999f594a87fcdd Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 11:01:23 +0200 Subject: [PATCH 21/41] On click of XYParamTable mapping button, disconnect if it is currently connected --- app/reducers/mapping.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/reducers/mapping.js b/app/reducers/mapping.js index d746a8e..8e19fa3 100644 --- a/app/reducers/mapping.js +++ b/app/reducers/mapping.js @@ -11,11 +11,19 @@ export default function(state = {}, action) { switch (action.type) { case MAP_XY_TO_PARAM: - // if not already mapped to this + // if already mapped to this then disconnect it if (_.get(state, `xy.${action.payload.xy}.param`) === action.payload.param) { - return state; + return u({ + mode: 'xy', + xy: { + [action.payload.xy]: { + param: null + } + } + }, state); } + // connect it return u({ mode: 'xy', xy: { From aa7837e1489829b205c877f798a3b3e19938049a Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 11:13:36 +0200 Subject: [PATCH 22/41] Add more margin between plots --- app/selectors/ui.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/selectors/ui.js b/app/selectors/ui.js index bfa6b87..769743e 100644 --- a/app/selectors/ui.js +++ b/app/selectors/ui.js @@ -19,6 +19,9 @@ export const getNumFeatures = createSelector( } ); +const OUTSIDE_MARGIN = 48; +const MARGIN_BETWEEN_PLOTS = 24; + /** * Layout sizes and style depending on windowSize * and the dataset numFeatures @@ -32,7 +35,7 @@ export const getLayout = createSelector( const sidebarWidth = big ? 300 : 0; layout.showSidebar = big; layout.svgWidth = windowSize.width - sidebarWidth; - layout.margin = muiTheme.spacing.desktopGutterMini; + layout.margin = MARGIN_BETWEEN_PLOTS; if (layout.showSidebar) { layout.sideBarStyle = { @@ -48,7 +51,7 @@ export const getLayout = createSelector( // console.log(muiTheme); layout.svgStyle = centeredSquareWithMargin(layout.svgWidth, windowSize.height, muiTheme.spacing.desktopGutter); - layout.scatterPlotsMargin = 60; + layout.scatterPlotsMargin = OUTSIDE_MARGIN; layout.plotsWidth = (layout.svgStyle.right - layout.svgStyle.left) - (2 * layout.scatterPlotsMargin); From 740692d7b158f9e4f70731d8172b9e634b4894bf Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 11:29:35 +0200 Subject: [PATCH 23/41] Make loopTime timeScale exponential --- app/components/LoopControl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/LoopControl.js b/app/components/LoopControl.js index 86a72ee..4fd9e54 100644 --- a/app/components/LoopControl.js +++ b/app/components/LoopControl.js @@ -10,8 +10,8 @@ import style from './XYParamTable.css'; const MIN = 0.1; const MAX = 60.0; -const mapv = d3.scale.linear().domain([0, 1]).range([MIN, MAX]); -const unmapv = d3.scale.linear().domain([MIN, MAX]).range([0, 1]); +const mapv = d3.scale.pow().exponent(2).range([MIN, MAX]); +const unmapv = mapv.invert; /** From 0124689818bfb8258ccb95f557d7f50a04ee7b44 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 12:56:45 +0200 Subject: [PATCH 24/41] fix #8 : missing babel-register in package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 14a53ed..1390639 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "babel-preset-react-hmre": "^1.1.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", + "babel-register": "^6.11.6", "chai": "^3.5.0", "co-mocha": "^1.1.2", "concurrently": "^2.1.0", From 73b55a6ad4a13357b43eac2ecc84c6aeafc84ddc Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 13:06:00 +0200 Subject: [PATCH 25/41] Change 'sustain' spec to exponential 0.05 .. 4 --- app/synthdefs/grain.json | 8 ++++---- app/synthdefs/grain.scd | 2 +- app/synthdefs/grain2.json | 8 ++++---- app/synthdefs/grain2.scd | 2 +- app/synthdefs/grain3.json | 8 ++++---- app/synthdefs/grain3.scd | 2 +- app/synthdefs/grainFM.json | 4 ++-- app/synthdefs/grainFM.scd | 2 +- app/synthdefs/noiseburst.json | 4 ++-- app/synthdefs/noiseburst.scd | 2 +- app/synthdefs/shockwave-microsound-2.json | 4 ++-- app/synthdefs/shockwave-microsound-2.scd | 2 +- app/synthdefs/shockwave-microsound.json | 4 ++-- app/synthdefs/shockwave-microsound.scd | 2 +- app/synthdefs/stereoGrain3.json | 4 ++-- app/synthdefs/stereoGrain3.scd | 2 +- 16 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/synthdefs/grain.json b/app/synthdefs/grain.json index 194649c..34593c0 100644 --- a/app/synthdefs/grain.json +++ b/app/synthdefs/grain.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 20000, + "maxval": 10000, "class": "ControlSpec", - "minval": 20, + "minval": 40, "warp": "exp", "step": 0, "units": " Hz" @@ -61,8 +61,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/grain.scd b/app/synthdefs/grain.scd index 4f81296..d3f4a57 100644 --- a/app/synthdefs/grain.scd +++ b/app/synthdefs/grain.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("grain", { |out, amp=0.1, freq=440, sustain=0.01, pan| var snd = FSinOsc.ar(freq); diff --git a/app/synthdefs/grain2.json b/app/synthdefs/grain2.json index 0d15aa1..579e0dc 100644 --- a/app/synthdefs/grain2.json +++ b/app/synthdefs/grain2.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 20000, + "maxval": 10000, "class": "ControlSpec", - "minval": 20, + "minval": 40, "warp": "exp", "step": 0, "units": " Hz" @@ -61,8 +61,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/grain2.scd b/app/synthdefs/grain2.scd index 783f1cd..f49d944 100644 --- a/app/synthdefs/grain2.scd +++ b/app/synthdefs/grain2.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("grain2", { |out, amp=0.1, freq=440, sustain=0.01, pan| var snd = FSinOsc.ar(freq); diff --git a/app/synthdefs/grain3.json b/app/synthdefs/grain3.json index b5d086e..f1aab2a 100644 --- a/app/synthdefs/grain3.json +++ b/app/synthdefs/grain3.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 20000, + "maxval": 10000, "class": "ControlSpec", - "minval": 20, + "minval": 40, "warp": "exp", "step": 0, "units": " Hz" @@ -61,8 +61,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/grain3.scd b/app/synthdefs/grain3.scd index 8106194..466f406 100644 --- a/app/synthdefs/grain3.scd +++ b/app/synthdefs/grain3.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("grain3", { |out, amp=0.1, freq=440, sustain=0.01, pan| var snd = LFSaw.ar(freq); diff --git a/app/synthdefs/grainFM.json b/app/synthdefs/grainFM.json index 629476f..ccd8b4a 100644 --- a/app/synthdefs/grainFM.json +++ b/app/synthdefs/grainFM.json @@ -68,8 +68,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/grainFM.scd b/app/synthdefs/grainFM.scd index d280e44..f1eb5cf 100644 --- a/app/synthdefs/grainFM.scd +++ b/app/synthdefs/grainFM.scd @@ -1,6 +1,6 @@ Spec.specs.put(\carfreq, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); Spec.specs.put(\modfreq, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("grainFM", {|out, carfreq=440, modfreq=20, moddepth = 1, sustain=0.02, amp=0.1, pan| var env = EnvGen.ar(Env.sine(sustain, amp), doneAction: 2); diff --git a/app/synthdefs/noiseburst.json b/app/synthdefs/noiseburst.json index 6e04ffd..103d637 100644 --- a/app/synthdefs/noiseburst.json +++ b/app/synthdefs/noiseburst.json @@ -45,8 +45,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/noiseburst.scd b/app/synthdefs/noiseburst.scd index 42ac5af..9848e82 100644 --- a/app/synthdefs/noiseburst.scd +++ b/app/synthdefs/noiseburst.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("noiseburst", { |out, amp=0.1, sustain=0.01, pan| var snd = PinkNoise.ar(1.0); diff --git a/app/synthdefs/shockwave-microsound-2.json b/app/synthdefs/shockwave-microsound-2.json index 366cfd4..3e7223e 100644 --- a/app/synthdefs/shockwave-microsound-2.json +++ b/app/synthdefs/shockwave-microsound-2.json @@ -61,8 +61,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/shockwave-microsound-2.scd b/app/synthdefs/shockwave-microsound-2.scd index 016e44f..81c4055 100644 --- a/app/synthdefs/shockwave-microsound-2.scd +++ b/app/synthdefs/shockwave-microsound-2.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("shockwave-microsound-2", { |out, amp=0.1, freq=440, sustain=0.01, pan| var snd = FSinOsc.ar(freq); diff --git a/app/synthdefs/shockwave-microsound.json b/app/synthdefs/shockwave-microsound.json index d138dc4..9280f61 100644 --- a/app/synthdefs/shockwave-microsound.json +++ b/app/synthdefs/shockwave-microsound.json @@ -61,8 +61,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/shockwave-microsound.scd b/app/synthdefs/shockwave-microsound.scd index 81d83cd..1706e54 100644 --- a/app/synthdefs/shockwave-microsound.scd +++ b/app/synthdefs/shockwave-microsound.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); SynthDef("shockwave-microsound", { |out, amp=0.1, freq=440, sustain=0.01, pan| var snd = FSinOsc.ar(freq); diff --git a/app/synthdefs/stereoGrain3.json b/app/synthdefs/stereoGrain3.json index f4c7ca2..57b2134 100644 --- a/app/synthdefs/stereoGrain3.json +++ b/app/synthdefs/stereoGrain3.json @@ -77,8 +77,8 @@ "default": 1, "maxval": 4, "class": "ControlSpec", - "minval": 0, - "warp": "linear", + "minval": 0.05, + "warp": "exp", "step": 0, "units": "" }, diff --git a/app/synthdefs/stereoGrain3.scd b/app/synthdefs/stereoGrain3.scd index 696c371..5cc77ef 100644 --- a/app/synthdefs/stereoGrain3.scd +++ b/app/synthdefs/stereoGrain3.scd @@ -1,4 +1,4 @@ -Spec.specs.put(\sustain, ControlSpec.new(0, 4, 'lin', 0, 1)); +Spec.specs.put(\sustain, ControlSpec.new(0.05, 4, 'exp', 0, 1)); Spec.specs.put(\freq, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); Spec.specs.put(\freq2, ControlSpec(40, 10000, 'exp', 0, 440, " Hz")); From f967f5c0c0127c7bf35ba653ea6f0238b7ca6601 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 13:06:45 +0200 Subject: [PATCH 26/41] Protect Points from plotting with NaNs --- app/components/Points.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Points.js b/app/components/Points.js index 4d34205..c045889 100644 --- a/app/components/Points.js +++ b/app/components/Points.js @@ -12,8 +12,8 @@ export default (props) => { const flip = props.sideLength; return h('g', props.points.map((xy, i) => { return React.createElement('circle', { - cx: xy[0], - cy: flip - xy[1], + cx: xy[0] || 0, + cy: (flip - xy[1]) || 0, r: radius, key: String(i), className: props.className From dcf925571ed92d28a55dd866b2ff2d0a3b48ce1f Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 13:28:55 +0200 Subject: [PATCH 27/41] misc styling --- app/app.global.css | 3 +++ app/containers/DatasetSelector.js | 3 ++- app/containers/Sidebar.css | 16 ++++++---------- app/containers/Sidebar.js | 10 +++++----- app/containers/SoundSelector.js | 6 +++++- app/stylesheets/skeleton.css | 2 +- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/app.global.css b/app/app.global.css index 04811cc..79f91cb 100644 --- a/app/app.global.css +++ b/app/app.global.css @@ -1,4 +1,7 @@ +/** + * TODO keep this locally + */ @import url(https://fonts.googleapis.com/css?family=Open+Sans); html, diff --git a/app/containers/DatasetSelector.js b/app/containers/DatasetSelector.js index dd64e0d..49f4d8b 100644 --- a/app/containers/DatasetSelector.js +++ b/app/containers/DatasetSelector.js @@ -4,6 +4,7 @@ import connect from '../utils/reduxers'; import RaisedButton from 'material-ui/RaisedButton'; import { List, ListItem, MakeSelectable } from 'material-ui/List'; const SelectableList = MakeSelectable(List); +import styles from './Sidebar.css'; import { loadDataset, @@ -25,7 +26,7 @@ class DatasetSelector extends Component { }; render() { - return h('div.dataset-selector', [ + return h(`div.dataset-selector.${styles.datasets}`, [ h('h6', 'Datasets'), h(SelectableList, { diff --git a/app/containers/Sidebar.css b/app/containers/Sidebar.css index dc41502..fe9aef3 100644 --- a/app/containers/Sidebar.css +++ b/app/containers/Sidebar.css @@ -19,17 +19,13 @@ font-variant: small-caps; } -/*.sidebar .selectable-list { - overflow-y: auto; - margin-bottom: 3px; -}*/ -/*.sounds { - height: 100px; -}*/ +@media (min-height: 1000px) { + .datasets { + margin-top: 7rem; + } +} + .params { flex: 1; } -/*.help { - height: 150px; -}*/ diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index ad978ce..507c086 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -16,11 +16,11 @@ export default class Sidebar extends Component { return h('aside', { className: styles.sidebar }, [ - h(DatasetSelector, { className: styles.datasets }), - h(SoundSelector, { className: styles.sounds }), - h(LoopControl, { className: styles.loopControl }), - h(ParamMapping, { className: styles.params }), - h(Help, { className: styles.help }) + h(DatasetSelector), + h(LoopControl), + h(SoundSelector), + h(ParamMapping), + h(Help) ]); } } diff --git a/app/containers/SoundSelector.js b/app/containers/SoundSelector.js index d7cb9d4..5daf356 100644 --- a/app/containers/SoundSelector.js +++ b/app/containers/SoundSelector.js @@ -17,7 +17,11 @@ class SoundSelector extends React.Component { { value: this.props.selectedSound, onChange: this.props.onSelect, - className: 'selectable-list' + className: 'selectable-list', + style: { + borderBottom: '1px solid #eee', + borderTop: '1px solid #eee' + } }, this.props.sounds.map((sound) => { return h(ListItem, { diff --git a/app/stylesheets/skeleton.css b/app/stylesheets/skeleton.css index 0117711..ab1ddfa 100644 --- a/app/stylesheets/skeleton.css +++ b/app/stylesheets/skeleton.css @@ -124,7 +124,7 @@ html { font-size: 62.5%; } body { font-size: 1.3em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; + line-height: 1.2; font-weight: 400; font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; color: #222; } From 0d024b566f158e185b575f513e90882463bf8eae Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 13:29:28 +0200 Subject: [PATCH 28/41] fix: toggle looping with the loop button Was not retrieving last state correctly --- app/reducers/interaction.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/reducers/interaction.js b/app/reducers/interaction.js index 619e127..58017c3 100644 --- a/app/reducers/interaction.js +++ b/app/reducers/interaction.js @@ -134,14 +134,16 @@ function setLoopBox(state, action) { function toggleLoopMode(state) { /* , action */ const loopMode = {}; + const box = get(state, 'loopMode.box'); - if (get(state, 'loopMode.box')) { + if (box) { + loopMode.lastBox = box; loopMode.box = null; loopMode.epoch = null; } else { // or last hovered - const box = get(state, 'loopMode.lastBox') || {m: 0, n: 0}; - return setLoopBox(state, {payload: box}) + const lastBox = get(state, 'loopMode.lastBox') || {m: 0, n: 0}; + return setLoopBox(state, {payload: lastBox}); } return u({ loopMode }, state); From b92035dd589900b355ceaec4b143a4d53d0c983b Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 14:17:03 +0200 Subject: [PATCH 29/41] improve styling of title in bottom left --- app/components/ScatterPlots.css | 7 +++++-- app/components/ScatterPlots.js | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/ScatterPlots.css b/app/components/ScatterPlots.css index 635e4f9..f0a314a 100644 --- a/app/components/ScatterPlots.css +++ b/app/components/ScatterPlots.css @@ -1,9 +1,12 @@ .title { - font-size: 5em; - opacity: 0.05; + font-size: 7em; + opacity: 0.2; text-transform: uppercase; /*font-variant: small-caps;*/ letter-spacing: .16em; + fill: #dddddd; + stroke: #bbbbbb; + stroke-width: 1px; } .pending { diff --git a/app/components/ScatterPlots.js b/app/components/ScatterPlots.js index 7c00c82..126fd0d 100644 --- a/app/components/ScatterPlots.js +++ b/app/components/ScatterPlots.js @@ -30,10 +30,7 @@ class ScatterPlots extends React.Component { const title = h('text', { x: 50, y: this.props.layout.svgStyle.height - 200, - className: style.title, - style: { - fill: this.props.muiTheme.palette.textColor - } + className: style.title }, [this.props.dataset.name]); children.push(title); From 15cb6cb5432c35a833dc3c92e77617ca39c23f3b Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 14:17:38 +0200 Subject: [PATCH 30/41] notifications: center screen with overlay --- app/app.global.css | 25 +++++++++++++++++++++++++ app/components/Notification.js | 12 ++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/app.global.css b/app/app.global.css index 79f91cb..1c060df 100644 --- a/app/app.global.css +++ b/app/app.global.css @@ -45,3 +45,28 @@ body { /*.dataset-selector .selectable-list { height: 73px; }*/ + +/* notifications */ +.notification--outer { + width: 100%; + height: auto; + bottom: 0px; + top: 0px; + left: 0; + position: absolute; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +.notification--inner { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.inform { + color: #bbb; + font-size: 3em; + text-align: center; +} diff --git a/app/components/Notification.js b/app/components/Notification.js index d940cf7..f4c8356 100644 --- a/app/components/Notification.js +++ b/app/components/Notification.js @@ -10,11 +10,15 @@ import connect from '../utils/reduxers'; class Notification extends React.Component { render() { - const n = this.props.notification; - if (n) { + let n = this.props.notification; + if (n && n.type) { return ( -
- {n.message} +
+
+
+ {n.message} +
+
); } From 73baccf90e25026b11dc062e952cd0f8a4a61efc Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 14:18:04 +0200 Subject: [PATCH 31/41] Increase maximum loop speed --- app/components/LoopControl.js | 2 +- app/components/LoopPlayHead.js | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/components/LoopControl.js b/app/components/LoopControl.js index 4fd9e54..1959237 100644 --- a/app/components/LoopControl.js +++ b/app/components/LoopControl.js @@ -8,7 +8,7 @@ import { debounce } from 'lodash'; import d3 from 'd3'; import style from './XYParamTable.css'; -const MIN = 0.1; +const MIN = 0.05; const MAX = 60.0; const mapv = d3.scale.pow().exponent(2).range([MIN, MAX]); const unmapv = mapv.invert; diff --git a/app/components/LoopPlayHead.js b/app/components/LoopPlayHead.js index 25f21ba..b7022e4 100644 --- a/app/components/LoopPlayHead.js +++ b/app/components/LoopPlayHead.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import connect from '../utils/reduxers'; import { getLoop, getLoopBox } from '../selectors'; import { now } from 'lodash'; @@ -41,20 +41,22 @@ class LoopPlayHead extends React.Component { render() { if (this.props.loopBox) { - const x = (this.state && this.state.pos || 0) * this.props.loopBox.width + this.props.loopBox.x; + const x = (this.state && this.state.pos || 0) * this.props.loopBox.width + + this.props.loopBox.x; const y = this.props.loopBox.y; return ( - + height={this.props.loopBox.height} + > + ); } From c2c58a011c69e798270a4a42f25ea6b4a06edefe Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 15:54:20 +0200 Subject: [PATCH 32/41] fix incorrect .gitignore main.js -> ./main.js --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 32795b2..e489e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ /app/vendor/supercollider .tags .tags1 -main.js -main.js.map +/main.js +/main.js.map From 522822e8843ec3960e78bee47cd28dfd708d348b Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 15:54:38 +0200 Subject: [PATCH 33/41] update README and LICENSE --- LICENSE | 2 +- README.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 3 files changed, 129 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index e82b970..2f87eaf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) crucialfelix (http://ixdm.ch/critical-media-lab) +Copyright (c) Chris Sattinger (http://ixdm.ch/critical-media-lab) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0fa03f4..76b51bf 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,61 @@ # PlaySPLOM -status: ALPHA +status: ALPHA / Work in Progress -## Install +This application explores multi-variate datasets through scatter plot matrices and multi-parameter sonification. It is a downloadable, cross-platform application for visualization and sonification. -### Node +Scatterplot matrices are a way to roughly determine if you have a linear correlation between multiple variables. -You should use `nodejs` >= `4.2.0` +Sound events are spawned when you brush over plotted data points. Many sounds are included and the and sonification mapping controls are all + +### Features + +- Load CSV datasets +- Plot scatterplots for each combination of data features +- Automatically parses well-formed datetimes +- Sonifies datapoints as you brush over them +- Includes embedded SuperCollider synthesis engine +- Many sounds included (more to come) +- Selectable sounds and adjustable sound parameters +- Adjustable data -> sound parameter mapping +- Play sounds in loop mode: the dataset is treated as a sequence + +Some background to the project is described here: + +https://medium.com/@crucialfelix/experimental-data-aesthetics-4563d01a5ebb#.3oirsvtq1 + + +## Architecture + +It is built on [Electron](http://electron.atom.io/), so it is a desktop app for OS X / Windows / Linux written in [Node.js](https://nodejs.org/) + +The interface and plots are written in [React.js](https://facebook.github.io/react/) with [Redux](http://redux.js.org/) and [Reselect](https://github.com/reactjs/reselect). + +[D3.js](https://d3js.org/) is used only for some scaling functions - the SVG nodes are rendered with React. There were some tradeoffs here between ease of data flow (with React and Redux) and just grabbing a D3 demo and hooking in some handlers to it. I opted to have full application control using React. D3 is nice for simple examples but it gets messy when you want to scale up application complexity. + +The sound engine is [SuperCollider](http://supercollider.github.io/) controlled by [supercollider.js](https://github.com/crucialfelix/supercolliderjs) + +All groups, synths, synthdefs and sequencing are managed by the new work-in-progress [Dryadic](https://github.com/crucialfelix/dryadic) library. This is a way to write declarative documents in JSON (from any language) that specify all the synths, connections, resources that you want playing and then it plays it. If you update the document then the server updates accordingly. + +This application is the first real use of that library and is an exploration and demonstration of how supercollider.js apps can be written with Electron. + +That said if you are not knee deep in contemporary (mid-August 2016) ES6/ES7/JSX JavaScript erm ... ECMAScript, webpacking, jsx, juggling transpiling wonder-of-the-week technology then this isn't the simplest example to just show supercollider.js running in Electron. There is a lot of stuff here to wonder and learn about. By mid-september some of it will be no longer current. + +The supercollider.js / dryadic part will become even simpler once updating/streams and remote clients are implemented. + +## Installation + +To just play with it, download the most recent release. + +These instructions are for those who want to hack or explore the code. + +### Node JS + +You should use `nodejs` >= `4.2.1` + +Probably the lastest in the 4 series (LTM) but the latest 6 should work as well. I developed using 4.2.1 + +The code is all transpiled anyway, so the latest language features aren't needed. The best way to install `node` is to use `nvm`: @@ -14,22 +63,85 @@ https://github.com/creationix/nvm This allows you to install different node versions and easily to switch between them. - nvm install 4.2.1 + nvm install 4.5.0 Switch to it: - nvm use 4.2.1 + nvm use 4.5.0 -Now when you run `node` it is the `4.2.1` version +Now when you run `node` it is the `4.5.0` version -### PlaySPLOM +### play-splom Install this app: npm install [SuperCollider](https://supercollider.github.io) is included by manually putting a prebuilt version into `app/vendor/supercollider` -Later on this will be installable automatically by npm (node package manager), but for now use the google drive version. + +Later on a prebuilt version will be installable automatically by npm (node package manager), but for now it has to be put in there. + +```sh +> app/vendor +└── supercollider + ├── COPYING + ├── README.md + └── osx + ├── MacOS + │   ├── libFLAC.8.dylib + │   ├── libogg.0.dylib + │   ├── libreadline.6.dylib + │   ├── libsndfile.1.dylib + │   ├── libvorbis.0.dylib + │   └── libvorbisenc.2.dylib + ├── bin + │   ├── plugins + │   │   ├── BinaryOpUGens.scx + │   │   ├── ChaosUGens.scx + │   │   ├── DelayUGens.scx + │   │   ├── DemandUGens.scx + │   │   ├── DiskIO_UGens.scx + │   │   ├── DynNoiseUGens.scx + │   │   ├── FFT_UGens.scx + │   │   ├── FilterUGens.scx + │   │   ├── GendynUGens.scx + │   │   ├── GrainUGens.scx + │   │   ├── IOUGens.scx + │   │   ├── LFUGens.scx + │   │   ├── ML_UGens.scx + │   │   ├── MulAddUGens.scx + │   │   ├── NoiseUGens.scx + │   │   ├── OscUGens.scx + │   │   ├── PV_ThirdParty.scx + │   │   ├── PanUGens.scx + │   │   ├── PhysicalModelingUGens.scx + │   │   ├── ReverbUGens.scx + │   │   ├── TestUGens.scx + │   │   ├── TriggerUGens.scx + │   │   ├── UIUGens.scx + │   │   ├── UnaryOpUGens.scx + │   │   └── UnpackFFTUGens.scx + │   ├── scsynth + │   └── synthdefs + └── scsynth +``` + +Where the outer `scsynth` is a script that launches the actual `scsynth` binary: + +``` +#!/bin/bash +DIR="${BASH_SOURCE%/*}/bin"; +if [[ -z "$@" ]]; then + ARGS="-u 57110"; +else + ARGS="$@"; +fi +if [[ -z "$SC_SYNTHDEF_PATH" ]]; then + export SC_SYNTHDEF_PATH="$DIR/synthdefs/" +fi +export SC_PLUGIN_PATH="$DIR/plugins/"; +exec "$DIR/scsynth" $ARGS; +``` ## Run @@ -53,3 +165,9 @@ You only have to do this once; it should be there each time you open up this cop ## Build npm run build + +## Architecture + +A more technical overview of the architecture will follow. + +The frontend is fairly standard React, Redux, Reselect diff --git a/package.json b/package.json index 1390639..903aa27 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "play-splom", "productName": "PlaySPLOM", - "companyName": "experimentalDataAesthetics", + "companyName": "Critical Media Lab (IXDM)", "version": "0.2.0", "description": "Scatter plot sonification for exploring datasets with sound", "license": "MIT", From 63f18d2d49b482f8ed7c24a92b73145a2537dd3e Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 15:57:52 +0200 Subject: [PATCH 34/41] misc lint, cleanups, debugging --- app/containers/App.js | 1 + app/index.js | 6 ++---- app/ipc/callActionOnMain.js | 1 + app/reducers/dataset.js | 2 +- app/utils/reduxers.js | 3 +++ main.development.js | 12 ++++++------ 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/containers/App.js b/app/containers/App.js index 1eade00..d78c672 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -1,3 +1,4 @@ +/* eslint global-require: 0 */ import React, { Component, PropTypes } from 'react'; import styles from './App.css'; diff --git a/app/index.js b/app/index.js index e640c5d..65e5fe1 100644 --- a/app/index.js +++ b/app/index.js @@ -1,4 +1,6 @@ /* eslint global-require: 0 */ +/* eslint import/no-unresolved: 0 */ + /** * The frontend application. */ @@ -17,10 +19,6 @@ import { mapXYtoParam } from './actions/mapping'; import './app.global.css'; import { join } from 'path'; -// how ironic, the path to config is different -// const config = require(production ? './config/index' : '../config/index'); -// console.log('dirname', __dirname); - const store = configureStore(); const history = syncHistoryWithStore(hashHistory, store); diff --git a/app/ipc/callActionOnMain.js b/app/ipc/callActionOnMain.js index c2cb0c3..31ceb9b 100644 --- a/app/ipc/callActionOnMain.js +++ b/app/ipc/callActionOnMain.js @@ -1,3 +1,4 @@ +/* eslint import/no-unresolved: 0 */ const ipcRenderer = require('electron').ipcRenderer; diff --git a/app/reducers/dataset.js b/app/reducers/dataset.js index 6424c60..7a5d0f1 100644 --- a/app/reducers/dataset.js +++ b/app/reducers/dataset.js @@ -1,6 +1,6 @@ import {SELECT_DATASET} from '../actionTypes'; -export default function(state=null, action) { +export default function(state = null, action) { switch (action.type) { case SELECT_DATASET: return action.payload; diff --git a/app/utils/reduxers.js b/app/utils/reduxers.js index 1479ea4..24bbb7e 100644 --- a/app/utils/reduxers.js +++ b/app/utils/reduxers.js @@ -18,6 +18,9 @@ export function selectState(selectors) { if (_.isString(getter)) { return (state) => state[getter]; } + if (!_.isFunction(getter)) { + throw new Error(`${k} is not a selector Function: ${getter}`); + } return getter; }); diff --git a/main.development.js b/main.development.js index d91780b..d14d94f 100644 --- a/main.development.js +++ b/main.development.js @@ -1,5 +1,5 @@ -/* eslint strict: 0 */ -'use strict'; +/* eslint global-require: 0 */ +/* eslint import/no-unresolved: 0 */ /** * The backend application that creates windows * and launches the frontend application app/index.js @@ -66,16 +66,16 @@ function loadSounds(window) { // connect two-way calling of actions // the other half is in app.js -const ipcMain = require('electron').ipcMain; // eslint-disable-line global-require import/no-unresolved +const ipcMain = require('electron').ipcMain; const handleActionOnMain = require('./app/ipc/handleActionOnMain'); ipcMain.on('call-action-on-main', (event, payload) => { log.debug('call-action-on-main', payload); handleActionOnMain(event, payload, soundApp); }); -// if (debug) { -// require('electron-debug')({enabled: debug}); // eslint-disable-line global-require -// } +if (debug) { + require('electron-debug')({showDevTools: true}); +} app.on('window-all-closed', () => { if (process.platform !== 'darwin') { From 65492ff8fd6f15e8e9276b436c17ee349648dd36 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 15:58:22 +0200 Subject: [PATCH 35/41] Change: Points to 2px if sideLength > 150 --- app/components/Points.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Points.js b/app/components/Points.js index c045889..bb0a3aa 100644 --- a/app/components/Points.js +++ b/app/components/Points.js @@ -7,7 +7,7 @@ import h from 'react-hyperscript'; */ export default (props) => { // move to layout - const radius = props.sideLength < 100 ? 1 : 3; + const radius = props.sideLength < 150 ? 2 : 3; // should not have to flip here const flip = props.sideLength; return h('g', props.points.map((xy, i) => { From 6bc250478ee663478e46102d4657abbe1118c692 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 15:58:31 +0200 Subject: [PATCH 36/41] remove unused css --- app/stylesheets/skeleton.css | 163 ----------------------------------- 1 file changed, 163 deletions(-) diff --git a/app/stylesheets/skeleton.css b/app/stylesheets/skeleton.css index ab1ddfa..7e73abe 100644 --- a/app/stylesheets/skeleton.css +++ b/app/stylesheets/skeleton.css @@ -165,146 +165,6 @@ a:hover { color: #0FA0CE; } -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -// .button, -// button, -// input[type="submit"], -// input[type="reset"], -// input[type="button"] { -// display: inline-block; -// height: 38px; -// padding: 0 30px; -// color: #555; -// text-align: center; -// font-size: 11px; -// font-weight: 600; -// line-height: 38px; -// letter-spacing: .1rem; -// text-transform: uppercase; -// text-decoration: none; -// white-space: nowrap; -// background-color: transparent; -// border-radius: 4px; -// border: 1px solid #bbb; -// cursor: pointer; -// box-sizing: border-box; } -// .button:hover, -// button:hover, -// input[type="submit"]:hover, -// input[type="reset"]:hover, -// input[type="button"]:hover, -// .button:focus, -// button:focus, -// input[type="submit"]:focus, -// input[type="reset"]:focus, -// input[type="button"]:focus { -// color: #333; -// border-color: #888; -// outline: 0; } -// .button.button-primary, -// button.button-primary, -// input[type="submit"].button-primary, -// input[type="reset"].button-primary, -// input[type="button"].button-primary { -// color: #FFF; -// background-color: #33C3F0; -// border-color: #33C3F0; } -// .button.button-primary:hover, -// button.button-primary:hover, -// input[type="submit"].button-primary:hover, -// input[type="reset"].button-primary:hover, -// input[type="button"].button-primary:hover, -// .button.button-primary:focus, -// button.button-primary:focus, -// input[type="submit"].button-primary:focus, -// input[type="reset"].button-primary:focus, -// input[type="button"].button-primary:focus { -// color: #FFF; -// background-color: #1EAEDB; -// border-color: #1EAEDB; } -// - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -// input[type="email"], -// input[type="number"], -// input[type="search"], -// input[type="text"], -// input[type="tel"], -// input[type="url"], -// input[type="password"], -// textarea, -// select { -// height: 38px; -// padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ -// background-color: #fff; -// border: 1px solid #D1D1D1; -// border-radius: 4px; -// box-shadow: none; -// box-sizing: border-box; } -// /* Removes awkward default styles on some inputs for iOS */ -// input[type="email"], -// input[type="number"], -// input[type="search"], -// input[type="text"], -// input[type="tel"], -// input[type="url"], -// input[type="password"], -// textarea { -// -webkit-appearance: none; -// -moz-appearance: none; -// appearance: none; } -// textarea { -// min-height: 65px; -// padding-top: 6px; -// padding-bottom: 6px; } -// input[type="email"]:focus, -// input[type="number"]:focus, -// input[type="search"]:focus, -// input[type="text"]:focus, -// input[type="tel"]:focus, -// input[type="url"]:focus, -// input[type="password"]:focus, -// textarea:focus, -// select:focus { -// border: 1px solid #33C3F0; -// outline: 0; } -// label, -// legend { -// display: block; -// margin-bottom: .5rem; -// font-weight: 600; } -// fieldset { -// padding: 0; -// border-width: 0; } -// input[type="checkbox"], -// input[type="radio"] { -// display: inline; } -// label > .label-body { -// display: inline-block; -// margin-left: .5rem; -// font-weight: normal; } -// - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -// ul { -// list-style: circle inside; } -// ol { -// list-style: decimal inside; } -// ol, ul { -// padding-left: 0; -// margin-top: 0; } -// ul ul, -// ul ol, -// ol ol, -// ol ul { -// margin: 1.5rem 0 1.5rem 3rem; -// font-size: 90%; } -// li { -// margin-bottom: 1rem; } -// /* Code –––––––––––––––––––––––––––––––––––––––––––––––––– */ @@ -340,39 +200,16 @@ td:last-child { /* Spacing –––––––––––––––––––––––––––––––––––––––––––––––––– */ -// button, -// .button { -// margin-bottom: 1rem; } -// input, -// textarea, -// select, -// fieldset { - // margin-bottom: 1.5rem; } pre, blockquote, dl, figure, table, p, -// ul, -// ol, form { margin-bottom: 2.5rem; } -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -// .u-full-width { -// width: 100%; -// box-sizing: border-box; } -// .u-max-full-width { -// max-width: 100%; -// box-sizing: border-box; } -// .u-pull-right { -// float: right; } -// .u-pull-left { -// float: left; } -// /* Misc –––––––––––––––––––––––––––––––––––––––––––––––––– */ From fc3796169e092e77d5c8b2c9be6a3fa1c43bcc94 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 17:45:30 +0200 Subject: [PATCH 37/41] styling: new colors Later I will separate all the colors into a single configurable and switchable theme file. --- app/components/LoopPlayHead.css | 2 +- app/components/Points.js | 15 +++++++++++++-- app/components/ScatterPlot.css | 4 ++-- app/components/ScatterPlots.css | 4 ++-- app/components/ScatterPlots.js | 2 +- app/theme.js | 11 +++++++++-- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/app/components/LoopPlayHead.css b/app/components/LoopPlayHead.css index 20350ac..5b35e14 100644 --- a/app/components/LoopPlayHead.css +++ b/app/components/LoopPlayHead.css @@ -1,5 +1,5 @@ .playHead { - fill: rgba(22, 249, 45, 1); + fill: #5EB1BF; opacity: 0.5; } diff --git a/app/components/Points.js b/app/components/Points.js index bb0a3aa..1e85f11 100644 --- a/app/components/Points.js +++ b/app/components/Points.js @@ -2,12 +2,23 @@ import React from 'react'; import h from 'react-hyperscript'; +function pointSize(sideLength) { + if (sideLength < 100) { + return 1; + } + + if (sideLength < 150) { + return 2; + } + + return 3; +} + /** * Renders points as SVG circles in a g */ export default (props) => { - // move to layout - const radius = props.sideLength < 150 ? 2 : 3; + const radius = pointSize(props.sideLength); // should not have to flip here const flip = props.sideLength; return h('g', props.points.map((xy, i) => { diff --git a/app/components/ScatterPlot.css b/app/components/ScatterPlot.css index 5fa3828..3a40cc1 100644 --- a/app/components/ScatterPlot.css +++ b/app/components/ScatterPlot.css @@ -1,8 +1,8 @@ .point { - fill: #999999; + fill: #A4B2B2; } .active { - fill: green; + fill: #FF6B6B; } diff --git a/app/components/ScatterPlots.css b/app/components/ScatterPlots.css index f0a314a..2220346 100644 --- a/app/components/ScatterPlots.css +++ b/app/components/ScatterPlots.css @@ -1,6 +1,6 @@ .title { font-size: 7em; - opacity: 0.2; + opacity: 0.7; text-transform: uppercase; /*font-variant: small-caps;*/ letter-spacing: .16em; @@ -14,7 +14,7 @@ } .looping { - stroke: rgba(22, 249, 45, 1); + stroke: #5EB1BF; } .focused { diff --git a/app/components/ScatterPlots.js b/app/components/ScatterPlots.js index 126fd0d..7688bda 100644 --- a/app/components/ScatterPlots.js +++ b/app/components/ScatterPlots.js @@ -29,7 +29,7 @@ class ScatterPlots extends React.Component { if (this.props.dataset) { const title = h('text', { x: 50, - y: this.props.layout.svgStyle.height - 200, + y: this.props.layout.svgStyle.height - 150, className: style.title }, [this.props.dataset.name]); children.push(title); diff --git a/app/theme.js b/app/theme.js index c520b20..dad5aab 100644 --- a/app/theme.js +++ b/app/theme.js @@ -4,15 +4,22 @@ import baseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; import u from 'updeep'; +// https://coolors.co/a4b2b2-363636-5eb1bf-e0e1dd-ff6b6b + const customize = { palette: { - textColor: '#777777', + canvasColor: '#E0E1DD', + textColor: '#363636', // buttons - primary1Color: '#ff0082', + primary1Color: '#FF6B6B', disabledColor: '#eeeeee' }, spacing: { desktopDrawerMenuItemHeight: 12 + }, + // axis + tableRow: { + borderColor: '#A4B2B2' } }; From c166a3e4c8b16dae36fef86e12d0ef135b164c06 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 17:46:05 +0200 Subject: [PATCH 38/41] update dependencies --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 903aa27..8a889fd 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "redux-thunk": "^2.1.0", "reselect": "^2.5.3", "source-map-support": "^0.4.2", - "supercolliderjs": "^0.11.1", + "supercolliderjs": "^0.12.0", "updeep": "^0.16.1", "winston": "^2.2.0" }, @@ -79,7 +79,7 @@ "babel-plugin-dev-expression": "^0.2.1", "babel-plugin-transform-remove-console": "^6.8.0", "babel-plugin-transform-remove-debugger": "^6.8.0", - "babel-plugin-webpack-loaders": "^0.6.1", + "babel-plugin-webpack-loaders": "^0.7.1", "babel-polyfill": "^6.9.1", "babel-preset-es2015-node4": "^2.1.0", "babel-preset-react": "^6.5.0", @@ -91,19 +91,19 @@ "co-mocha": "^1.1.2", "concurrently": "^2.1.0", "copy-webpack-plugin": "^3.0.1", - "cross-env": "^1.0.8", + "cross-env": "^2.0.0", "css-loader": "^0.23.1", - "css-modules-require-hook": "^4.0.1", + "css-modules-require-hook": "^4.0.2", "debug-menu": "^0.4.0", "del": "^2.2.1", "electron-packager": "^7.3.0", "electron-prebuilt": "^1.2.8", "electron-rebuild": "^1.1.5", - "eslint": "^2.13.1", - "eslint-config-airbnb": "^9.0.1", + "eslint": "^3.3.1", + "eslint-config-airbnb": "^10.0.1", "eslint-plugin-import": "^1.9.2", - "eslint-plugin-jsx-a11y": "^1.5.3", - "eslint-plugin-react": "^5.2.2", + "eslint-plugin-jsx-a11y": "^2.1.0", + "eslint-plugin-react": "^6.1.2", "express": "^4.14.0", "extract-text-webpack-plugin": "^1.0.1", "fbjs-scripts": "^0.7.1", @@ -121,7 +121,7 @@ "selenium-webdriver": "^2.53.2", "sinon": "^1.17.4", "style-loader": "^0.13.1", - "webpack": "^1.13.1", + "webpack": "^1.13.2", "webpack-dev-middleware": "^1.6.1", "webpack-hot-middleware": "^2.10.0" }, From a6ab73612b0d56647db2c0689d707704de0be191 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 17:46:31 +0200 Subject: [PATCH 39/41] update test for TOGGLE_LOOP_MODE which is now SET_LOOP_BOX --- test/reducers/interaction.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/reducers/interaction.spec.js b/test/reducers/interaction.spec.js index f1bd42f..d9031db 100644 --- a/test/reducers/interaction.spec.js +++ b/test/reducers/interaction.spec.js @@ -4,22 +4,22 @@ import { } from 'chai'; import interaction from '../../app/reducers/interaction'; import { - TOGGLE_LOOP_MODE, + SET_LOOP_BOX, SET_POINTS_UNDER_BRUSH } from '../../app/actionTypes'; describe('reducers/interaction', function() { - describe('TOGGLE_LOOP_MODE', function() { + describe('SET_LOOP_BOX', function() { const click1 = { - type: TOGGLE_LOOP_MODE, + type: SET_LOOP_BOX, payload: { m: 1, n: 1 } }; const click2 = { - type: TOGGLE_LOOP_MODE, + type: SET_LOOP_BOX, payload: { m: 2, n: 2 From 6ecac08ac1a0726dbe9167c0febeb058dc36bce0 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 17:47:22 +0200 Subject: [PATCH 40/41] frequency ranges all increased to 20k --- app/synthdefs/grain.json | 4 ++-- app/synthdefs/grain2.json | 4 ++-- app/synthdefs/grain3.json | 4 ++-- app/synthdefs/shockwave-microsound-2.json | 4 ++-- app/synthdefs/shockwave-microsound.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/synthdefs/grain.json b/app/synthdefs/grain.json index 34593c0..182818d 100644 --- a/app/synthdefs/grain.json +++ b/app/synthdefs/grain.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 10000, + "maxval": 20000, "class": "ControlSpec", - "minval": 40, + "minval": 20, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/grain2.json b/app/synthdefs/grain2.json index 579e0dc..fe16b30 100644 --- a/app/synthdefs/grain2.json +++ b/app/synthdefs/grain2.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 10000, + "maxval": 20000, "class": "ControlSpec", - "minval": 40, + "minval": 20, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/grain3.json b/app/synthdefs/grain3.json index f1aab2a..e5012a2 100644 --- a/app/synthdefs/grain3.json +++ b/app/synthdefs/grain3.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 10000, + "maxval": 20000, "class": "ControlSpec", - "minval": 40, + "minval": 20, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/shockwave-microsound-2.json b/app/synthdefs/shockwave-microsound-2.json index 3e7223e..6d6db3e 100644 --- a/app/synthdefs/shockwave-microsound-2.json +++ b/app/synthdefs/shockwave-microsound-2.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 10000, + "maxval": 20000, "class": "ControlSpec", - "minval": 40, + "minval": 20, "warp": "exp", "step": 0, "units": " Hz" diff --git a/app/synthdefs/shockwave-microsound.json b/app/synthdefs/shockwave-microsound.json index 9280f61..486c72a 100644 --- a/app/synthdefs/shockwave-microsound.json +++ b/app/synthdefs/shockwave-microsound.json @@ -43,9 +43,9 @@ "rate": "scalar", "spec": { "default": 440, - "maxval": 10000, + "maxval": 20000, "class": "ControlSpec", - "minval": 40, + "minval": 20, "warp": "exp", "step": 0, "units": " Hz" From 8544742afd0711544d8f856b5c4c209fe96eca83 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Fri, 19 Aug 2016 17:57:51 +0200 Subject: [PATCH 41/41] 0.3.0 --- README.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76b51bf..633accd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ Some background to the project is described here: https://medium.com/@crucialfelix/experimental-data-aesthetics-4563d01a5ebb#.3oirsvtq1 +### Try it out + +Easiest is to download the [latest release from github](https://github.com/experimentalDataAesthetics/play-splom/releases) + +- Click and drag on any plot to create a brush and move it around. +- command-click on a plot to start or change looping. ## Architecture diff --git a/package.json b/package.json index 8a889fd..fe8e0c9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "play-splom", "productName": "PlaySPLOM", "companyName": "Critical Media Lab (IXDM)", - "version": "0.2.0", + "version": "0.3.0", "description": "Scatter plot sonification for exploring datasets with sound", "license": "MIT", "repository": {