From 2c3a2a235bad27a99013886f07c2b036545fdefd Mon Sep 17 00:00:00 2001 From: ejarzo Date: Sun, 16 Sep 2018 19:15:04 -0400 Subject: [PATCH] add download to wav functionality --- .eslintrc.json | 14 +- package.json | 3 +- src/components/Button/index.jsx | 22 +- src/components/Downloads/Component.jsx | 43 +++ src/components/Downloads/Container.jsx | 31 ++ src/components/Downloads/index.jsx | 8 + src/components/Downloads/styles.module.css | 14 + src/components/Shape/Container.jsx | 396 ++++++++++++--------- src/components/Toolbar/Component.jsx | 75 ++-- src/views/Project/Container.jsx | 228 +++++++----- yarn.lock | 16 +- 11 files changed, 526 insertions(+), 324 deletions(-) create mode 100644 src/components/Downloads/Component.jsx create mode 100644 src/components/Downloads/Container.jsx create mode 100644 src/components/Downloads/index.jsx create mode 100644 src/components/Downloads/styles.module.css diff --git a/.eslintrc.json b/.eslintrc.json index aa5d757..7b759c4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,9 @@ { - "env": { - "browser": true, - "es6": true, - "jasmine": true - }, - "extends": ["react-app"], - "plugins": ["prettier", "react", "jasmine"] + "env": { + "browser": true, + "es6": true, + "jasmine": true + }, + "extends": ["react-app"], + "plugins": ["prettier", "react", "jasmine"] } diff --git a/package.json b/package.json index fa8ec17..cd88c5c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "classnames": "^2.2.5", "color": "^2.0.1", "konva": "^2.1.3", - "lodash": "^3.10.1", + "lodash": "^4.17.11", "prop-types": "^15.6.0", "radium": "^0.24.0", "rc-select": "^7.7.6", @@ -33,6 +33,7 @@ "react-portal": "^3.2.0", "react-scripts-cssmodules": "^1.1.10", "react-select": "^1.2.1", + "record-audio-js": "^0.0.4", "teoria": "^2.5.0", "tone": "^0.11.11" }, diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx index 35f75a2..b6c6df0 100644 --- a/src/components/Button/index.jsx +++ b/src/components/Button/index.jsx @@ -9,17 +9,23 @@ const propTypes = { children: PropTypes.node, darkHover: PropTypes.bool, hasBorder: PropTypes.bool, + href: PropTypes.bool, + download: PropTypes.bool, onClick: PropTypes.func.isRequired, }; -function Button (props) { +function Button(props) { const style = { backgroundColor: props.color, ':hover': { - backgroundColor: props.darkHover ? ColorUtils.getDarker(props.color) : ColorUtils.getLighter(props.color), + backgroundColor: props.darkHover + ? ColorUtils.getDarker(props.color) + : ColorUtils.getLighter(props.color), }, ':active': { - backgroundColor: props.darkHover ? ColorUtils.getDarker(props.color, 0.2) : ColorUtils.getDarker(props.color), + backgroundColor: props.darkHover + ? ColorUtils.getDarker(props.color, 0.2) + : ColorUtils.getDarker(props.color), }, padding: 6, borderColor: ColorUtils.getDarker(props.color), @@ -29,13 +35,19 @@ function Button (props) { borderRadius: props.hasBorder ? 3 : 0, }; - return ( - ); } diff --git a/src/components/Downloads/Component.jsx b/src/components/Downloads/Component.jsx new file mode 100644 index 0000000..58a3e0a --- /dev/null +++ b/src/components/Downloads/Component.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Portal from 'react-portal'; +import styles from './styles.module.css'; + +import Button from 'components/Button'; + +const propTypes = { + downloadUrls: PropTypes.array.isRequired, +}; + +/* ================================ Toolbar ================================ */ +function Downloads(props) { + const { downloadUrls } = props; + return ( +
+

Downloads

+ +
+ ); +} + +Downloads.propTypes = propTypes; + +export default Downloads; diff --git a/src/components/Downloads/Container.jsx b/src/components/Downloads/Container.jsx new file mode 100644 index 0000000..12e1990 --- /dev/null +++ b/src/components/Downloads/Container.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ControlsComponent from './Component'; + +class ControlsContainer extends React.Component { + constructor (props) { + super(props); + this.state = { + isColorPickerOpen: false, + }; + + this.handleColorSelectClick = this.handleColorSelectClick.bind(this); + } + + handleColorSelectClick () { + this.setState({ + isColorPickerOpen: !this.state.isColorPickerOpen, + }); + } + + render () { + return ( + + ); + } +} + +export default ControlsContainer; diff --git a/src/components/Downloads/index.jsx b/src/components/Downloads/index.jsx new file mode 100644 index 0000000..489d3cf --- /dev/null +++ b/src/components/Downloads/index.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ControlsContainer from './Container'; + +function Controls (props) { + return ; +} + +export default Controls; diff --git a/src/components/Downloads/styles.module.css b/src/components/Downloads/styles.module.css new file mode 100644 index 0000000..51fb7db --- /dev/null +++ b/src/components/Downloads/styles.module.css @@ -0,0 +1,14 @@ +.downloads { + position: absolute; + z-index: 200; + left: 0; + padding: 20px; + top: 100px; + background: white; +} + +.downloads a { + display: block; + margin: 10px 0; + border: 1px solid #ddd; +} diff --git a/src/components/Shape/Container.jsx b/src/components/Shape/Container.jsx index 18ef065..f63ac17 100644 --- a/src/components/Shape/Container.jsx +++ b/src/components/Shape/Container.jsx @@ -33,17 +33,17 @@ const propTypes = { }; class ShapeContainer extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { points: props.points, colorIndex: props.colorIndex, - + volume: -5, isMuted: false, quantizeFactor: 1, - + averagePoint: { x: 0, y: 0 }, firstNoteIndex: 1, noteIndexModifier: 0, @@ -52,104 +52,122 @@ class ShapeContainer extends Component { isDragging: false, editorX: 0, editorY: 0, - + animCircleX: 0, - animCircleY: 0 + animCircleY: 0, }; this.quantizeLength = 500; // shape attribute changes this.handleVolumeChange = this.handleVolumeChange.bind(this); - this.handleColorChange = this.handleColorChange.bind(this); - this.handleMuteChange = this.handleMuteChange.bind(this); - + this.handleColorChange = this.handleColorChange.bind(this); + this.handleMuteChange = this.handleMuteChange.bind(this); + // shape events - this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseDown = this.handleMouseDown.bind(this); this.handleClick = this.handleClick.bind(this); this.handleMouseOver = this.handleMouseOver.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); - this.handleDrag = this.handleDrag.bind(this); - this.handleDragStart = this.handleDragStart.bind(this); - this.handleDragEnd = this.handleDragEnd.bind(this); - this.dragBoundFunc = this.dragBoundFunc.bind(this); - + this.handleDrag = this.handleDrag.bind(this); + this.handleDragStart = this.handleDragStart.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + this.dragBoundFunc = this.dragBoundFunc.bind(this); + // vertices this.handleVertexDragMove = this.handleVertexDragMove.bind(this); - - // perimeter - this.getPointsForFixedPerimeterLength = this.getPointsForFixedPerimeterLength.bind(this); - + + // perimeter + this.getPointsForFixedPerimeterLength = this.getPointsForFixedPerimeterLength.bind( + this + ); + // shape editor handlers - this.handleDelete = this.handleDelete.bind(this); - this.handleQuantizeClick = this.handleQuantizeClick.bind(this); - this.handleQuantizeFactorChange = this.handleQuantizeFactorChange.bind(this); - this.handleToTopClick = this.handleToTopClick.bind(this); - this.handleToBottomClick = this.handleToBottomClick.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.handleQuantizeClick = this.handleQuantizeClick.bind(this); + this.handleQuantizeFactorChange = this.handleQuantizeFactorChange.bind( + this + ); + this.handleToTopClick = this.handleToTopClick.bind(this); + this.handleToBottomClick = this.handleToBottomClick.bind(this); } - componentWillMount () { + componentWillMount() { this.setSynth(this.props, this.state.colorIndex); this.part = this.getPart(); // TODO ugly if (this.props.isAutoQuantizeActive) { - const newPoints = this.getPointsForFixedPerimeterLength(this.state.points, this.quantizeLength * this.state.quantizeFactor); + const newPoints = this.getPointsForFixedPerimeterLength( + this.state.points, + this.quantizeLength * this.state.quantizeFactor + ); this.setNoteEvents(this.props.scaleObj, newPoints); this.setState({ - points: newPoints + points: newPoints, }); } else { this.setNoteEvents(this.props.scaleObj, this.state.points); } } - - componentDidMount () { + + componentDidMount() { this.handleDrag(); } - componentWillUnmount () { + componentWillUnmount() { // this.shapeElement.destroy(); this.part.dispose(); this.synth.dispose(); } - - componentWillUpdate (nextProps, nextState) { + + componentWillUpdate(nextProps, nextState) { /* change instrument when color's instrument changes, or when shape's color changes */ - if (nextProps.selectedInstruments[nextState.colorIndex] !== + if ( + nextProps.selectedInstruments[nextState.colorIndex] !== this.props.selectedInstruments[nextState.colorIndex] || - nextState.colorIndex !== this.state.colorIndex) { + nextState.colorIndex !== this.state.colorIndex + ) { this.setSynth(nextProps, nextState.colorIndex); } } - componentWillReceiveProps (nextProps) { + componentWillReceiveProps(nextProps) { /* remove hover styles when switching to draw mode */ if (nextProps.activeTool === 'draw' && this.props.activeTool === 'edit') { this.setState({ - isHoveredOver: false + isHoveredOver: false, }); } /* set to fixed perimeter */ - if (nextProps.isAutoQuantizeActive && nextProps.isAutoQuantizeActive !== this.props.isAutoQuantizeActive) { - const newPoints = this.getPointsForFixedPerimeterLength(this.state.points, this.quantizeLength * this.state.quantizeFactor); + if ( + nextProps.isAutoQuantizeActive && + nextProps.isAutoQuantizeActive !== this.props.isAutoQuantizeActive + ) { + const newPoints = this.getPointsForFixedPerimeterLength( + this.state.points, + this.quantizeLength * this.state.quantizeFactor + ); this.setNoteEvents(nextProps.scaleObj, newPoints); this.setState({ - points: newPoints + points: newPoints, }); } /* update note events if new scale or new tonic */ - if (this.props.scaleObj.name !== nextProps.scaleObj.name || - this.props.scaleObj.tonic.toString() !== nextProps.scaleObj.tonic.toString()) { + if ( + this.props.scaleObj.name !== nextProps.scaleObj.name || + this.props.scaleObj.tonic.toString() !== + nextProps.scaleObj.tonic.toString() + ) { this.setNoteEvents(nextProps.scaleObj, this.state.points); } /* on tempo update */ if (this.props.tempo !== nextProps.tempo) { - this.part.playbackRate = nextProps.tempo/50; + this.part.playbackRate = nextProps.tempo / 50; } - + /* update effect values (knobs) */ nextProps.knobVals[this.state.colorIndex].forEach((val, i) => { // TODO @@ -164,26 +182,34 @@ class ShapeContainer extends Component { this.solo.solo = isSoloed; } } - - shouldComponentUpdate (nextProps, nextState) { - return !(Utils.isEquivalent(this.props, nextProps) && - Utils.isEquivalent(this.state, nextState)); + + shouldComponentUpdate(nextProps, nextState) { + return !( + Utils.isEquivalent(this.props, nextProps) && + Utils.isEquivalent(this.state, nextState) + ); } /* ================================ AUDIO =============================== */ - - getPart () { + + getPart() { const part = new Tone.Part((time, val) => { - //console.log("Playing note", val.note, "for", val.duration, "INDEX:", val.pIndex); + //console.log("Playing note", val.note, "for", val.duration, "INDEX:", val.pIndex); const dur = val.duration / this.part.playbackRate; - + // animation Tone.Draw.schedule(() => { - const xFrom = this.state.points[val.pIndex-2]; - const yFrom = this.state.points[val.pIndex-1]; - const xTo = val.pIndex >= this.state.points.length ? this.state.points[0] : this.state.points[val.pIndex]; - const yTo = val.pIndex >= this.state.points.length ? this.state.points[1] : this.state.points[val.pIndex+1]; - + const xFrom = this.state.points[val.pIndex - 2]; + const yFrom = this.state.points[val.pIndex - 1]; + const xTo = + val.pIndex >= this.state.points.length + ? this.state.points[0] + : this.state.points[val.pIndex]; + const yTo = + val.pIndex >= this.state.points.length + ? this.state.points[1] + : this.state.points[val.pIndex + 1]; + const animCircle = this.shapeComponentElement.getAnimCircle(); const shapeElement = this.shapeComponentElement.getShapeElement(); if (animCircle) { @@ -195,86 +221,84 @@ class ShapeContainer extends Component { }); shapeElement.to({ fill: shapeFill, - duration: 0.2 + duration: 0.2, }); animCircle.setAttrs({ x: xFrom, y: yFrom, fill: '#FFF', - radius: 8 + radius: 8, }); animCircle.to({ x: xTo, y: yTo, - duration: dur + duration: dur, }); animCircle.to({ radius: 5, fill: this.props.colorsList[this.state.colorIndex], - duration: 0.3 + duration: 0.3, }); } }, time); - + const noteIndex = val.noteIndex + this.state.noteIndexModifier; const noteString = this.props.scaleObj.get(noteIndex).toString(); - + // trigger synth this.synth.triggerAttackRelease(noteString, dur, time); - }, []).start(0); - + part.loop = true; - part.playbackRate = this.props.tempo/50; - + part.playbackRate = this.props.tempo / 50; + return part; } - getNoteInfo (points, scaleObj, i, iPrev, iPrevPrev, prevNoteIndex) { + getNoteInfo(points, scaleObj, i, iPrev, iPrevPrev, prevNoteIndex) { const tempoModifier = 200; - + const p = { x: points[i], - y: points[i+1] + y: points[i + 1], }; const prev = { x: points[iPrev], - y: points[iPrev+1] + y: points[iPrev + 1], }; const prevPrev = { x: points[iPrevPrev], - y: points[iPrevPrev+1] + y: points[iPrevPrev + 1], }; - + const edgeLength = Utils.dist(p.x, p.y, prev.x, prev.y) / tempoModifier; const theta = Utils.getAngle(p, prev, prevPrev); const degreeDiff = Utils.thetaToScaleDegree(theta, scaleObj); - + const noteIndex = prevNoteIndex + degreeDiff; - + return { - duration: edgeLength, + duration: edgeLength, noteIndex: noteIndex, - pIndex: i === 0 ? points.length : i + pIndex: i === 0 ? points.length : i, }; } - setSynth (props, colorIndex) { - + setSynth(props, colorIndex) { const selectedInstrumentIndex = props.selectedInstruments[colorIndex]; const knobVals = props.knobVals[colorIndex]; const synthObj = InstrumentPresets[selectedInstrumentIndex]; - + // console.log('__SETTING SYNTH___'); // console.log('new instrument:', selectedInstrumentIndex); // console.log('new color:', colorIndex); // console.log('knob vals:', knobVals); // console.log('sending', `colorFx-${colorIndex}`); - + if (this.synth) { this.synth.triggerRelease(); - + this.panner.disconnect(); this.panner.dispose(); this.solo.disconnect(); @@ -282,17 +306,21 @@ class ShapeContainer extends Component { this.gain.disconnect(); this.gain.dispose(); - this.synth.volume.exponentialRampToValueAtTime(-Infinity, Tone.now()+0.2) - + this.synth.volume.exponentialRampToValueAtTime( + -Infinity, + Tone.now() + 0.2 + ); + this.synth.disconnect(); this.synth.dispose(); } - - + this.synth = new synthObj.baseSynth(synthObj.params); - this.synth.volume.exponentialRampToValueAtTime(this.state.volume, Tone.now()+0.2) + this.synth.volume.exponentialRampToValueAtTime( + this.state.volume, + Tone.now() + 0.2 + ); - knobVals.forEach((val, i) => { if (synthObj.dynamicParams[i].target === 'instrument') { synthObj.dynamicParams[i].func(this, val); @@ -306,15 +334,22 @@ class ShapeContainer extends Component { this.synth.chain(this.panner, this.solo, this.gain); } - setNoteEvents (scaleObj, points) { + setNoteEvents(scaleObj, points) { this.part.removeAll(); - + let delay = 0; let prevNoteIndex = this.state.firstNoteIndex; Utils.forEachPoint(points, (p, i) => { if (i >= 2) { - const noteInfo = this.getNoteInfo(points, scaleObj, i, i-2, i-4, prevNoteIndex); + const noteInfo = this.getNoteInfo( + points, + scaleObj, + i, + i - 2, + i - 4, + prevNoteIndex + ); this.part.add(delay, noteInfo); delay += noteInfo.duration; prevNoteIndex = noteInfo.noteIndex; @@ -323,18 +358,27 @@ class ShapeContainer extends Component { // last edge const n = points.length; - const lastNoteInfo = this.getNoteInfo(points, scaleObj, 0, n-2, n-4, prevNoteIndex); + const lastNoteInfo = this.getNoteInfo( + points, + scaleObj, + 0, + n - 2, + n - 4, + prevNoteIndex + ); this.part.add(delay, lastNoteInfo); this.part.loopEnd = delay + lastNoteInfo.duration; } - setPan (val) { + setPan(val) { this.panner.pan.value = val * 0.9; } - setEffectVal (val, i) { - const synthParamsIndex = this.props.selectedInstruments[this.state.colorIndex]; + setEffectVal(val, i) { + const synthParamsIndex = this.props.selectedInstruments[ + this.state.colorIndex + ]; const synthParams = InstrumentPresets[synthParamsIndex]; // set synth value when knobs are changed // values for connected effects are set with the colorController @@ -348,30 +392,29 @@ class ShapeContainer extends Component { /* --- Shape ------------------------------------------------------------ */ /* Click */ - handleMouseDown (e) { + handleMouseDown(e) { this.setState({ editorX: e.evt.offsetX, editorY: e.evt.offsetY, }); } - handleClick () { + handleClick() { this.props.onShapeClick(this.props.index); } - handleDelete () { + handleDelete() { this.props.onDelete(this.props.index); } - + /* Drag */ - handleDragStart () { + handleDragStart() { this.setState({ - isDragging: true + isDragging: true, }); } - - handleDrag () { - console.log('drage'); + + handleDrag() { const shapeElement = this.shapeComponentElement.getShapeElement(); const absPos = shapeElement.getAbsolutePosition(); const avgPoint = Utils.getAveragePoint(this.state.points); @@ -380,175 +423,193 @@ class ShapeContainer extends Component { const y = parseInt(absPos.y + avgPoint.y, 10); const panVal = Utils.convertValToRange(x, 0, window.innerWidth, -1, 1); - const noteIndexVal = parseInt(Utils.convertValToRange(y, 0, window.innerHeight, 5, -7), 10); - + const noteIndexVal = parseInt( + Utils.convertValToRange(y, 0, window.innerHeight, 5, -7), + 10 + ); + this.setPan(panVal); this.setState({ averagePoint: { x: x, y: y }, - noteIndexModifier: noteIndexVal + noteIndexModifier: noteIndexVal, }); } - - handleDragEnd () { + + handleDragEnd() { this.setState({ - isDragging: false - }); + isDragging: false, + }); } - dragBoundFunc (pos) { + dragBoundFunc(pos) { return { x: this.props.snapToGrid(pos.x), - y: this.props.snapToGrid(pos.y) + y: this.props.snapToGrid(pos.y), }; } - + /* Hover */ - handleMouseOver () { + handleMouseOver() { this.setState({ isHoveredOver: true }); } - handleMouseOut () { + handleMouseOut() { this.setState({ isHoveredOver: false }); } /* --- Editor Panel ----------------------------------------------------- */ /* --- Color --- */ - handleColorChange (colorObj) { + handleColorChange(colorObj) { this.setState({ - colorIndex: this.props.colorsList.indexOf(colorObj.hex) + colorIndex: this.props.colorsList.indexOf(colorObj.hex), }); } /* --- Volume --- */ - handleVolumeChange (val) { - this.synth.volume.exponentialRampToValueAtTime(val, Tone.now()+0.2); + handleVolumeChange(val) { + this.synth.volume.exponentialRampToValueAtTime(val, Tone.now() + 0.2); this.setState({ - volume: val + volume: val, }); } - handleMuteChange () { + handleMuteChange() { this.part.mute = !this.state.isMuted; this.setState({ - isMuted: !this.state.isMuted + isMuted: !this.state.isMuted, }); } /* --- Quantization --- */ - handleQuantizeClick () { - const newPoints = this.getPointsForFixedPerimeterLength(this.state.points, - this.quantizeLength * this.state.quantizeFactor); - + handleQuantizeClick() { + const newPoints = this.getPointsForFixedPerimeterLength( + this.state.points, + this.quantizeLength * this.state.quantizeFactor + ); + this.setNoteEvents(this.props.scaleObj, newPoints); this.setState({ - points: newPoints + points: newPoints, }); } - handleQuantizeFactorChange (factor) { + handleQuantizeFactorChange(factor) { return () => { - if ((factor < 1 && this.state.quantizeFactor >= 0.25) || - (factor > 1 && this.state.quantizeFactor <= 4)) { - const newPerim = this.props.isAutoQuantizeActive ? - factor * this.state.quantizeFactor * this.quantizeLength : - Utils.getTotalLength(this.state.points) * factor; - const newPoints = this.getPointsForFixedPerimeterLength(this.state.points, newPerim); - + if ( + (factor < 1 && this.state.quantizeFactor >= 0.25) || + (factor > 1 && this.state.quantizeFactor <= 4) + ) { + const newPerim = this.props.isAutoQuantizeActive + ? factor * this.state.quantizeFactor * this.quantizeLength + : Utils.getTotalLength(this.state.points) * factor; + const newPoints = this.getPointsForFixedPerimeterLength( + this.state.points, + newPerim + ); + this.setNoteEvents(this.props.scaleObj, newPoints); - + this.setState({ points: newPoints, - quantizeFactor: factor * this.state.quantizeFactor + quantizeFactor: factor * this.state.quantizeFactor, }); } }; } /* --- Arrangement --- */ - handleToTopClick () { + handleToTopClick() { const groupElement = this.shapeComponentElement.getGroupElement(); groupElement.moveToTop(); // TODO way to hacky this.setState({ - isHoveredOver: true + isHoveredOver: true, }); this.setState({ - isHoveredOver: false + isHoveredOver: false, }); } - handleToBottomClick () { + handleToBottomClick() { const groupElement = this.shapeComponentElement.getGroupElement(); groupElement.moveToBottom(); // TODO way to hacky this.setState({ - isHoveredOver: true + isHoveredOver: true, }); this.setState({ - isHoveredOver: false + isHoveredOver: false, }); } - + /* --- Vertices --------------------------------------------------------- */ - handleVertexDragMove (i) { - return (e) => { + handleVertexDragMove(i) { + return e => { const pos = e.target.position(); let points = this.state.points.slice(); points[i] = this.props.snapToGrid(pos.x); - points[i+1] = this.props.snapToGrid(pos.y); + points[i + 1] = this.props.snapToGrid(pos.y); if (this.props.isAutoQuantizeActive) { - points = this.getPointsForFixedPerimeterLength(points, this.quantizeLength * this.state.quantizeFactor); + points = this.getPointsForFixedPerimeterLength( + points, + this.quantizeLength * this.state.quantizeFactor + ); } this.setNoteEvents(this.props.scaleObj, points); - + this.setState({ - points: points + points: points, }); }; } /* --- Helper ----------------------------------------------------------- */ - getFillColor () { + getFillColor() { const color = this.props.colorsList[this.state.colorIndex]; const alphaAmount = this.props.isSelected ? 0.8 : 0.4; - return Color(color).alpha(alphaAmount).toString(); + return Color(color) + .alpha(alphaAmount) + .toString(); } - getPointsForFixedPerimeterLength (points, length) { + getPointsForFixedPerimeterLength(points, length) { const currLen = Utils.getTotalLength(points); const avgPoint = Utils.getAveragePoint(points); const ratio = length / currLen; const newPoints = points.slice(); - + Utils.forEachPoint(points, (p, i) => { newPoints[i] = p.x * ratio + (1 - ratio) * avgPoint.x; - newPoints[i+1] = p.y * ratio + (1 - ratio) * avgPoint.y; + newPoints[i + 1] = p.y * ratio + (1 - ratio) * avgPoint.y; }); return newPoints; } - /* =============================== RENDER =============================== */ - render () { + render() { // console.log('shape render'); const color = this.props.colorsList[this.state.colorIndex]; const isEditMode = this.props.activeTool === 'edit'; let opacity = 1; - if (this.props.soloedShapeIndex >= 0 && - this.props.soloedShapeIndex !== this.props.index) - { opacity = 0.4; } - if (this.state.isMuted) - { opacity = 0.2; } + if ( + this.props.soloedShapeIndex >= 0 && + this.props.soloedShapeIndex !== this.props.index + ) { + opacity = 0.4; + } + if (this.state.isMuted) { + opacity = 0.2; + } const attrs = { strokeWidth: isEditMode ? (this.state.isHoveredOver ? 4 : 2) : 2, @@ -559,7 +620,7 @@ class ShapeContainer extends Component { return ( this.shapeComponentElement = c} + ref={c => (this.shapeComponentElement = c)} project={{ scaleObj: this.props.scaleObj, colorsList: this.props.colorsList, @@ -567,48 +628,39 @@ class ShapeContainer extends Component { isPlaying: this.props.isPlaying, tempo: this.props.tempo, }} - index={this.props.index} points={this.state.points} attrs={attrs} volume={this.state.volume} colorIndex={this.state.colorIndex} noteIndexModifier={this.state.noteIndexModifier} - isDragging={this.state.isDragging} isSelected={this.props.isSelected} isMuted={this.state.isMuted} soloedShapeIndex={this.props.soloedShapeIndex} - averagePoint={this.state.averagePoint} editorPosition={{ x: this.state.editorX, - y: this.state.editorY + y: this.state.editorY, }} - // shape event handlers dragBoundFunc={this.dragBoundFunc} handleDrag={this.handleDrag} handleDragStart={this.handleDragStart} handleDragEnd={this.handleDragEnd} - handleClick={this.handleClick} handleMouseDown={this.handleMouseDown} handleMouseOver={this.handleMouseOver} handleMouseOut={this.handleMouseOut} - handleVertexDragMove={this.handleVertexDragMove} - // editor panel handlers handleColorChange={this.handleColorChange} handleQuantizeClick={this.handleQuantizeClick} handleDelete={this.handleDelete} handleQuantizeFactorChange={this.handleQuantizeFactorChange} - handleVolumeChange={this.handleVolumeChange} handleMuteChange={this.handleMuteChange} handleSoloChange={this.props.onSoloChange} - handleToTopClick={this.handleToTopClick} handleToBottomClick={this.handleToBottomClick} /> diff --git a/src/components/Toolbar/Component.jsx b/src/components/Toolbar/Component.jsx index 330843e..cf2e238 100644 --- a/src/components/Toolbar/Component.jsx +++ b/src/components/Toolbar/Component.jsx @@ -22,8 +22,10 @@ const propTypes = { onColorSelectClick: PropTypes.func.isRequired, isPlaying: PropTypes.bool.isRequired, + isRecording: PropTypes.bool.isRequired, activeTool: PropTypes.string.isRequired, handlePlayClick: PropTypes.func.isRequired, + handleRecordClick: PropTypes.func.isRequired, colorsList: PropTypes.array.isRequired, activeColorIndex: PropTypes.number.isRequired, handleColorChange: PropTypes.func.isRequired, @@ -52,7 +54,7 @@ const propTypes = { /* ---------------------- Transport ---------------------- */ -function TransportControls (props) { +function TransportControls(props) { const playButtonClass = props.isPlaying ? 'ion-stop' : 'ion-play'; return (
@@ -63,10 +65,10 @@ function TransportControls (props) { title="Play project (SPACE)" />
-
+
@@ -76,12 +78,14 @@ function TransportControls (props) { TransportControls.propTypes = { isPlaying: PropTypes.bool.isRequired, + isRecording: PropTypes.bool.isRequired, handlePlayClick: PropTypes.func.isRequired, + handleRecordClick: PropTypes.func.isRequired, }; /* ---------------------- Tool Select ---------------------- */ -function ToolSelect (props) { +function ToolSelect(props) { const isDrawTool = props.activeTool === 'draw'; const activeColor = props.colorsList[props.activeColorIndex]; return ( @@ -90,19 +94,17 @@ function ToolSelect (props) { hasBorder color={activeColor} onClick={props.onColorSelectClick} - > - - -
+ /> + +
- + title="Draw Tool (TAB to toggle)" + > + + title="Edit Tool (TAB to toggle)" + > + +
); } @@ -147,7 +147,7 @@ ToolSelect.propTypes = { /* ---------------------- Canvas ---------------------- */ -function CanvasControls (props) { +function CanvasControls(props) { // TODO theme const lightGray = ColorUtils.getDarker('#f1f1f1'); return ( @@ -198,7 +198,7 @@ CanvasControls.propTypes = { /* ---------------------- Musical ---------------------- */ -function MusicalControls (props) { +function MusicalControls(props) { return (
@@ -271,13 +270,16 @@ OtherControls.propTypes = { }; /* ================================ Toolbar ================================ */ -function ToolbarComponent (props) { +function ToolbarComponent(props) { return (
+ {/* TODO Color */}
); - - } ToolbarComponent.propTypes = propTypes; export default ToolbarComponent; - /* return ( @@ -429,4 +428,4 @@ export default ToolbarComponent;
); -*/ \ No newline at end of file +*/ diff --git a/src/views/Project/Container.jsx b/src/views/Project/Container.jsx index 4dfb0e8..5069529 100644 --- a/src/views/Project/Container.jsx +++ b/src/views/Project/Container.jsx @@ -4,8 +4,10 @@ import PropTypes from 'prop-types'; import Fullscreen from 'react-full-screen'; import Teoria from 'teoria'; import Tone from 'tone'; +import Recorder from 'record-audio-js'; import Toolbar from 'components/Toolbar'; +import Downloads from 'components/Downloads'; import ShapeCanvas from 'components/ShapeCanvas'; import ColorControllerPanel from 'components/ColorControllerPanel'; import InstrumentPresets from 'presets/InstrumentPresets'; @@ -17,22 +19,22 @@ const colorsList = [ '#f4b549', // yellow '#2a548e', // blue '#705498', // purple - '#33936b' // green + '#33936b', // green ]; const tonicsList = [ - { value: 'a', label: 'A' }, + { value: 'a', label: 'A' }, { value: 'a#', label: 'A#' }, - { value: 'b', label: 'B' }, - { value: 'c', label: 'C' }, + { value: 'b', label: 'B' }, + { value: 'c', label: 'C' }, { value: 'c#', label: 'C#' }, - { value: 'd', label: 'D' }, + { value: 'd', label: 'D' }, { value: 'd#', label: 'D#' }, - { value: 'e', label: 'E' }, - { value: 'f', label: 'F' }, + { value: 'e', label: 'E' }, + { value: 'f', label: 'F' }, { value: 'f#', label: 'F#' }, - { value: 'g', label: 'G' }, - { value: 'g#', label: 'G#' } + { value: 'g', label: 'G' }, + { value: 'g#', label: 'G#' }, ]; const scalesList = [ @@ -51,7 +53,7 @@ const scalesList = [ { value: 'flamenco', label: 'Flamenco' }, { value: 'harmonicminor', label: 'Harmonic Minor' }, { value: 'melodicminor', label: 'Melodic Minor' }, - { value: 'wholetone', label: 'Wholetone' } + { value: 'wholetone', label: 'Wholetone' }, ]; const instNamesList = InstrumentPresets.map(preset => ({ @@ -65,14 +67,13 @@ const masterCompressor = new Tone.Compressor({ threshold: -30, release: 0.25, attack: 0.003, - knee: 30 + knee: 30, }); const masterLimiter = new Tone.Limiter(-2); const masterOutput = new Tone.Gain(0.9).receive('masterOutput'); masterOutput.chain(masterCompressor, masterLimiter, Tone.Master); - /* ========================================================================== */ const propTypes = { @@ -85,15 +86,16 @@ const propTypes = { }; class Project extends Component { - constructor (props) { + constructor(props) { super(props); // indeces of default instruments - const selectedInstruments = [0,1,2,1,0]; + const selectedInstruments = [0, 1, 2, 1, 0]; const knobVals = []; selectedInstruments.forEach(instrumentIndex => { - const instrumentDefaults = InstrumentPresets[instrumentIndex] - .dynamicParams.map(param => param.default); + const instrumentDefaults = InstrumentPresets[ + instrumentIndex + ].dynamicParams.map(param => param.default); knobVals.push(instrumentDefaults); }); @@ -105,22 +107,23 @@ class Project extends Component { isSnapToGridActive: false, isAutoQuantizeActive: false, isPlaying: false, - + isRecording: false, + quantizeLength: 700, tempo: props.initState.tempo, - scaleObj: Teoria - .note(props.initState.tonic) - .scale(props.initState.scale), + scaleObj: Teoria.note(props.initState.tonic).scale(props.initState.scale), activeTool: 'draw', activeColorIndex: 0, + downloadUrls: [], selectedInstruments, knobVals, }; // transport this.handlePlayClick = this.handlePlayClick.bind(this); + this.handleRecordClick = this.handleRecordClick.bind(this); // color and tool this.handleColorChange = this.handleColorChange.bind(this); @@ -130,7 +133,9 @@ class Project extends Component { // toggles this.handleGridToggleChange = this.handleGridToggleChange.bind(this); - this.handleSnapToGridToggleChange = this.handleSnapToGridToggleChange.bind(this); + this.handleSnapToGridToggleChange = this.handleSnapToGridToggleChange.bind( + this + ); this.handleAutoQuantizeChange = this.handleAutoQuantizeChange.bind(this); // music options @@ -144,101 +149,128 @@ class Project extends Component { // canvas this.handleClearButtonClick = this.handleClearButtonClick.bind(this); - this.handleFullscreenButtonClick = this.handleFullscreenButtonClick.bind(this); + this.handleFullscreenButtonClick = this.handleFullscreenButtonClick.bind( + this + ); + + // recorder + this.recorder = new Recorder(Tone.Master); } - + /* ============================= LIFECYCLE ============================== */ - componentWillMount () { + componentWillMount() { document.addEventListener('keydown', this.handleKeyDown.bind(this)); } - componentWillUnmount () { + componentWillUnmount() { document.removeEventListener('keydown', this.handleKeyDown.bind(this)); } /* ============================== HANDLERS ============================== */ - + /* --- Transport -------------------------------------------------------- */ - handlePlayClick () { + handlePlayClick() { Tone.Transport.toggle(); - this.setState((prevState) => ({ - isPlaying: !prevState.isPlaying + this.setState(prevState => ({ + isPlaying: !prevState.isPlaying, })); } - + + handleRecordClick() { + if (this.state.isRecording) { + // this.recorder.stop(); + console.log('stopping'); + this.recorder.exportWAV(blob => { + const url = URL.createObjectURL(blob); + const downloadUrls = this.state.downloadUrls.slice(); + console.log(url); + downloadUrls.push(url); + this.setState({ + downloadUrls, + }); + // Recorder.forceDownload(blob); + }); + } else { + this.recorder.record(); + } + this.setState({ + isRecording: !this.state.isRecording, + }); + } + /* --- Tool ------------------------------------------------------------- */ - toggleActiveTool () { + toggleActiveTool() { let newTool = 'draw'; - if(this.shapeCanvas.canChangeTool()) { + if (this.shapeCanvas.canChangeTool()) { if (this.state.activeTool === 'draw') { newTool = 'edit'; } this.setState({ - activeTool: newTool + activeTool: newTool, }); } } - handleDrawToolClick () { + handleDrawToolClick() { this.setAciveTool('draw'); } - handleEditToolClick () { + handleEditToolClick() { this.setAciveTool('edit'); } - setAciveTool (tool) { - if(this.shapeCanvas.canChangeTool()) { + setAciveTool(tool) { + if (this.shapeCanvas.canChangeTool()) { this.setState({ - activeTool: tool + activeTool: tool, }); } } - - handleColorChange (colorObj) { + + handleColorChange(colorObj) { this.setState({ - activeColorIndex: colorsList.indexOf(colorObj.hex) + activeColorIndex: colorsList.indexOf(colorObj.hex), }); } - + /* --- Canvas ----------------------------------------------------------- */ - handleGridToggleChange () { + handleGridToggleChange() { this.setState({ - isGridActive: !this.state.isGridActive + isGridActive: !this.state.isGridActive, }); } - handleSnapToGridToggleChange () { + handleSnapToGridToggleChange() { this.setState({ - isSnapToGridActive: !this.state.isSnapToGridActive + isSnapToGridActive: !this.state.isSnapToGridActive, }); } - handleAutoQuantizeChange () { + handleAutoQuantizeChange() { this.setState({ - isAutoQuantizeActive: !this.state.isAutoQuantizeActive + isAutoQuantizeActive: !this.state.isAutoQuantizeActive, }); } /* --- Musical ---------------------------------------------------------- */ - handleTempoChange (val) { + handleTempoChange(val) { this.setState({ - tempo: val + tempo: val, }); } - handleTonicChange (val) { - this.setState((prevState) => ({ + handleTonicChange(val) { + this.setState(prevState => ({ scaleObj: Teoria.note(val.value).scale(prevState.scaleObj.name), })); } - handleScaleChange (val) { + handleScaleChange(val) { if (val) { const tonic = this.state.scaleObj.tonic; this.setState({ @@ -248,25 +280,26 @@ class Project extends Component { } /* --- Canvas ----------------------------------------------------------- */ - - handleClearButtonClick () { + + handleClearButtonClick() { this.shapeCanvas.clearAll(); } - handleFullscreenButtonClick () { + handleFullscreenButtonClick() { this.setState({ - isFullscreenEnabled: !this.state.isFullscreenEnabled + isFullscreenEnabled: !this.state.isFullscreenEnabled, }); } /* --- Color Controllers ------------------------------------------------ */ - handleInstChange (colorIndex) { + handleInstChange(colorIndex) { return instrumentIndex => { const selectedInstruments = this.state.selectedInstruments.slice(); selectedInstruments[colorIndex] = instrumentIndex; - const defaultKnobvals = InstrumentPresets[instrumentIndex] - .dynamicParams.map(param => param.default); + const defaultKnobvals = InstrumentPresets[ + instrumentIndex + ].dynamicParams.map(param => param.default); const knobVals = this.state.knobVals.slice(); knobVals[colorIndex] = defaultKnobvals; @@ -278,81 +311,82 @@ class Project extends Component { }; } - handleKnobChange (colorIndex) { - return effectIndex => - val => { - this.setState( - (prevState) => { - const knobVals = prevState.knobVals.slice(); - const colorKnobVals = knobVals[colorIndex].slice(); - colorKnobVals[effectIndex] = val; - knobVals[colorIndex] = colorKnobVals; - return { - knobVals: knobVals, - }; - } - ); - }; + handleKnobChange(colorIndex) { + return effectIndex => val => { + this.setState(prevState => { + const knobVals = prevState.knobVals.slice(); + const colorKnobVals = knobVals[colorIndex].slice(); + colorKnobVals[effectIndex] = val; + knobVals[colorIndex] = colorKnobVals; + return { + knobVals: knobVals, + }; + }); + }; } /* --- Keyboard Shortcuts ----------------------------------------------- */ - handleKeyDown (event) { + handleKeyDown(event) { console.warn('Keypress:', event.key); /* Space toggles play */ - if(event.key === ' ') { + if (event.key === ' ') { //event.preventDefault(); // stop from clicking focused buttons this.handlePlayClick(); } /* tab toggles active tool */ - if(event.key === 'Tab') { - event.preventDefault(); + if (event.key === 'Tab') { + event.preventDefault(); this.toggleActiveTool(); } - + /* numbers control draw color */ - if (event.key === '1' || event.key === '2' || event.key === '3' || - event.key === '4' || event.key === '5') { + if ( + event.key === '1' || + event.key === '2' || + event.key === '3' || + event.key === '4' || + event.key === '5' + ) { this.setState({ - activeColorIndex: parseInt(event.key, 10) - 1 + activeColorIndex: parseInt(event.key, 10) - 1, }); } /* backspace deletes the selected shape */ - if(event.key === 'Backspace') { + if (event.key === 'Backspace') { this.shapeCanvas.deleteSelectedShape(); } } /* =============================== RENDER =============================== */ - render () { + render() { return ( this.setState({isFullscreenEnabled})}> - - {/* The Controls */} + onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })} + > + {/* The Controls */} - + {/* The Canvas */} this.shapeCanvas = c} + ref={c => (this.shapeCanvas = c)} colorsList={colorsList} colorIndex={this.state.activeColorIndex} activeTool={this.state.activeTool} selectedInstruments={this.state.selectedInstruments} - knobVals={this.state.knobVals} - isAutoQuantizeActive={this.state.isAutoQuantizeActive} isPlaying={this.state.isPlaying} scaleObj={this.state.scaleObj} tempo={this.state.tempo} quantizeLength={this.state.quantizeLength} - isGridActive={this.state.isGridActive} isSnapToGridActive={this.state.isSnapToGridActive} /> @@ -396,7 +426,9 @@ class Project extends Component { onKnobChange={this.handleKnobChange} knobVals={this.state.knobVals} /> - + + + ); } } diff --git a/yarn.lock b/yarn.lock index 7c5da12..4b62ddc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3693,6 +3693,10 @@ inline-style-prefixer@^4.0.0: bowser "^1.7.3" css-in-js-utils "^2.0.0" +inline-worker@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/inline-worker/-/inline-worker-1.1.0.tgz#55e96f54915a642b00872a2daa6fe832b424c98d" + inquirer@3.3.0, inquirer@^3.0.6: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -4660,9 +4664,9 @@ lodash.uniq@^4.5.0: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" -lodash@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" +lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" loglevel@^1.4.1: version "1.6.1" @@ -6391,6 +6395,12 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +record-audio-js@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/record-audio-js/-/record-audio-js-0.0.4.tgz#3b5a95500b01577d77e0f001cbe33386c167627b" + dependencies: + inline-worker "^1.1.0" + recursive-readdir@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"