From c2cae279b75b8911f68e76379384ff28aa0e9423 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 22 Aug 2017 15:53:35 -0400 Subject: [PATCH 01/10] Add line mode --- src/components/line-mode.jsx | 19 ++ src/components/paint-editor.jsx | 2 + src/containers/line-mode.jsx | 265 ++++++++++++++++++ src/containers/paint-editor.jsx | 2 + src/modes/modes.js | 3 +- src/reducers/line-mode.js | 11 + .../messages/src/components/brush-mode.json | 2 +- .../messages/src/components/line-mode.json | 7 + 8 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/components/line-mode.jsx create mode 100644 src/containers/line-mode.jsx create mode 100644 src/reducers/line-mode.js create mode 100644 translations/messages/src/components/line-mode.json diff --git a/src/components/line-mode.jsx b/src/components/line-mode.jsx new file mode 100644 index 00000000..17affefa --- /dev/null +++ b/src/components/line-mode.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +const LineModeComponent = props => ( + +); + +LineModeComponent.propTypes = { + onMouseDown: PropTypes.func.isRequired +}; + +export default LineModeComponent; diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 1459fe35..8f7ff57f 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushMode from '../containers/brush-mode.jsx'; import EraserMode from '../containers/eraser-mode.jsx'; +import LineMode from '../containers/line-mode.jsx'; class PaintEditorComponent extends React.Component { constructor (props) { @@ -23,6 +24,7 @@ class PaintEditorComponent extends React.Component { + ); } diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx new file mode 100644 index 00000000..0b892bfd --- /dev/null +++ b/src/containers/line-mode.jsx @@ -0,0 +1,265 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; +import Modes from '../modes/modes'; +import LineModeComponent from '../components/line-mode.jsx'; +import {changeMode} from '../reducers/modes'; +import paper from 'paper'; + +class LineMode extends React.Component { + static get SNAP_TOLERANCE () { + return 6; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'toleranceSquared', + 'findLineEnd' + ]); + } + componentDidMount () { + if (this.props.isLineModeActive) { + this.activateTool(); + } + } + componentWillReceiveProps (nextProps) { + if (nextProps.isLineModeActive && !this.props.isLineModeActive) { + this.activateTool(); + } else if (!nextProps.isLineModeActive && this.props.isLineModeActive) { + this.deactivateTool(); + } else if (nextProps.isLineModeActive && this.props.isLineModeActive) { + this.blob.setOptions(nextProps.lineModeState); + } + } + shouldComponentUpdate () { + return false; // Static component, for now + } + activateTool () { + // TODO add back selection + // pg.selection.clearSelection(); + + this.tool = new paper.Tool(); + + this.path = null; + this.hitResult = null; + + // TODO add back colors + // Make sure a stroke color is set on the line tool + // if(!pg.stylebar.getStrokeColor()) { + // pg.stylebar.setStrokeColor(pg.stylebar.getFillColor()); + // pg.stylebar.setFillColor(null); + // } + + const lineMode = this; + this.tool.onMouseDown = function (event) { + if (event.event.button > 0) return; // only first mouse button + + if (this.path) { + this.path.setSelected(false); + this.path = null; + } + + // If you click near a point, continue that line instead of making a new line + this.hitResult = lineMode.findLineEnd(event.point); + if (this.hitResult) { + this.path = this.hitResult.path; + if (this.hitResult.isFirst) { + this.path.reverse(); + } + this.path.lastSegment.setSelected(true); + this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged + this.path.lastSegment.handleOut = null; // Make sure line isn't curvy + this.path.lastSegment.handleIn = null; + } + + // If not near other path, start a new path + if (!this.path) { + this.path = new paper.Path(); + + // TODO add back style + // this.path = pg.stylebar.applyActiveToolbarStyle(path); + this.path.setStrokeColor('black'); + + this.path.setSelected(true); + this.path.add(event.point); + this.path.add(event.point); // Add second point, which is what will move when dragged + paper.view.draw(); + } + }; + + this.tool.onMouseMove = function (event) { + // If near another path's endpoint, or this path's beginpoint, clip to it to suggest + // joining/closing the paths. + if (this.hitResult) { + this.hitResult.path.setSelected(false); + this.hitResult = null; + } + + if (this.path && !this.path.closed && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) { + this.hitResult = { + path: this.path, + segment: this.path.firstSegment, + isFirst: true + }; + } else { + this.hitResult = lineMode.findLineEnd(event.point); + } + + if (this.hitResult) { + const hitPath = this.hitResult.path; + hitPath.setSelected(true); + if (this.hitResult.isFirst) { + hitPath.firstSegment.setSelected(true); + } else { + hitPath.lastSegment.setSelected(true); + } + } + }; + + this.tool.onMouseDrag = function (event) { + if (event.event.button > 0) return; // only first mouse button + // If near another path's endpoint, or this path's beginpoint, clip to it to suggest + // joining/closing the paths. + if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false); + this.hitResult = null; + + if (this.path && this.path.segments.length > 3 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) { + this.hitResult = { + path: this.path, + segment: this.path.firstSegment, + isFirst: true + }; + } else { + this.hitResult = lineMode.findLineEnd(event.point, this.path); + if (this.hitResult) { + const hitPath = this.hitResult.path; + hitPath.setSelected(true); + if (this.hitResult.isFirst) { + hitPath.firstSegment.setSelected(true); + } else { + hitPath.lastSegment.setSelected(true); + } + } + } + + // snapping + if (this.path) { + if (this.hitResult) { + this.path.lastSegment.point = this.hitResult.segment.point; + } else { + this.path.lastSegment.point = event.point; + } + } + }; + + + this.tool.onMouseUp = function (event) { + if (event.event.button > 0) return; // only first mouse button + + // If I single clicked, don't do anything + if (this.path.segments.length < 2 || (this.path.segments.length === 2 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared())) { + this.path.remove(); + this.path = null; + // TODO don't erase the line if both ends are snapped to different points + return; + } else if (this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < lineMode.toleranceSquared()) { + this.path.removeSegment(this.path.segments.length - 1); + return; + } + + // If I intersect other line end points, join or close + if (this.hitResult) { + this.path.removeSegment(this.path.segments.length - 1); + if (this.path.firstSegment === this.hitResult.segment) { + // close path + this.path.closed = true; + this.path.setSelected(false); + } else { + // joining two paths + if (!this.hitResult.isFirst) { + this.hitResult.path.reverse(); + } + this.path.join(this.hitResult.path); + } + this.hitResult = null; + } + + // TODO add back undo + // if (this.path) { + // pg.undo.snapshot('line'); + // } + + }; + + this.tool.activate(); + } + toleranceSquared () { + return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2); + } + findLineEnd (point, excludePath) { + const lines = paper.project.getItems({ + class: paper.Path + }); + // Prefer more recent lines + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].closed) { + continue; + } + if (excludePath && lines[i] === excludePath) { + continue; + } + if (lines[i].firstSegment && lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) { + return { + path: lines[i], + segment: lines[i].firstSegment, + isFirst: true + }; + } + if (lines[i].lastSegment && lines[i].lastSegment.point.getDistance(point, true) < this.toleranceSquared()) { + return { + path: lines[i], + segment: lines[i].lastSegment, + isFirst: false + }; + } + } + return null; + } + deactivateTool () { + if (this.path) { + this.path.setSelected(false); + this.path = null; + } + } + render () { + return ( + + ); + } +} + +LineMode.propTypes = { + handleMouseDown: PropTypes.func.isRequired, + isLineModeActive: PropTypes.bool.isRequired, + lineModeState: PropTypes.shape({ + lineWidth: PropTypes.number.isRequired + }) +}; + +const mapStateToProps = state => ({ + lineModeState: state.lineMode, + isLineModeActive: state.mode === Modes.LINE +}); +const mapDispatchToProps = dispatch => ({ + handleMouseDown: () => { + dispatch(changeMode(Modes.LINE)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(LineMode); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 303546ba..772d8985 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -29,6 +29,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.ERASER)); } else if (event.key === 'b') { dispatch(changeMode(Modes.BRUSH)); + } else if (event.key === 'l') { + dispatch(changeMode(Modes.LINE)); } } }); diff --git a/src/modes/modes.js b/src/modes/modes.js index c485f0fe..c1b64738 100644 --- a/src/modes/modes.js +++ b/src/modes/modes.js @@ -2,7 +2,8 @@ import keyMirror from 'keymirror'; const Modes = keyMirror({ BRUSH: null, - ERASER: null + ERASER: null, + LINE: null }); export default Modes; diff --git a/src/reducers/line-mode.js b/src/reducers/line-mode.js new file mode 100644 index 00000000..e2c780fe --- /dev/null +++ b/src/reducers/line-mode.js @@ -0,0 +1,11 @@ +const initialState = {lineWidth: 2}; + +const reducer = function (state) { + if (typeof state === 'undefined') state = initialState; + return state; +}; + +// Action creators ================================== + + +export default reducer; diff --git a/translations/messages/src/components/brush-mode.json b/translations/messages/src/components/brush-mode.json index cc898d9b..fb8440ae 100644 --- a/translations/messages/src/components/brush-mode.json +++ b/translations/messages/src/components/brush-mode.json @@ -4,4 +4,4 @@ "description": "Label for the brush tool", "defaultMessage": "Brush" } -] +] \ No newline at end of file diff --git a/translations/messages/src/components/line-mode.json b/translations/messages/src/components/line-mode.json new file mode 100644 index 00000000..8c9c98e1 --- /dev/null +++ b/translations/messages/src/components/line-mode.json @@ -0,0 +1,7 @@ +[ + { + "id": "paint.lineMode.line", + "description": "Label for the line tool, which draws straight line segments", + "defaultMessage": "Line" + } +] \ No newline at end of file From a622d0d3e9672d6c2a38e999639fb14697414304 Mon Sep 17 00:00:00 2001 From: Date: Thu, 24 Aug 2017 17:49:22 -0400 Subject: [PATCH 02/10] add tests --- package.json | 2 +- src/components/paint-editor.jsx | 2 +- src/containers/line-mode.jsx | 284 ++++++++++-------- src/reducers/line-mode.js | 28 +- .../unit/components/eraser-mode.test copy.jsx | 15 - 5 files changed, 185 insertions(+), 146 deletions(-) delete mode 100644 test/unit/components/eraser-mode.test copy.jsx diff --git a/package.json b/package.json index 1d7fd76a..9725942a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "eslint-config-import": "^0.13.0", "eslint-config-scratch": "^3.0.0", "eslint-plugin-import": "^2.7.0", - "eslint-plugin-react": "^7.0.1", + "eslint-plugin-react": "^7.3.0", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", "html-webpack-plugin": "2.28.0", "jest": "^20.0.4", diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 8f7ff57f..090b9bed 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -24,7 +24,7 @@ class PaintEditorComponent extends React.Component { - + ); } diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 0b892bfd..dd9216e8 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -3,6 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; +import {changeLineWidth} from '../reducers/line-mode'; import LineModeComponent from '../components/line-mode.jsx'; import {changeMode} from '../reducers/modes'; import paper from 'paper'; @@ -16,8 +17,13 @@ class LineMode extends React.Component { bindAll(this, [ 'activateTool', 'deactivateTool', + 'onMouseDown', + 'onMouseMove', + 'onMouseDrag', + 'onMouseUp', 'toleranceSquared', - 'findLineEnd' + 'findLineEnd', + 'onScroll' ]); } componentDidMount () { @@ -30,8 +36,6 @@ class LineMode extends React.Component { this.activateTool(); } else if (!nextProps.isLineModeActive && this.props.isLineModeActive) { this.deactivateTool(); - } else if (nextProps.isLineModeActive && this.props.isLineModeActive) { - this.blob.setOptions(nextProps.lineModeState); } } shouldComponentUpdate () { @@ -40,7 +44,7 @@ class LineMode extends React.Component { activateTool () { // TODO add back selection // pg.selection.clearSelection(); - + this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); this.path = null; @@ -56,58 +60,103 @@ class LineMode extends React.Component { const lineMode = this; this.tool.onMouseDown = function (event) { if (event.event.button > 0) return; // only first mouse button - - if (this.path) { - this.path.setSelected(false); - this.path = null; - } - - // If you click near a point, continue that line instead of making a new line - this.hitResult = lineMode.findLineEnd(event.point); - if (this.hitResult) { - this.path = this.hitResult.path; - if (this.hitResult.isFirst) { - this.path.reverse(); - } - this.path.lastSegment.setSelected(true); - this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged - this.path.lastSegment.handleOut = null; // Make sure line isn't curvy - this.path.lastSegment.handleIn = null; - } - - // If not near other path, start a new path - if (!this.path) { - this.path = new paper.Path(); - - // TODO add back style - // this.path = pg.stylebar.applyActiveToolbarStyle(path); - this.path.setStrokeColor('black'); - - this.path.setSelected(true); - this.path.add(event.point); - this.path.add(event.point); // Add second point, which is what will move when dragged - paper.view.draw(); - } + lineMode.onMouseDown(event); + }; + this.tool.onMouseMove = function (event) { + lineMode.onMouseMove(event); + }; + this.tool.onMouseDrag = function (event) { + if (event.event.button > 0) return; // only first mouse button + lineMode.onMouseDrag(event); + }; + this.tool.onMouseUp = function (event) { + if (event.event.button > 0) return; // only first mouse button + lineMode.onMouseUp(event); }; - this.tool.onMouseMove = function (event) { - // If near another path's endpoint, or this path's beginpoint, clip to it to suggest - // joining/closing the paths. - if (this.hitResult) { - this.hitResult.path.setSelected(false); - this.hitResult = null; - } + this.tool.activate(); + } + onMouseDown (event) { + // Deselect old path + if (this.path) { + this.path.setSelected(false); + this.path = null; + } - if (this.path && !this.path.closed && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) { - this.hitResult = { - path: this.path, - segment: this.path.firstSegment, - isFirst: true - }; + // If you click near a point, continue that line instead of making a new line + this.hitResult = this.findLineEnd(event.point); + if (this.hitResult) { + this.path = this.hitResult.path; + if (this.hitResult.isFirst) { + this.path.reverse(); + } + this.path.lastSegment.setSelected(true); + this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged + this.path.lastSegment.handleOut = null; // Make sure line isn't curvy + this.path.lastSegment.handleIn = null; + } + + // If not near other path, start a new path + if (!this.path) { + this.path = new paper.Path(); + + // TODO add back style + // this.path = pg.stylebar.applyActiveToolbarStyle(path); + this.path.setStrokeColor('black'); + this.path.setStrokeWidth(this.props.lineModeState.lineWidth); + + this.path.setSelected(true); + this.path.add(event.point); + this.path.add(event.point); // Add second point, which is what will move when dragged + paper.view.draw(); + } + } + onMouseMove (event) { + // If near another path's endpoint, or this path's beginpoint, clip to it to suggest + // joining/closing the paths. + if (this.hitResult) { + this.hitResult.path.setSelected(false); + this.hitResult = null; + } + + if (this.path && + !this.path.closed && + this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) { + this.hitResult = { + path: this.path, + segment: this.path.firstSegment, + isFirst: true + }; + } else { + this.hitResult = this.findLineEnd(event.point); + } + + if (this.hitResult) { + const hitPath = this.hitResult.path; + hitPath.setSelected(true); + if (this.hitResult.isFirst) { + hitPath.firstSegment.setSelected(true); } else { - this.hitResult = lineMode.findLineEnd(event.point); + hitPath.lastSegment.setSelected(true); } + } + } + onMouseDrag (event) { + // If near another path's endpoint, or this path's beginpoint, clip to it to suggest + // joining/closing the paths. + if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false); + this.hitResult = null; + if (this.path && + this.path.segments.length > 3 && + this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) { + this.hitResult = { + path: this.path, + segment: this.path.firstSegment, + isFirst: true + }; + } else { + this.hitResult = this.findLineEnd(event.point, this.path); if (this.hitResult) { const hitPath = this.hitResult.path; hitPath.setSelected(true); @@ -117,84 +166,54 @@ class LineMode extends React.Component { hitPath.lastSegment.setSelected(true); } } - }; - - this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button - // If near another path's endpoint, or this path's beginpoint, clip to it to suggest - // joining/closing the paths. - if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false); - this.hitResult = null; + } - if (this.path && this.path.segments.length > 3 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared()) { - this.hitResult = { - path: this.path, - segment: this.path.firstSegment, - isFirst: true - }; - } else { - this.hitResult = lineMode.findLineEnd(event.point, this.path); - if (this.hitResult) { - const hitPath = this.hitResult.path; - hitPath.setSelected(true); - if (this.hitResult.isFirst) { - hitPath.firstSegment.setSelected(true); - } else { - hitPath.lastSegment.setSelected(true); - } - } - } - - // snapping - if (this.path) { - if (this.hitResult) { - this.path.lastSegment.point = this.hitResult.segment.point; - } else { - this.path.lastSegment.point = event.point; - } - } - }; - - - this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button - - // If I single clicked, don't do anything - if (this.path.segments.length < 2 || (this.path.segments.length === 2 && this.path.firstSegment.point.getDistance(event.point, true) < lineMode.toleranceSquared())) { - this.path.remove(); - this.path = null; - // TODO don't erase the line if both ends are snapped to different points - return; - } else if (this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < lineMode.toleranceSquared()) { - this.path.removeSegment(this.path.segments.length - 1); - return; - } - - // If I intersect other line end points, join or close + // snapping + if (this.path) { if (this.hitResult) { - this.path.removeSegment(this.path.segments.length - 1); - if (this.path.firstSegment === this.hitResult.segment) { - // close path - this.path.closed = true; - this.path.setSelected(false); - } else { - // joining two paths - if (!this.hitResult.isFirst) { - this.hitResult.path.reverse(); - } - this.path.join(this.hitResult.path); - } - this.hitResult = null; + this.path.lastSegment.point = this.hitResult.segment.point; + } else { + this.path.lastSegment.point = event.point; } + } + } + onMouseUp (event) { + // If I single clicked, don't do anything + if (this.path.segments.length < 2 || + (this.path.segments.length === 2 && + this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared())) { + this.path.remove(); + this.path = null; + // TODO don't erase the line if both ends are snapped to different points + return; + } else if ( + this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < + this.toleranceSquared()) { + this.path.removeSegment(this.path.segments.length - 1); + return; + } + + // If I intersect other line end points, join or close + if (this.hitResult) { + this.path.removeSegment(this.path.segments.length - 1); + if (this.path.firstSegment === this.hitResult.segment) { + // close path + this.path.closed = true; + this.path.setSelected(false); + } else { + // joining two paths + if (!this.hitResult.isFirst) { + this.hitResult.path.reverse(); + } + this.path.join(this.hitResult.path); + } + this.hitResult = null; + } - // TODO add back undo - // if (this.path) { - // pg.undo.snapshot('line'); - // } - - }; - - this.tool.activate(); + // TODO add back undo + // if (this.path) { + // pg.undo.snapshot('line'); + // } } toleranceSquared () { return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2); @@ -211,7 +230,8 @@ class LineMode extends React.Component { if (excludePath && lines[i] === excludePath) { continue; } - if (lines[i].firstSegment && lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) { + if (lines[i].firstSegment && + lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) { return { path: lines[i], segment: lines[i].firstSegment, @@ -229,11 +249,20 @@ class LineMode extends React.Component { return null; } deactivateTool () { + this.props.canvas.removeEventListener('mousewheel', this.onScroll); if (this.path) { this.path.setSelected(false); this.path = null; } } + onScroll (event) { + if (event.deltaY < 0) { + this.props.changeLineWidth(this.props.lineModeState.lineWidth + 1); + } else if (event.deltaY > 0 && this.props.lineModeState.lineWidth > 1) { + this.props.changeLineWidth(this.props.lineModeState.lineWidth - 1); + } + return true; + } render () { return ( @@ -242,6 +271,8 @@ class LineMode extends React.Component { } LineMode.propTypes = { + canvas: PropTypes.instanceOf(Element).isRequired, + changeLineWidth: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, isLineModeActive: PropTypes.bool.isRequired, lineModeState: PropTypes.shape({ @@ -254,6 +285,9 @@ const mapStateToProps = state => ({ isLineModeActive: state.mode === Modes.LINE }); const mapDispatchToProps = dispatch => ({ + changeLineWidth: lineWidth => { + dispatch(changeLineWidth(lineWidth)); + }, handleMouseDown: () => { dispatch(changeMode(Modes.LINE)); } diff --git a/src/reducers/line-mode.js b/src/reducers/line-mode.js index e2c780fe..8845ace4 100644 --- a/src/reducers/line-mode.js +++ b/src/reducers/line-mode.js @@ -1,11 +1,31 @@ +import log from '../log/log'; + +const CHANGE_LINE_WIDTH = 'scratch-paint/line-mode/CHANGE_LINE_WIDTH'; const initialState = {lineWidth: 2}; -const reducer = function (state) { +const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; - return state; + switch (action.type) { + case CHANGE_LINE_WIDTH: + if (isNaN(action.lineWidth)) { + log.warn(`Invalid line width: ${action.lineWidth}`); + return state; + } + return {lineWidth: Math.max(1, action.lineWidth)}; + default: + return state; + } }; // Action creators ================================== +const changeLineWidth = function (lineWidth) { + return { + type: CHANGE_LINE_WIDTH, + lineWidth: lineWidth + }; +}; - -export default reducer; +export { + reducer as default, + changeLineWidth +}; diff --git a/test/unit/components/eraser-mode.test copy.jsx b/test/unit/components/eraser-mode.test copy.jsx deleted file mode 100644 index 4772581c..00000000 --- a/test/unit/components/eraser-mode.test copy.jsx +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars -import {shallow} from 'enzyme'; -import EraserModeComponent from '../../../src/components/eraser-mode.jsx'; // eslint-disable-line no-unused-vars - -describe('EraserModeComponent', () => { - test('triggers callback when clicked', () => { - const onClick = jest.fn(); - const componentShallowWrapper = shallow( - - ); - componentShallowWrapper.simulate('click'); - expect(onClick).toHaveBeenCalled(); - }); -}); From 34ce004a14eb905c5f5abbc012903af37fb2d271 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 25 Aug 2017 10:45:22 -0400 Subject: [PATCH 03/10] Add tests --- test/unit/components/eraser-mode.test.jsx | 15 +++++++++++ test/unit/components/line-mode.test.jsx | 15 +++++++++++ test/unit/line-mode-reducer.test.js | 31 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 test/unit/components/eraser-mode.test.jsx create mode 100644 test/unit/components/line-mode.test.jsx create mode 100644 test/unit/line-mode-reducer.test.js diff --git a/test/unit/components/eraser-mode.test.jsx b/test/unit/components/eraser-mode.test.jsx new file mode 100644 index 00000000..4772581c --- /dev/null +++ b/test/unit/components/eraser-mode.test.jsx @@ -0,0 +1,15 @@ +/* eslint-env jest */ +import React from 'react'; // eslint-disable-line no-unused-vars +import {shallow} from 'enzyme'; +import EraserModeComponent from '../../../src/components/eraser-mode.jsx'; // eslint-disable-line no-unused-vars + +describe('EraserModeComponent', () => { + test('triggers callback when clicked', () => { + const onClick = jest.fn(); + const componentShallowWrapper = shallow( + + ); + componentShallowWrapper.simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/components/line-mode.test.jsx b/test/unit/components/line-mode.test.jsx new file mode 100644 index 00000000..7be2ff14 --- /dev/null +++ b/test/unit/components/line-mode.test.jsx @@ -0,0 +1,15 @@ +/* eslint-env jest */ +import React from 'react'; // eslint-disable-line no-unused-vars +import {shallow} from 'enzyme'; +import LineModeComponent from '../../../src/components/line-mode.jsx'; // eslint-disable-line no-unused-vars + +describe('LineModeComponent', () => { + test('triggers callback when clicked', () => { + const onClick = jest.fn(); + const componentShallowWrapper = shallow( + + ); + componentShallowWrapper.simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/line-mode-reducer.test.js b/test/unit/line-mode-reducer.test.js new file mode 100644 index 00000000..cd24cb99 --- /dev/null +++ b/test/unit/line-mode-reducer.test.js @@ -0,0 +1,31 @@ +/* eslint-env jest */ +import lineReducer from '../../src/reducers/line-mode'; +import {changeLineWidth} from '../../src/reducers/line-mode'; + +test('initialState', () => { + let defaultState; + + expect(lineReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); + expect(lineReducer(defaultState /* state */, {type: 'anything'} /* action */).lineWidth).toBeGreaterThan(0); +}); + +test('changeLineWidth', () => { + let defaultState; + const newLineWidth = 8078; + + expect(lineReducer(defaultState /* state */, changeLineWidth(newLineWidth) /* action */)) + .toEqual({lineWidth: newLineWidth}); + expect(lineReducer(2 /* state */, changeLineWidth(newLineWidth) /* action */)) + .toEqual({lineWidth: newLineWidth}); + expect(lineReducer(2 /* state */, changeLineWidth(-1) /* action */)) + .toEqual({lineWidth: 1}); +}); + +test('invalidChangeLineWidth', () => { + const origState = {lineWidth: 2}; + + expect(lineReducer(origState /* state */, changeLineWidth('invalid argument') /* action */)) + .toBe(origState); + expect(lineReducer(origState /* state */, changeLineWidth() /* action */)) + .toBe(origState); +}); From dd27974d071ccdaccec5aa51c4c82940c1fa317e Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 25 Aug 2017 14:00:30 -0400 Subject: [PATCH 04/10] clean up in deactivate --- src/containers/line-mode.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 9b61546c..1a84c394 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -250,6 +250,9 @@ class LineMode extends React.Component { } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); + this.tool.remove(); + this.tool = null; + this.hitResult = null; if (this.path) { this.path.setSelected(false); this.path = null; From 2c64c4510157534d96630bac68d7cffb87278d62 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 29 Aug 2017 17:24:40 -0400 Subject: [PATCH 05/10] fix lint errors --- src/containers/blob/blob.js | 6 +++--- src/containers/blob/broad-brush-helper.js | 2 +- src/containers/blob/segment-brush-helper.js | 6 +++--- test/unit/modes-reducer.test.js | 2 +- webpack.config.js | 15 +++++++-------- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index b06199b8..bc7f63d1 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -64,7 +64,7 @@ class Blobbiness { this.tool.onMouseDown = function (event) { blob.resizeCursorIfNeeded(event.point); - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button if (blob.options.brushSize < Blobbiness.THRESHOLD) { blob.brush = Blobbiness.BROAD; @@ -80,7 +80,7 @@ class Blobbiness { this.tool.onMouseDrag = function (event) { blob.resizeCursorIfNeeded(event.point); - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button if (blob.brush === Blobbiness.BROAD) { blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options); } else if (blob.brush === Blobbiness.SEGMENT) { @@ -96,7 +96,7 @@ class Blobbiness { this.tool.onMouseUp = function (event) { blob.resizeCursorIfNeeded(event.point); - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button let lastPath; if (blob.brush === Blobbiness.BROAD) { diff --git a/src/containers/blob/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js index b3bc1ca3..c9cfee25 100644 --- a/src/containers/blob/broad-brush-helper.js +++ b/src/containers/blob/broad-brush-helper.js @@ -22,7 +22,7 @@ class BroadBrushHelper { onBroadMouseDown (event, tool, options) { tool.minDistance = options.brushSize / 2; tool.maxDistance = options.brushSize; - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button this.finalPath = new paper.Path(); stylePath(this.finalPath, options.isEraser); diff --git a/src/containers/blob/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js index 88f7debd..c745b9d8 100644 --- a/src/containers/blob/segment-brush-helper.js +++ b/src/containers/blob/segment-brush-helper.js @@ -22,7 +22,7 @@ class SegmentBrushHelper { } onSegmentMouseDown (event, tool, options) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button tool.minDistance = 1; tool.maxDistance = options.brushSize; @@ -37,7 +37,7 @@ class SegmentBrushHelper { } onSegmentMouseDrag (event, tool, options) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button const step = (event.delta).normalize(options.brushSize / 2); const handleVec = step.clone(); @@ -75,7 +75,7 @@ class SegmentBrushHelper { } onSegmentMouseUp (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button // TODO: This smoothing tends to cut off large portions of the path! Would like to eventually // add back smoothing, maybe a custom implementation that only applies to a subset of the line? diff --git a/test/unit/modes-reducer.test.js b/test/unit/modes-reducer.test.js index 8f08ffc9..df81f64c 100644 --- a/test/unit/modes-reducer.test.js +++ b/test/unit/modes-reducer.test.js @@ -19,6 +19,6 @@ test('changeMode', () => { test('invalidChangeMode', () => { expect(reducer(Modes.BRUSH /* state */, changeMode('non-existant mode') /* action */)) - .toBe(Modes.BRUSH); + .toBe(Modes.BRUSH); expect(reducer(Modes.BRUSH /* state */, changeMode() /* action */)).toBe(Modes.BRUSH); }); diff --git a/webpack.config.js b/webpack.config.js index 2b093a84..fd5935d6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,3 @@ - const defaultsDeep = require('lodash.defaultsdeep'); const path = require('path'); const webpack = require('webpack'); @@ -53,12 +52,12 @@ const base = { }] }, plugins: [] - .concat(process.env.NODE_ENV === 'production' ? [ - new webpack.optimize.UglifyJsPlugin({ - include: /\.min\.js$/, - minimize: true - }) - ] : []) + .concat(process.env.NODE_ENV === 'production' ? [ + new webpack.optimize.UglifyJsPlugin({ + include: /\.min\.js$/, + minimize: true + }) + ] : []) }; module.exports = [ @@ -96,7 +95,7 @@ module.exports = [ output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js', - libraryTarget: 'commonjs2', + libraryTarget: 'commonjs2' } }) ]; From 633ecd2399409515221a1486976ddb7964990a1c Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 30 Aug 2017 10:50:05 -0400 Subject: [PATCH 06/10] load an svg string --- src/components/paint-editor.jsx | 10 ++++++++- src/containers/paint-editor.jsx | 27 +++++++++++++++++++++-- src/containers/paper-canvas.jsx | 39 +++++++++++++++++++++------------ src/playground/playground.jsx | 9 +++++++- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 1459fe35..faef8b9b 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushMode from '../containers/brush-mode.jsx'; import EraserMode from '../containers/eraser-mode.jsx'; +import PropTypes from 'prop-types'; class PaintEditorComponent extends React.Component { constructor (props) { @@ -20,7 +21,10 @@ class PaintEditorComponent extends React.Component { if (this.state.canvas) { return (
- +
@@ -34,4 +38,8 @@ class PaintEditorComponent extends React.Component { } } +PaintEditorComponent.propTypes = { + svg: PropTypes.string +}; + export default PaintEditorComponent; diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 303546ba..219e3fe9 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -4,23 +4,46 @@ import PaintEditorComponent from '../components/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; class PaintEditor extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'updateSvg' + ]); + } componentDidMount () { document.addEventListener('keydown', this.props.onKeyPress); } + shouldComponentUpdate (newProps) { + return newProps.assetId !== this.props.assetId; + } componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); } + updateSvg (svg) { + if (!this.props.onUpdate) { + return; + } + this.props.onUpdate( + this.props.assetIndex, + svg + ); + } render () { return ( - + ); } } PaintEditor.propTypes = { - onKeyPress: PropTypes.func.isRequired + assetId: PropTypes.string, + assetIndex: PropTypes.number, + onKeyPress: PropTypes.func.isRequired, + onUpdate: PropTypes.func, + svg: PropTypes.string }; const mapDispatchToProps = dispatch => ({ diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 4ccb0b58..f44bc10d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -7,27 +7,37 @@ class PaperCanvas extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'setCanvas' + 'setCanvas', + 'importSvg' ]); } componentDidMount () { paper.setup(this.canvas); - // Create a Paper.js Path to draw a line into it: - const path = new paper.Path(); - // Give the stroke a color - path.strokeColor = 'black'; - const start = new paper.Point(100, 100); - // Move to start and draw a line from there - path.moveTo(start); - // Note that the plus operator on Point objects does not work - // in JavaScript. Instead, we need to call the add() function: - path.lineTo(start.add([200, -50])); - // Draw the view now: - paper.view.draw(); + if (this.props.svg) { + this.importSvg(this.props.svg); + } + } + componentWillReceiveProps (newProps) { + if (newProps.svg !== this.props.svg) { + paper.project.activeLayer.removeChildren(); + this.importSvg(newProps.svg); + } } componentWillUnmount () { paper.remove(); } + importSvg (svg) { + paper.project.importSVG(svg, + { + expandShapes: true, + onLoad: function (item) { + while (item.reduce() !== item) { + item = item.reduce(); + } + } + }); + paper.project.view.update(); + } setCanvas (canvas) { this.canvas = canvas; if (this.props.canvasRef) { @@ -44,7 +54,8 @@ class PaperCanvas extends React.Component { } PaperCanvas.propTypes = { - canvasRef: PropTypes.func + canvasRef: PropTypes.func, + svg: PropTypes.string }; export default PaperCanvas; diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 9f68704a..a3f2a15f 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -13,10 +13,17 @@ const store = createStore( intlInitialState, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); +const svgString = + '' + + '' + + '' + + '' + + '' + + ''; ReactDOM.render(( - + ), appTarget); From c228a191cb4b44cf0033c4519fe62004bc413b54 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 30 Aug 2017 10:56:30 -0400 Subject: [PATCH 07/10] fix lint --- src/containers/line-mode.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 1a84c394..030b33ba 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -59,18 +59,18 @@ class LineMode extends React.Component { const lineMode = this; this.tool.onMouseDown = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button lineMode.onMouseDown(event); }; this.tool.onMouseMove = function (event) { lineMode.onMouseMove(event); }; this.tool.onMouseDrag = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button lineMode.onMouseDrag(event); }; this.tool.onMouseUp = function (event) { - if (event.event.button > 0) return; // only first mouse button + if (event.event.button > 0) return; // only first mouse button lineMode.onMouseUp(event); }; @@ -187,8 +187,8 @@ class LineMode extends React.Component { // TODO don't erase the line if both ends are snapped to different points return; } else if ( - this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < - this.toleranceSquared()) { + this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) < + this.toleranceSquared()) { this.path.removeSegment(this.path.segments.length - 1); return; } From b0d29a946ba3ce7bafba3ecfaecd804e92792cc0 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 30 Aug 2017 18:43:34 -0400 Subject: [PATCH 08/10] Pipe through updateSvg and call it whenever a tool finishes an action --- src/components/paint-editor.jsx | 16 +++++++++++++--- src/containers/blob/blob.js | 9 ++++++++- src/containers/brush-mode.jsx | 5 +++-- src/containers/eraser-mode.jsx | 5 +++-- src/containers/line-mode.jsx | 4 +++- src/containers/paint-editor.jsx | 24 +++++++++++------------- src/playground/playground.jsx | 8 +++++++- 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index cb07fcd1..8cdaaf69 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -26,9 +26,18 @@ class PaintEditorComponent extends React.Component { canvasRef={this.setCanvas} svg={this.props.svg} /> - - - + + + ); } @@ -41,6 +50,7 @@ class PaintEditorComponent extends React.Component { } PaintEditorComponent.propTypes = { + onUpdateSvg: PropTypes.func.isRequired, svg: PropTypes.string }; diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index bc7f63d1..22f4cda5 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -24,9 +24,13 @@ class Blobbiness { return 9; } - constructor () { + /** + * @param {function} updateCallback call when the drawing has changed to let listeners know + */ + constructor (updateCallback) { this.broadBrushHelper = new BroadBrushHelper(); this.segmentBrushHelper = new SegmentBrushHelper(); + this.updateCallback = updateCallback; } /** @@ -113,6 +117,9 @@ class Blobbiness { blob.mergeBrush(lastPath); } + blob.cursorPreview.visible = false; + blob.updateCallback(); + blob.cursorPreview.visible = true; blob.cursorPreview.bringToFront(); blob.cursorPreview.position = event.point; diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index e751030f..c89e08f9 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -16,7 +16,7 @@ class BrushMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(); + this.blob = new Blobbiness(this.props.onUpdateSvg); } componentDidMount () { if (this.props.isBrushModeActive) { @@ -70,7 +70,8 @@ BrushMode.propTypes = { canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired, - isBrushModeActive: PropTypes.bool.isRequired + isBrushModeActive: PropTypes.bool.isRequired, + onUpdateSvg: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index af932329..0972ed36 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -16,7 +16,7 @@ class EraserMode extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new Blobbiness(); + this.blob = new Blobbiness(this.props.onUpdateSvg); } componentDidMount () { if (this.props.isEraserModeActive) { @@ -66,7 +66,8 @@ EraserMode.propTypes = { brushSize: PropTypes.number.isRequired }), handleMouseDown: PropTypes.func.isRequired, - isEraserModeActive: PropTypes.bool.isRequired + isEraserModeActive: PropTypes.bool.isRequired, + onUpdateSvg: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 030b33ba..7648c045 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -209,6 +209,7 @@ class LineMode extends React.Component { } this.hitResult = null; } + this.props.onUpdateSvg(); // TODO add back undo // if (this.path) { @@ -280,7 +281,8 @@ LineMode.propTypes = { isLineModeActive: PropTypes.bool.isRequired, lineModeState: PropTypes.shape({ lineWidth: PropTypes.number.isRequired - }) + }), + onUpdateSvg: PropTypes.func.isRequired }; const mapStateToProps = state => ({ diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index cb2a5af9..ac081516 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -5,44 +5,42 @@ import {changeMode} from '../reducers/modes'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; +import paper from 'paper'; class PaintEditor extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'updateSvg' + 'handleUpdateSvg' ]); } componentDidMount () { document.addEventListener('keydown', this.props.onKeyPress); } - shouldComponentUpdate (newProps) { - return newProps.assetId !== this.props.assetId; - } componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); } - updateSvg (svg) { - if (!this.props.onUpdate) { + handleUpdateSvg () { + if (!this.props.onUpdateSvg) { return; } - this.props.onUpdate( - this.props.assetIndex, - svg + this.props.onUpdateSvg( + paper.project.exportSVG({asString: true}) // TODO can this be made independent of paper ); } render () { return ( - + ); } } PaintEditor.propTypes = { - assetId: PropTypes.string, - assetIndex: PropTypes.number, onKeyPress: PropTypes.func.isRequired, - onUpdate: PropTypes.func, + onUpdateSvg: PropTypes.func.isRequired, svg: PropTypes.string }; diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index a3f2a15f..39cdc45c 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -20,10 +20,16 @@ const svgString = '' + '' + ''; +const onUpdateSvg = function () { + return; +}; ReactDOM.render(( - + ), appTarget); From 36016bbd117d8f214b4e54b822e4d90c3c33e2e4 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 5 Sep 2017 15:45:56 -0400 Subject: [PATCH 09/10] Handle removing the viewbox and centering --- src/components/paint-editor.jsx | 4 ++++ src/containers/paint-editor.jsx | 13 +++++++++++-- src/containers/paper-canvas.jsx | 29 ++++++++++++++++++++++------- src/playground/playground.jsx | 19 ++++++++++++------- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 8cdaaf69..306bde8f 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -24,6 +24,8 @@ class PaintEditorComponent extends React.Component {
@@ -41,6 +48,8 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { onKeyPress: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, + rotationCenterX: PropTypes.number, + rotationCenterY: PropTypes.number, svg: PropTypes.string }; diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index f44bc10d..fdd6e44d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -14,28 +14,41 @@ class PaperCanvas extends React.Component { componentDidMount () { paper.setup(this.canvas); if (this.props.svg) { - this.importSvg(this.props.svg); + this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY); } } componentWillReceiveProps (newProps) { - if (newProps.svg !== this.props.svg) { - paper.project.activeLayer.removeChildren(); - this.importSvg(newProps.svg); - } + paper.project.activeLayer.removeChildren(); + this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY); } componentWillUnmount () { paper.remove(); } - importSvg (svg) { - paper.project.importSVG(svg, + importSvg (svg, rotationCenterX, rotationCenterY) { + const imported = paper.project.importSVG(svg, { expandShapes: true, onLoad: function (item) { + // Remove viewbox + if (item.clipped) { + item.clipped = false; + // Consider removing clip mask here? + } while (item.reduce() !== item) { item = item.reduce(); } } }); + if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') { + imported.position = + paper.project.view.center + .add(imported.bounds.width / 2, imported.bounds.height / 2) + .subtract(rotationCenterX, rotationCenterY); + } else { + // Center + imported.position = paper.project.view.center; + } + paper.project.view.update(); } setCanvas (canvas) { @@ -55,6 +68,8 @@ class PaperCanvas extends React.Component { PaperCanvas.propTypes = { canvasRef: PropTypes.func, + rotationCenterX: PropTypes.number, + rotationCenterY: PropTypes.number, svg: PropTypes.string }; diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 39cdc45c..f59ef2b0 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -14,19 +14,24 @@ const store = createStore( window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); const svgString = - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + ''; -const onUpdateSvg = function () { - return; +const onUpdateSvg = function (newSvgString, rotationCenterX, rotationCenterY) { + console.log(newSvgString); + console.log(`rotationCenterX: ${rotationCenterX} rotationCenterY: ${rotationCenterY}`); }; ReactDOM.render(( From 23693f3764f9f04577b43d92161b126a4913e2cf Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 5 Sep 2017 18:00:41 -0400 Subject: [PATCH 10/10] get rid of null check, since onUpdateSvg is required --- src/containers/paint-editor.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index eefca91d..ba260313 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -21,9 +21,6 @@ class PaintEditor extends React.Component { document.removeEventListener('keydown', this.props.onKeyPress); } handleUpdateSvg () { - if (!this.props.onUpdateSvg) { - return; - } const bounds = paper.project.activeLayer.bounds; this.props.onUpdateSvg( paper.project.exportSVG({