From 1d5e8b070eb5d90598035fbf17071355f92849a4 Mon Sep 17 00:00:00 2001 From: Wang Yu Date: Mon, 27 Mar 2017 13:46:38 +0800 Subject: [PATCH 01/50] fix #108 add button is not centered in firefox --- src/components/target-pane/target-pane.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css index 5f70c5878..448295da1 100644 --- a/src/components/target-pane/target-pane.css +++ b/src/components/target-pane/target-pane.css @@ -44,6 +44,7 @@ .add-button { height: 0.8rem; + margin: auto; } /* @todo: This is hacky. Better: move buttons in their corresponding panes, and set values relatively */ From c4c450dc7929c5395e685b6b525deafb6737f415 Mon Sep 17 00:00:00 2001 From: mrmeku Date: Mon, 5 Jun 2017 13:47:58 -0600 Subject: [PATCH 02/50] Documentation for the inputs of the box component --- src/components/box/box.jsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/box/box.jsx b/src/components/box/box.jsx index cabafd501..c7dbb8b54 100644 --- a/src/components/box/box.jsx +++ b/src/components/box/box.jsx @@ -75,40 +75,59 @@ const Box = props => { }, children); }; Box.propTypes = { + /** Defines how the browser distributes space between and around content items vertically within this box. */ alignContent: PropTypes.oneOf([ 'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch' ]), + /** Defines how the browser distributes space between and around flex items horizontally within this box. */ alignItems: PropTypes.oneOf([ 'flex-start', 'flex-end', 'center', 'baseline', 'stretch' ]), + /** Specifies how this box should be aligned inside of its container (requires the container to be flexable). */ alignSelf: PropTypes.oneOf([ 'auto', 'flex-start', 'flex-end', 'center', 'baseline', 'stretch' ]), + /** Specifies the initial length of this box */ basis: PropTypes.oneOfType([ PropTypes.number, PropTypes.oneOf(['auto']) ]), + /** Specifies the the HTML nodes which will be child elements of this box. */ children: PropTypes.node, + /** Specifies the class name that will be set on this box */ className: PropTypes.string, + /** + * A callback function whose first parameter is the underlying dom elements. + * This call back will be executed immediately after the component is mounted or unmounted + */ componentRef: PropTypes.func, + /** https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction */ direction: PropTypes.oneOf([ 'row', 'row-reverse', 'column', 'column-reverse' ]), + /** Specifies the type of HTML element of this box. Defaults to div. */ element: PropTypes.string, + /** Specifies the flex grow factor of a flex item. */ grow: PropTypes.number, + /** The height in pixels (if specified as a number) or a string if different units are required. */ height: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), + /** https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content */ justifyContent: PropTypes.oneOf([ 'flex-start', 'flex-end', 'center', 'space-between', 'space-around' ]), + /** Specifies the flex shrink factor of a flex item. */ shrink: PropTypes.number, + /** An object whose keys are css property names and whose values correspond the the css property. */ style: stylePropType, + /** The width in pixels (if specified as a number) or a string if different units are required. */ width: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]), + /** How whitespace should wrap within this block. */ wrap: PropTypes.oneOf([ 'nowrap', 'wrap', 'wrap-reverse' ]) From 69f4090c78f45ef78fd6b208d4fc81cd220775b5 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 14 Jun 2017 22:39:34 +0000 Subject: [PATCH 03/50] chore(package): update react-tabs to version 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75bd48ebc..f26157966 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "react-modal": "1.7.7", "react-redux": "5.0.5", "react-style-proptype": "3.0.0", - "react-tabs": "1.0.0", + "react-tabs": "1.1.0", "redux": "3.6.0", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", From 0fcf7cb54418386d912170af781f5ff7ccbbf9f6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 15 Jun 2017 02:09:25 +0000 Subject: [PATCH 04/50] chore(package): update react to version 15.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75bd48ebc..46a3767c5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "url": "git+ssh://git@github.com/LLK/scratch-gui.git" }, "peerDependencies": { - "react": "^15", + "react": "^15.6.0", "react-dom": "^15" }, "devDependencies": { From 0fd6c4d8c9c65dd218b2880f51fee4d31f04a083 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 15 Jun 2017 11:52:02 -0400 Subject: [PATCH 05/50] refresh workspace when setting to visible --- src/containers/blocks.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 996741267..3bce0f3c5 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -63,8 +63,10 @@ class Blocks extends React.Component { } // @todo hack to resize blockly manually in case resize happened while hidden + // @todo hack to reload the workspace due to gui bug #413 if (this.props.isVisible) { // Scripts tab this.workspace.setVisible(true); + this.props.vm.refreshWorkspace(); window.dispatchEvent(new Event('resize')); this.workspace.toolbox_.refreshSelection(); } else { From fb1c44a65604bb126464c6894f2693c1d7f3e0e4 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Jun 2017 10:12:09 -0400 Subject: [PATCH 06/50] Add pop sound to all sprites --- src/lib/libraries/sprites.json | 138 +++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 13 deletions(-) diff --git a/src/lib/libraries/sprites.json b/src/lib/libraries/sprites.json index 1aab55be8..352dac342 100644 --- a/src/lib/libraries/sprites.json +++ b/src/lib/libraries/sprites.json @@ -1231,7 +1231,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -1318,7 +1328,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -2667,7 +2687,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -4728,7 +4758,8 @@ "format": "" } ], - "costumes": [{ + "costumes": [ + { "costumeName": "Dinosaur4-16", "baseLayerID": -1, "baseLayerMD5": "4e330592c1f1b80a4b182cc00cf8d6bf.png", @@ -4855,7 +4886,8 @@ "bitmapResolution": 2, "rotationCenterX": 82, "rotationCenterY": 134 - }], + } + ], "currentCostumeIndex": 0, "scratchX": 0, "scratchY": 0, @@ -5366,7 +5398,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -6698,7 +6740,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -7045,7 +7097,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -10594,7 +10656,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -12588,7 +12660,17 @@ "isDraggable": true, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -12692,7 +12774,17 @@ "isDraggable": true, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -13725,7 +13817,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { @@ -13818,7 +13920,17 @@ "isDraggable": false, "indexInLibrary": 100000, "visible": true, - "spriteInfo": {} + "spriteInfo": {}, + "sounds": [ + { + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + } + ] } }, { From d9bae290d6fa1b3eb82145d0619b81539d1e7de2 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Jun 2017 11:52:36 -0400 Subject: [PATCH 07/50] Revert "Explicitly create variables in the VM" --- src/containers/blocks.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 3bce0f3c5..59e868436 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -150,6 +150,7 @@ class Blocks extends React.Component { if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) { this.onWorkspaceMetricsChange(); } + this.ScratchBlocks.Events.disable(); this.workspace.clear(); @@ -174,7 +175,6 @@ class Blocks extends React.Component { } handlePromptCallback (data) { this.state.prompt.callback(data); - this.props.vm.createVariable(data); this.handlePromptClose(); } handlePromptClose () { From dca9e70cedfd03b98e394cee418746f220c1fdfb Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 12 Jun 2017 07:54:38 -0400 Subject: [PATCH 08/50] Add recorder and playback --- package.json | 1 + src/components/asset-panel/asset-panel.jsx | 2 +- src/components/record-modal/meter.jsx | 87 ++++++++ src/components/record-modal/playback-step.jsx | 109 ++++++++++ src/components/record-modal/record-modal.css | 201 ++++++++++++++++++ src/components/record-modal/record-modal.jsx | 72 +++++++ .../record-modal/recording-step.jsx | 105 +++++++++ src/components/record-modal/waveform.jsx | 51 +++++ src/containers/playback-step.jsx | 50 +++++ src/containers/record-modal.jsx | 120 +++++++++++ src/containers/recording-step.jsx | 65 ++++++ src/containers/sound-tab.jsx | 23 +- src/lib/audio/audio-buffer-player.js | 26 +++ src/lib/audio/audio-recorder.js | 165 ++++++++++++++ src/lib/audio/shared-audio-context.js | 6 + src/reducers/modals.js | 10 +- 16 files changed, 1084 insertions(+), 9 deletions(-) create mode 100644 src/components/record-modal/meter.jsx create mode 100644 src/components/record-modal/playback-step.jsx create mode 100644 src/components/record-modal/record-modal.css create mode 100644 src/components/record-modal/record-modal.jsx create mode 100644 src/components/record-modal/recording-step.jsx create mode 100644 src/components/record-modal/waveform.jsx create mode 100644 src/containers/playback-step.jsx create mode 100644 src/containers/record-modal.jsx create mode 100644 src/containers/recording-step.jsx create mode 100644 src/lib/audio/audio-buffer-player.js create mode 100644 src/lib/audio/audio-recorder.js create mode 100644 src/lib/audio/shared-audio-context.js diff --git a/package.json b/package.json index 75bd48ebc..71930ca57 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "style-loader": "^0.18.0", "svg-to-image": "1.1.3", "svg-url-loader": "2.0.2", + "wav-encoder": "^1.1.0", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.1", "xhr": "2.4.0" diff --git a/src/components/asset-panel/asset-panel.jsx b/src/components/asset-panel/asset-panel.jsx index 2d6ea710b..7672171d5 100644 --- a/src/components/asset-panel/asset-panel.jsx +++ b/src/components/asset-panel/asset-panel.jsx @@ -11,7 +11,7 @@ const AssetPanel = props => ( {...props} /> - {/* @todo editor area */} + {props.children} ); diff --git a/src/components/record-modal/meter.jsx b/src/components/record-modal/meter.jsx new file mode 100644 index 000000000..e40245b92 --- /dev/null +++ b/src/components/record-modal/meter.jsx @@ -0,0 +1,87 @@ +const React = require('react'); +const PropTypes = require('prop-types'); + +const Meter = props => { + const level = Math.min(1, Math.max(0, props.level || 0)); + const { + width, + height + } = props; + + const nGreen = 11; + const nYellow = 5; + const nRed = 3; + + const nBars = nGreen + nYellow + nRed; + + const barSpacing = 2.5; + const barRounding = 3; + const barHeight = (height + barSpacing / 2 - barSpacing * nBars) / nBars; + const barWidth = width; + + const greenFill = 'rgb(171, 220, 170)'; + const yellowFill = 'rgb(251, 219, 130)'; + const redFill = 'rgb(251, 194, 142)'; + + const indexToFill = index => { + if (index < nGreen) return greenFill; + if (index < nGreen + nYellow) return yellowFill; + return redFill; + }; + + const greenStroke = 'rgb(174, 211, 168)'; + const yellowStroke = 'rgb(239, 212, 134)'; + const redStroke = 'rgb(235, 189, 142)'; + + const indexToStroke = index => { + if (index < nGreen) return greenStroke; + if (index < nGreen + nYellow) return yellowStroke; + return redStroke; + }; + + const bars = Array(nBars).fill(0) + .map((value, index) => ( + + )); + + const maskHeight = Math.floor(nBars * (1 - level)) * (barHeight + barSpacing); + const mask = ( + + ); + + return ( + + {bars} + {mask} + + ); +}; + +Meter.propTypes = { + height: PropTypes.number, + level: PropTypes.number, + width: PropTypes.number +}; + +module.exports = Meter; diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx new file mode 100644 index 000000000..807d1c4a9 --- /dev/null +++ b/src/components/record-modal/playback-step.jsx @@ -0,0 +1,109 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const Box = require('../box/box.jsx'); +const Waveform = require('./waveform.jsx'); +const Meter = require('./meter.jsx'); +const styles = require('./record-modal.css'); + +const PlaybackStep = props => ( + + + + + + + + + + + + + + + + + + +); + +PlaybackStep.propTypes = { + encoding: PropTypes.bool.isRequired, + levels: PropTypes.arrayOf(PropTypes.number).isRequired, + onBack: PropTypes.func.isRequired, + onPlay: PropTypes.func.isRequired, + onStopPlaying: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + playing: PropTypes.bool.isRequired +}; + +module.exports = PlaybackStep; diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css new file mode 100644 index 000000000..16d5107a8 --- /dev/null +++ b/src/components/record-modal/record-modal.css @@ -0,0 +1,201 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(94, 102, 125, .95); +} + +.modal-content { + width: 600px; + margin: 100px auto; + outline: none; + border: 3px solid rgb(126, 133, 151); + padding: 0; + border-radius: $space; + user-select: none; + + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #5d647a; + overflow: hidden; +} + +.header { + font-size: 1.25rem; + height: 3.25rem; + background-color: $blue; + font-weight: 500; + color: #fff; + text-align: center; + line-height: 3.25rem; +} + +.body { + background: $ui-pane-gray; + padding: 1.5rem 2.25rem; +} +.waveform-container { + padding-bottom: 15px; + + padding: 15px; + margin: auto; + display: flex; + justify-content: space-around; + align-items: center; + background: white; + border: 1px solid #ddd; + border-radius: 5px; + text-align: center; + height: 180px; + width: 100%; +} + +.help-text { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: rgb(167, 170, 181); + font-size: 0.95rem; + font-weight: 500; +} + +.playing-text { + color: #4C97FF; +} +.recording-text { + color: rgb(237, 111, 54); +} + +.main-button-row { + display: flex; + justify-content: space-around; + margin-top: 15px; +} + +.main-button-row button { + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + background: transparent; + border: none; +} + +.main-button-row button:active, .main-button-row button:focus { + outline: none; +} + +.button-row { + font-weight: bolder; + text-align: right; + display: flex; + justify-content: space-between; +} + +.button-row button { + padding: 0.75rem 1rem; + border-radius: 0.25rem; + background: white; + border: 1px solid $ui-pane-border; + font-weight: 600; + font-size: 0.85rem; + color: $blue; +} + +.button-row button.ok-button { + background: $blue; + border: $blue; + color: white; +} + +.button-row button + button { + margin-left: 0.5rem; +} + +.main-button { + text-align: center; +} + +.visualization-container { + display: flex; + justify-content: space-around; + +} + +.meter-container { + background: white; + border: 1px solid #ddd; + border-radius: 5px; + margin-right: 5px; + padding: 3px; + height: 180px; +} + +/* SVG styling */ +.pulser { + transform: scale(1.0); + transform-origin: center center; + animation: pulse-me 0.5s ease-in-out infinite alternate; +} + +@keyframes pulse-me{ + 0%{ + transform: scale(1.05); + opacity: 0.05; + } + 100%{ + opacity: 0.2; + transform: scale(1.25); + } +} + + + +.header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + height: $library-header-height; + + box-sizing: border-box; + width: 100%; + background-color: $blue; + + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: normal; +} + + +$sides: 5rem; + +.header-item { + display: flex; + align-items: center; + padding: 1rem; + text-decoration: none; + color: white; + user-select: none; +} + +.header-item-filter { + display: flex; + flex-basis: $sides; + justify-content: flex-start; +} + +.header-item-title { + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + user-select: none; + letter-spacing: 0.4px; + cursor: default; +} + +.header-item-close { + flex-basis: $sides; + justify-content: flex-end; +} diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx new file mode 100644 index 000000000..a807ab0b5 --- /dev/null +++ b/src/components/record-modal/record-modal.jsx @@ -0,0 +1,72 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const ReactModal = require('react-modal'); +const Box = require('../box/box.jsx'); +const styles = require('./record-modal.css'); +const classNames = require('classnames'); +const CloseButton = require('../close-button/close-button.jsx'); + +const RecordingStep = require('../../containers/recording-step.jsx'); +const PlaybackStep = require('../../containers/playback-step.jsx'); + +const RecordModal = props => ( + +
+
+ {/* Nothing here, just for spacing */} +
+
+ Record Sound +
+
+ +
+
+ + {props.channelData ? ( + + ) : ( + + )} + +
+); + +RecordModal.propTypes = { + channelData: PropTypes.arrayOf(PropTypes.instanceOf(Float32Array)), + encoding: PropTypes.bool.isRequired, + levels: PropTypes.arrayOf(PropTypes.number), + onBack: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onPlay: PropTypes.func.isRequired, + onRecord: PropTypes.func.isRequired, + onStopPlaying: PropTypes.func.isRequired, + onStopRecording: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + playing: PropTypes.bool, + recording: PropTypes.bool +}; + +module.exports = RecordModal; diff --git a/src/components/record-modal/recording-step.jsx b/src/components/record-modal/recording-step.jsx new file mode 100644 index 000000000..9322d78f1 --- /dev/null +++ b/src/components/record-modal/recording-step.jsx @@ -0,0 +1,105 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const Box = require('../box/box.jsx'); +const Meter = require('./meter.jsx'); +const styles = require('./record-modal.css'); +const Waveform = require('./waveform.jsx'); + +const RecordingStep = props => ( + + + + + + + {props.levels ? ( + + ) : ( + + Begin recording by clicking the button below + + )} + + + + + + +); + +RecordingStep.propTypes = { + level: PropTypes.number, + levels: PropTypes.arrayOf(PropTypes.number), + onRecord: PropTypes.func.isRequired, + onStopRecording: PropTypes.func.isRequired, + recording: PropTypes.bool +}; + +module.exports = RecordingStep; diff --git a/src/components/record-modal/waveform.jsx b/src/components/record-modal/waveform.jsx new file mode 100644 index 000000000..84340f8b3 --- /dev/null +++ b/src/components/record-modal/waveform.jsx @@ -0,0 +1,51 @@ +const React = require('react'); +const PropTypes = require('prop-types'); + +const Waveform = props => { + const { + width, + height, + data + } = props; + + const cappedData = [0, ...data, 0]; + + const points = [ + ...cappedData.map((v, i) => + [width * i / cappedData.length, height * v / 2] + ), + ...cappedData.reverse().map((v, i) => + [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] + ) + ]; + + const pathComponents = points.map(([x, y], i) => { + const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0]; + return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`; + }); + + return ( + + + + + + ); +}; + +Waveform.propTypes = { + data: PropTypes.arrayOf(PropTypes.number), + height: PropTypes.number, + width: PropTypes.number +}; + +module.exports = Waveform; diff --git a/src/containers/playback-step.jsx b/src/containers/playback-step.jsx new file mode 100644 index 000000000..2aa2411d3 --- /dev/null +++ b/src/containers/playback-step.jsx @@ -0,0 +1,50 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const bindAll = require('lodash.bindall'); +const PlaybackStepComponent = require('../components/record-modal/playback-step.jsx'); +const AudioBufferPlayer = require('../lib/audio/audio-buffer-player.js'); + +class PlaybackStep extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handlePlay', + 'handleStopPlaying' + ]); + } + componentDidMount () { + this.audioBufferPlayer = new AudioBufferPlayer(this.props.channelData); + } + componentWillUnmount () { + this.audioBufferPlayer.stop(); + } + handlePlay () { + this.audioBufferPlayer.play(this.props.onStopPlaying); + this.props.onPlay(); + } + handleStopPlaying () { + this.audioBufferPlayer.stop(); + this.props.onStopPlaying(); + } + render () { + const { + onPlay, // eslint-disable-line no-unused-vars + onStopPlaying, // eslint-disable-line no-unused-vars + ...componentProps + } = this.props; + return ( + + ); + } +} + +PlaybackStep.propTypes = { + channelData: PropTypes.arrayOf(PropTypes.instanceOf(Float32Array)).isRequired, + ...PlaybackStepComponent.propTypes +}; + +module.exports = PlaybackStep; diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx new file mode 100644 index 000000000..049f6e782 --- /dev/null +++ b/src/containers/record-modal.jsx @@ -0,0 +1,120 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const VM = require('scratch-vm'); +const WavEncoder = require('wav-encoder'); +const {connect} = require('react-redux'); + +const RecordModalComponent = require('../components/record-modal/record-modal.jsx'); + +const { + closeSoundRecorder +} = require('../reducers/modals'); + +class RecordModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleRecord', + 'handleStopRecording', + 'handlePlay', + 'handleStopPlaying', + 'handleBack', + 'handleSubmit', + 'handleCancel' + ]); + + this.state = { + channelData: null, + encoding: false, + levels: null, + playing: false, + recording: false, + sampleRate: null + }; + } + handleRecord () { + this.setState({recording: true}); + } + handleStopRecording (channelData, sampleRate, levels) { + this.setState({channelData, sampleRate, levels, recording: false}); + } + handlePlay () { + this.setState({playing: true}); + } + handleStopPlaying () { + this.setState({playing: false}); + } + handleBack () { + this.setState({playing: false, channelData: null}); + } + handleSubmit () { + this.setState({encoding: true}); + + WavEncoder.encode({ + sampleRate: this.state.sampleRate, + channelData: this.state.channelData + }).then(wavBuffer => { + const md5 = String(Math.floor(100000 * Math.random())); + const vmSound = { + format: '', + md5: `${md5}.wav`, + name: `recording ${this.props.vm.editingTarget.sprite.sounds.length}` + }; + + // Load the encoded .wav into the storage cache + const storage = this.props.vm.runtime.storage; + storage.builtinHelper.cache( + storage.AssetType.Sound, + storage.DataFormat.WAV, + new Uint8Array(wavBuffer), // @todo ask cwf about the need for this? + md5 + ); + + this.props.vm.addSound(vmSound); + this.handleCancel(); + }); + } + handleCancel () { + this.props.onClose(); + } + render () { + return ( + + ); + } +} + +RecordModal.propTypes = { + onClose: PropTypes.func, + vm: PropTypes.instanceOf(VM) +}; + +const mapStateToProps = state => ({ + visible: state.modals.soundRecorder, + vm: state.vm +}); + +const mapDispatchToProps = dispatch => ({ + onClose: () => { + dispatch(closeSoundRecorder()); + } +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(RecordModal); diff --git a/src/containers/recording-step.jsx b/src/containers/recording-step.jsx new file mode 100644 index 000000000..66a643e27 --- /dev/null +++ b/src/containers/recording-step.jsx @@ -0,0 +1,65 @@ +const React = require('react'); +const bindAll = require('lodash.bindall'); +const RecordingStepComponent = require('../components/record-modal/recording-step.jsx'); +const AudioRecorder = require('../lib/audio/audio-recorder.js'); + +class RecordingStep extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleRecord', + 'handleStopRecording', + 'handleLevelUpdate', + 'handleRecordingError' + ]); + + this.state = { + level: 0, + levels: null + }; + } + componentDidMount () { + this.audioRecorder = new AudioRecorder(); + this.audioRecorder.startListening(this.handleLevelUpdate, this.handleRecordingError); + } + componentWillUnmount () { + this.audioRecorder.dispose(); + } + handleRecordingError () { + alert('Could not start recording'); // eslint-disable-line no-alert + } + handleLevelUpdate (level) { + this.setState({level}); + if (this.props.recording) { + this.setState({levels: (this.state.levels || []).concat([level])}); + } + } + handleRecord () { + this.audioRecorder.startRecording(); + this.props.onRecord(); + } + handleStopRecording () { + const {channelData, sampleRate, levels} = this.audioRecorder.stop(); + this.props.onStopRecording(channelData, sampleRate, levels); + } + render () { + const { + onRecord, // eslint-disable-line no-unused-vars + onStopRecording, // eslint-disable-line no-unused-vars + ...componentProps + } = this.props; + return ( + + ); + } +} + +RecordingStep.propTypes = RecordingStepComponent.propTypes; + +module.exports = RecordingStep; diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index 70dfbdef7..1828caf92 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -6,11 +6,13 @@ const VM = require('scratch-vm'); const AssetPanel = require('../components/asset-panel/asset-panel.jsx'); const soundIcon = require('../components/asset-panel/icon--sound.svg'); +const RecordModal = require('./record-modal.jsx'); const {connect} = require('react-redux'); const { - openSoundLibrary + openSoundLibrary, + openSoundRecorder } = require('../reducers/modals'); class SoundTab extends React.Component { @@ -51,8 +53,7 @@ class SoundTab extends React.Component { const { editingTarget, sprites, - stage, - onNewSoundClick + stage } = this.props; const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; @@ -78,15 +79,20 @@ class SoundTab extends React.Component { selectedItemIndex={this.state.selectedSoundIndex} onDeleteClick={this.handleDeleteSound} onItemClick={this.handleSelectSound} - onNewClick={onNewSoundClick} - /> + onNewClick={this.props.onSoundRecorder} + > + {this.props.soundRecorderVisible ? ( + + ) : null} + ); } } SoundTab.propTypes = { editingTarget: PropTypes.string, - onNewSoundClick: PropTypes.func.isRequired, + onSoundRecorder: PropTypes.func.isRequired, + soundRecorderVisible: PropTypes.bool, sprites: PropTypes.shape({ id: PropTypes.shape({ sounds: PropTypes.arrayOf(PropTypes.shape({ @@ -106,13 +112,16 @@ const mapStateToProps = state => ({ editingTarget: state.targets.editingTarget, sprites: state.targets.sprites, stage: state.targets.stage, - soundLibraryVisible: state.modals.soundLibrary + soundRecorderVisible: state.modals.soundRecorder }); const mapDispatchToProps = dispatch => ({ onNewSoundClick: e => { e.preventDefault(); dispatch(openSoundLibrary()); + }, + onSoundRecorder: () => { + dispatch(openSoundRecorder()); } }); diff --git a/src/lib/audio/audio-buffer-player.js b/src/lib/audio/audio-buffer-player.js new file mode 100644 index 000000000..7f7083764 --- /dev/null +++ b/src/lib/audio/audio-buffer-player.js @@ -0,0 +1,26 @@ +const SharedAudioContext = require('./shared-audio-context.js'); + +const AudioBufferPlayer = function (channelData) { + this.audioContext = new SharedAudioContext(); + this.buffer = this.audioContext.createBuffer(1, channelData[0].length, this.audioContext.sampleRate); + this.buffer.getChannelData(0).set(channelData[0]); + this.source = null; +}; + +AudioBufferPlayer.prototype.play = function (onEnded) { + // Buffer source nodes are one time use only. Must do this every play. + this.source = this.audioContext.createBufferSource(); + this.source.onended = onEnded; + this.source.buffer = this.buffer; + this.source.connect(this.audioContext.destination); + this.source.start(); +}; + +AudioBufferPlayer.prototype.stop = function () { + if (this.source) { + this.source.onended = null; // Do not call onEnded callback if manually stopped + this.source.stop(); + } +}; + +module.exports = AudioBufferPlayer; diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js new file mode 100644 index 000000000..87f39ce03 --- /dev/null +++ b/src/lib/audio/audio-recorder.js @@ -0,0 +1,165 @@ +const SharedAudioContext = require('./shared-audio-context.js'); + +const AudioRecorder = function () { + this.audioContext = new SharedAudioContext(); + this.bufferLength = 1024; + + this.userMediaStream = null; + this.mediaStreamSource = null; + this.sourceNode = null; + this.scriptProcessorNode = null; + + this.recordedSamples = 0; + this.recording = false; + this.leftBuffers = []; + this.rightBuffers = []; + this.levels = []; + + this.disposed = false; +}; + +AudioRecorder.prototype.startListening = function (onUpdate, onError) { + try { + navigator.getUserMedia({audio: true}, userMediaStream => { + this.attachUserMediaStream(userMediaStream, onUpdate); + }, e => { + onError(e); + }); + } catch (e) { + onError(e); + } +}; + +AudioRecorder.prototype.startRecording = function () { + this.recording = true; +}; + +AudioRecorder.prototype.calculateRMS = function (samples) { + // Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88 + const sum = samples.reduce((acc, v) => acc + Math.pow(v, 2), 0); + const rms = Math.sqrt(sum / samples.length); + // Scale it + const unity = 0.35; + const val = rms / unity; + // Scale the output curve + return Math.sqrt(val); +}; + +AudioRecorder.prototype.attachUserMediaStream = function (userMediaStream, onUpdate) { + this.userMediaStream = userMediaStream; + this.mediaStreamSource = this.audioContext.createMediaStreamSource(userMediaStream); + this.sourceNode = this.audioContext.createGain(); + this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 2, 2); + + + this.scriptProcessorNode.onaudioprocess = processEvent => { + if (this.recording) { + const leftBuffer = new Float32Array(processEvent.inputBuffer.getChannelData(0)); + const rightBuffer = new Float32Array(processEvent.inputBuffer.getChannelData(1)); + + this.leftBuffers.push(leftBuffer); + this.rightBuffers.push(rightBuffer); + this.recordedSamples += this.bufferLength; + } + }; + + this.analyserNode = this.audioContext.createAnalyser(); + + this.analyserNode.fftSize = 2048; + + const bufferLength = this.analyserNode.frequencyBinCount; + const dataArray = new Float32Array(bufferLength); + + const update = () => { + if (!this.disposed) { + requestAnimationFrame(update); + } else { + return; + } + + this.analyserNode.getFloatTimeDomainData(dataArray); + const rms = this.calculateRMS(dataArray); + if (this.recording) { + this.levels.push(rms); + } + onUpdate(rms); + }; + + requestAnimationFrame(update); + + // Wire everything together, ending in the destination + this.mediaStreamSource.connect(this.sourceNode); + this.sourceNode.connect(this.analyserNode); + this.analyserNode.connect(this.scriptProcessorNode); + this.scriptProcessorNode.connect(this.audioContext.destination); +}; + +AudioRecorder.prototype.stop = function () { + let offset = 0; + let maxRMS = 0; + const chunkLevels = []; + for (let i = 0; i < this.leftBuffers.length; i++) { + const leftBufferChunk = this.leftBuffers[i]; + const rms = this.calculateRMS(leftBufferChunk); + maxRMS = Math.max(maxRMS, rms); + chunkLevels.push(rms); + } + + const threshold = maxRMS / 8; + + let firstChunkAboveThreshold = null; + let chunkIndex = 0; + while (firstChunkAboveThreshold === null && chunkIndex < chunkLevels.length - 1) { + if (chunkLevels[chunkIndex] > threshold) { + firstChunkAboveThreshold = chunkIndex; + } else { + chunkIndex += 1; + } + } + + let lastChunkAboveThreshold = null; + chunkIndex = chunkLevels.length - 1; + while (lastChunkAboveThreshold === null && chunkIndex > 0) { + if (chunkLevels[chunkIndex] > threshold) { + lastChunkAboveThreshold = chunkIndex; + } else { + chunkIndex -= 1; + } + } + + const usedSamples = lastChunkAboveThreshold - firstChunkAboveThreshold + 2; + const buffers = [ + new Float32Array(usedSamples * this.bufferLength), + new Float32Array(usedSamples * this.bufferLength) + ]; + + const usedChunkLevels = []; + for (let i = 0; i < this.leftBuffers.length; i++) { + const leftBufferChunk = this.leftBuffers[i]; + const rightBufferChunk = this.rightBuffers[i]; + + if (i > firstChunkAboveThreshold - 2 && i < lastChunkAboveThreshold + 1) { + usedChunkLevels.push(chunkLevels[i]); + buffers[0].set(leftBufferChunk, offset); + buffers[1].set(rightBufferChunk, offset); + offset += leftBufferChunk.length; + } + } + + return { + levels: usedChunkLevels, + channelData: buffers, + sampleRate: this.audioContext.sampleRate + }; +}; + +AudioRecorder.prototype.dispose = function () { + this.scriptProcessorNode.onaudioprocess = null; + this.scriptProcessorNode.disconnect(); + this.sourceNode.disconnect(); + this.mediaStreamSource.disconnect(); + this.userMediaStream.getAudioTracks()[0].stop(); + this.disposed = true; +}; + +module.exports = AudioRecorder; diff --git a/src/lib/audio/shared-audio-context.js b/src/lib/audio/shared-audio-context.js new file mode 100644 index 000000000..89832d458 --- /dev/null +++ b/src/lib/audio/shared-audio-context.js @@ -0,0 +1,6 @@ +// Wrap browser AudioContext because we shouldn't create more than one +const AUDIO_CONTEXT = new AudioContext(); + +module.exports = function () { + return AUDIO_CONTEXT; +}; diff --git a/src/reducers/modals.js b/src/reducers/modals.js index a55cb71d6..2e59b144a 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -5,12 +5,14 @@ const MODAL_BACKDROP_LIBRARY = 'backdropLibrary'; const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; +const MODAL_SOUND_RECORDER = 'soundRecorder'; const initialState = { [MODAL_BACKDROP_LIBRARY]: false, [MODAL_COSTUME_LIBRARY]: false, [MODAL_SOUND_LIBRARY]: false, - [MODAL_SPRITE_LIBRARY]: false + [MODAL_SPRITE_LIBRARY]: false, + [MODAL_SOUND_RECORDER]: false }; const reducer = function (state, action) { @@ -52,6 +54,9 @@ reducer.openSoundLibrary = function () { reducer.openSpriteLibrary = function () { return reducer.openModal(MODAL_SPRITE_LIBRARY); }; +reducer.openSoundRecorder = function () { + return reducer.openModal(MODAL_SOUND_RECORDER); +}; reducer.closeBackdropLibrary = function () { return reducer.closeModal(MODAL_BACKDROP_LIBRARY); }; @@ -64,4 +69,7 @@ reducer.closeSpriteLibrary = function () { reducer.closeSoundLibrary = function () { return reducer.closeModal(MODAL_SOUND_LIBRARY); }; +reducer.closeSoundRecorder = function () { + return reducer.closeModal(MODAL_SOUND_RECORDER); +}; module.exports = reducer; From 2c9b946b7774de75de49b4207031739da44f7d8b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Jun 2017 15:30:52 -0400 Subject: [PATCH 09/50] Clean up the meter code --- src/components/record-modal/meter.css | 18 +++++++ src/components/record-modal/meter.jsx | 77 +++++++++------------------ 2 files changed, 44 insertions(+), 51 deletions(-) create mode 100644 src/components/record-modal/meter.css diff --git a/src/components/record-modal/meter.css b/src/components/record-modal/meter.css new file mode 100644 index 000000000..7099b3093 --- /dev/null +++ b/src/components/record-modal/meter.css @@ -0,0 +1,18 @@ +.green { + fill: rgb(171, 220, 170); + stroke: rgb(174, 211, 168); +} + +.yellow { + fill: rgb(251, 219, 130); + stroke: rgb(239, 212, 134); +} + +.red { + fill: rgb(251, 194, 142); + stroke: rgb(235, 189, 142); +} + +.container { + overflow: visible; +} diff --git a/src/components/record-modal/meter.jsx b/src/components/record-modal/meter.jsx index e40245b92..b0efee165 100644 --- a/src/components/record-modal/meter.jsx +++ b/src/components/record-modal/meter.jsx @@ -1,12 +1,14 @@ const React = require('react'); const PropTypes = require('prop-types'); +const styles = require('./meter.css'); const Meter = props => { - const level = Math.min(1, Math.max(0, props.level || 0)); const { width, height } = props; + + const level = Math.min(1, Math.max(0, props.level || 0)); const nGreen = 11; const nYellow = 5; @@ -19,61 +21,34 @@ const Meter = props => { const barHeight = (height + barSpacing / 2 - barSpacing * nBars) / nBars; const barWidth = width; - const greenFill = 'rgb(171, 220, 170)'; - const yellowFill = 'rgb(251, 219, 130)'; - const redFill = 'rgb(251, 194, 142)'; - - const indexToFill = index => { - if (index < nGreen) return greenFill; - if (index < nGreen + nYellow) return yellowFill; - return redFill; - }; - - const greenStroke = 'rgb(174, 211, 168)'; - const yellowStroke = 'rgb(239, 212, 134)'; - const redStroke = 'rgb(235, 189, 142)'; - - const indexToStroke = index => { - if (index < nGreen) return greenStroke; - if (index < nGreen + nYellow) return yellowStroke; - return redStroke; - }; - - const bars = Array(nBars).fill(0) - .map((value, index) => ( - - )); - - const maskHeight = Math.floor(nBars * (1 - level)) * (barHeight + barSpacing); - const mask = ( - - ); - return ( - {bars} - {mask} + {Array(nBars).fill(0) + .map((value, index) => ( + + ))} + ); }; From 059726d626accd863f7ded68fe404eb64ffe4e08 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Jun 2017 16:34:02 -0400 Subject: [PATCH 10/50] Remove unused code from audio recorder --- src/lib/audio/audio-recorder.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js index 87f39ce03..7da31f3f9 100644 --- a/src/lib/audio/audio-recorder.js +++ b/src/lib/audio/audio-recorder.js @@ -13,7 +13,6 @@ const AudioRecorder = function () { this.recording = false; this.leftBuffers = []; this.rightBuffers = []; - this.levels = []; this.disposed = false; }; @@ -71,18 +70,10 @@ AudioRecorder.prototype.attachUserMediaStream = function (userMediaStream, onUpd const dataArray = new Float32Array(bufferLength); const update = () => { - if (!this.disposed) { - requestAnimationFrame(update); - } else { - return; - } - + if (this.disposed) return; + requestAnimationFrame(update); this.analyserNode.getFloatTimeDomainData(dataArray); - const rms = this.calculateRMS(dataArray); - if (this.recording) { - this.levels.push(rms); - } - onUpdate(rms); + onUpdate(this.calculateRMS(dataArray)); }; requestAnimationFrame(update); From 5ce93bc3bdc31b6f801db617d91363d9066930d4 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Jun 2017 17:20:48 -0400 Subject: [PATCH 11/50] Record mono --- src/components/record-modal/record-modal.jsx | 8 +-- src/containers/playback-step.jsx | 4 +- src/containers/record-modal.jsx | 55 +++++++++++--------- src/containers/recording-step.jsx | 4 +- src/lib/audio/audio-buffer-player.js | 6 +-- src/lib/audio/audio-recorder.js | 32 ++++-------- 6 files changed, 50 insertions(+), 59 deletions(-) diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx index a807ab0b5..1fd5fa852 100644 --- a/src/components/record-modal/record-modal.jsx +++ b/src/components/record-modal/record-modal.jsx @@ -32,12 +32,12 @@ const RecordModal = props => ( - {props.channelData ? ( + {props.samples ? ( ( ); RecordModal.propTypes = { - channelData: PropTypes.arrayOf(PropTypes.instanceOf(Float32Array)), encoding: PropTypes.bool.isRequired, levels: PropTypes.arrayOf(PropTypes.number), onBack: PropTypes.func.isRequired, @@ -66,7 +65,8 @@ RecordModal.propTypes = { onStopRecording: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, playing: PropTypes.bool, - recording: PropTypes.bool + recording: PropTypes.bool, + samples: PropTypes.instanceOf(Float32Array) }; module.exports = RecordModal; diff --git a/src/containers/playback-step.jsx b/src/containers/playback-step.jsx index 2aa2411d3..fa7a35fe1 100644 --- a/src/containers/playback-step.jsx +++ b/src/containers/playback-step.jsx @@ -13,7 +13,7 @@ class PlaybackStep extends React.Component { ]); } componentDidMount () { - this.audioBufferPlayer = new AudioBufferPlayer(this.props.channelData); + this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples); } componentWillUnmount () { this.audioBufferPlayer.stop(); @@ -43,7 +43,7 @@ class PlaybackStep extends React.Component { } PlaybackStep.propTypes = { - channelData: PropTypes.arrayOf(PropTypes.instanceOf(Float32Array)).isRequired, + samples: PropTypes.instanceOf(Float32Array).isRequired, ...PlaybackStepComponent.propTypes }; diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index 049f6e782..b5fdad91c 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -25,7 +25,7 @@ class RecordModal extends React.Component { ]); this.state = { - channelData: null, + samples: null, encoding: false, levels: null, playing: false, @@ -36,8 +36,8 @@ class RecordModal extends React.Component { handleRecord () { this.setState({recording: true}); } - handleStopRecording (channelData, sampleRate, levels) { - this.setState({channelData, sampleRate, levels, recording: false}); + handleStopRecording (samples, sampleRate, levels) { + this.setState({samples, sampleRate, levels, recording: false}); } handlePlay () { this.setState({playing: true}); @@ -46,34 +46,37 @@ class RecordModal extends React.Component { this.setState({playing: false}); } handleBack () { - this.setState({playing: false, channelData: null}); + this.setState({playing: false, samples: null}); } handleSubmit () { this.setState({encoding: true}); - WavEncoder.encode({ - sampleRate: this.state.sampleRate, - channelData: this.state.channelData - }).then(wavBuffer => { - const md5 = String(Math.floor(100000 * Math.random())); - const vmSound = { - format: '', - md5: `${md5}.wav`, - name: `recording ${this.props.vm.editingTarget.sprite.sounds.length}` - }; + // Allow enough time for UI refresh before starting encoding + setTimeout(() => { + WavEncoder.encode({ + sampleRate: this.state.sampleRate, + channelData: [this.state.samples] + }).then(wavBuffer => { + const md5 = String(Math.floor(100000 * Math.random())); + const vmSound = { + format: '', + md5: `${md5}.wav`, + name: `recording ${this.props.vm.editingTarget.sprite.sounds.length}` + }; - // Load the encoded .wav into the storage cache - const storage = this.props.vm.runtime.storage; - storage.builtinHelper.cache( - storage.AssetType.Sound, - storage.DataFormat.WAV, - new Uint8Array(wavBuffer), // @todo ask cwf about the need for this? - md5 - ); + // Load the encoded .wav into the storage cache + const storage = this.props.vm.runtime.storage; + storage.builtinHelper.cache( + storage.AssetType.Sound, + storage.DataFormat.WAV, + new Uint8Array(wavBuffer), // @todo ask cwf about the need for this? + md5 + ); - this.props.vm.addSound(vmSound); - this.handleCancel(); - }); + this.props.vm.addSound(vmSound); + this.handleCancel(); + }); + }, 100); } handleCancel () { this.props.onClose(); @@ -81,11 +84,11 @@ class RecordModal extends React.Component { render () { return ( { if (this.recording) { - const leftBuffer = new Float32Array(processEvent.inputBuffer.getChannelData(0)); - const rightBuffer = new Float32Array(processEvent.inputBuffer.getChannelData(1)); - - this.leftBuffers.push(leftBuffer); - this.rightBuffers.push(rightBuffer); - this.recordedSamples += this.bufferLength; + this.buffers.push(new Float32Array(processEvent.inputBuffer.getChannelData(0))); } }; @@ -89,9 +83,8 @@ AudioRecorder.prototype.stop = function () { let offset = 0; let maxRMS = 0; const chunkLevels = []; - for (let i = 0; i < this.leftBuffers.length; i++) { - const leftBufferChunk = this.leftBuffers[i]; - const rms = this.calculateRMS(leftBufferChunk); + for (let i = 0; i < this.buffers.length; i++) { + const rms = this.calculateRMS(this.buffers[i]); maxRMS = Math.max(maxRMS, rms); chunkLevels.push(rms); } @@ -119,27 +112,22 @@ AudioRecorder.prototype.stop = function () { } const usedSamples = lastChunkAboveThreshold - firstChunkAboveThreshold + 2; - const buffers = [ - new Float32Array(usedSamples * this.bufferLength), - new Float32Array(usedSamples * this.bufferLength) - ]; + const buffer = new Float32Array(usedSamples * this.bufferLength); const usedChunkLevels = []; - for (let i = 0; i < this.leftBuffers.length; i++) { - const leftBufferChunk = this.leftBuffers[i]; - const rightBufferChunk = this.rightBuffers[i]; + for (let i = 0; i < this.buffers.length; i++) { + const bufferChunk = this.buffers[i]; if (i > firstChunkAboveThreshold - 2 && i < lastChunkAboveThreshold + 1) { usedChunkLevels.push(chunkLevels[i]); - buffers[0].set(leftBufferChunk, offset); - buffers[1].set(rightBufferChunk, offset); - offset += leftBufferChunk.length; + buffer.set(bufferChunk, offset); + offset += bufferChunk.length; } } return { levels: usedChunkLevels, - channelData: buffers, + samples: buffer, sampleRate: this.audioContext.sampleRate }; }; From 11d8c61030b89e266cf48a1f1ebedeca35ef46c8 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 23 Jun 2017 10:11:07 -0400 Subject: [PATCH 12/50] Use re-record icon --- src/components/record-modal/playback-step.jsx | 30 ++++++++++++++----- .../record-modal/re-record-icon.svg | 16 ++++++++++ .../record-modal/recording-step.jsx | 11 ++++--- 3 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 src/components/record-modal/re-record-icon.svg diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx index 807d1c4a9..88893e87a 100644 --- a/src/components/record-modal/playback-step.jsx +++ b/src/components/record-modal/playback-step.jsx @@ -4,6 +4,7 @@ const Box = require('../box/box.jsx'); const Waveform = require('./waveform.jsx'); const Meter = require('./meter.jsx'); const styles = require('./record-modal.css'); +const reRecordIcon = require('./re-record-icon.svg'); const PlaybackStep = props => ( @@ -45,6 +46,7 @@ const PlaybackStep = props => ( height={30} rx={3} ry={3} + stroke="#4280D7" width={30} x={10} y={10} @@ -55,19 +57,31 @@ const PlaybackStep = props => ( height={30} rx={3} ry={3} + stroke="#4280D7" width={30} x={10} y={10} /> ) : ( - + + + + )} @@ -83,7 +97,7 @@ const PlaybackStep = props => ( className={styles.cancelButton} onClick={props.onBack} > - ⬅ Re-record + Re-record - + ); PromptComponent.propTypes = { diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css index b880daf78..26b396c03 100644 --- a/src/components/record-modal/record-modal.css +++ b/src/components/record-modal/record-modal.css @@ -1,76 +1,8 @@ @import "../../css/colors.css"; @import "../../css/units.css"; -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - background-color: rgba(94, 102, 125, .95); -} - .modal-content { width: 600px; - margin: 100px auto; - outline: none; - border: 3px solid rgb(126, 133, 151); - padding: 0; - border-radius: $space; - user-select: none; - - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #5d647a; - overflow: hidden; -} - -.header { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-start; - height: $library-header-height; - - box-sizing: border-box; - width: 100%; - background-color: $motion-primary; - - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1rem; - font-weight: normal; -} - - -$sides: 5rem; - -.header-item { - display: flex; - align-items: center; - padding: 1rem; - text-decoration: none; - color: white; - user-select: none; -} - -.header-item-filter { - display: flex; - flex-basis: $sides; - justify-content: flex-start; -} - -.header-item-title { - flex-grow: 1; - flex-shrink: 0; - justify-content: center; - user-select: none; - letter-spacing: 0.4px; - cursor: default; -} - -.header-item-close { - flex-basis: $sides; - justify-content: flex-end; } .body { diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx index 14a194ff4..657d47799 100644 --- a/src/components/record-modal/record-modal.jsx +++ b/src/components/record-modal/record-modal.jsx @@ -1,36 +1,18 @@ -const classNames = require('classnames'); const PropTypes = require('prop-types'); const React = require('react'); -const ReactModal = require('react-modal'); const Box = require('../box/box.jsx'); -const CloseButton = require('../close-button/close-button.jsx'); const RecordingStep = require('../../containers/recording-step.jsx'); const PlaybackStep = require('../../containers/playback-step.jsx'); - +const Modal = require('../modal/modal.jsx'); const styles = require('./record-modal.css'); const RecordModal = props => ( - -
-
- {/* Nothing here, just for spacing */} -
-
- Record Sound -
-
- -
-
{props.samples ? ( ( /> )} -
+ ); RecordModal.propTypes = { From 74e543b12f2c69da20a44883ab003aef4978e07d Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 27 Jun 2017 11:10:14 -0400 Subject: [PATCH 23/50] Add costume icon and bump font size --- .../asset-panel/icon--add-costume-lib.svg | 30 +++++++++++++++++++ src/components/asset-panel/selector.css | 2 +- src/containers/costume-tab.jsx | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/components/asset-panel/icon--add-costume-lib.svg diff --git a/src/components/asset-panel/icon--add-costume-lib.svg b/src/components/asset-panel/icon--add-costume-lib.svg new file mode 100644 index 000000000..cb364d3b0 --- /dev/null +++ b/src/components/asset-panel/icon--add-costume-lib.svg @@ -0,0 +1,30 @@ + + + + add-sprite + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index ef5bba68d..2a4de1a25 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -22,7 +22,7 @@ display: flex; flex-direction: column; align-items: center; - font-size: 0.6rem; + font-size: 0.75rem; cursor: pointer; transition: 0.2s; } diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 855c1d266..e2c7c8193 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -5,7 +5,7 @@ const bindAll = require('lodash.bindall'); const VM = require('scratch-vm'); const AssetPanel = require('../components/asset-panel/asset-panel.jsx'); -const addCostumeIcon = require('../components/asset-panel/icon--add-sound-lib.svg'); +const addCostumeIcon = require('../components/asset-panel/icon--add-costume-lib.svg'); const {connect} = require('react-redux'); From cf34ff15af77b8fb3be2c2b586e68c8f1bed4446 Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 27 Jun 2017 21:10:01 -0300 Subject: [PATCH 24/50] Set costume when selected in costumes tab --- src/containers/costume-tab.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index e2c7c8193..c6c323891 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -38,6 +38,7 @@ class CostumeTab extends React.Component { } handleSelectCostume (costumeIndex) { + this.props.vm.editingTarget.setCostume(costumeIndex); this.setState({selectedCostumeIndex: costumeIndex}); } From c8248799c200fe0318d9e3750cfca27d42ee3f3d Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 28 Jun 2017 07:56:57 +0000 Subject: [PATCH 25/50] chore(package): update svg-url-loader to version 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 299fcd8d8..e7dd98d6c 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "scratch-vm": "latest", "style-loader": "^0.18.0", "svg-to-image": "1.1.3", - "svg-url-loader": "2.0.2", + "svg-url-loader": "2.1.0", "wav-encoder": "1.1.0", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.1", From 52441bbb516c3ff8dbee01f990d21907a9244fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrew=20Sun=CD=88=CC=AE?= Date: Wed, 28 Jun 2017 18:59:04 -0400 Subject: [PATCH 26/50] Fix AudioContext issue in Safari --- src/lib/audio/shared-audio-context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/audio/shared-audio-context.js b/src/lib/audio/shared-audio-context.js index 89832d458..f6c4f716a 100644 --- a/src/lib/audio/shared-audio-context.js +++ b/src/lib/audio/shared-audio-context.js @@ -1,5 +1,5 @@ // Wrap browser AudioContext because we shouldn't create more than one -const AUDIO_CONTEXT = new AudioContext(); +const AUDIO_CONTEXT = new (AudioContext || webkitAudioContext)(); module.exports = function () { return AUDIO_CONTEXT; From 5162c6c175d467a3fec7abd40bb512d66cb6aa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrew=20Sun=CD=88=CC=AE?= Date: Thu, 29 Jun 2017 10:08:52 -0400 Subject: [PATCH 27/50] Satisfy linter --- src/lib/audio/shared-audio-context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/audio/shared-audio-context.js b/src/lib/audio/shared-audio-context.js index f6c4f716a..4f09015f2 100644 --- a/src/lib/audio/shared-audio-context.js +++ b/src/lib/audio/shared-audio-context.js @@ -1,5 +1,5 @@ // Wrap browser AudioContext because we shouldn't create more than one -const AUDIO_CONTEXT = new (AudioContext || webkitAudioContext)(); +const AUDIO_CONTEXT = new (AudioContext || window.webkitAudioContext)(); module.exports = function () { return AUDIO_CONTEXT; From 4d3be0c75fef3c10bef0f7b4596990968009c630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrew=20Sun=CD=88=CC=AE?= Date: Thu, 29 Jun 2017 10:33:08 -0400 Subject: [PATCH 28/50] Actually make it work --- src/lib/audio/shared-audio-context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/audio/shared-audio-context.js b/src/lib/audio/shared-audio-context.js index 4f09015f2..c76e79667 100644 --- a/src/lib/audio/shared-audio-context.js +++ b/src/lib/audio/shared-audio-context.js @@ -1,5 +1,5 @@ // Wrap browser AudioContext because we shouldn't create more than one -const AUDIO_CONTEXT = new (AudioContext || window.webkitAudioContext)(); +const AUDIO_CONTEXT = new (window.AudioContext || window.webkitAudioContext)(); module.exports = function () { return AUDIO_CONTEXT; From d909b03532e50e237418de32f9edddcc7efde522 Mon Sep 17 00:00:00 2001 From: Nicolas Terray Date: Thu, 29 Jun 2017 20:23:15 +0200 Subject: [PATCH 29/50] Add sprite/backdrop should have a discernible text In order to comply to WCAG 2.0[1], buttons must have discernible text that clearly describe the purpose, so that it can be used by screen readers. [1] https://www.w3.org/TR/2016/NOTE-WCAG20-TECHS-20161007/H65 --- src/components/target-pane/target-pane.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index c779f2747..c8f16a9c4 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -78,6 +78,7 @@ const TargetPane = ({ @@ -65,9 +73,13 @@ PlaybackStep.propTypes = { levels: PropTypes.arrayOf(PropTypes.number).isRequired, onBack: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, + onSetTrimEnd: PropTypes.func.isRequired, + onSetTrimStart: PropTypes.func.isRequired, onStopPlaying: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, - playing: PropTypes.bool.isRequired + playing: PropTypes.bool.isRequired, + trimEnd: PropTypes.number.isRequired, + trimStart: PropTypes.number.isRequired }; module.exports = PlaybackStep; diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css index 1b866b52a..52bdded5b 100644 --- a/src/components/record-modal/record-modal.css +++ b/src/components/record-modal/record-modal.css @@ -33,6 +33,7 @@ align-items: center; width: 100%; height: 180px; + position: relative; } .help-text { diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx index bca743648..21ca6b8e9 100644 --- a/src/components/record-modal/record-modal.jsx +++ b/src/components/record-modal/record-modal.jsx @@ -20,8 +20,12 @@ const RecordModal = props => ( levels={props.levels} playing={props.playing} samples={props.samples} + trimEnd={props.trimEnd} + trimStart={props.trimStart} onBack={props.onBack} onPlay={props.onPlay} + onSetTrimEnd={props.onSetTrimEnd} + onSetTrimStart={props.onSetTrimStart} onStopPlaying={props.onStopPlaying} onSubmit={props.onSubmit} /> @@ -43,12 +47,16 @@ RecordModal.propTypes = { onCancel: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, onRecord: PropTypes.func.isRequired, + onSetTrimEnd: PropTypes.func.isRequired, + onSetTrimStart: PropTypes.func.isRequired, onStopPlaying: PropTypes.func.isRequired, onStopRecording: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, playing: PropTypes.bool, recording: PropTypes.bool, - samples: PropTypes.instanceOf(Float32Array) + samples: PropTypes.instanceOf(Float32Array), + trimEnd: PropTypes.number.isRequired, + trimStart: PropTypes.number.isRequired }; module.exports = RecordModal; diff --git a/src/components/waveform/waveform.css b/src/components/waveform/waveform.css index dc4c7fcc7..7951c618f 100644 --- a/src/components/waveform/waveform.css +++ b/src/components/waveform/waveform.css @@ -1,5 +1,8 @@ @import "../../css/colors.css"; +.container { + width: 100%; +} .waveform-path { fill: $sound-primary; stroke: $sound-tertiary; diff --git a/src/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx index c6edd5c83..502bbb280 100644 --- a/src/components/waveform/waveform.jsx +++ b/src/components/waveform/waveform.jsx @@ -27,8 +27,8 @@ const Waveform = props => { return ( + ); + } +} + +AudioTrimmer.propTypes = { + onSetTrimEnd: PropTypes.func, + onSetTrimStart: PropTypes.func, + trimEnd: PropTypes.number, + trimStart: PropTypes.number +}; + +module.exports = AudioTrimmer; diff --git a/src/containers/playback-step.jsx b/src/containers/playback-step.jsx index 5276e4d9d..96f63bcd5 100644 --- a/src/containers/playback-step.jsx +++ b/src/containers/playback-step.jsx @@ -19,7 +19,7 @@ class PlaybackStep extends React.Component { this.audioBufferPlayer.stop(); } handlePlay () { - this.audioBufferPlayer.play(this.props.onStopPlaying); + this.audioBufferPlayer.play(this.props.trimStart, this.props.trimEnd, this.props.onStopPlaying); this.props.onPlay(); } handleStopPlaying () { diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index 38c3336d2..664d65169 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -21,7 +21,9 @@ class RecordModal extends React.Component { 'handleStopPlaying', 'handleBack', 'handleSubmit', - 'handleCancel' + 'handleCancel', + 'handleSetTrimStart', + 'handleSetTrimEnd' ]); this.state = { @@ -30,14 +32,16 @@ class RecordModal extends React.Component { levels: null, playing: false, recording: false, - sampleRate: null + sampleRate: null, + trimStart: 0, + trimEnd: 100 }; } handleRecord () { this.setState({recording: true}); } - handleStopRecording (samples, sampleRate, levels) { - this.setState({samples, sampleRate, levels, recording: false}); + handleStopRecording (samples, sampleRate, levels, trimStart, trimEnd) { + this.setState({samples, sampleRate, levels, trimStart, trimEnd, recording: false}); } handlePlay () { this.setState({playing: true}); @@ -48,11 +52,21 @@ class RecordModal extends React.Component { handleBack () { this.setState({playing: false, samples: null}); } + handleSetTrimEnd (trimEnd) { + this.setState({trimEnd}); + } + handleSetTrimStart (trimStart) { + this.setState({trimStart}); + } handleSubmit () { this.setState({encoding: true}, () => { + const sampleCount = this.state.samples.length; + const startIndex = Math.floor(this.state.trimStart * sampleCount / 100); + const endIndex = Math.floor(this.state.trimEnd * sampleCount / 100); + const clippedSamples = this.state.samples.slice(startIndex, endIndex); WavEncoder.encode({ sampleRate: this.state.sampleRate, - channelData: [this.state.samples] + channelData: [clippedSamples] }).then(wavBuffer => { const md5 = String(Math.floor(100000 * Math.random())); const vmSound = { @@ -86,10 +100,14 @@ class RecordModal extends React.Component { playing={this.state.playing} recording={this.state.recording} samples={this.state.samples} + trimEnd={this.state.trimEnd} + trimStart={this.state.trimStart} onBack={this.handleBack} onCancel={this.handleCancel} onPlay={this.handlePlay} onRecord={this.handleRecord} + onSetTrimEnd={this.handleSetTrimEnd} + onSetTrimStart={this.handleSetTrimStart} onStopPlaying={this.handleStopPlaying} onStopRecording={this.handleStopRecording} onSubmit={this.handleSubmit} diff --git a/src/containers/recording-step.jsx b/src/containers/recording-step.jsx index aa135b871..ed9601635 100644 --- a/src/containers/recording-step.jsx +++ b/src/containers/recording-step.jsx @@ -44,8 +44,8 @@ class RecordingStep extends React.Component { this.props.onRecord(); } handleStopRecording () { - const {samples, sampleRate, levels} = this.audioRecorder.stop(); - this.props.onStopRecording(samples, sampleRate, levels); + const {samples, sampleRate, levels, trimStart, trimEnd} = this.audioRecorder.stop(); + this.props.onStopRecording(samples, sampleRate, levels, trimStart, trimEnd); } render () { const { diff --git a/src/lib/audio/audio-buffer-player.js b/src/lib/audio/audio-buffer-player.js index 0d1beb527..06875df52 100644 --- a/src/lib/audio/audio-buffer-player.js +++ b/src/lib/audio/audio-buffer-player.js @@ -8,13 +8,15 @@ class AudioBufferPlayer { this.source = null; } - play (onEnded) { - // Buffer source nodes are one time use only. Must do this every play. + play (trimStart, trimEnd, onEnded) { + const trimStartTime = this.buffer.duration * trimStart / 100; + const trimmedDuration = this.buffer.duration * trimEnd / 100 - trimStartTime; + this.source = this.audioContext.createBufferSource(); this.source.onended = onEnded; this.source.buffer = this.buffer; this.source.connect(this.audioContext.destination); - this.source.start(); + this.source.start(0, trimStartTime, trimmedDuration); } stop () { diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js index 9dc20d005..8c4918839 100644 --- a/src/lib/audio/audio-recorder.js +++ b/src/lib/audio/audio-recorder.js @@ -97,30 +97,29 @@ class AudioRecorder { let lastChunkAboveThreshold = null; for (let i = 0; i < chunkLevels.length; i++) { if (chunkLevels[i] > threshold) { - if (firstChunkAboveThreshold === null) firstChunkAboveThreshold = i; - lastChunkAboveThreshold = i; + if (firstChunkAboveThreshold === null) firstChunkAboveThreshold = i + 1; + lastChunkAboveThreshold = i + 1; } } - const usedSamples = lastChunkAboveThreshold - firstChunkAboveThreshold + 2; - const buffer = new Float32Array(usedSamples * this.bufferLength); + const trimStart = 100 * Math.max(1, firstChunkAboveThreshold - 2) / this.buffers.length; + const trimEnd = 100 * Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length; - const usedChunkLevels = []; + const buffer = new Float32Array(this.buffers.length * this.bufferLength); let offset = 0; for (let i = 0; i < this.buffers.length; i++) { const bufferChunk = this.buffers[i]; - if (i > firstChunkAboveThreshold - 2 && i < lastChunkAboveThreshold + 1) { - usedChunkLevels.push(chunkLevels[i]); - buffer.set(bufferChunk, offset); - offset += bufferChunk.length; - } + buffer.set(bufferChunk, offset); + offset += bufferChunk.length; } return { - levels: usedChunkLevels, + levels: chunkLevels, samples: buffer, - sampleRate: this.audioContext.sampleRate + sampleRate: this.audioContext.sampleRate, + trimStart: trimStart, + trimEnd: trimEnd }; } From 1a2f06da8a48f6d5ccf1236453ab06486013b0cb Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 27 Jun 2017 12:27:17 -0400 Subject: [PATCH 38/50] Add playhead --- .../audio-trimmer/audio-trimmer.css | 5 +++++ .../audio-trimmer/audio-trimmer.jsx | 10 ++++++++++ src/components/record-modal/playback-step.jsx | 2 ++ src/components/record-modal/record-modal.jsx | 4 ++++ src/containers/audio-trimmer.jsx | 2 ++ src/containers/playback-step.jsx | 8 +++++++- src/containers/record-modal.jsx | 9 ++++++++- src/lib/audio/audio-buffer-player.js | 20 ++++++++++++++++++- src/lib/audio/audio-recorder.js | 2 +- 9 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index 26dabc816..7769331c5 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -1,3 +1,4 @@ +@import "../../css/colors.css"; .trim-container { position: absolute; @@ -52,6 +53,10 @@ top: 0; } +.playhead { + border: 1px solid $motion-primary; +} + .start-trim-line { right: 0; } diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index 5f2e7e8f2..9e5d85286 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -25,6 +25,15 @@ const AudioTrimmer = props => ( + {props.playhead ? ( + + ) : null} + ( AudioTrimmer.propTypes = { onTrimEndMouseDown: PropTypes.func.isRequired, onTrimStartMouseDown: PropTypes.func.isRequired, + playhead: PropTypes.number, trimEnd: PropTypes.number.isRequired, trimStart: PropTypes.number.isRequired }; diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx index 8167cffbb..7d5bfc888 100644 --- a/src/components/record-modal/playback-step.jsx +++ b/src/components/record-modal/playback-step.jsx @@ -30,6 +30,7 @@ const PlaybackStep = props => ( width={480} /> ( { const sampleCount = this.state.samples.length; @@ -97,6 +102,7 @@ class RecordModal extends React.Component { Date: Tue, 27 Jun 2017 12:32:31 -0400 Subject: [PATCH 39/50] Clamp trim ends --- src/containers/audio-trimmer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index 02536a9ce..8a07350d3 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -17,12 +17,12 @@ class AudioTrimmer extends React.Component { } handleTrimStartMouseMove (e) { const dx = 100 * (e.clientX - this.initialX) / 480; - const newTrim = Math.min(this.props.trimEnd, this.initialTrim + dx); + const newTrim = Math.max(1, Math.min(this.props.trimEnd, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); } handleTrimEndMouseMove (e) { const dx = 100 * (e.clientX - this.initialX) / 480; - const newTrim = Math.max(this.props.trimStart, this.initialTrim + dx); + const newTrim = Math.min(99, Math.max(this.props.trimStart, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); } handleTrimStartMouseUp () { From cd75a9881acd371117f29d116426f7247bd5cb60 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 27 Jun 2017 12:38:17 -0400 Subject: [PATCH 40/50] Add scale hover --- src/components/audio-trimmer/audio-trimmer.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index 7769331c5..f3a13f48f 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -84,12 +84,23 @@ $trim-handle-height: 14px; user-select: none; user-drag: none; -webkit-user-drag: none; /* Why doesn't autoprefixer work for this? */ + + transition: 0.2s; +} + +.trim-background:hover img { + transform: scale(2); +} + +.trim-background:hover .top-trim-handle img { + transform: rotate(180deg) scale(2); } .top-trim-handle img { transform: rotate(180deg); } + .top-trim-handle { top: -$trim-handle-height; } From afab86f2fbf6da40002892c19e5afe2618ff7a3f Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Tue, 27 Jun 2017 12:39:45 -0400 Subject: [PATCH 41/50] Make trim => save --- src/components/record-modal/icon--trim.svg | 21 ------------------- src/components/record-modal/playback-step.jsx | 3 +-- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 src/components/record-modal/icon--trim.svg diff --git a/src/components/record-modal/icon--trim.svg b/src/components/record-modal/icon--trim.svg deleted file mode 100644 index 066e99b25..000000000 --- a/src/components/record-modal/icon--trim.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - sissors - Created with Sketch. - - - - - - - - - - - - - - - - diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx index 7d5bfc888..0eddd9142 100644 --- a/src/components/record-modal/playback-step.jsx +++ b/src/components/record-modal/playback-step.jsx @@ -9,7 +9,6 @@ import styles from './record-modal.css'; import backIcon from './icon--back.svg'; import stopIcon from './icon--stop-playback.svg'; import playIcon from './icon--play.svg'; -import trimIcon from './icon--trim.svg'; const PlaybackStep = props => ( @@ -63,7 +62,7 @@ const PlaybackStep = props => ( disabled={props.encoding} onClick={props.onSubmit} > - {props.encoding ? 'Loading...' : 'Trim'} + {props.encoding ? 'Loading...' : 'Save'} From 3c3780c895212bd05371f4a8e6325f6eb7f2af5c Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 28 Jun 2017 08:32:06 -0400 Subject: [PATCH 42/50] Use refs instead of fixed size --- src/components/audio-trimmer/audio-trimmer.jsx | 8 ++++++-- src/containers/audio-trimmer.jsx | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index 9e5d85286..a03e7bcd1 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -6,7 +6,10 @@ const styles = require('./audio-trimmer.css'); const handleIcon = require('./icon--handle.svg'); const AudioTrimmer = props => ( - +
( - +
); AudioTrimmer.propTypes = { + containerRef: PropTypes.func, onTrimEndMouseDown: PropTypes.func.isRequired, onTrimStartMouseDown: PropTypes.func.isRequired, playhead: PropTypes.number, diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index 8a07350d3..460769729 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -16,12 +16,14 @@ class AudioTrimmer extends React.Component { ]); } handleTrimStartMouseMove (e) { - const dx = 100 * (e.clientX - this.initialX) / 480; + const containerSize = this.containerElement.getBoundingClientRect().width; + const dx = 100 * (e.clientX - this.initialX) / containerSize; const newTrim = Math.max(1, Math.min(this.props.trimEnd, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); } handleTrimEndMouseMove (e) { - const dx = 100 * (e.clientX - this.initialX) / 480; + const containerSize = this.containerElement.getBoundingClientRect().width; + const dx = 100 * (e.clientX - this.initialX) / containerSize; const newTrim = Math.min(99, Math.max(this.props.trimStart, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); } @@ -48,6 +50,7 @@ class AudioTrimmer extends React.Component { render () { return ( (this.containerElement = el)} playhead={this.props.playhead} trimEnd={this.props.trimEnd} trimStart={this.props.trimStart} From b3d0a1c653c72b85e28174e95d53ac76e13319bb Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 28 Jun 2017 08:35:01 -0400 Subject: [PATCH 43/50] Move to using 0-1 instead of 0-100 range for trim --- src/components/audio-trimmer/audio-trimmer.jsx | 8 ++++---- src/containers/audio-trimmer.jsx | 8 ++++---- src/containers/record-modal.jsx | 4 ++-- src/lib/audio/audio-buffer-player.js | 6 +++--- src/lib/audio/audio-recorder.js | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index a03e7bcd1..f785704fe 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -13,7 +13,7 @@ const AudioTrimmer = props => ( @@ -32,7 +32,7 @@ const AudioTrimmer = props => ( ) : null} @@ -40,8 +40,8 @@ const AudioTrimmer = props => ( diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index 460769729..cfc077ded 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -17,14 +17,14 @@ class AudioTrimmer extends React.Component { } handleTrimStartMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; - const dx = 100 * (e.clientX - this.initialX) / containerSize; - const newTrim = Math.max(1, Math.min(this.props.trimEnd, this.initialTrim + dx)); + const dx = (e.clientX - this.initialX) / containerSize; + const newTrim = Math.max(0.01, Math.min(this.props.trimEnd, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); } handleTrimEndMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; - const dx = 100 * (e.clientX - this.initialX) / containerSize; - const newTrim = Math.min(99, Math.max(this.props.trimStart, this.initialTrim + dx)); + const dx = (e.clientX - this.initialX) / containerSize; + const newTrim = Math.min(0.99, Math.max(this.props.trimStart, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); } handleTrimStartMouseUp () { diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index ab05d6490..6a4f2f113 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -66,8 +66,8 @@ class RecordModal extends React.Component { handleSubmit () { this.setState({encoding: true}, () => { const sampleCount = this.state.samples.length; - const startIndex = Math.floor(this.state.trimStart * sampleCount / 100); - const endIndex = Math.floor(this.state.trimEnd * sampleCount / 100); + const startIndex = Math.floor(this.state.trimStart * sampleCount); + const endIndex = Math.floor(this.state.trimEnd * sampleCount); const clippedSamples = this.state.samples.slice(startIndex, endIndex); WavEncoder.encode({ sampleRate: this.state.sampleRate, diff --git a/src/lib/audio/audio-buffer-player.js b/src/lib/audio/audio-buffer-player.js index 73bd5bcb8..502e265e1 100644 --- a/src/lib/audio/audio-buffer-player.js +++ b/src/lib/audio/audio-buffer-player.js @@ -16,8 +16,8 @@ class AudioBufferPlayer { this.trimEnd = trimEnd; this.startTime = Date.now(); - const trimStartTime = this.buffer.duration * trimStart / 100; - const trimmedDuration = this.buffer.duration * trimEnd / 100 - trimStartTime; + const trimStartTime = this.buffer.duration * trimStart; + const trimmedDuration = this.buffer.duration * trimEnd - trimStartTime; this.source = this.audioContext.createBufferSource(); this.source.onended = onEnded; @@ -30,7 +30,7 @@ class AudioBufferPlayer { update () { const timeSinceStart = (Date.now() - this.startTime) / 1000; - const percentage = 100 * timeSinceStart / this.buffer.duration; + const percentage = timeSinceStart / this.buffer.duration; if (percentage + this.trimStart < this.trimEnd && this.source.onended) { requestAnimationFrame(this.update.bind(this)); this.updateCallback(percentage + this.trimStart); diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js index b43439999..ace2b9807 100644 --- a/src/lib/audio/audio-recorder.js +++ b/src/lib/audio/audio-recorder.js @@ -102,8 +102,8 @@ class AudioRecorder { } } - const trimStart = 100 * Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length; - const trimEnd = 100 * Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length; + const trimStart = Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length; + const trimEnd = Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length; const buffer = new Float32Array(this.buffers.length * this.bufferLength); From 5a82459ee8d876006f9af3598247fa25bc49f682 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 28 Jun 2017 08:43:19 -0400 Subject: [PATCH 44/50] Cleanup audio buffer player --- src/lib/audio/audio-buffer-player.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/audio/audio-buffer-player.js b/src/lib/audio/audio-buffer-player.js index 502e265e1..ba333f1f9 100644 --- a/src/lib/audio/audio-buffer-player.js +++ b/src/lib/audio/audio-buffer-player.js @@ -6,11 +6,14 @@ class AudioBufferPlayer { this.buffer = this.audioContext.createBuffer(1, samples.length, this.audioContext.sampleRate); this.buffer.getChannelData(0).set(samples); this.source = null; - this.stopped = true; + + this.startTime = null; + this.updateCallback = null; + this.trimStart = null; + this.trimEnd = null; } play (trimStart, trimEnd, onUpdate, onEnded) { - this.stopped = false; this.updateCallback = onUpdate; this.trimStart = trimStart; this.trimEnd = trimEnd; @@ -34,6 +37,8 @@ class AudioBufferPlayer { if (percentage + this.trimStart < this.trimEnd && this.source.onended) { requestAnimationFrame(this.update.bind(this)); this.updateCallback(percentage + this.trimStart); + } else { + this.updateCallback = null; } } From 350ae84ea883860ee3b1fabf07d79c8b606982c3 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 28 Jun 2017 09:04:37 -0400 Subject: [PATCH 45/50] Refactor css a bit --- .../audio-trimmer/audio-trimmer.css | 71 +++++++++---------- .../audio-trimmer/audio-trimmer.jsx | 10 +-- src/components/record-modal/record-modal.css | 6 +- src/containers/record-modal.jsx | 2 +- 4 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index f3a13f48f..e1294b8c9 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -1,6 +1,11 @@ @import "../../css/colors.css"; -.trim-container { +$border-radius: 4px; +$trim-handle-width: 12px; +$trim-handle-height: 14px; +$hover-scale: 2; + +.absolute { position: absolute; width: 100%; top: 0; @@ -9,47 +14,38 @@ } .trim-background { - position: absolute; - top: 0; - left: 0; - height: 100%; cursor: pointer; } .trim-background-mask { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #FF661A; - border: 1px solid #E64D00; + background: $red-primary; + border: 1px solid $red-tertiary; opacity: 0.5; background: repeating-linear-gradient( 45deg, - #FF661A, - #FF661A 10px, - #E64D00 10px, - #E64D00 20px + $red-primary, + $red-primary 10px, + $red-tertiary 10px, + $red-tertiary 20px ); } .start-trim-background .trim-background-mask { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; } .end-trim-background .trim-background-mask { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; } .trim-line { + position: absolute; width: 0px; height: 100%; - border: 1px solid #E64D00; - position: absolute; + border: 1px solid $red-tertiary; top: 0; } @@ -65,9 +61,6 @@ left: 0; } -$trim-handle-width: 12px; -$trim-handle-height: 14px; - .trim-handle { width: $trim-handle-width; height: $trim-handle-height; @@ -83,24 +76,11 @@ $trim-handle-height: 14px; user-select: none; user-drag: none; - -webkit-user-drag: none; /* Why doesn't autoprefixer work for this? */ + -webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */ transition: 0.2s; } -.trim-background:hover img { - transform: scale(2); -} - -.trim-background:hover .top-trim-handle img { - transform: rotate(180deg) scale(2); -} - -.top-trim-handle img { - transform: rotate(180deg); -} - - .top-trim-handle { top: -$trim-handle-height; } @@ -108,3 +88,16 @@ $trim-handle-height: 14px; .bottom-trim-handle { bottom: -$trim-handle-height; } + +.top-trim-handle img { + transform: rotate(180deg); +} + +/* Increase handle size when anywhere on draggable area is hovered */ +.trim-background:hover img { + transform: scale($hover-scale); +} + +.trim-background:hover .top-trim-handle img { + transform: rotate(180deg) scale($hover-scale); +} diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index f785704fe..7f00beef2 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -7,17 +7,17 @@ const handleIcon = require('./icon--handle.svg'); const AudioTrimmer = props => (
- + @@ -38,14 +38,14 @@ const AudioTrimmer = props => ( ) : null} - + diff --git a/src/components/record-modal/record-modal.css b/src/components/record-modal/record-modal.css index 52bdded5b..9a750fd4b 100644 --- a/src/components/record-modal/record-modal.css +++ b/src/components/record-modal/record-modal.css @@ -109,12 +109,12 @@ } .record-button-circle { - fill: #FF661A; - stroke: #E64D00; + fill: $red-primary; + stroke: $red-tertiary; } .record-button-circle-outline { - fill: #FF661A; + fill: $red-primary; opacity: 0.2; transition: 0.1s; } diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index 6a4f2f113..a59adaafa 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -36,7 +36,7 @@ class RecordModal extends React.Component { recording: false, sampleRate: null, trimStart: 0, - trimEnd: 100 + trimEnd: 1 }; } handleRecord () { From 51b0cd81c8fe2fa415e083ca7a6027cfae0d1cf4 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 28 Jun 2017 09:08:42 -0400 Subject: [PATCH 46/50] More CSS refactor --- src/components/audio-trimmer/audio-trimmer.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index e1294b8c9..454887ab3 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -3,13 +3,14 @@ $border-radius: 4px; $trim-handle-width: 12px; $trim-handle-height: 14px; +$stripe-size: 10px; $hover-scale: 2; .absolute { position: absolute; - width: 100%; top: 0; left: 0; + width: 100%; height: 100%; } @@ -18,16 +19,15 @@ $hover-scale: 2; } .trim-background-mask { - background: $red-primary; border: 1px solid $red-tertiary; opacity: 0.5; background: repeating-linear-gradient( 45deg, $red-primary, - $red-primary 10px, - $red-tertiary 10px, - $red-tertiary 20px + $red-primary $stripe-size, + $red-tertiary $stripe-size, + $red-tertiary calc(2 * $stripe-size) ); } @@ -43,10 +43,10 @@ $hover-scale: 2; .trim-line { position: absolute; + top: 0; width: 0px; height: 100%; border: 1px solid $red-tertiary; - top: 0; } .playhead { @@ -62,11 +62,10 @@ $hover-scale: 2; } .trim-handle { - width: $trim-handle-width; - height: $trim-handle-height; - position: absolute; left: calc(-$trim-handle-width / 2); + width: $trim-handle-width; + height: $trim-handle-height; } .trim-handle img { @@ -74,6 +73,7 @@ $hover-scale: 2; width: $trim-handle-width; height: $trim-handle-height; + /* Make sure image dragging isn't triggered */ user-select: none; user-drag: none; -webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */ From ca50f489c000bc407c77f1a7b42e1d17ab0de9f5 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 29 Jun 2017 13:25:43 -0400 Subject: [PATCH 47/50] Clamp to 0 and 1 --- src/containers/audio-trimmer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index cfc077ded..497d2d812 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -18,13 +18,13 @@ class AudioTrimmer extends React.Component { handleTrimStartMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; const dx = (e.clientX - this.initialX) / containerSize; - const newTrim = Math.max(0.01, Math.min(this.props.trimEnd, this.initialTrim + dx)); + const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); } handleTrimEndMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; const dx = (e.clientX - this.initialX) / containerSize; - const newTrim = Math.min(0.99, Math.max(this.props.trimStart, this.initialTrim + dx)); + const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); } handleTrimStartMouseUp () { From a29603ea40e39bb5bfadbafc55ea60c3caf73f94 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Mon, 10 Jul 2017 08:58:30 -0400 Subject: [PATCH 48/50] Remove trailing spaces --- src/components/box/box.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/box/box.jsx b/src/components/box/box.jsx index c7dbb8b54..d11497838 100644 --- a/src/components/box/box.jsx +++ b/src/components/box/box.jsx @@ -96,9 +96,9 @@ Box.propTypes = { children: PropTypes.node, /** Specifies the class name that will be set on this box */ className: PropTypes.string, - /** - * A callback function whose first parameter is the underlying dom elements. - * This call back will be executed immediately after the component is mounted or unmounted + /** + * A callback function whose first parameter is the underlying dom elements. + * This call back will be executed immediately after the component is mounted or unmounted */ componentRef: PropTypes.func, /** https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction */ From 528fb9cf23542b64e2ade9c2aad4a29fc358e9d0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 10 Jul 2017 08:58:57 -0400 Subject: [PATCH 49/50] Convert old requires to imports --- src/components/audio-trimmer/audio-trimmer.jsx | 12 ++++++------ src/components/buffered-input/buffered-input.jsx | 6 +++--- src/containers/audio-trimmer.jsx | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index 7f00beef2..840b240f5 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -1,9 +1,9 @@ -const PropTypes = require('prop-types'); -const React = require('react'); -const classNames = require('classnames'); -const Box = require('../box/box.jsx'); -const styles = require('./audio-trimmer.css'); -const handleIcon = require('./icon--handle.svg'); +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import Box from '../box/box.jsx'; +import styles from './audio-trimmer.css'; +import handleIcon from './icon--handle.svg'; const AudioTrimmer = props => (
Date: Mon, 10 Jul 2017 10:47:07 -0400 Subject: [PATCH 50/50] Change 'Scripts' tab to 'Blocks'. Resolves GH-497 --- src/components/gui/gui.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index a2ecf7395..3eafbb71c 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -62,7 +62,7 @@ const GUIComponent = props => { onSelect={onTabSelect} > - Scripts + Blocks Costumes Sounds @@ -70,7 +70,7 @@ const GUIComponent = props => {