From 014907ba98417a2a9465b7ee3ef8396f11a04165 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 20 Jul 2017 22:48:07 -0400 Subject: [PATCH 01/19] get broad brush working --- src/components/paint-editor.jsx | 12 +- src/containers/paint-editor.jsx | 7 +- src/containers/paper-canvas.jsx | 3 +- src/containers/tools/brush-tool.jsx | 102 +++++++++ src/reducers/brush-tool.js | 25 +++ src/reducers/combine-reducers.js | 3 +- src/tools/blob.js | 327 ++++++++++++++++++++++++++++ src/tools/broad-brush-helper.js | 107 +++++++++ src/tools/eraser.js | 44 ++++ 9 files changed, 622 insertions(+), 8 deletions(-) create mode 100644 src/containers/tools/brush-tool.jsx create mode 100644 src/reducers/brush-tool.js create mode 100644 src/tools/blob.js create mode 100644 src/tools/broad-brush-helper.js create mode 100644 src/tools/eraser.js diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index db93953f..3ab5fb07 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -1,14 +1,20 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; +import BrushTool from '../containers/tools/brush-tool.jsx'; const PaintEditorComponent = props => ( - +
+ + +
); PaintEditorComponent.propTypes = { + canvasId: PropTypes.string.isRequired, tool: PropTypes.shape({ name: PropTypes.string.isRequired }) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index efe05522..c9c0bcdf 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; -import tools from '../reducers/tools'; +import ToolsReducer from '../reducers/tools'; import ToolTypes from '../tools/tool-types.js'; import {connect} from 'react-redux'; @@ -16,6 +16,7 @@ class PaintEditor extends React.Component { render () { return ( ); @@ -35,9 +36,9 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ onKeyPress: e => { if (e.key === 'e') { - dispatch(tools.changeTool(ToolTypes.ERASER)); + dispatch(ToolsReducer.changeTool(ToolTypes.ERASER)); } else if (e.key === 'b') { - dispatch(tools.changeTool(ToolTypes.BRUSH)); + dispatch(ToolsReducer.changeTool(ToolTypes.BRUSH)); } } }); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 5d813a0f..5d08911d 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -33,12 +33,13 @@ class PaperCanvas extends React.Component { } render () { return ( - + ); } } PaperCanvas.propTypes = { + canvasId: PropTypes.string.isRequired, tool: PropTypes.shape({ name: PropTypes.string.isRequired }) diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/tools/brush-tool.jsx new file mode 100644 index 00000000..604b56eb --- /dev/null +++ b/src/containers/tools/brush-tool.jsx @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; +import ToolTypes from '../../tools/tool-types.js'; +import BlobTool from '../../tools/blob.js'; +import BrushToolReducer from '../../reducers/brush-tool'; +import paper from 'paper'; + +class BrushTool extends React.Component { + static get TOOL_TYPE () { + return ToolTypes.BRUSH; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'onScroll' + ]); + this.blob = new BlobTool(); + } + componentDidMount () { + if (this.props.tool === BrushTool.TOOL_TYPE) { + this.activateTool(); + } + } + componentWillReceiveProps (nextProps) { + if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool !== BrushTool.TOOL_TYPE) { + this.activateTool(); + } else if (nextProps.tool !== BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) { + this.deactivateTool(); + } else if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) { + this.blob.setOptions(nextProps.brushToolState); + } + } + shouldComponentUpdate () { + return false; // Logic only component + } + activateTool () { + document.getElementById(this.props.canvasId) + .addEventListener('mousewheel', this.onScroll); + + const tool = new paper.Tool(); + this.blob.activateTool(false /* isEraser */, tool, this.props.brushToolState); + + // // Make sure a fill color is set on the brush + // if(!pg.stylebar.getFillColor()) { + // pg.stylebar.setFillColor(pg.stylebar.getStrokeColor()); + // pg.stylebar.setStrokeColor(null); + // } + + // // setup floating tool options panel in the editor + // pg.toolOptionPanel.setup(options, components, function() {}); + + tool.activate(); + } + deactivateTool () { + document.getElementById(this.props.canvasId) + .removeEventListener('mousewheel', this.onScroll); + this.blob.deactivateTool(); + } + onScroll (event) { + if (event.deltaY < 0) { + this.props.changeBrushSize(this.props.brushToolState.brushSize + 1); + } else if (event.deltaY > 0 && this.props.brushToolState.brushSize > 1) { + this.props.changeBrushSize(this.props.brushToolState.brushSize - 1); + } + return false; + } + render () { + return ( +
+ ); + } +} + +BrushTool.propTypes = { + brushToolState: PropTypes.shape({ + brushSize: PropTypes.number.isRequired + }), + canvasId: PropTypes.string.isRequired, + changeBrushSize: PropTypes.func.isRequired, + tool: PropTypes.shape({ + name: PropTypes.string.isRequired + }) +}; + +const mapStateToProps = state => ({ + brushToolState: state.brushTool, + tool: state.tool +}); +const mapDispatchToProps = dispatch => ({ + changeBrushSize: brushSize => { + dispatch(BrushToolReducer.changeBrushSize(brushSize)); + } +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(BrushTool); diff --git a/src/reducers/brush-tool.js b/src/reducers/brush-tool.js new file mode 100644 index 00000000..7e59c560 --- /dev/null +++ b/src/reducers/brush-tool.js @@ -0,0 +1,25 @@ +const CHANGE_BRUSH_SIZE = 'scratch-paint/tools/CHANGE_BRUSH_SIZE'; +const initialState = {brushSize: 5}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_BRUSH_SIZE: + return {brushSize: Math.max(1, action.brushSize)}; + default: + return state; + } +}; + +// Action creators ================================== +reducer.changeBrushSize = function (brushSize) { + return { + type: CHANGE_BRUSH_SIZE, + brushSize: brushSize, + meta: { + throttle: 30 + } + }; +}; + +module.exports = reducer; diff --git a/src/reducers/combine-reducers.js b/src/reducers/combine-reducers.js index a533b763..c237d7bb 100644 --- a/src/reducers/combine-reducers.js +++ b/src/reducers/combine-reducers.js @@ -1,5 +1,6 @@ import {combineReducers} from 'redux'; module.exports = combineReducers({ - tool: require('./tools') + tool: require('./tools'), + brushTool: require('./brush-tool') }); diff --git a/src/tools/blob.js b/src/tools/blob.js new file mode 100644 index 00000000..ba5e86f5 --- /dev/null +++ b/src/tools/blob.js @@ -0,0 +1,327 @@ +const paper = require('paper'); +const log = require('../log/log'); +const broadBrushHelper = require('./broad-brush-helper'); + +class BlobTool { + + static get BROAD () { + return 'broadbrush'; + } + static get SEGMENT () { + return 'segmentbrush'; + } + + // brush size >= threshold use segment brush, else use broadbrush + // Segment brush has performance issues at low threshold, but broad brush has weird corners + // which are more obvious the bigger it is + static get THRESHOLD () { + return 100000; + } + + setOptions (options) { + console.log('setOptions'); + this.options = options; + if (this.cursorPreview) { + this.cursorPreview = new paper.Path.Circle({ + center: [this.cursorPreview.center.x, this.cursorPreview.center.y], + radius: options.brushSize / 2 + }); + } + } + + activateTool (isEraser, tool, options) { + console.log('activateTool isEraser?'+isEraser); + this.tool = tool; + this.options = options; + + let cursorPreview = this.cursorPreview = new paper.Path.Circle({ + center: [-10000, -10000], + radius: options.brushSize / 2 + }); + this.brushSize = options.brushSize; + + tool.stylePath = function (path) { + if (isEraser) { + path.fillColor = 'white'; + if (path === cursorPreview) { + path.strokeColor = 'cornflowerblue'; + path.strokeWidth = 1; + } + } else { + // TODO keep a separate active toolbar style for brush vs pen? + //path = pg.stylebar.applyActiveToolbarStyle(path); + + //TODO FIX + + path.fillColor = 'black'; + if (path === cursorPreview) { + path.strokeColor = 'cornflowerblue'; + path.strokeWidth = 1; + } + } + }; + + tool.stylePath(cursorPreview); + + tool.fixedDistance = 1; + + broadBrushHelper(tool, options); + // TODO add + //pg.segmentbrushhelper(tool, options); + tool.onMouseMove = function (event) { + if (this.brushSize !== options.brushSize) { + cursorPreview.remove(); + cursorPreview = new paper.Path.Circle({ + center: event.point, + radius: options.brushSize / 2 + }); + this.brushSize = options.brushSize; + } + tool.stylePath(cursorPreview); + cursorPreview.bringToFront(); + cursorPreview.position = event.point; + }; + + tool.onMouseDown = function (event) { + if (event.event.button > 0) return; // only first mouse button + + if (options.brushSize < BlobTool.THRESHOLD) { + this.brush = BlobTool.BROAD; + this.onBroadMouseDown(event); + } else { + this.brush = BlobTool.SEGMENT; + this.onSegmentMouseDown(event); + } + cursorPreview.bringToFront(); + cursorPreview.position = event.point; + paper.view.draw(); + }; + + tool.onMouseDrag = function (event) { + if (event.event.button > 0) return; // only first mouse button + if (this.brush === BlobTool.BROAD) { + this.onBroadMouseDrag(event); + } else if (this.brush === Blob.SEGMENT) { + this.onSegmentMouseDrag(event); + } else { + log.warning(`Brush type does not exist: ${this.brush}`); + } + + cursorPreview.bringToFront(); + cursorPreview.position = event.point; + paper.view.draw(); + }; + + tool.onMouseUp = function (event) { + if (event.event.button > 0) return; // only first mouse button + + let lastPath; + if (this.brush === BlobTool.BROAD) { + lastPath = this.onBroadMouseUp(event); + } else if (this.brush === BlobTool.SEGMENT) { + lastPath = this.onSegmentMouseUp(event); + } else { + log.warning(`Brush type does not exist: ${this.brush}`); + } + + if (isEraser) { + tool.mergeEraser(lastPath); + } else { + tool.mergeBrush(lastPath); + } + + cursorPreview.bringToFront(); + cursorPreview.position = event.point; + + // Reset + this.brush = null; + tool.fixedDistance = 1; + }; + + tool.mergeBrush = function (lastPath) { + // Get all path items to merge with + const paths = paper.project.getItems({ + match: function (item) { + return tool.isMergeable(lastPath, item); + } + }); + + let mergedPath = lastPath; + let i; + // Move down z order to first overlapping item + for (i = paths.length - 1; i >= 0 && !tool.touches(paths[i], lastPath); i--) { + continue; + } + let mergedPathIndex = i; + for (; i >= 0; i--) { + if (!tool.touches(paths[i], lastPath)) { + continue; + } + if (!paths[i].getFillColor()) { + // Ignore for merge. Paths without fill need to be in paths though, + // since they can visibly change if z order changes + } else if (tool.colorMatch(paths[i], lastPath)) { + // Make sure the new shape isn't overlapped by anything that would + // visibly change if we change its z order + for (let j = mergedPathIndex; j > i; j--) { + if (tool.touches(paths[j], paths[i])) { + continue; + } + } + // Merge same fill color + const tempPath = mergedPath.unite(paths[i]); + tempPath.strokeColor = paths[i].strokeColor; + tempPath.strokeWidth = paths[i].strokeWidth; + if (mergedPath === lastPath) { + tempPath.insertAbove(paths[i]); // First intersected path determines z position of the new path + } else { + tempPath.insertAbove(mergedPath); // Rest of merges join z index of merged path + mergedPathIndex--; // Removed an item, so the merged path index decreases + } + mergedPath.remove(); + mergedPath = tempPath; + paths[i].remove(); + paths.splice(i, 1); + } + } + // TODO FIX + //pg.undo.snapshot('broadbrush'); + }; + + tool.mergeEraser = function (lastPath) { + // Get all path items to merge with + // If there are selected items, try to erase from amongst those. + let items = paper.project.getItems({ + match: function (item) { + return item.selected && tool.isMergeable(lastPath, item) && tool.touches(lastPath, item); + } + }); + // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset + // and deselect the selection + if (items.length === 0) { + // TODO FIX + //pg.selection.clearSelection(); + items = paper.project.getItems({ + match: function (item) { + return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item); + } + }); + } + + for (let i = items.length - 1; i >= 0; i--) { + // Erase + const newPath = items[i].subtract(lastPath); + + // Gather path segments + const subpaths = []; + if (items[i] instanceof paper.PathItem && !items[i].closed) { + const firstSeg = items[i].clone(); + const intersections = firstSeg.getIntersections(lastPath); + // keep first and last segments + if (intersections.length === 0) { + continue; + } + for (let j = intersections.length - 1; j >= 0; j--) { + subpaths.push(firstSeg.splitAt(intersections[j])); + } + subpaths.push(firstSeg); + } + + // Remove the ones that are within the eraser stroke boundary, or are already part of new path. + // This way subpaths only remain if they didn't get turned into a shape by subtract. + for (let k = subpaths.length - 1; k >= 0; k--) { + const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point; + if (lastPath.contains(segMidpoint) || newPath.contains(segMidpoint)) { + subpaths[k].remove(); + subpaths.splice(k, 1); + } + } + + // Divide topologically separate shapes into their own compound paths, instead of + // everything being stuck together. + // Assume that result of erase operation returns clockwise paths for positive shapes + const clockwiseChildren = []; + const ccwChildren = []; + if (newPath.children) { + for (let j = newPath.children.length - 1; j >= 0; j--) { + const child = newPath.children[j]; + if (child.isClockwise()) { + clockwiseChildren.push(child); + } else { + ccwChildren.push(child); + } + } + for (let j = 0; j < clockwiseChildren.length; j++) { + const cw = clockwiseChildren[j]; + cw.copyAttributes(newPath); + cw.fillColor = newPath.fillColor; + cw.strokeColor = newPath.strokeColor; + cw.strokeWidth = newPath.strokeWidth; + cw.insertAbove(items[i]); + + // Go backward since we are deleting elements + let newCw = cw; + for (let k = ccwChildren.length - 1; k >= 0; k--) { + const ccw = ccwChildren[k]; + if (tool.firstEnclosesSecond(ccw, cw) || tool.firstEnclosesSecond(cw, ccw)) { + const temp = newCw.subtract(ccw); + temp.insertAbove(newCw); + newCw.remove(); + newCw = temp; + ccw.remove(); + ccwChildren.splice(k, 1); + } + } + } + newPath.remove(); + } + items[i].remove(); + } + lastPath.remove(); + // TODO FIX + //pg.undo.snapshot('eraser'); + }; + + tool.colorMatch = function (existingPath, addedPath) { + // Note: transparent fill colors do notdetect as touching + return existingPath.getFillColor().equals(addedPath.getFillColor()) && + (addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null + (addedPath.getStrokeColor() && + addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) && + addedPath.getStrokeWidth() === existingPath.getStrokeWidth() && + tool.touches(existingPath, addedPath); + }; + + tool.touches = function (path1, path2) { + // Two shapes are touching if their paths intersect + if (path1 && path2 && path1.intersects(path2)) { + return true; + } + return tool.firstEnclosesSecond(path1, path2) || tool.firstEnclosesSecond(path2, path1); + }; + + tool.firstEnclosesSecond = function (path1, path2) { + // Two shapes are also touching if one is completely inside the other + if (path1 && path2 && path2.firstSegment && path2.firstSegment.point && + path1.hitTest(path2.firstSegment.point)) { + return true; + } + // TODO clean up these no point paths + return false; + }; + + tool.isMergeable = function (newPath, existingPath) { + return existingPath instanceof paper.PathItem && // path or compound path + existingPath !== cursorPreview && // don't merge with the mouse preview + existingPath !== newPath && // don't merge with self + existingPath.parent instanceof paper.Layer; // don't merge with nested in group + }; + } + + deactivateTool () { + console.log('deactivateTool'); + this.cursorPreview.remove(); + } +} + +module.exports = BlobTool; diff --git a/src/tools/broad-brush-helper.js b/src/tools/broad-brush-helper.js new file mode 100644 index 00000000..4d6684ca --- /dev/null +++ b/src/tools/broad-brush-helper.js @@ -0,0 +1,107 @@ +// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/ +const paper = require('paper'); + +/** + * Applies segment brush functions to the tool. + * @param {!Tool} tool paper.js mouse object + * @param {!options} options brush tool state object + * @param {!options.brushSize} brush tool diameter + */ +const broadBrushHelper = function (tool, options) { + let lastPoint; + let secondLastPoint; + let finalPath; + + tool.onBroadMouseDown = function (event) { + tool.minDistance = options.brushSize / 4; + tool.maxDistance = options.brushSize; + if (event.event.button > 0) return; // only first mouse button + + finalPath = new paper.Path(); + tool.stylePath(finalPath); + finalPath.add(event.point); + lastPoint = secondLastPoint = event.point; + }; + + tool.onBroadMouseDrag = function (event) { + const step = (event.delta).normalize(options.brushSize / 2); + + // Move the first point out away from the drag so that the end of the path is rounded + if (finalPath.segments && finalPath.segments.length === 1) { + const removedPoint = finalPath.removeSegment(0).point; + // Add handles to round the end caps + const handleVec = step.clone(); + handleVec.length = options.brushSize / 2; + handleVec.angle += 90; + finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec)); + } + step.angle += 90; + const top = event.middlePoint.add(step); + const bottom = event.middlePoint.subtract(step); + + if (finalPath.segments.length > 3) { + finalPath.removeSegment(finalPath.segments.length - 1); + finalPath.removeSegment(0); + } + finalPath.add(top); + finalPath.add(event.point.add(step)); + finalPath.insert(0, bottom); + finalPath.insert(0, event.point.subtract(step)); + if (finalPath.segments.length === 5) { + // Flatten is necessary to prevent smooth from getting rid of the effect + // of the handles on the first point. + finalPath.flatten(options.brushSize / 5); + } + finalPath.smooth(); + lastPoint = event.point; + secondLastPoint = event.lastPoint; + }; + + tool.onBroadMouseUp = function (event) { + // If the mouse up is at the same point as the mouse drag event then we need + // the second to last point to get the right direction vector for the end cap + if (event.point.equals(lastPoint)) { + lastPoint = secondLastPoint; + } + // If the points are still equal, then there was no drag, so just draw a circle. + if (event.point.equals(lastPoint)) { + finalPath.remove(); + finalPath = new paper.Path.Circle({ + center: event.point, + radius: options.brushSize / 2 + }); + tool.stylePath(finalPath); + } else { + const step = (event.point.subtract(lastPoint)).normalize(options.brushSize / 2); + step.angle += 90; + const handleVec = step.clone(); + handleVec.length = options.brushSize / 2; + + const top = event.point.add(step); + const bottom = event.point.subtract(step); + finalPath.add(top); + finalPath.insert(0, bottom); + + // Simplify before adding end cap so cap doesn't get warped + finalPath.simplify(1); + + // Add end cap + step.angle -= 90; + finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec)); + finalPath.closed = true; + } + + // Resolve self-crossings + const newPath = + finalPath + .resolveCrossings() + .reorient(true /* nonZero */, true /* clockwise */) + .reduce({simplify: true}); + newPath.copyAttributes(finalPath); + newPath.fillColor = finalPath.fillColor; + finalPath = newPath; + return finalPath; + }; +}; + +module.exports = broadBrushHelper; diff --git a/src/tools/eraser.js b/src/tools/eraser.js new file mode 100644 index 00000000..525958b0 --- /dev/null +++ b/src/tools/eraser.js @@ -0,0 +1,44 @@ +// TODO share code with brush + +pg.tools.registerTool({ + id: 'eraser', + name: 'Eraser' +}); + +pg.tools.eraser = function() { + var blob = new pg.blob(); + + var options = { + brushWidth: 20 + }; + + var components = { + brushWidth: { + type: 'float', + label: 'Eraser width', + min: 0 + } + }; + + var activateTool = function() { + // get options from local storage if present + options = pg.tools.getLocalOptions(options); + var tool = new Tool(); + blob.activateTool(true /* isEraser */, tool, options); + + // setup floating tool options panel in the editor + pg.toolOptionPanel.setup(options, components, function() {}); + + tool.activate(); + }; + + var deactivateTool = function() { + blob.deactivateTool(); + }; + + return { + options: options, + activateTool : activateTool, + deactivateTool : deactivateTool + }; +}; \ No newline at end of file From 6d0119eebe7ecda803fe0556d71cf22050a6b401 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 25 Jul 2017 10:22:31 -0400 Subject: [PATCH 02/19] add missing file --- src/tools/segment-brush-helper.js | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/tools/segment-brush-helper.js diff --git a/src/tools/segment-brush-helper.js b/src/tools/segment-brush-helper.js new file mode 100644 index 00000000..ca037941 --- /dev/null +++ b/src/tools/segment-brush-helper.js @@ -0,0 +1,59 @@ +// Applies segment brush functions to the tool +pg.segmentbrushhelper = function(tool, options) { + var lastPoint, finalPath; + + tool.onSegmentMouseDown = function(event) { + tool.minDistance = options.brushSize/4; + tool.maxDistance = options.brushSize; + + finalPath = new Path.Circle({ + center: event.point, + radius: options.brushSize/2 + }); + tool.stylePath(finalPath); + lastPoint = event.point; + }; + + tool.onSegmentMouseDrag = function(event) { + var step = (event.delta).normalize(options.brushSize/2); + var handleVec = step.clone(); + handleVec.length = options.brushSize/2; + handleVec.angle += 90; + + var path = new Path(); + path = pg.stylebar.applyActiveToolbarStyle(path); + path.strokeColor = null; + // Add handles to round the end caps + path.add(new Segment(lastPoint - step, -handleVec, handleVec)); + step.angle += 90; + + path.add(event.lastPoint + step); + path.insert(0, event.lastPoint - step); + path.add(event.point + step); + path.insert(0, event.point - step); + + // Add end cap + step.angle -= 90; + path.add(new Segment(event.point + step, handleVec, -handleVec)); + path.closed = true; + // The unite function on curved paths does not always work (sometimes deletes half the path) + // so we have to flatten. + path.flatten(options.brushSize/5); + + lastPoint = event.point; + var newPath = finalPath.unite(path); + path.remove(); + finalPath.remove(); + finalPath = newPath; + }; + + tool.onSegmentMouseUp = function(event) { + // 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? + + // Smooth the path. + finalPath.simplify(2); + //console.log(finalPath.segments); + return finalPath; + }; +} \ No newline at end of file From 02730cbd58911646ec5a5fe39b4694c6e3508052 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 25 Jul 2017 11:53:54 -0400 Subject: [PATCH 03/19] add eraser --- src/components/paint-editor.jsx | 2 + src/containers/tools/brush-tool.jsx | 6 +- src/containers/tools/eraser-tool.jsx | 104 +++++++++++++++++++++++++++ src/index.js | 2 +- src/reducers/combine-reducers.js | 3 +- src/reducers/eraser-tool.js | 25 +++++++ src/tools/eraser.js | 44 ------------ 7 files changed, 137 insertions(+), 49 deletions(-) create mode 100644 src/containers/tools/eraser-tool.jsx create mode 100644 src/reducers/eraser-tool.js delete mode 100644 src/tools/eraser.js diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 3ab5fb07..c55ef2c5 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushTool from '../containers/tools/brush-tool.jsx'; +import EraserTool from '../containers/tools/eraser-tool.jsx'; const PaintEditorComponent = props => (
@@ -10,6 +11,7 @@ const PaintEditorComponent = props => ( tool={props.tool} /> +
); diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/tools/brush-tool.jsx index 604b56eb..56091ba0 100644 --- a/src/containers/tools/brush-tool.jsx +++ b/src/containers/tools/brush-tool.jsx @@ -41,8 +41,8 @@ class BrushTool extends React.Component { document.getElementById(this.props.canvasId) .addEventListener('mousewheel', this.onScroll); - const tool = new paper.Tool(); - this.blob.activateTool(false /* isEraser */, tool, this.props.brushToolState); + this.tool = new paper.Tool(); + this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushToolState); // // Make sure a fill color is set on the brush // if(!pg.stylebar.getFillColor()) { @@ -53,7 +53,7 @@ class BrushTool extends React.Component { // // setup floating tool options panel in the editor // pg.toolOptionPanel.setup(options, components, function() {}); - tool.activate(); + this.tool.activate(); } deactivateTool () { document.getElementById(this.props.canvasId) diff --git a/src/containers/tools/eraser-tool.jsx b/src/containers/tools/eraser-tool.jsx new file mode 100644 index 00000000..79ca40a1 --- /dev/null +++ b/src/containers/tools/eraser-tool.jsx @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import bindAll from 'lodash.bindall'; +import ToolTypes from '../../tools/tool-types.js'; +import BlobTool from '../../tools/blob.js'; +import EraserToolReducer from '../../reducers/eraser-tool'; +import paper from 'paper'; + +class EraserTool extends React.Component { + static get TOOL_TYPE () { + return ToolTypes.ERASER; + } + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool', + 'onScroll' + ]); + this.blob = new BlobTool(); + } + componentDidMount () { + if (this.props.tool === EraserTool.TOOL_TYPE) { + this.activateTool(); + } + } + componentWillReceiveProps (nextProps) { + if (nextProps.tool === EraserTool.TOOL_TYPE && this.props.tool !== EraserTool.TOOL_TYPE) { + this.activateTool(); + } else if (nextProps.tool !== EraserTool.TOOL_TYPE && this.props.tool === EraserTool.TOOL_TYPE) { + this.deactivateTool(); + } else if (nextProps.tool === EraserTool.TOOL_TYPE && this.props.tool === EraserTool.TOOL_TYPE) { + this.blob.setOptions(nextProps.eraserToolState); + } + } + shouldComponentUpdate () { + return false; // Logic only component + } + activateTool () { + document.getElementById(this.props.canvasId) + .addEventListener('mousewheel', this.onScroll); + + this.tool = new paper.Tool(); + this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserToolState); + + // // Make sure a fill color is set on the brush + // if(!pg.stylebar.getFillColor()) { + // pg.stylebar.setFillColor(pg.stylebar.getStrokeColor()); + // pg.stylebar.setStrokeColor(null); + // } + + // // setup floating tool options panel in the editor + // pg.toolOptionPanel.setup(options, components, function() {}); + // get options from local storage if presentz + + this.tool.activate(); + } + deactivateTool () { + document.getElementById(this.props.canvasId) + .removeEventListener('mousewheel', this.onScroll); + this.blob.deactivateTool(); + this.tool.remove(); + } + onScroll (event) { + if (event.deltaY < 0) { + this.props.changeBrushSize(this.props.eraserToolState.brushSize + 1); + } else if (event.deltaY > 0 && this.props.eraserToolState.brushSize > 1) { + this.props.changeBrushSize(this.props.eraserToolState.brushSize - 1); + } + return false; + } + render () { + return ( +
+ ); + } +} + +EraserTool.propTypes = { + canvasId: PropTypes.string.isRequired, + changeBrushSize: PropTypes.func.isRequired, + eraserToolState: PropTypes.shape({ + brushSize: PropTypes.number.isRequired + }), + tool: PropTypes.shape({ + name: PropTypes.string.isRequired + }) +}; + +const mapStateToProps = state => ({ + eraserToolState: state.brushTool, + tool: state.tool +}); +const mapDispatchToProps = dispatch => ({ + changeBrushSize: brushSize => { + dispatch(EraserToolReducer.changeBrushSize(brushSize)); + } +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(EraserTool); diff --git a/src/index.js b/src/index.js index 986f2302..6611b719 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ import PaintEditor from './containers/paint-editor.jsx'; -export default PaintEditorComponent; +export default PaintEditor; diff --git a/src/reducers/combine-reducers.js b/src/reducers/combine-reducers.js index c237d7bb..75a429b1 100644 --- a/src/reducers/combine-reducers.js +++ b/src/reducers/combine-reducers.js @@ -2,5 +2,6 @@ import {combineReducers} from 'redux'; module.exports = combineReducers({ tool: require('./tools'), - brushTool: require('./brush-tool') + brushTool: require('./brush-tool'), + eraserTool: require('./eraser-tool') }); diff --git a/src/reducers/eraser-tool.js b/src/reducers/eraser-tool.js new file mode 100644 index 00000000..e8371e3e --- /dev/null +++ b/src/reducers/eraser-tool.js @@ -0,0 +1,25 @@ +const CHANGE_ERASER_SIZE = 'scratch-paint/tools/CHANGE_ERASER_SIZE'; +const initialState = {brushSize: 20}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_ERASER_SIZE: + return {brushSize: Math.max(1, action.brushSize)}; + default: + return state; + } +}; + +// Action creators ================================== +reducer.changeBrushSize = function (brushSize) { + return { + type: CHANGE_ERASER_SIZE, + brushSize: brushSize, + meta: { + throttle: 30 + } + }; +}; + +module.exports = reducer; diff --git a/src/tools/eraser.js b/src/tools/eraser.js deleted file mode 100644 index 525958b0..00000000 --- a/src/tools/eraser.js +++ /dev/null @@ -1,44 +0,0 @@ -// TODO share code with brush - -pg.tools.registerTool({ - id: 'eraser', - name: 'Eraser' -}); - -pg.tools.eraser = function() { - var blob = new pg.blob(); - - var options = { - brushWidth: 20 - }; - - var components = { - brushWidth: { - type: 'float', - label: 'Eraser width', - min: 0 - } - }; - - var activateTool = function() { - // get options from local storage if present - options = pg.tools.getLocalOptions(options); - var tool = new Tool(); - blob.activateTool(true /* isEraser */, tool, options); - - // setup floating tool options panel in the editor - pg.toolOptionPanel.setup(options, components, function() {}); - - tool.activate(); - }; - - var deactivateTool = function() { - blob.deactivateTool(); - }; - - return { - options: options, - activateTool : activateTool, - deactivateTool : deactivateTool - }; -}; \ No newline at end of file From f139bfada030198a583d4db2dafb3b5a45dbdf97 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 25 Jul 2017 15:00:35 -0400 Subject: [PATCH 04/19] fix cursor preview --- src/containers/tools/brush-tool.jsx | 5 +- src/containers/tools/eraser-tool.jsx | 14 +--- src/tools/blob.js | 96 ++++++++++++++++------------ src/tools/broad-brush-helper.js | 20 +++--- 4 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/tools/brush-tool.jsx index 56091ba0..931f2134 100644 --- a/src/containers/tools/brush-tool.jsx +++ b/src/containers/tools/brush-tool.jsx @@ -44,13 +44,13 @@ class BrushTool extends React.Component { this.tool = new paper.Tool(); this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushToolState); - // // Make sure a fill color is set on the brush + // TODO Make sure a fill color is set on the brush // if(!pg.stylebar.getFillColor()) { // pg.stylebar.setFillColor(pg.stylebar.getStrokeColor()); // pg.stylebar.setStrokeColor(null); // } - // // setup floating tool options panel in the editor + // TODO setup floating tool options panel in the editor // pg.toolOptionPanel.setup(options, components, function() {}); this.tool.activate(); @@ -58,7 +58,6 @@ class BrushTool extends React.Component { deactivateTool () { document.getElementById(this.props.canvasId) .removeEventListener('mousewheel', this.onScroll); - this.blob.deactivateTool(); } onScroll (event) { if (event.deltaY < 0) { diff --git a/src/containers/tools/eraser-tool.jsx b/src/containers/tools/eraser-tool.jsx index 79ca40a1..c3fa1656 100644 --- a/src/containers/tools/eraser-tool.jsx +++ b/src/containers/tools/eraser-tool.jsx @@ -43,24 +43,12 @@ class EraserTool extends React.Component { this.tool = new paper.Tool(); this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserToolState); - - // // Make sure a fill color is set on the brush - // if(!pg.stylebar.getFillColor()) { - // pg.stylebar.setFillColor(pg.stylebar.getStrokeColor()); - // pg.stylebar.setStrokeColor(null); - // } - - // // setup floating tool options panel in the editor - // pg.toolOptionPanel.setup(options, components, function() {}); - // get options from local storage if presentz - this.tool.activate(); } deactivateTool () { document.getElementById(this.props.canvasId) .removeEventListener('mousewheel', this.onScroll); this.blob.deactivateTool(); - this.tool.remove(); } onScroll (event) { if (event.deltaY < 0) { @@ -89,7 +77,7 @@ EraserTool.propTypes = { }; const mapStateToProps = state => ({ - eraserToolState: state.brushTool, + eraserToolState: state.eraserTool, tool: state.tool }); const mapDispatchToProps = dispatch => ({ diff --git a/src/tools/blob.js b/src/tools/blob.js index ba5e86f5..d2617fc7 100644 --- a/src/tools/blob.js +++ b/src/tools/blob.js @@ -19,31 +19,45 @@ class BlobTool { } setOptions (options) { - console.log('setOptions'); - this.options = options; - if (this.cursorPreview) { - this.cursorPreview = new paper.Path.Circle({ - center: [this.cursorPreview.center.x, this.cursorPreview.center.y], - radius: options.brushSize / 2 - }); + if (this.tool) { + this.tool.options = options; + this.tool.resizeCursorIfNeeded(); } } activateTool (isEraser, tool, options) { - console.log('activateTool isEraser?'+isEraser); this.tool = tool; - this.options = options; - - let cursorPreview = this.cursorPreview = new paper.Path.Circle({ - center: [-10000, -10000], - radius: options.brushSize / 2 - }); - this.brushSize = options.brushSize; + + tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000); + tool.resizeCursorIfNeeded = function (point) { + if (typeof point === 'undefined') { + point = this.cursorPreviewLastPoint; + } else { + this.cursorPreviewLastPoint = point; + } + + if (this.brushSize === this.options.brushSize) { + return; + } + const newPreview = new paper.Path.Circle({ + center: point, + radius: this.options.brushSize / 2 + }); + if (this.cursorPreview) { + this.cursorPreview.segments = newPreview.segments; + newPreview.remove(); + } else { + this.cursorPreview = newPreview; + } + this.brushSize = this.options.brushSize; + }; + + this.setOptions(options); tool.stylePath = function (path) { if (isEraser) { path.fillColor = 'white'; - if (path === cursorPreview) { + if (path === this.cursorPreview) { path.strokeColor = 'cornflowerblue'; path.strokeWidth = 1; } @@ -54,50 +68,46 @@ class BlobTool { //TODO FIX path.fillColor = 'black'; - if (path === cursorPreview) { + if (path === this.cursorPreview) { path.strokeColor = 'cornflowerblue'; path.strokeWidth = 1; } } }; - tool.stylePath(cursorPreview); + tool.stylePath(this.tool.cursorPreview); tool.fixedDistance = 1; - broadBrushHelper(tool, options); + broadBrushHelper(tool); // TODO add //pg.segmentbrushhelper(tool, options); + tool.onMouseMove = function (event) { - if (this.brushSize !== options.brushSize) { - cursorPreview.remove(); - cursorPreview = new paper.Path.Circle({ - center: event.point, - radius: options.brushSize / 2 - }); - this.brushSize = options.brushSize; - } - tool.stylePath(cursorPreview); - cursorPreview.bringToFront(); - cursorPreview.position = event.point; + tool.resizeCursorIfNeeded(event.point); + tool.stylePath(this.cursorPreview); + this.cursorPreview.bringToFront(); + this.cursorPreview.position = event.point; }; tool.onMouseDown = function (event) { + tool.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button - if (options.brushSize < BlobTool.THRESHOLD) { + if (this.options.brushSize < BlobTool.THRESHOLD) { this.brush = BlobTool.BROAD; this.onBroadMouseDown(event); } else { this.brush = BlobTool.SEGMENT; this.onSegmentMouseDown(event); } - cursorPreview.bringToFront(); - cursorPreview.position = event.point; + this.cursorPreview.bringToFront(); + this.cursorPreview.position = event.point; paper.view.draw(); }; tool.onMouseDrag = function (event) { + tool.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button if (this.brush === BlobTool.BROAD) { this.onBroadMouseDrag(event); @@ -107,12 +117,13 @@ class BlobTool { log.warning(`Brush type does not exist: ${this.brush}`); } - cursorPreview.bringToFront(); - cursorPreview.position = event.point; + this.cursorPreview.bringToFront(); + this.cursorPreview.position = event.point; paper.view.draw(); }; tool.onMouseUp = function (event) { + tool.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button let lastPath; @@ -130,8 +141,8 @@ class BlobTool { tool.mergeBrush(lastPath); } - cursorPreview.bringToFront(); - cursorPreview.position = event.point; + this.cursorPreview.bringToFront(); + this.cursorPreview.position = event.point; // Reset this.brush = null; @@ -214,7 +225,8 @@ class BlobTool { // Gather path segments const subpaths = []; - if (items[i] instanceof paper.PathItem && !items[i].closed) { + // TODO handle compound path + if (items[i] instanceof paper.Path && !items[i].closed) { const firstSeg = items[i].clone(); const intersections = firstSeg.getIntersections(lastPath); // keep first and last segments @@ -312,15 +324,17 @@ class BlobTool { tool.isMergeable = function (newPath, existingPath) { return existingPath instanceof paper.PathItem && // path or compound path - existingPath !== cursorPreview && // don't merge with the mouse preview + existingPath !== this.cursorPreview && // don't merge with the mouse preview existingPath !== newPath && // don't merge with self existingPath.parent instanceof paper.Layer; // don't merge with nested in group }; } deactivateTool () { - console.log('deactivateTool'); - this.cursorPreview.remove(); + if (this.tool) { + this.tool.cursorPreview.remove(); + this.tool.remove(); + } } } diff --git a/src/tools/broad-brush-helper.js b/src/tools/broad-brush-helper.js index 4d6684ca..989e5749 100644 --- a/src/tools/broad-brush-helper.js +++ b/src/tools/broad-brush-helper.js @@ -4,17 +4,15 @@ const paper = require('paper'); /** * Applies segment brush functions to the tool. * @param {!Tool} tool paper.js mouse object - * @param {!options} options brush tool state object - * @param {!options.brushSize} brush tool diameter */ -const broadBrushHelper = function (tool, options) { +const broadBrushHelper = function (tool) { let lastPoint; let secondLastPoint; let finalPath; tool.onBroadMouseDown = function (event) { - tool.minDistance = options.brushSize / 4; - tool.maxDistance = options.brushSize; + tool.minDistance = this.options.brushSize / 4; + tool.maxDistance = this.options.brushSize; if (event.event.button > 0) return; // only first mouse button finalPath = new paper.Path(); @@ -24,14 +22,14 @@ const broadBrushHelper = function (tool, options) { }; tool.onBroadMouseDrag = function (event) { - const step = (event.delta).normalize(options.brushSize / 2); + const step = (event.delta).normalize(this.options.brushSize / 2); // Move the first point out away from the drag so that the end of the path is rounded if (finalPath.segments && finalPath.segments.length === 1) { const removedPoint = finalPath.removeSegment(0).point; // Add handles to round the end caps const handleVec = step.clone(); - handleVec.length = options.brushSize / 2; + handleVec.length = this.options.brushSize / 2; handleVec.angle += 90; finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec)); } @@ -50,7 +48,7 @@ const broadBrushHelper = function (tool, options) { if (finalPath.segments.length === 5) { // Flatten is necessary to prevent smooth from getting rid of the effect // of the handles on the first point. - finalPath.flatten(options.brushSize / 5); + finalPath.flatten(this.options.brushSize / 5); } finalPath.smooth(); lastPoint = event.point; @@ -68,14 +66,14 @@ const broadBrushHelper = function (tool, options) { finalPath.remove(); finalPath = new paper.Path.Circle({ center: event.point, - radius: options.brushSize / 2 + radius: this.options.brushSize / 2 }); tool.stylePath(finalPath); } else { - const step = (event.point.subtract(lastPoint)).normalize(options.brushSize / 2); + const step = (event.point.subtract(lastPoint)).normalize(this.options.brushSize / 2); step.angle += 90; const handleVec = step.clone(); - handleVec.length = options.brushSize / 2; + handleVec.length = this.options.brushSize / 2; const top = event.point.add(step); const bottom = event.point.subtract(step); From 0b3798436f947ed5763f83a36a0d01d5c18c2314 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 25 Jul 2017 15:35:32 -0400 Subject: [PATCH 05/19] random change --- src/tools/segment-brush-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/segment-brush-helper.js b/src/tools/segment-brush-helper.js index ca037941..449b5e52 100644 --- a/src/tools/segment-brush-helper.js +++ b/src/tools/segment-brush-helper.js @@ -53,7 +53,7 @@ pg.segmentbrushhelper = function(tool, options) { // Smooth the path. finalPath.simplify(2); - //console.log(finalPath.segments); + // console.log(finalPath.segments); return finalPath; }; } \ No newline at end of file From dc2fea3dd6970f3271b54ac98bb6febc748aa034 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 16:41:41 -0400 Subject: [PATCH 06/19] some clean up in react code' --- src/components/paint-editor.jsx | 36 +++++++++++++++++----------- src/containers/paint-editor.jsx | 20 +++++----------- src/containers/paper-canvas.jsx | 21 ++++++++++++---- src/containers/tools/brush-tool.jsx | 15 ++++++------ src/containers/tools/eraser-tool.jsx | 10 ++++---- 5 files changed, 55 insertions(+), 47 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 1bad869e..68661388 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -1,29 +1,37 @@ -import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; import BrushTool from '../containers/tools/brush-tool.jsx'; import EraserTool from '../containers/tools/eraser-tool.jsx'; -import ToolTypes from '../tools/tool-types.js'; class PaintEditorComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setCanvas' + ]); + this.state = {}; + } + setCanvas (canvas) { + this.setState({canvas: canvas}); + } render () { + // Tools can't work without a canvas, so we might as well not render them until we have it + if (this.state.canvas) { + return ( +
+ + + +
+ ); + } return (
- { - this.canvas = canvas; - }} - tool={this.props.tool} - /> - - +
); } } -PaintEditorComponent.propTypes = { - tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired -}; - export default PaintEditorComponent; diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 12d40e54..78eae432 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -13,35 +13,27 @@ class PaintEditor extends React.Component { document.removeEventListener('keydown', this.props.onKeyPress); } render () { - const { - onKeyPress, // eslint-disable-line no-unused-vars - ...props - } = this.props; return ( - + ); } } PaintEditor.propTypes = { - onKeyPress: PropTypes.func.isRequired, - tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired + onKeyPress: PropTypes.func.isRequired }; -const mapStateToProps = state => ({ - tool: state.tool -}); const mapDispatchToProps = dispatch => ({ - onKeyPress: e => { - if (e.key === 'e') { + onKeyPress: event => { + if (event.key === 'e') { dispatch(ToolsReducer.changeTool(ToolTypes.ERASER)); - } else if (e.key === 'b') { + } else if (event.key === 'b') { dispatch(ToolsReducer.changeTool(ToolTypes.BRUSH)); } } }); export default connect( - mapStateToProps, + null, mapDispatchToProps )(PaintEditor); diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index fed3620a..a3b524ff 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -1,9 +1,15 @@ +import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import paper from 'paper'; -import ToolTypes from '../tools/tool-types.js'; class PaperCanvas extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setCanvas' + ]); + } componentDidMount () { paper.setup(this.canvas); // Create a Paper.js Path to draw a line into it: @@ -22,19 +28,24 @@ class PaperCanvas extends React.Component { componentWillUnmount () { paper.remove(); } + setCanvas (canvas) { + debugger; + this.canvas = canvas; + if (this.props.canvasRef) { + this.props.canvasRef(canvas); + } + } render () { return ( { - this.canvas = canvas; - }} + ref={this.setCanvas} /> ); } } PaperCanvas.propTypes = { - tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired + canvasRef: PropTypes.func }; export default PaperCanvas; diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/tools/brush-tool.jsx index 53bc763c..00c1b285 100644 --- a/src/containers/tools/brush-tool.jsx +++ b/src/containers/tools/brush-tool.jsx @@ -22,8 +22,7 @@ class BrushTool extends React.Component { } componentDidMount () { if (this.props.tool === BrushTool.TOOL_TYPE) { - debugger; - this.activateTool(); + this.activateTool(this.props); } } componentWillReceiveProps (nextProps) { @@ -39,6 +38,7 @@ class BrushTool extends React.Component { return false; // Logic only component } activateTool () { + // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); @@ -57,6 +57,7 @@ class BrushTool extends React.Component { } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); + this.blob.deactivateTool(); } onScroll (event) { if (event.deltaY < 0) { @@ -64,11 +65,11 @@ class BrushTool extends React.Component { } else if (event.deltaY > 0 && this.props.brushToolState.brushSize > 1) { this.props.changeBrushSize(this.props.brushToolState.brushSize - 1); } - return false; + return true; } render () { return ( -
Brush Tool
+
Brush Tool
); } } @@ -77,11 +78,9 @@ BrushTool.propTypes = { brushToolState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), - canvas: PropTypes.element, + canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, - tool: PropTypes.shape({ - name: PropTypes.string.isRequired - }) + tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired }; const mapStateToProps = state => ({ diff --git a/src/containers/tools/eraser-tool.jsx b/src/containers/tools/eraser-tool.jsx index 9ee9fa6a..aa0e1268 100644 --- a/src/containers/tools/eraser-tool.jsx +++ b/src/containers/tools/eraser-tool.jsx @@ -49,29 +49,27 @@ class EraserTool extends React.Component { this.blob.deactivateTool(); } onScroll (event) { + event.preventDefault(); if (event.deltaY < 0) { this.props.changeBrushSize(this.props.eraserToolState.brushSize + 1); } else if (event.deltaY > 0 && this.props.eraserToolState.brushSize > 1) { this.props.changeBrushSize(this.props.eraserToolState.brushSize - 1); } - return false; } render () { return ( -
Eraser Tool
+
Eraser Tool
); } } EraserTool.propTypes = { - canvas: PropTypes.element, + canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, eraserToolState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), - tool: PropTypes.shape({ - name: PropTypes.string.isRequired - }) + tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired }; const mapStateToProps = state => ({ From 0c1e7ed961adfba4bf42f48a14f00c63dc12883a Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 16:48:37 -0400 Subject: [PATCH 07/19] some comments --- src/components/paint-editor.jsx | 2 +- src/containers/paper-canvas.jsx | 1 - src/tools/blob.js | 10 +++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 68661388..39b570f0 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -16,7 +16,7 @@ class PaintEditorComponent extends React.Component { this.setState({canvas: canvas}); } render () { - // Tools can't work without a canvas, so we might as well not render them until we have it + // Tools can't work without a canvas, so we don't render them until we have it if (this.state.canvas) { return (
diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index a3b524ff..4ccb0b58 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -29,7 +29,6 @@ class PaperCanvas extends React.Component { paper.remove(); } setCanvas (canvas) { - debugger; this.canvas = canvas; if (this.props.canvasRef) { this.props.canvasRef(canvas); diff --git a/src/tools/blob.js b/src/tools/blob.js index d2617fc7..c6540633 100644 --- a/src/tools/blob.js +++ b/src/tools/blob.js @@ -1,6 +1,6 @@ -const paper = require('paper'); -const log = require('../log/log'); -const broadBrushHelper = require('./broad-brush-helper'); +import paper from 'paper'; +import log from '../log/log'; +import broadBrushHelper from './broad-brush-helper'; class BlobTool { @@ -11,9 +11,9 @@ class BlobTool { return 'segmentbrush'; } - // brush size >= threshold use segment brush, else use broadbrush + // If brush size >= threshold use segment brush, else use broadbrush // Segment brush has performance issues at low threshold, but broad brush has weird corners - // which are more obvious the bigger it is + // which get more obvious the bigger it is static get THRESHOLD () { return 100000; } From 4a5c9f0b544934a403912e6864f091e7bb50add6 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 16:51:42 -0400 Subject: [PATCH 08/19] re-add linting for webpack config --- .eslintignore | 1 - webpack.config.js | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.eslintignore b/.eslintignore index deb1753c..fe1b4898 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ node_modules/* dist/* playground/ -webpack.config.js diff --git a/webpack.config.js b/webpack.config.js index ff26572e..8049965b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,12 +3,12 @@ const path = require('path'); const webpack = require('webpack'); // Plugins -var HtmlWebpackPlugin = require('html-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); // PostCss -var autoprefixer = require('autoprefixer'); -var postcssVars = require('postcss-simple-vars'); -var postcssImport = require('postcss-import'); +const autoprefixer = require('autoprefixer'); +const postcssVars = require('postcss-simple-vars'); +const postcssImport = require('postcss-import'); const base = { devtool: 'cheap-module-source-map', From 27f7102b06c4d2369ac6d1b9180496c972c4aec6 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 17:36:17 -0400 Subject: [PATCH 09/19] add segment brush --- src/tools/blob.js | 41 +++++----- src/tools/broad-brush-helper.js | 12 ++- src/tools/segment-brush-helper.js | 132 ++++++++++++++++++------------ 3 files changed, 110 insertions(+), 75 deletions(-) diff --git a/src/tools/blob.js b/src/tools/blob.js index c6540633..509f28e4 100644 --- a/src/tools/blob.js +++ b/src/tools/blob.js @@ -1,7 +1,13 @@ import paper from 'paper'; import log from '../log/log'; import broadBrushHelper from './broad-brush-helper'; +import segmentBrushHelper from './segment-brush-helper'; +/** + * Shared code for the brush and eraser tool. Adds functions on the paper tool object + * to handle mouse events, which are delegated to broad-brush-helper and segment-brush-helper + * based on the brushSize in the state. + */ class BlobTool { static get BROAD () { @@ -15,7 +21,7 @@ class BlobTool { // Segment brush has performance issues at low threshold, but broad brush has weird corners // which get more obvious the bigger it is static get THRESHOLD () { - return 100000; + return 9; } setOptions (options) { @@ -62,10 +68,8 @@ class BlobTool { path.strokeWidth = 1; } } else { - // TODO keep a separate active toolbar style for brush vs pen? - //path = pg.stylebar.applyActiveToolbarStyle(path); - - //TODO FIX + // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. + // path = pg.stylebar.applyActiveToolbarStyle(path); path.fillColor = 'black'; if (path === this.cursorPreview) { @@ -80,8 +84,7 @@ class BlobTool { tool.fixedDistance = 1; broadBrushHelper(tool); - // TODO add - //pg.segmentbrushhelper(tool, options); + segmentBrushHelper(tool); tool.onMouseMove = function (event) { tool.resizeCursorIfNeeded(event.point); @@ -111,10 +114,10 @@ class BlobTool { if (event.event.button > 0) return; // only first mouse button if (this.brush === BlobTool.BROAD) { this.onBroadMouseDrag(event); - } else if (this.brush === Blob.SEGMENT) { + } else if (this.brush === BlobTool.SEGMENT) { this.onSegmentMouseDrag(event); } else { - log.warning(`Brush type does not exist: ${this.brush}`); + log.warn(`Brush type does not exist: ${this.brush}`); } this.cursorPreview.bringToFront(); @@ -132,7 +135,7 @@ class BlobTool { } else if (this.brush === BlobTool.SEGMENT) { lastPath = this.onSegmentMouseUp(event); } else { - log.warning(`Brush type does not exist: ${this.brush}`); + log.warn(`Brush type does not exist: ${this.brush}`); } if (isEraser) { @@ -195,8 +198,8 @@ class BlobTool { paths.splice(i, 1); } } - // TODO FIX - //pg.undo.snapshot('broadbrush'); + // TODO: Add back undo + // pg.undo.snapshot('broadbrush'); }; tool.mergeEraser = function (lastPath) { @@ -210,8 +213,8 @@ class BlobTool { // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset // and deselect the selection if (items.length === 0) { - // TODO FIX - //pg.selection.clearSelection(); + // TODO: Add back selection handling + // pg.selection.clearSelection(); items = paper.project.getItems({ match: function (item) { return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item); @@ -225,7 +228,7 @@ class BlobTool { // Gather path segments const subpaths = []; - // TODO handle compound path + // TODO: Handle compound path if (items[i] instanceof paper.Path && !items[i].closed) { const firstSeg = items[i].clone(); const intersections = firstSeg.getIntersections(lastPath); @@ -290,8 +293,8 @@ class BlobTool { items[i].remove(); } lastPath.remove(); - // TODO FIX - //pg.undo.snapshot('eraser'); + // TODO: Add back undo handling + // pg.undo.snapshot('eraser'); }; tool.colorMatch = function (existingPath, addedPath) { @@ -318,7 +321,7 @@ class BlobTool { path1.hitTest(path2.firstSegment.point)) { return true; } - // TODO clean up these no point paths + // TODO: clean up these no point paths return false; }; @@ -338,4 +341,4 @@ class BlobTool { } } -module.exports = BlobTool; +export default BlobTool; diff --git a/src/tools/broad-brush-helper.js b/src/tools/broad-brush-helper.js index 989e5749..718d5a4c 100644 --- a/src/tools/broad-brush-helper.js +++ b/src/tools/broad-brush-helper.js @@ -1,8 +1,14 @@ // Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/ -const paper = require('paper'); +import paper from 'paper'; /** - * Applies segment brush functions to the tool. + * Applies broad brush functions to the tool. Call them when the corresponding mouse event happens + * to get the broad brush behavior. + * + * Broad brush draws strokes by drawing points equidistant from the mouse event, perpendicular to the + * direction of motion. Shortcomings are that this path can cross itself, and 180 degree turns result + * in a flat edge. + * * @param {!Tool} tool paper.js mouse object */ const broadBrushHelper = function (tool) { @@ -102,4 +108,4 @@ const broadBrushHelper = function (tool) { }; }; -module.exports = broadBrushHelper; +export default broadBrushHelper; diff --git a/src/tools/segment-brush-helper.js b/src/tools/segment-brush-helper.js index 449b5e52..d62772e1 100644 --- a/src/tools/segment-brush-helper.js +++ b/src/tools/segment-brush-helper.js @@ -1,59 +1,85 @@ -// Applies segment brush functions to the tool -pg.segmentbrushhelper = function(tool, options) { - var lastPoint, finalPath; +import paper from 'paper'; - tool.onSegmentMouseDown = function(event) { - tool.minDistance = options.brushSize/4; - tool.maxDistance = options.brushSize; - - finalPath = new Path.Circle({ - center: event.point, - radius: options.brushSize/2 - }); - tool.stylePath(finalPath); - lastPoint = event.point; - }; - - tool.onSegmentMouseDrag = function(event) { - var step = (event.delta).normalize(options.brushSize/2); - var handleVec = step.clone(); - handleVec.length = options.brushSize/2; - handleVec.angle += 90; +/** + * Applies segment brush functions to the tool. Call them when the corresponding mouse event happens + * to get the broad brush behavior. + * + * Segment brush draws by creating a rounded rectangle for each mouse move event and merging all of + * those shapes. Unlike the broad brush, the resulting shape will not self-intersect and when you make + * 180 degree turns, you will get a rounded point as expected. Shortcomings include that performance is + * worse, especially as the number of segments to join increase, and that there are problems in paper.js + * with union on shapes with curves, so that chunks of the union tend to disappear. + * (https://github.com/paperjs/paper.js/issues/1321) + * + * @param {!Tool} tool paper.js mouse object + */ +const segmentBrushHelper = function (tool) { + let lastPoint; + let finalPath; - var path = new Path(); - path = pg.stylebar.applyActiveToolbarStyle(path); - path.strokeColor = null; - // Add handles to round the end caps - path.add(new Segment(lastPoint - step, -handleVec, handleVec)); - step.angle += 90; + tool.onSegmentMouseDown = function (event) { + if (event.event.button > 0) return; // only first mouse button - path.add(event.lastPoint + step); - path.insert(0, event.lastPoint - step); - path.add(event.point + step); - path.insert(0, event.point - step); + tool.minDistance = this.options.brushSize / 4; + tool.maxDistance = this.options.brushSize; + + finalPath = new paper.Path.Circle({ + center: event.point, + radius: this.options.brushSize / 2 + }); + tool.stylePath(finalPath); + lastPoint = event.point; + }; + + tool.onSegmentMouseDrag = function (event) { + if (event.event.button > 0) return; // only first mouse button - // Add end cap - step.angle -= 90; - path.add(new Segment(event.point + step, handleVec, -handleVec)); - path.closed = true; - // The unite function on curved paths does not always work (sometimes deletes half the path) - // so we have to flatten. - path.flatten(options.brushSize/5); - - lastPoint = event.point; - var newPath = finalPath.unite(path); - path.remove(); - finalPath.remove(); - finalPath = newPath; - }; + const step = (event.delta).normalize(this.options.brushSize / 2); + const handleVec = step.clone(); + handleVec.length = this.options.brushSize / 2; + handleVec.angle += 90; - tool.onSegmentMouseUp = function(event) { - // 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? + const path = new paper.Path(); + + // TODO: Add back brush styling + // path = pg.stylebar.applyActiveToolbarStyle(path); + path.fillColor = 'black'; - // Smooth the path. - finalPath.simplify(2); - // console.log(finalPath.segments); - return finalPath; - }; -} \ No newline at end of file + // Add handles to round the end caps + path.add(new paper.Segment(lastPoint.subtract(step), handleVec.multiply(-1), handleVec)); + step.angle += 90; + + path.add(event.lastPoint.add(step)); + path.insert(0, event.lastPoint.subtract(step)); + path.add(event.point.add(step)); + path.insert(0, event.point.subtract(step)); + + // Add end cap + step.angle -= 90; + path.add(new paper.Segment(event.point.add(step), handleVec, handleVec.multiply(-1))); + path.closed = true; + // The unite function on curved paths does not always work (sometimes deletes half the path) + // so we have to flatten. + path.flatten(this.options.brushSize / 5); + + lastPoint = event.point; + const newPath = finalPath.unite(path); + path.remove(); + finalPath.remove(); + finalPath = newPath; + }; + + tool.onSegmentMouseUp = function (event) { + 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? + + // Smooth the path. + finalPath.simplify(2); + // console.log(finalPath.segments); + return finalPath; + }; +}; + +export default segmentBrushHelper; From 975bfdc464b696dfc0bf8a7ac9d4a43dfe8a3f76 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 22:58:31 -0400 Subject: [PATCH 10/19] add tests --- src/reducers/brush-tool.js | 6 +++++ src/reducers/eraser-tool.js | 6 +++++ test/unit/blob-tool-reducer.test.js | 42 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 test/unit/blob-tool-reducer.test.js diff --git a/src/reducers/brush-tool.js b/src/reducers/brush-tool.js index 0bc422ce..7faca7f6 100644 --- a/src/reducers/brush-tool.js +++ b/src/reducers/brush-tool.js @@ -1,3 +1,5 @@ +import log from '../log/log'; + const CHANGE_BRUSH_SIZE = 'scratch-paint/tools/CHANGE_BRUSH_SIZE'; const initialState = {brushSize: 5}; @@ -5,6 +7,10 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_BRUSH_SIZE: + if (isNaN(action.brushSize)) { + log.warn(`Invalid brush size: ${action.brushSize}`); + return state; + } return {brushSize: Math.max(1, action.brushSize)}; default: return state; diff --git a/src/reducers/eraser-tool.js b/src/reducers/eraser-tool.js index a985e666..50000eb4 100644 --- a/src/reducers/eraser-tool.js +++ b/src/reducers/eraser-tool.js @@ -1,3 +1,5 @@ +import log from '../log/log'; + const CHANGE_ERASER_SIZE = 'scratch-paint/tools/CHANGE_ERASER_SIZE'; const initialState = {brushSize: 20}; @@ -5,6 +7,10 @@ const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_ERASER_SIZE: + if (isNaN(action.brushSize)) { + log.warn(`Invalid brush size: ${action.brushSize}`); + return state; + } return {brushSize: Math.max(1, action.brushSize)}; default: return state; diff --git a/test/unit/blob-tool-reducer.test.js b/test/unit/blob-tool-reducer.test.js new file mode 100644 index 00000000..4ffe37fe --- /dev/null +++ b/test/unit/blob-tool-reducer.test.js @@ -0,0 +1,42 @@ +/* eslint-env jest */ +import brushReducer from '../../src/reducers/brush-tool'; +import eraserReducer from '../../src/reducers/eraser-tool'; + +test('initialState', () => { + let defaultState; + + expect(brushReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); + expect(brushReducer(defaultState /* state */, {type: 'anything'} /* action */).brushSize).toBeGreaterThan(0); + + expect(eraserReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeTruthy(); + expect(eraserReducer(defaultState /* state */, {type: 'anything'} /* action */).brushSize).toBeGreaterThan(0); +}); + +test('changeBrushSize', () => { + let defaultState; + const newBrushSize = 8078; + + expect(brushReducer(defaultState /* state */, brushReducer.changeBrushSize(newBrushSize) /* action */)) + .toEqual({brushSize: newBrushSize}); + expect(brushReducer(1 /* state */, brushReducer.changeBrushSize(newBrushSize) /* action */)) + .toEqual({brushSize: newBrushSize}); + + expect(eraserReducer(defaultState /* state */, eraserReducer.changeBrushSize(newBrushSize) /* action */)) + .toEqual({brushSize: newBrushSize}); + expect(eraserReducer(1 /* state */, eraserReducer.changeBrushSize(newBrushSize) /* action */)) + .toEqual({brushSize: newBrushSize}); +}); + +test('invalidChangeBrushSize', () => { + const origState = {brushSize: 1}; + + expect(brushReducer(origState /* state */, brushReducer.changeBrushSize('invalid argument') /* action */)) + .toBe(origState); + expect(brushReducer(origState /* state */, brushReducer.changeBrushSize() /* action */)) + .toBe(origState); + + expect(eraserReducer(origState /* state */, eraserReducer.changeBrushSize('invalid argument') /* action */)) + .toBe(origState); + expect(eraserReducer(origState /* state */, eraserReducer.changeBrushSize() /* action */)) + .toBe(origState); +}); From 6e27d133a829d6af0a60a006ac5fa784094adf39 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 27 Jul 2017 23:05:43 -0400 Subject: [PATCH 11/19] remove throttles --- src/reducers/brush-tool.js | 5 +---- src/reducers/eraser-tool.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/reducers/brush-tool.js b/src/reducers/brush-tool.js index 7faca7f6..a155b9fc 100644 --- a/src/reducers/brush-tool.js +++ b/src/reducers/brush-tool.js @@ -21,10 +21,7 @@ const reducer = function (state, action) { reducer.changeBrushSize = function (brushSize) { return { type: CHANGE_BRUSH_SIZE, - brushSize: brushSize, - meta: { - throttle: 30 - } + brushSize: brushSize }; }; diff --git a/src/reducers/eraser-tool.js b/src/reducers/eraser-tool.js index 50000eb4..71a0c8d1 100644 --- a/src/reducers/eraser-tool.js +++ b/src/reducers/eraser-tool.js @@ -21,10 +21,7 @@ const reducer = function (state, action) { reducer.changeBrushSize = function (brushSize) { return { type: CHANGE_ERASER_SIZE, - brushSize: brushSize, - meta: { - throttle: 30 - } + brushSize: brushSize }; }; From 69666f3b8a4d921d4e832b0abb822d16880d9db2 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 1 Aug 2017 11:21:26 -0400 Subject: [PATCH 12/19] smooth the motion of large brushes --- src/tools/broad-brush-helper.js | 4 ++-- src/tools/segment-brush-helper.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/broad-brush-helper.js b/src/tools/broad-brush-helper.js index 718d5a4c..92887833 100644 --- a/src/tools/broad-brush-helper.js +++ b/src/tools/broad-brush-helper.js @@ -17,7 +17,7 @@ const broadBrushHelper = function (tool) { let finalPath; tool.onBroadMouseDown = function (event) { - tool.minDistance = this.options.brushSize / 4; + tool.minDistance = 1; tool.maxDistance = this.options.brushSize; if (event.event.button > 0) return; // only first mouse button @@ -54,7 +54,7 @@ const broadBrushHelper = function (tool) { if (finalPath.segments.length === 5) { // Flatten is necessary to prevent smooth from getting rid of the effect // of the handles on the first point. - finalPath.flatten(this.options.brushSize / 5); + finalPath.flatten(Math.min(5, this.options.brushSize / 5)); } finalPath.smooth(); lastPoint = event.point; diff --git a/src/tools/segment-brush-helper.js b/src/tools/segment-brush-helper.js index d62772e1..659ad00a 100644 --- a/src/tools/segment-brush-helper.js +++ b/src/tools/segment-brush-helper.js @@ -20,7 +20,7 @@ const segmentBrushHelper = function (tool) { tool.onSegmentMouseDown = function (event) { if (event.event.button > 0) return; // only first mouse button - tool.minDistance = this.options.brushSize / 4; + tool.minDistance = 1; tool.maxDistance = this.options.brushSize; finalPath = new paper.Path.Circle({ @@ -60,7 +60,7 @@ const segmentBrushHelper = function (tool) { path.closed = true; // The unite function on curved paths does not always work (sometimes deletes half the path) // so we have to flatten. - path.flatten(this.options.brushSize / 5); + path.flatten(Math.min(5, this.options.brushSize / 5)); lastPoint = event.point; const newPath = finalPath.unite(path); From 087a6264a0fbcc7c71193f9ef432ecf7b240273c Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 15 Aug 2017 18:11:02 -0400 Subject: [PATCH 13/19] rename stuff from tool to mode so it isn't confused with paper.tool' --- src/components/paint-editor.jsx | 10 ++-- .../{tools/brush-tool.jsx => brush-mode.jsx} | 49 ++++++++++--------- .../eraser-tool.jsx => eraser-mode.jsx} | 48 +++++++++--------- src/containers/paint-editor.jsx | 8 +-- src/{tools => modes}/blob.js | 20 ++++---- src/{tools => modes}/broad-brush-helper.js | 0 src/{tools/tool-types.js => modes/modes.js} | 4 +- src/{tools => modes}/segment-brush-helper.js | 0 src/reducers/{brush-tool.js => brush-mode.js} | 9 ++-- src/reducers/combine-reducers.js | 12 ++--- .../{eraser-tool.js => eraser-mode.js} | 2 +- src/reducers/modes.js | 29 +++++++++++ src/reducers/tools.js | 29 ----------- 13 files changed, 112 insertions(+), 108 deletions(-) rename src/containers/{tools/brush-tool.jsx => brush-mode.jsx} (62%) rename src/containers/{tools/eraser-tool.jsx => eraser-mode.jsx} (55%) rename src/{tools => modes}/blob.js (96%) rename src/{tools => modes}/broad-brush-helper.js (100%) rename src/{tools/tool-types.js => modes/modes.js} (57%) rename src/{tools => modes}/segment-brush-helper.js (100%) rename src/reducers/{brush-tool.js => brush-mode.js} (77%) rename src/reducers/{eraser-tool.js => eraser-mode.js} (90%) create mode 100644 src/reducers/modes.js delete mode 100644 src/reducers/tools.js diff --git a/src/components/paint-editor.jsx b/src/components/paint-editor.jsx index 39b570f0..1459fe35 100644 --- a/src/components/paint-editor.jsx +++ b/src/components/paint-editor.jsx @@ -1,8 +1,8 @@ import bindAll from 'lodash.bindall'; import React from 'react'; import PaperCanvas from '../containers/paper-canvas.jsx'; -import BrushTool from '../containers/tools/brush-tool.jsx'; -import EraserTool from '../containers/tools/eraser-tool.jsx'; +import BrushMode from '../containers/brush-mode.jsx'; +import EraserMode from '../containers/eraser-mode.jsx'; class PaintEditorComponent extends React.Component { constructor (props) { @@ -16,13 +16,13 @@ class PaintEditorComponent extends React.Component { this.setState({canvas: canvas}); } render () { - // Tools can't work without a canvas, so we don't render them until we have it + // Modes can't work without a canvas, so we don't render them until we have it if (this.state.canvas) { return (
- - + +
); } diff --git a/src/containers/tools/brush-tool.jsx b/src/containers/brush-mode.jsx similarity index 62% rename from src/containers/tools/brush-tool.jsx rename to src/containers/brush-mode.jsx index 00c1b285..98054cc3 100644 --- a/src/containers/tools/brush-tool.jsx +++ b/src/containers/brush-mode.jsx @@ -2,14 +2,15 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; -import ToolTypes from '../../tools/tool-types.js'; -import BlobTool from '../../tools/blob.js'; -import BrushToolReducer from '../../reducers/brush-tool'; +import Modes from '../modes/modes'; +import Blobbiness from '../modes/blob'; +import BrushModeReducer from '../reducers/brush-mode'; +import {changeBrushSize} from '../reducers/brush-mode'; import paper from 'paper'; -class BrushTool extends React.Component { - static get TOOL_TYPE () { - return ToolTypes.BRUSH; +class BrushMode extends React.Component { + static get MODE () { + return Modes.BRUSH; } constructor (props) { super(props); @@ -18,20 +19,20 @@ class BrushTool extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new BlobTool(); + this.blob = new Blobbiness(); } componentDidMount () { - if (this.props.tool === BrushTool.TOOL_TYPE) { + if (this.props.isBrushModeActive) { this.activateTool(this.props); } } componentWillReceiveProps (nextProps) { - if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool !== BrushTool.TOOL_TYPE) { + if (nextProps.isBrushModeActive && !this.props.isBrushModeActive) { this.activateTool(); - } else if (nextProps.tool !== BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) { + } else if (!nextProps.isBrushModeActive && this.props.isBrushModeActive) { this.deactivateTool(); - } else if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) { - this.blob.setOptions(nextProps.brushToolState); + } else if (nextProps.isBrushModeActive && this.props.isBrushModeActive) { + this.blob.setOptions(nextProps.brushModeState); } } shouldComponentUpdate () { @@ -42,7 +43,7 @@ class BrushTool extends React.Component { this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); - this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushToolState); + this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushModeState); // TODO Make sure a fill color is set on the brush // if(!pg.stylebar.getFillColor()) { @@ -61,39 +62,39 @@ class BrushTool extends React.Component { } onScroll (event) { if (event.deltaY < 0) { - this.props.changeBrushSize(this.props.brushToolState.brushSize + 1); - } else if (event.deltaY > 0 && this.props.brushToolState.brushSize > 1) { - this.props.changeBrushSize(this.props.brushToolState.brushSize - 1); + this.props.changeBrushSize(this.props.brushModeState.brushSize + 1); + } else if (event.deltaY > 0 && this.props.brushModeState.brushSize > 1) { + this.props.changeBrushSize(this.props.brushModeState.brushSize - 1); } return true; } render () { return ( -
Brush Tool
+
Brush Mode
); } } -BrushTool.propTypes = { - brushToolState: PropTypes.shape({ +BrushMode.propTypes = { + brushModeState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, - tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired + isBrushModeActive: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ - brushToolState: state.brushTool, - tool: state.tool + brushModeState: state.brushMode, + isBrushModeActive: state.mode === BrushMode.MODE }); const mapDispatchToProps = dispatch => ({ changeBrushSize: brushSize => { - dispatch(BrushToolReducer.changeBrushSize(brushSize)); + dispatch(changeBrushSize(brushSize)); } }); export default connect( mapStateToProps, mapDispatchToProps -)(BrushTool); +)(BrushMode); diff --git a/src/containers/tools/eraser-tool.jsx b/src/containers/eraser-mode.jsx similarity index 55% rename from src/containers/tools/eraser-tool.jsx rename to src/containers/eraser-mode.jsx index aa0e1268..ad804a32 100644 --- a/src/containers/tools/eraser-tool.jsx +++ b/src/containers/eraser-mode.jsx @@ -2,14 +2,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; -import ToolTypes from '../../tools/tool-types.js'; -import BlobTool from '../../tools/blob.js'; -import EraserToolReducer from '../../reducers/eraser-tool'; +import Modes from '../modes/modes'; +import Blobbiness from '../modes/blob'; +import EraserModeReducer from '../reducers/eraser-mode'; import paper from 'paper'; -class EraserTool extends React.Component { - static get TOOL_TYPE () { - return ToolTypes.ERASER; +class EraserMode extends React.Component { + static get MODE () { + return Modes.ERASER; } constructor (props) { super(props); @@ -18,20 +18,20 @@ class EraserTool extends React.Component { 'deactivateTool', 'onScroll' ]); - this.blob = new BlobTool(); + this.blob = new Blobbiness(); } componentDidMount () { - if (this.props.tool === EraserTool.TOOL_TYPE) { + if (this.props.isEraserModeActive) { this.activateTool(); } } componentWillReceiveProps (nextProps) { - if (nextProps.tool === EraserTool.TOOL_TYPE && this.props.tool !== EraserTool.TOOL_TYPE) { + if (nextProps.isEraserModeActive && !this.props.isEraserModeActive) { this.activateTool(); - } else if (nextProps.tool !== EraserTool.TOOL_TYPE && this.props.tool === EraserTool.TOOL_TYPE) { + } else if (!nextProps.isEraserModeActive && this.props.isEraserModeActive) { this.deactivateTool(); - } else if (nextProps.tool === EraserTool.TOOL_TYPE && this.props.tool === EraserTool.TOOL_TYPE) { - this.blob.setOptions(nextProps.eraserToolState); + } else if (nextProps.isEraserModeActive && this.props.isEraserModeActive) { + this.blob.setOptions(nextProps.eraserModeState); } } shouldComponentUpdate () { @@ -41,7 +41,7 @@ class EraserTool extends React.Component { this.props.canvas.addEventListener('mousewheel', this.onScroll); this.tool = new paper.Tool(); - this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserToolState); + this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserModeState); this.tool.activate(); } deactivateTool () { @@ -51,38 +51,38 @@ class EraserTool extends React.Component { onScroll (event) { event.preventDefault(); if (event.deltaY < 0) { - this.props.changeBrushSize(this.props.eraserToolState.brushSize + 1); - } else if (event.deltaY > 0 && this.props.eraserToolState.brushSize > 1) { - this.props.changeBrushSize(this.props.eraserToolState.brushSize - 1); + this.props.changeBrushSize(this.props.eraserModeState.brushSize + 1); + } else if (event.deltaY > 0 && this.props.eraserModeState.brushSize > 1) { + this.props.changeBrushSize(this.props.eraserModeState.brushSize - 1); } } render () { return ( -
Eraser Tool
+
Eraser Mode
); } } -EraserTool.propTypes = { +EraserMode.propTypes = { canvas: PropTypes.instanceOf(Element).isRequired, changeBrushSize: PropTypes.func.isRequired, - eraserToolState: PropTypes.shape({ + eraserModeState: PropTypes.shape({ brushSize: PropTypes.number.isRequired }), - tool: PropTypes.oneOf(Object.keys(ToolTypes)).isRequired + isEraserModeActive: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ - eraserToolState: state.eraserTool, - tool: state.tool + eraserModeState: state.eraserMode, + isEraserModeActive: state.mode === EraserMode.MODE }); const mapDispatchToProps = dispatch => ({ changeBrushSize: brushSize => { - dispatch(EraserToolReducer.changeBrushSize(brushSize)); + dispatch(EraserModeReducer.changeBrushSize(brushSize)); } }); export default connect( mapStateToProps, mapDispatchToProps -)(EraserTool); +)(EraserMode); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 78eae432..203a8f17 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; -import ToolsReducer from '../reducers/tools'; -import ToolTypes from '../tools/tool-types.js'; +import ModesReducer from '../reducers/modes'; +import Modes from '../modes/modes'; import {connect} from 'react-redux'; class PaintEditor extends React.Component { @@ -26,9 +26,9 @@ PaintEditor.propTypes = { const mapDispatchToProps = dispatch => ({ onKeyPress: event => { if (event.key === 'e') { - dispatch(ToolsReducer.changeTool(ToolTypes.ERASER)); + dispatch(ToolsReducer.changeMode(Modes.ERASER)); } else if (event.key === 'b') { - dispatch(ToolsReducer.changeTool(ToolTypes.BRUSH)); + dispatch(ToolsReducer.changeMode(Modes.BRUSH)); } } }); diff --git a/src/tools/blob.js b/src/modes/blob.js similarity index 96% rename from src/tools/blob.js rename to src/modes/blob.js index 509f28e4..c822f45f 100644 --- a/src/tools/blob.js +++ b/src/modes/blob.js @@ -4,11 +4,11 @@ import broadBrushHelper from './broad-brush-helper'; import segmentBrushHelper from './segment-brush-helper'; /** - * Shared code for the brush and eraser tool. Adds functions on the paper tool object + * Shared code for the brush and eraser mode. Adds functions on the paper tool object * to handle mouse events, which are delegated to broad-brush-helper and segment-brush-helper * based on the brushSize in the state. */ -class BlobTool { +class Blobbiness { static get BROAD () { return 'broadbrush'; @@ -97,11 +97,11 @@ class BlobTool { tool.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button - if (this.options.brushSize < BlobTool.THRESHOLD) { - this.brush = BlobTool.BROAD; + if (this.options.brushSize < Blobbiness.THRESHOLD) { + this.brush = Blobbiness.BROAD; this.onBroadMouseDown(event); } else { - this.brush = BlobTool.SEGMENT; + this.brush = Blobbiness.SEGMENT; this.onSegmentMouseDown(event); } this.cursorPreview.bringToFront(); @@ -112,9 +112,9 @@ class BlobTool { tool.onMouseDrag = function (event) { tool.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button - if (this.brush === BlobTool.BROAD) { + if (this.brush === Blobbiness.BROAD) { this.onBroadMouseDrag(event); - } else if (this.brush === BlobTool.SEGMENT) { + } else if (this.brush === Blobbiness.SEGMENT) { this.onSegmentMouseDrag(event); } else { log.warn(`Brush type does not exist: ${this.brush}`); @@ -130,9 +130,9 @@ class BlobTool { if (event.event.button > 0) return; // only first mouse button let lastPath; - if (this.brush === BlobTool.BROAD) { + if (this.brush === Blobbiness.BROAD) { lastPath = this.onBroadMouseUp(event); - } else if (this.brush === BlobTool.SEGMENT) { + } else if (this.brush === Blobbiness.SEGMENT) { lastPath = this.onSegmentMouseUp(event); } else { log.warn(`Brush type does not exist: ${this.brush}`); @@ -341,4 +341,4 @@ class BlobTool { } } -export default BlobTool; +export default Blobbiness; diff --git a/src/tools/broad-brush-helper.js b/src/modes/broad-brush-helper.js similarity index 100% rename from src/tools/broad-brush-helper.js rename to src/modes/broad-brush-helper.js diff --git a/src/tools/tool-types.js b/src/modes/modes.js similarity index 57% rename from src/tools/tool-types.js rename to src/modes/modes.js index 9a850d80..c485f0fe 100644 --- a/src/tools/tool-types.js +++ b/src/modes/modes.js @@ -1,8 +1,8 @@ import keyMirror from 'keymirror'; -const ToolTypes = keyMirror({ +const Modes = keyMirror({ BRUSH: null, ERASER: null }); -export default ToolTypes; +export default Modes; diff --git a/src/tools/segment-brush-helper.js b/src/modes/segment-brush-helper.js similarity index 100% rename from src/tools/segment-brush-helper.js rename to src/modes/segment-brush-helper.js diff --git a/src/reducers/brush-tool.js b/src/reducers/brush-mode.js similarity index 77% rename from src/reducers/brush-tool.js rename to src/reducers/brush-mode.js index a155b9fc..16315bef 100644 --- a/src/reducers/brush-tool.js +++ b/src/reducers/brush-mode.js @@ -1,6 +1,6 @@ import log from '../log/log'; -const CHANGE_BRUSH_SIZE = 'scratch-paint/tools/CHANGE_BRUSH_SIZE'; +const CHANGE_BRUSH_SIZE = 'scratch-paint/brush-mode/CHANGE_BRUSH_SIZE'; const initialState = {brushSize: 5}; const reducer = function (state, action) { @@ -18,11 +18,14 @@ const reducer = function (state, action) { }; // Action creators ================================== -reducer.changeBrushSize = function (brushSize) { +const changeBrushSize = function (brushSize) { return { type: CHANGE_BRUSH_SIZE, brushSize: brushSize }; }; -export default reducer; +export { + reducer as default, + changeBrushSize +}; diff --git a/src/reducers/combine-reducers.js b/src/reducers/combine-reducers.js index a5654a63..31cfccc0 100644 --- a/src/reducers/combine-reducers.js +++ b/src/reducers/combine-reducers.js @@ -1,10 +1,10 @@ import {combineReducers} from 'redux'; -import toolReducer from './tools'; -import brushToolReducer from './brush-tool'; -import eraserToolReducer from './eraser-tool'; +import modeReducer from './modes'; +import brushModeReducer from './brush-mode'; +import eraserModeReducer from './eraser-mode'; export default combineReducers({ - tool: toolReducer, - brushTool: brushToolReducer, - eraserTool: eraserToolReducer + mode: modeReducer, + brushMode: brushModeReducer, + eraserMode: eraserModeReducer }); diff --git a/src/reducers/eraser-tool.js b/src/reducers/eraser-mode.js similarity index 90% rename from src/reducers/eraser-tool.js rename to src/reducers/eraser-mode.js index 71a0c8d1..4f4ca50e 100644 --- a/src/reducers/eraser-tool.js +++ b/src/reducers/eraser-mode.js @@ -1,6 +1,6 @@ import log from '../log/log'; -const CHANGE_ERASER_SIZE = 'scratch-paint/tools/CHANGE_ERASER_SIZE'; +const CHANGE_ERASER_SIZE = 'scratch-paint/eraser-mode/CHANGE_ERASER_SIZE'; const initialState = {brushSize: 20}; const reducer = function (state, action) { diff --git a/src/reducers/modes.js b/src/reducers/modes.js new file mode 100644 index 00000000..fa2f756a --- /dev/null +++ b/src/reducers/modes.js @@ -0,0 +1,29 @@ +import Modes from '../modes/modes'; +import log from '../log/log'; + +const CHANGE_MODE = 'scratch-paint/modes/CHANGE_MODE'; +const initialState = Modes.BRUSH; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_MODE: + if (action.mode in Modes) { + return action.mode; + } + log.warn(`Mode does not exist: ${action.mode}`); + /* falls through */ + default: + return state; + } +}; + +// Action creators ================================== +reducer.changeMode = function (mode) { + return { + type: CHANGE_MODE, + mode: mode + }; +}; + +export default reducer; diff --git a/src/reducers/tools.js b/src/reducers/tools.js deleted file mode 100644 index 57b7d4c7..00000000 --- a/src/reducers/tools.js +++ /dev/null @@ -1,29 +0,0 @@ -import ToolTypes from '../tools/tool-types'; -import log from '../log/log'; - -const CHANGE_TOOL = 'scratch-paint/tools/CHANGE_TOOL'; -const initialState = ToolTypes.BRUSH; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case CHANGE_TOOL: - if (action.tool in ToolTypes) { - return action.tool; - } - log.warn(`Tool type does not exist: ${action.tool}`); - /* falls through */ - default: - return state; - } -}; - -// Action creators ================================== -reducer.changeTool = function (tool) { - return { - type: CHANGE_TOOL, - tool: tool - }; -}; - -export default reducer; From 33a01c1396fa43a4f9d05d0d2144952a916310db Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 16 Aug 2017 15:34:33 -0400 Subject: [PATCH 14/19] make modes independent of tool. Fix tests --- src/containers/brush-mode.jsx | 6 +---- src/containers/eraser-mode.jsx | 4 +--- src/modes/blob.js | 4 ++-- src/modes/segment-brush-helper.js | 1 - src/reducers/eraser-mode.js | 7 ++++-- src/reducers/modes.js | 7 ++++-- ...ucer.test.js => blob-mode-reducer.test.js} | 22 +++++++++-------- test/unit/modes-reducer.test.js | 24 +++++++++++++++++++ test/unit/tools-reducer.test.js | 23 ------------------ 9 files changed, 50 insertions(+), 48 deletions(-) rename test/unit/{blob-tool-reducer.test.js => blob-mode-reducer.test.js} (51%) create mode 100644 test/unit/modes-reducer.test.js delete mode 100644 test/unit/tools-reducer.test.js diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 98054cc3..e81da224 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -41,9 +41,7 @@ class BrushMode extends React.Component { activateTool () { // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); - - this.tool = new paper.Tool(); - this.blob.activateTool(false /* isEraser */, this.tool, this.props.brushModeState); + this.blob.activateTool(false /* isEraser */, this.props.brushModeState); // TODO Make sure a fill color is set on the brush // if(!pg.stylebar.getFillColor()) { @@ -53,8 +51,6 @@ class BrushMode extends React.Component { // TODO setup floating tool options panel in the editor // pg.toolOptionPanel.setup(options, components, function() {}); - - this.tool.activate(); } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index ad804a32..f27f5997 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -40,9 +40,7 @@ class EraserMode extends React.Component { activateTool () { this.props.canvas.addEventListener('mousewheel', this.onScroll); - this.tool = new paper.Tool(); - this.blob.activateTool(true /* isEraser */, this.tool, this.props.eraserModeState); - this.tool.activate(); + this.blob.activateTool(true /* isEraser */, this.props.eraserModeState); } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); diff --git a/src/modes/blob.js b/src/modes/blob.js index c822f45f..07f7de34 100644 --- a/src/modes/blob.js +++ b/src/modes/blob.js @@ -31,8 +31,8 @@ class Blobbiness { } } - activateTool (isEraser, tool, options) { - this.tool = tool; + activateTool (isEraser, options) { + this.tool = new paper.Tool(); tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000); tool.resizeCursorIfNeeded = function (point) { diff --git a/src/modes/segment-brush-helper.js b/src/modes/segment-brush-helper.js index 659ad00a..26d19a49 100644 --- a/src/modes/segment-brush-helper.js +++ b/src/modes/segment-brush-helper.js @@ -77,7 +77,6 @@ const segmentBrushHelper = function (tool) { // Smooth the path. finalPath.simplify(2); - // console.log(finalPath.segments); return finalPath; }; }; diff --git a/src/reducers/eraser-mode.js b/src/reducers/eraser-mode.js index 4f4ca50e..b543cfc6 100644 --- a/src/reducers/eraser-mode.js +++ b/src/reducers/eraser-mode.js @@ -18,11 +18,14 @@ const reducer = function (state, action) { }; // Action creators ================================== -reducer.changeBrushSize = function (brushSize) { +const changeBrushSize = function (brushSize) { return { type: CHANGE_ERASER_SIZE, brushSize: brushSize }; }; -export default reducer; +export { + reducer as default, + changeBrushSize +}; \ No newline at end of file diff --git a/src/reducers/modes.js b/src/reducers/modes.js index fa2f756a..00f12c50 100644 --- a/src/reducers/modes.js +++ b/src/reducers/modes.js @@ -19,11 +19,14 @@ const reducer = function (state, action) { }; // Action creators ================================== -reducer.changeMode = function (mode) { +const changeMode = function (mode) { return { type: CHANGE_MODE, mode: mode }; }; -export default reducer; +export { + reducer as default, + changeMode +}; diff --git a/test/unit/blob-tool-reducer.test.js b/test/unit/blob-mode-reducer.test.js similarity index 51% rename from test/unit/blob-tool-reducer.test.js rename to test/unit/blob-mode-reducer.test.js index 4ffe37fe..845b9111 100644 --- a/test/unit/blob-tool-reducer.test.js +++ b/test/unit/blob-mode-reducer.test.js @@ -1,6 +1,8 @@ /* eslint-env jest */ -import brushReducer from '../../src/reducers/brush-tool'; -import eraserReducer from '../../src/reducers/eraser-tool'; +import brushReducer from '../../src/reducers/brush-mode'; +import {changeBrushSize} from '../../src/reducers/brush-mode'; +import eraserReducer from '../../src/reducers/eraser-mode'; +import {changeBrushSize as changeEraserSize} from '../../src/reducers/eraser-mode'; test('initialState', () => { let defaultState; @@ -16,27 +18,27 @@ test('changeBrushSize', () => { let defaultState; const newBrushSize = 8078; - expect(brushReducer(defaultState /* state */, brushReducer.changeBrushSize(newBrushSize) /* action */)) + expect(brushReducer(defaultState /* state */, changeBrushSize(newBrushSize) /* action */)) .toEqual({brushSize: newBrushSize}); - expect(brushReducer(1 /* state */, brushReducer.changeBrushSize(newBrushSize) /* action */)) + expect(brushReducer(1 /* state */, changeBrushSize(newBrushSize) /* action */)) .toEqual({brushSize: newBrushSize}); - expect(eraserReducer(defaultState /* state */, eraserReducer.changeBrushSize(newBrushSize) /* action */)) + expect(eraserReducer(defaultState /* state */, changeEraserSize(newBrushSize) /* action */)) .toEqual({brushSize: newBrushSize}); - expect(eraserReducer(1 /* state */, eraserReducer.changeBrushSize(newBrushSize) /* action */)) + expect(eraserReducer(1 /* state */, changeEraserSize(newBrushSize) /* action */)) .toEqual({brushSize: newBrushSize}); }); test('invalidChangeBrushSize', () => { const origState = {brushSize: 1}; - expect(brushReducer(origState /* state */, brushReducer.changeBrushSize('invalid argument') /* action */)) + expect(brushReducer(origState /* state */, changeBrushSize('invalid argument') /* action */)) .toBe(origState); - expect(brushReducer(origState /* state */, brushReducer.changeBrushSize() /* action */)) + expect(brushReducer(origState /* state */, changeBrushSize() /* action */)) .toBe(origState); - expect(eraserReducer(origState /* state */, eraserReducer.changeBrushSize('invalid argument') /* action */)) + expect(eraserReducer(origState /* state */, changeEraserSize('invalid argument') /* action */)) .toBe(origState); - expect(eraserReducer(origState /* state */, eraserReducer.changeBrushSize() /* action */)) + expect(eraserReducer(origState /* state */, changeEraserSize() /* action */)) .toBe(origState); }); diff --git a/test/unit/modes-reducer.test.js b/test/unit/modes-reducer.test.js new file mode 100644 index 00000000..8f08ffc9 --- /dev/null +++ b/test/unit/modes-reducer.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ +import Modes from '../../src/modes/modes'; +import reducer from '../../src/reducers/modes'; +import {changeMode} from '../../src/reducers/modes'; + +test('initialState', () => { + let defaultState; + expect(reducer(defaultState /* state */, {type: 'anything'} /* action */) in Modes).toBeTruthy(); +}); + +test('changeMode', () => { + let defaultState; + expect(reducer(defaultState /* state */, changeMode(Modes.ERASER) /* action */)).toBe(Modes.ERASER); + expect(reducer(Modes.ERASER /* state */, changeMode(Modes.ERASER) /* action */)) + .toBe(Modes.ERASER); + expect(reducer(Modes.BRUSH /* state */, changeMode(Modes.ERASER) /* action */)) + .toBe(Modes.ERASER); +}); + +test('invalidChangeMode', () => { + expect(reducer(Modes.BRUSH /* state */, changeMode('non-existant mode') /* action */)) + .toBe(Modes.BRUSH); + expect(reducer(Modes.BRUSH /* state */, changeMode() /* action */)).toBe(Modes.BRUSH); +}); diff --git a/test/unit/tools-reducer.test.js b/test/unit/tools-reducer.test.js deleted file mode 100644 index 428b3118..00000000 --- a/test/unit/tools-reducer.test.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-env jest */ -import ToolTypes from '../../src/tools/tool-types'; -import reducer from '../../src/reducers/tools'; - -test('initialState', () => { - let defaultState; - expect(reducer(defaultState /* state */, {type: 'anything'} /* action */) in ToolTypes).toBeTruthy(); -}); - -test('changeTool', () => { - let defaultState; - expect(reducer(defaultState /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)).toBe(ToolTypes.ERASER); - expect(reducer(ToolTypes.ERASER /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)) - .toBe(ToolTypes.ERASER); - expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool(ToolTypes.ERASER) /* action */)) - .toBe(ToolTypes.ERASER); -}); - -test('invalidChangeTool', () => { - expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool('non-existant tool') /* action */)) - .toBe(ToolTypes.BRUSH); - expect(reducer(ToolTypes.BRUSH /* state */, reducer.changeTool() /* action */)).toBe(ToolTypes.BRUSH); -}); From 4ea7d154eeba8436c6f88a6316b588eee4e798ef Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 16 Aug 2017 17:44:52 -0400 Subject: [PATCH 15/19] Change broad brush helper and segment brush helper into their own classes, instead of adding functions to tool --- src/containers/brush-mode.jsx | 2 - src/containers/eraser-mode.jsx | 1 - src/modes/blob.js | 542 +++++++++++++++--------------- src/modes/broad-brush-helper.js | 111 +++--- src/modes/segment-brush-helper.js | 57 ++-- src/modes/style-path.js | 28 ++ src/reducers/eraser-mode.js | 2 +- 7 files changed, 380 insertions(+), 363 deletions(-) create mode 100644 src/modes/style-path.js diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index e81da224..f7b5d5cc 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -4,9 +4,7 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import Blobbiness from '../modes/blob'; -import BrushModeReducer from '../reducers/brush-mode'; import {changeBrushSize} from '../reducers/brush-mode'; -import paper from 'paper'; class BrushMode extends React.Component { static get MODE () { diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index f27f5997..f1ecff96 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -5,7 +5,6 @@ import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import Blobbiness from '../modes/blob'; import EraserModeReducer from '../reducers/eraser-mode'; -import paper from 'paper'; class EraserMode extends React.Component { static get MODE () { diff --git a/src/modes/blob.js b/src/modes/blob.js index 07f7de34..f1bec3bb 100644 --- a/src/modes/blob.js +++ b/src/modes/blob.js @@ -1,7 +1,8 @@ import paper from 'paper'; import log from '../log/log'; -import broadBrushHelper from './broad-brush-helper'; -import segmentBrushHelper from './segment-brush-helper'; +import BroadBrushHelper from './broad-brush-helper'; +import SegmentBrushHelper from './segment-brush-helper'; +import {styleCursorPreview} from './style-path'; /** * Shared code for the brush and eraser mode. Adds functions on the paper tool object @@ -9,7 +10,6 @@ import segmentBrushHelper from './segment-brush-helper'; * based on the brushSize in the state. */ class Blobbiness { - static get BROAD () { return 'broadbrush'; } @@ -23,321 +23,307 @@ class Blobbiness { static get THRESHOLD () { return 9; } + + constructor () { + this.broadBrushHelper = new BroadBrushHelper(); + this.segmentBrushHelper = new SegmentBrushHelper(); + } setOptions (options) { - if (this.tool) { - this.tool.options = options; - this.tool.resizeCursorIfNeeded(); - } + this.options = options; + this.resizeCursorIfNeeded(); } activateTool (isEraser, options) { this.tool = new paper.Tool(); - - tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000); - tool.resizeCursorIfNeeded = function (point) { - if (typeof point === 'undefined') { - point = this.cursorPreviewLastPoint; - } else { - this.cursorPreviewLastPoint = point; - } - - if (this.brushSize === this.options.brushSize) { - return; - } - const newPreview = new paper.Path.Circle({ - center: point, - radius: this.options.brushSize / 2 - }); - if (this.cursorPreview) { - this.cursorPreview.segments = newPreview.segments; - newPreview.remove(); - } else { - this.cursorPreview = newPreview; - } - this.brushSize = this.options.brushSize; - }; - + this.isEraser = isEraser; + this.cursorPreviewLastPoint = new paper.Point(-10000, -10000); this.setOptions(options); + styleCursorPreview(this.cursorPreview); + this.tool.fixedDistance = 1; - tool.stylePath = function (path) { - if (isEraser) { - path.fillColor = 'white'; - if (path === this.cursorPreview) { - path.strokeColor = 'cornflowerblue'; - path.strokeWidth = 1; - } - } else { - // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. - // path = pg.stylebar.applyActiveToolbarStyle(path); - - path.fillColor = 'black'; - if (path === this.cursorPreview) { - path.strokeColor = 'cornflowerblue'; - path.strokeWidth = 1; - } - } - }; - - tool.stylePath(this.tool.cursorPreview); - - tool.fixedDistance = 1; - - broadBrushHelper(tool); - segmentBrushHelper(tool); - - tool.onMouseMove = function (event) { - tool.resizeCursorIfNeeded(event.point); - tool.stylePath(this.cursorPreview); - this.cursorPreview.bringToFront(); - this.cursorPreview.position = event.point; + const blob = this; + this.tool.onMouseMove = function (event) { + blob.resizeCursorIfNeeded(event.point); + styleCursorPreview(blob.cursorPreview); + blob.cursorPreview.bringToFront(); + blob.cursorPreview.position = event.point; }; - tool.onMouseDown = function (event) { - tool.resizeCursorIfNeeded(event.point); + this.tool.onMouseDown = function (event) { + blob.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button - if (this.options.brushSize < Blobbiness.THRESHOLD) { - this.brush = Blobbiness.BROAD; - this.onBroadMouseDown(event); + if (blob.options.brushSize < Blobbiness.THRESHOLD) { + blob.brush = Blobbiness.BROAD; + blob.broadBrushHelper.onBroadMouseDown(event, blob.tool, blob.options); } else { - this.brush = Blobbiness.SEGMENT; - this.onSegmentMouseDown(event); + blob.brush = Blobbiness.SEGMENT; + blob.segmentBrushHelper.onSegmentMouseDown(event, blob.tool, blob.options); } - this.cursorPreview.bringToFront(); - this.cursorPreview.position = event.point; + blob.cursorPreview.bringToFront(); + blob.cursorPreview.position = event.point; paper.view.draw(); }; - tool.onMouseDrag = function (event) { - tool.resizeCursorIfNeeded(event.point); + this.tool.onMouseDrag = function (event) { + blob.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button - if (this.brush === Blobbiness.BROAD) { - this.onBroadMouseDrag(event); - } else if (this.brush === Blobbiness.SEGMENT) { - this.onSegmentMouseDrag(event); + if (blob.brush === Blobbiness.BROAD) { + blob.broadBrushHelper.onBroadMouseDrag(event, blob.options); + } else if (blob.brush === Blobbiness.SEGMENT) { + blob.segmentBrushHelper.onSegmentMouseDrag(event, blob.options); } else { - log.warn(`Brush type does not exist: ${this.brush}`); + log.warn(`Brush type does not exist: ${blob.brush}`); } - this.cursorPreview.bringToFront(); - this.cursorPreview.position = event.point; + blob.cursorPreview.bringToFront(); + blob.cursorPreview.position = event.point; paper.view.draw(); }; - tool.onMouseUp = function (event) { - tool.resizeCursorIfNeeded(event.point); + this.tool.onMouseUp = function (event) { + blob.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button let lastPath; - if (this.brush === Blobbiness.BROAD) { - lastPath = this.onBroadMouseUp(event); - } else if (this.brush === Blobbiness.SEGMENT) { - lastPath = this.onSegmentMouseUp(event); + if (blob.brush === Blobbiness.BROAD) { + lastPath = blob.broadBrushHelper.onBroadMouseUp(event, blob.tool, blob.options); + } else if (blob.brush === Blobbiness.SEGMENT) { + lastPath = blob.segmentBrushHelper.onSegmentMouseUp(event); } else { - log.warn(`Brush type does not exist: ${this.brush}`); + log.warn(`Brush type does not exist: ${blob.brush}`); } if (isEraser) { - tool.mergeEraser(lastPath); + blob.mergeEraser(lastPath); } else { - tool.mergeBrush(lastPath); + blob.mergeBrush(lastPath); } - this.cursorPreview.bringToFront(); - this.cursorPreview.position = event.point; + blob.cursorPreview.bringToFront(); + blob.cursorPreview.position = event.point; // Reset - this.brush = null; - tool.fixedDistance = 1; - }; - - tool.mergeBrush = function (lastPath) { - // Get all path items to merge with - const paths = paper.project.getItems({ - match: function (item) { - return tool.isMergeable(lastPath, item); - } - }); - - let mergedPath = lastPath; - let i; - // Move down z order to first overlapping item - for (i = paths.length - 1; i >= 0 && !tool.touches(paths[i], lastPath); i--) { - continue; - } - let mergedPathIndex = i; - for (; i >= 0; i--) { - if (!tool.touches(paths[i], lastPath)) { - continue; - } - if (!paths[i].getFillColor()) { - // Ignore for merge. Paths without fill need to be in paths though, - // since they can visibly change if z order changes - } else if (tool.colorMatch(paths[i], lastPath)) { - // Make sure the new shape isn't overlapped by anything that would - // visibly change if we change its z order - for (let j = mergedPathIndex; j > i; j--) { - if (tool.touches(paths[j], paths[i])) { - continue; - } - } - // Merge same fill color - const tempPath = mergedPath.unite(paths[i]); - tempPath.strokeColor = paths[i].strokeColor; - tempPath.strokeWidth = paths[i].strokeWidth; - if (mergedPath === lastPath) { - tempPath.insertAbove(paths[i]); // First intersected path determines z position of the new path - } else { - tempPath.insertAbove(mergedPath); // Rest of merges join z index of merged path - mergedPathIndex--; // Removed an item, so the merged path index decreases - } - mergedPath.remove(); - mergedPath = tempPath; - paths[i].remove(); - paths.splice(i, 1); - } - } - // TODO: Add back undo - // pg.undo.snapshot('broadbrush'); - }; - - tool.mergeEraser = function (lastPath) { - // Get all path items to merge with - // If there are selected items, try to erase from amongst those. - let items = paper.project.getItems({ - match: function (item) { - return item.selected && tool.isMergeable(lastPath, item) && tool.touches(lastPath, item); - } - }); - // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset - // and deselect the selection - if (items.length === 0) { - // TODO: Add back selection handling - // pg.selection.clearSelection(); - items = paper.project.getItems({ - match: function (item) { - return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item); - } - }); - } - - for (let i = items.length - 1; i >= 0; i--) { - // Erase - const newPath = items[i].subtract(lastPath); - - // Gather path segments - const subpaths = []; - // TODO: Handle compound path - if (items[i] instanceof paper.Path && !items[i].closed) { - const firstSeg = items[i].clone(); - const intersections = firstSeg.getIntersections(lastPath); - // keep first and last segments - if (intersections.length === 0) { - continue; - } - for (let j = intersections.length - 1; j >= 0; j--) { - subpaths.push(firstSeg.splitAt(intersections[j])); - } - subpaths.push(firstSeg); - } - - // Remove the ones that are within the eraser stroke boundary, or are already part of new path. - // This way subpaths only remain if they didn't get turned into a shape by subtract. - for (let k = subpaths.length - 1; k >= 0; k--) { - const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point; - if (lastPath.contains(segMidpoint) || newPath.contains(segMidpoint)) { - subpaths[k].remove(); - subpaths.splice(k, 1); - } - } - - // Divide topologically separate shapes into their own compound paths, instead of - // everything being stuck together. - // Assume that result of erase operation returns clockwise paths for positive shapes - const clockwiseChildren = []; - const ccwChildren = []; - if (newPath.children) { - for (let j = newPath.children.length - 1; j >= 0; j--) { - const child = newPath.children[j]; - if (child.isClockwise()) { - clockwiseChildren.push(child); - } else { - ccwChildren.push(child); - } - } - for (let j = 0; j < clockwiseChildren.length; j++) { - const cw = clockwiseChildren[j]; - cw.copyAttributes(newPath); - cw.fillColor = newPath.fillColor; - cw.strokeColor = newPath.strokeColor; - cw.strokeWidth = newPath.strokeWidth; - cw.insertAbove(items[i]); - - // Go backward since we are deleting elements - let newCw = cw; - for (let k = ccwChildren.length - 1; k >= 0; k--) { - const ccw = ccwChildren[k]; - if (tool.firstEnclosesSecond(ccw, cw) || tool.firstEnclosesSecond(cw, ccw)) { - const temp = newCw.subtract(ccw); - temp.insertAbove(newCw); - newCw.remove(); - newCw = temp; - ccw.remove(); - ccwChildren.splice(k, 1); - } - } - } - newPath.remove(); - } - items[i].remove(); - } - lastPath.remove(); - // TODO: Add back undo handling - // pg.undo.snapshot('eraser'); - }; - - tool.colorMatch = function (existingPath, addedPath) { - // Note: transparent fill colors do notdetect as touching - return existingPath.getFillColor().equals(addedPath.getFillColor()) && - (addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null - (addedPath.getStrokeColor() && - addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) && - addedPath.getStrokeWidth() === existingPath.getStrokeWidth() && - tool.touches(existingPath, addedPath); - }; - - tool.touches = function (path1, path2) { - // Two shapes are touching if their paths intersect - if (path1 && path2 && path1.intersects(path2)) { - return true; - } - return tool.firstEnclosesSecond(path1, path2) || tool.firstEnclosesSecond(path2, path1); - }; - - tool.firstEnclosesSecond = function (path1, path2) { - // Two shapes are also touching if one is completely inside the other - if (path1 && path2 && path2.firstSegment && path2.firstSegment.point && - path1.hitTest(path2.firstSegment.point)) { - return true; - } - // TODO: clean up these no point paths - return false; - }; - - tool.isMergeable = function (newPath, existingPath) { - return existingPath instanceof paper.PathItem && // path or compound path - existingPath !== this.cursorPreview && // don't merge with the mouse preview - existingPath !== newPath && // don't merge with self - existingPath.parent instanceof paper.Layer; // don't merge with nested in group + blob.brush = null; + this.fixedDistance = 1; }; } - deactivateTool () { - if (this.tool) { - this.tool.cursorPreview.remove(); - this.tool.remove(); + resizeCursorIfNeeded (point) { + if (!this.options) { + return; } + + if (typeof point === 'undefined') { + point = this.cursorPreviewLastPoint; + } else { + this.cursorPreviewLastPoint = point; + } + + if (this.brushSize === this.options.brushSize) { + return; + } + const newPreview = new paper.Path.Circle({ + center: point, + radius: this.options.brushSize / 2 + }); + if (this.cursorPreview) { + this.cursorPreview.segments = newPreview.segments; + newPreview.remove(); + } else { + this.cursorPreview = newPreview; + } + this.brushSize = this.options.brushSize; + } + + mergeBrush (lastPath) { + const blob = this; + + // Get all path items to merge with + const paths = paper.project.getItems({ + match: function (item) { + return blob.isMergeable(lastPath, item); + } + }); + + let mergedPath = lastPath; + let i; + // Move down z order to first overlapping item + for (i = paths.length - 1; i >= 0 && !this.touches(paths[i], lastPath); i--) { + continue; + } + let mergedPathIndex = i; + for (; i >= 0; i--) { + if (!this.touches(paths[i], lastPath)) { + continue; + } + if (!paths[i].getFillColor()) { + // Ignore for merge. Paths without fill need to be in paths though, + // since they can visibly change if z order changes + } else if (this.colorMatch(paths[i], lastPath)) { + // Make sure the new shape isn't overlapped by anything that would + // visibly change if we change its z order + for (let j = mergedPathIndex; j > i; j--) { + if (this.touches(paths[j], paths[i])) { + continue; + } + } + // Merge same fill color + const tempPath = mergedPath.unite(paths[i]); + tempPath.strokeColor = paths[i].strokeColor; + tempPath.strokeWidth = paths[i].strokeWidth; + if (mergedPath === lastPath) { + tempPath.insertAbove(paths[i]); // First intersected path determines z position of the new path + } else { + tempPath.insertAbove(mergedPath); // Rest of merges join z index of merged path + mergedPathIndex--; // Removed an item, so the merged path index decreases + } + mergedPath.remove(); + mergedPath = tempPath; + paths[i].remove(); + paths.splice(i, 1); + } + } + // TODO: Add back undo + // pg.undo.snapshot('broadbrush'); + } + + mergeEraser (lastPath) { + const blob = this; + + // Get all path items to merge with + // If there are selected items, try to erase from amongst those. + let items = paper.project.getItems({ + match: function (item) { + return item.selected && blob.isMergeable(lastPath, item) && blob.touches(lastPath, item); + } + }); + // Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset + // and deselect the selection + if (items.length === 0) { + // TODO: Add back selection handling + // pg.selection.clearSelection(); + items = paper.project.getItems({ + match: function (item) { + return blob.isMergeable(lastPath, item) && blob.touches(lastPath, item); + } + }); + } + + for (let i = items.length - 1; i >= 0; i--) { + // Erase + const newPath = items[i].subtract(lastPath); + + // Gather path segments + const subpaths = []; + // TODO: Handle compound path + if (items[i] instanceof paper.Path && !items[i].closed) { + const firstSeg = items[i].clone(); + const intersections = firstSeg.getIntersections(lastPath); + // keep first and last segments + if (intersections.length === 0) { + continue; + } + for (let j = intersections.length - 1; j >= 0; j--) { + subpaths.push(firstSeg.splitAt(intersections[j])); + } + subpaths.push(firstSeg); + } + + // Remove the ones that are within the eraser stroke boundary, or are already part of new path. + // This way subpaths only remain if they didn't get turned into a shape by subtract. + for (let k = subpaths.length - 1; k >= 0; k--) { + const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point; + if (lastPath.contains(segMidpoint) || newPath.contains(segMidpoint)) { + subpaths[k].remove(); + subpaths.splice(k, 1); + } + } + + // Divide topologically separate shapes into their own compound paths, instead of + // everything being stuck together. + // Assume that result of erase operation returns clockwise paths for positive shapes + const clockwiseChildren = []; + const ccwChildren = []; + if (newPath.children) { + for (let j = newPath.children.length - 1; j >= 0; j--) { + const child = newPath.children[j]; + if (child.isClockwise()) { + clockwiseChildren.push(child); + } else { + ccwChildren.push(child); + } + } + for (let j = 0; j < clockwiseChildren.length; j++) { + const cw = clockwiseChildren[j]; + cw.copyAttributes(newPath); + cw.fillColor = newPath.fillColor; + cw.strokeColor = newPath.strokeColor; + cw.strokeWidth = newPath.strokeWidth; + cw.insertAbove(items[i]); + + // Go backward since we are deleting elements + let newCw = cw; + for (let k = ccwChildren.length - 1; k >= 0; k--) { + const ccw = ccwChildren[k]; + if (this.firstEnclosesSecond(ccw, cw) || this.firstEnclosesSecond(cw, ccw)) { + const temp = newCw.subtract(ccw); + temp.insertAbove(newCw); + newCw.remove(); + newCw = temp; + ccw.remove(); + ccwChildren.splice(k, 1); + } + } + } + newPath.remove(); + } + items[i].remove(); + } + lastPath.remove(); + // TODO: Add back undo handling + // pg.undo.snapshot('eraser'); + } + + colorMatch (existingPath, addedPath) { + // Note: transparent fill colors do notdetect as touching + return existingPath.getFillColor().equals(addedPath.getFillColor()) && + (addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null + (addedPath.getStrokeColor() && + addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) && + addedPath.getStrokeWidth() === existingPath.getStrokeWidth() && + this.touches(existingPath, addedPath); + } + + touches (path1, path2) { + // Two shapes are touching if their paths intersect + if (path1 && path2 && path1.intersects(path2)) { + return true; + } + return this.firstEnclosesSecond(path1, path2) || this.firstEnclosesSecond(path2, path1); + } + + firstEnclosesSecond (path1, path2) { + // Two shapes are also touching if one is completely inside the other + if (path1 && path2 && path2.firstSegment && path2.firstSegment.point && + path1.hitTest(path2.firstSegment.point)) { + return true; + } + // TODO: clean up these no point paths + return false; + } + + isMergeable (newPath, existingPath) { + return existingPath instanceof paper.PathItem && // path or compound path + existingPath !== this.cursorPreview && // don't merge with the mouse preview + existingPath !== newPath && // don't merge with self + existingPath.parent instanceof paper.Layer; // don't merge with nested in group + } + + deactivateTool () { + this.cursorPreview.remove(); + this.remove(); } } diff --git a/src/modes/broad-brush-helper.js b/src/modes/broad-brush-helper.js index 92887833..04acaa3d 100644 --- a/src/modes/broad-brush-helper.js +++ b/src/modes/broad-brush-helper.js @@ -1,8 +1,9 @@ // Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/ import paper from 'paper'; +import {stylePath} from './style-path'; /** - * Applies broad brush functions to the tool. Call them when the corresponding mouse event happens + * Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens * to get the broad brush behavior. * * Broad brush draws strokes by drawing points equidistant from the mouse event, perpendicular to the @@ -11,101 +12,103 @@ import paper from 'paper'; * * @param {!Tool} tool paper.js mouse object */ -const broadBrushHelper = function (tool) { - let lastPoint; - let secondLastPoint; - let finalPath; +class BroadBrushHelper { + constructor () { + this.lastPoint = null; + this.secondLastPoint = null; + this.finalPath = null; + } - tool.onBroadMouseDown = function (event) { + onBroadMouseDown (event, tool, options) { tool.minDistance = 1; - tool.maxDistance = this.options.brushSize; + tool.maxDistance = options.brushSize; if (event.event.button > 0) return; // only first mouse button - finalPath = new paper.Path(); - tool.stylePath(finalPath); - finalPath.add(event.point); - lastPoint = secondLastPoint = event.point; - }; + this.finalPath = new paper.Path(); + stylePath(this.finalPath); + this.finalPath.add(event.point); + this.lastPoint = this.secondLastPoint = event.point; + } - tool.onBroadMouseDrag = function (event) { - const step = (event.delta).normalize(this.options.brushSize / 2); + onBroadMouseDrag (event, options) { + const step = (event.delta).normalize(options.brushSize / 2); // Move the first point out away from the drag so that the end of the path is rounded - if (finalPath.segments && finalPath.segments.length === 1) { - const removedPoint = finalPath.removeSegment(0).point; + if (this.finalPath.segments && this.finalPath.segments.length === 1) { + const removedPoint = this.finalPath.removeSegment(0).point; // Add handles to round the end caps const handleVec = step.clone(); - handleVec.length = this.options.brushSize / 2; + handleVec.length = options.brushSize / 2; handleVec.angle += 90; - finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec)); + this.finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec)); } step.angle += 90; const top = event.middlePoint.add(step); const bottom = event.middlePoint.subtract(step); - if (finalPath.segments.length > 3) { - finalPath.removeSegment(finalPath.segments.length - 1); - finalPath.removeSegment(0); + if (this.finalPath.segments.length > 3) { + this.finalPath.removeSegment(this.finalPath.segments.length - 1); + this.finalPath.removeSegment(0); } - finalPath.add(top); - finalPath.add(event.point.add(step)); - finalPath.insert(0, bottom); - finalPath.insert(0, event.point.subtract(step)); - if (finalPath.segments.length === 5) { + this.finalPath.add(top); + this.finalPath.add(event.point.add(step)); + this.finalPath.insert(0, bottom); + this.finalPath.insert(0, event.point.subtract(step)); + if (this.finalPath.segments.length === 5) { // Flatten is necessary to prevent smooth from getting rid of the effect // of the handles on the first point. - finalPath.flatten(Math.min(5, this.options.brushSize / 5)); + this.finalPath.flatten(Math.min(5, options.brushSize / 5)); } - finalPath.smooth(); - lastPoint = event.point; - secondLastPoint = event.lastPoint; - }; + this.finalPath.smooth(); + this.lastPoint = event.point; + this.secondLastPoint = event.lastPoint; + } - tool.onBroadMouseUp = function (event) { + onBroadMouseUp (event, tool, options) { // If the mouse up is at the same point as the mouse drag event then we need // the second to last point to get the right direction vector for the end cap - if (event.point.equals(lastPoint)) { - lastPoint = secondLastPoint; + if (event.point.equals(this.lastPoint)) { + this.lastPoint = this.secondLastPoint; } // If the points are still equal, then there was no drag, so just draw a circle. - if (event.point.equals(lastPoint)) { - finalPath.remove(); - finalPath = new paper.Path.Circle({ + if (event.point.equals(this.lastPoint)) { + this.finalPath.remove(); + this.finalPath = new paper.Path.Circle({ center: event.point, - radius: this.options.brushSize / 2 + radius: options.brushSize / 2 }); - tool.stylePath(finalPath); + stylePath(this.finalPath); } else { - const step = (event.point.subtract(lastPoint)).normalize(this.options.brushSize / 2); + const step = (event.point.subtract(this.lastPoint)).normalize(options.brushSize / 2); step.angle += 90; const handleVec = step.clone(); - handleVec.length = this.options.brushSize / 2; + handleVec.length = options.brushSize / 2; const top = event.point.add(step); const bottom = event.point.subtract(step); - finalPath.add(top); - finalPath.insert(0, bottom); + this.finalPath.add(top); + this.finalPath.insert(0, bottom); // Simplify before adding end cap so cap doesn't get warped - finalPath.simplify(1); + this.finalPath.simplify(1); // Add end cap step.angle -= 90; - finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec)); - finalPath.closed = true; + this.finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec)); + this.finalPath.closed = true; } // Resolve self-crossings const newPath = - finalPath + this.finalPath .resolveCrossings() .reorient(true /* nonZero */, true /* clockwise */) .reduce({simplify: true}); - newPath.copyAttributes(finalPath); - newPath.fillColor = finalPath.fillColor; - finalPath = newPath; - return finalPath; - }; -}; + newPath.copyAttributes(this.finalPath); + newPath.fillColor = this.finalPath.fillColor; + this.finalPath = newPath; + return this.finalPath; + } +} -export default broadBrushHelper; +export default BroadBrushHelper; diff --git a/src/modes/segment-brush-helper.js b/src/modes/segment-brush-helper.js index 26d19a49..59396cb8 100644 --- a/src/modes/segment-brush-helper.js +++ b/src/modes/segment-brush-helper.js @@ -1,7 +1,8 @@ import paper from 'paper'; +import {stylePath} from './style-path'; /** - * Applies segment brush functions to the tool. Call them when the corresponding mouse event happens + * Segment brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens * to get the broad brush behavior. * * Segment brush draws by creating a rounded rectangle for each mouse move event and merging all of @@ -13,30 +14,32 @@ import paper from 'paper'; * * @param {!Tool} tool paper.js mouse object */ -const segmentBrushHelper = function (tool) { - let lastPoint; - let finalPath; +class SegmentBrushHelper { + constructor () { + this.lastPoint = null; + this.finalPath = null; + } - tool.onSegmentMouseDown = function (event) { + onSegmentMouseDown (event, tool, options) { if (event.event.button > 0) return; // only first mouse button tool.minDistance = 1; - tool.maxDistance = this.options.brushSize; + tool.maxDistance = options.brushSize; - finalPath = new paper.Path.Circle({ + this.finalPath = new paper.Path.Circle({ center: event.point, - radius: this.options.brushSize / 2 + radius: options.brushSize / 2 }); - tool.stylePath(finalPath); - lastPoint = event.point; - }; + stylePath(this.finalPath); + this.lastPoint = event.point; + } - tool.onSegmentMouseDrag = function (event) { + onSegmentMouseDrag (event, options) { if (event.event.button > 0) return; // only first mouse button - const step = (event.delta).normalize(this.options.brushSize / 2); + const step = (event.delta).normalize(options.brushSize / 2); const handleVec = step.clone(); - handleVec.length = this.options.brushSize / 2; + handleVec.length = options.brushSize / 2; handleVec.angle += 90; const path = new paper.Path(); @@ -46,7 +49,7 @@ const segmentBrushHelper = function (tool) { path.fillColor = 'black'; // Add handles to round the end caps - path.add(new paper.Segment(lastPoint.subtract(step), handleVec.multiply(-1), handleVec)); + path.add(new paper.Segment(this.lastPoint.subtract(step), handleVec.multiply(-1), handleVec)); step.angle += 90; path.add(event.lastPoint.add(step)); @@ -60,25 +63,25 @@ const segmentBrushHelper = function (tool) { path.closed = true; // The unite function on curved paths does not always work (sometimes deletes half the path) // so we have to flatten. - path.flatten(Math.min(5, this.options.brushSize / 5)); + path.flatten(Math.min(5, options.brushSize / 5)); - lastPoint = event.point; - const newPath = finalPath.unite(path); + this.lastPoint = event.point; + const newPath = this.finalPath.unite(path); path.remove(); - finalPath.remove(); - finalPath = newPath; - }; + this.finalPath.remove(); + this.finalPath = newPath; + } - tool.onSegmentMouseUp = function (event) { + onSegmentMouseUp (event) { 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? // Smooth the path. - finalPath.simplify(2); - return finalPath; - }; -}; + this.finalPath.simplify(2); + return this.finalPath; + } +} -export default segmentBrushHelper; +export default SegmentBrushHelper; diff --git a/src/modes/style-path.js b/src/modes/style-path.js new file mode 100644 index 00000000..e4380869 --- /dev/null +++ b/src/modes/style-path.js @@ -0,0 +1,28 @@ +const stylePath = function (path, isEraser) { + if (isEraser) { + path.fillColor = 'white'; + } else { + // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. + // path = pg.stylebar.applyActiveToolbarStyle(path); + path.fillColor = 'black'; + } +}; + +const styleCursorPreview = function (path, isEraser) { + if (isEraser) { + path.fillColor = 'white'; + path.strokeColor = 'cornflowerblue'; + path.strokeWidth = 1; + } else { + // TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen. + // path = pg.stylebar.applyActiveToolbarStyle(path); + path.fillColor = 'black'; + path.strokeColor = 'cornflowerblue'; + path.strokeWidth = 1; + } +}; + +export { + stylePath, + styleCursorPreview +}; diff --git a/src/reducers/eraser-mode.js b/src/reducers/eraser-mode.js index b543cfc6..28b19e10 100644 --- a/src/reducers/eraser-mode.js +++ b/src/reducers/eraser-mode.js @@ -28,4 +28,4 @@ const changeBrushSize = function (brushSize) { export { reducer as default, changeBrushSize -}; \ No newline at end of file +}; From f325ae43eef63a9ca73258e1f7db35fc2d8e77bb Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 16 Aug 2017 18:16:37 -0400 Subject: [PATCH 16/19] move isEraser to options --- src/containers/brush-mode.jsx | 4 ++-- src/containers/eraser-mode.jsx | 8 ++++---- src/containers/paint-editor.jsx | 6 +++--- src/modes/blob.js | 30 +++++++++++++++++++++++------- src/modes/broad-brush-helper.js | 4 ++-- src/modes/segment-brush-helper.js | 2 +- 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index f7b5d5cc..c1bbe6e6 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -30,7 +30,7 @@ class BrushMode extends React.Component { } else if (!nextProps.isBrushModeActive && this.props.isBrushModeActive) { this.deactivateTool(); } else if (nextProps.isBrushModeActive && this.props.isBrushModeActive) { - this.blob.setOptions(nextProps.brushModeState); + this.blob.setOptions({isEraser: false, ...nextProps.brushModeState}); } } shouldComponentUpdate () { @@ -39,7 +39,7 @@ class BrushMode extends React.Component { activateTool () { // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); - this.blob.activateTool(false /* isEraser */, this.props.brushModeState); + this.blob.activateTool({isEraser: false, ...this.props.brushModeState}); // TODO Make sure a fill color is set on the brush // if(!pg.stylebar.getFillColor()) { diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index f1ecff96..5782d7d4 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; import Blobbiness from '../modes/blob'; -import EraserModeReducer from '../reducers/eraser-mode'; +import {changeBrushSize} from '../reducers/eraser-mode'; class EraserMode extends React.Component { static get MODE () { @@ -30,7 +30,7 @@ class EraserMode extends React.Component { } else if (!nextProps.isEraserModeActive && this.props.isEraserModeActive) { this.deactivateTool(); } else if (nextProps.isEraserModeActive && this.props.isEraserModeActive) { - this.blob.setOptions(nextProps.eraserModeState); + this.blob.setOptions({isEraser: true, ...nextProps.eraserModeState}); } } shouldComponentUpdate () { @@ -39,7 +39,7 @@ class EraserMode extends React.Component { activateTool () { this.props.canvas.addEventListener('mousewheel', this.onScroll); - this.blob.activateTool(true /* isEraser */, this.props.eraserModeState); + this.blob.activateTool({isEraser: true, ...this.props.eraserModeState}); } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); @@ -75,7 +75,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ changeBrushSize: brushSize => { - dispatch(EraserModeReducer.changeBrushSize(brushSize)); + dispatch(changeBrushSize(brushSize)); } }); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 203a8f17..303546ba 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import PaintEditorComponent from '../components/paint-editor.jsx'; -import ModesReducer from '../reducers/modes'; +import {changeMode} from '../reducers/modes'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; @@ -26,9 +26,9 @@ PaintEditor.propTypes = { const mapDispatchToProps = dispatch => ({ onKeyPress: event => { if (event.key === 'e') { - dispatch(ToolsReducer.changeMode(Modes.ERASER)); + dispatch(changeMode(Modes.ERASER)); } else if (event.key === 'b') { - dispatch(ToolsReducer.changeMode(Modes.BRUSH)); + dispatch(changeMode(Modes.BRUSH)); } } }); diff --git a/src/modes/blob.js b/src/modes/blob.js index f1bec3bb..6c095bd7 100644 --- a/src/modes/blob.js +++ b/src/modes/blob.js @@ -29,23 +29,35 @@ class Blobbiness { this.segmentBrushHelper = new SegmentBrushHelper(); } + /** + * Set configuration options for a blob + * @param {!object} options Configuration + * @param {!number} options.brushSize Width of blob marking made by mouse + * @param {!boolean} options.isEraser Whether the stroke should be treated as an erase path. If false, + * the stroke is an additive path. + */ setOptions (options) { this.options = options; this.resizeCursorIfNeeded(); } - activateTool (isEraser, options) { + /** + * Adds handlers on the mouse tool to draw blobs. Initialize with configuration options for a blob. + * @param {!object} options Configuration + * @param {!number} options.brushSize Width of blob marking made by mouse + * @param {!boolean} options.isEraser Whether the stroke should be treated as an erase path. If false, + * the stroke is an additive path. + */ + activateTool (options) { this.tool = new paper.Tool(); - this.isEraser = isEraser; this.cursorPreviewLastPoint = new paper.Point(-10000, -10000); this.setOptions(options); - styleCursorPreview(this.cursorPreview); this.tool.fixedDistance = 1; const blob = this; this.tool.onMouseMove = function (event) { blob.resizeCursorIfNeeded(event.point); - styleCursorPreview(blob.cursorPreview); + styleCursorPreview(blob.cursorPreview, blob.options.isEraser); blob.cursorPreview.bringToFront(); blob.cursorPreview.position = event.point; }; @@ -95,7 +107,7 @@ class Blobbiness { log.warn(`Brush type does not exist: ${blob.brush}`); } - if (isEraser) { + if (blob.options.isEraser) { blob.mergeEraser(lastPath); } else { blob.mergeBrush(lastPath); @@ -108,6 +120,7 @@ class Blobbiness { blob.brush = null; this.fixedDistance = 1; }; + this.tool.activate(); } resizeCursorIfNeeded (point) { @@ -121,7 +134,7 @@ class Blobbiness { this.cursorPreviewLastPoint = point; } - if (this.brushSize === this.options.brushSize) { + if (this.cursorPreview && this.brushSize === this.options.brushSize) { return; } const newPreview = new paper.Path.Circle({ @@ -133,6 +146,7 @@ class Blobbiness { newPreview.remove(); } else { this.cursorPreview = newPreview; + styleCursorPreview(this.cursorPreview, this.options.isEraser); } this.brushSize = this.options.brushSize; } @@ -323,7 +337,9 @@ class Blobbiness { deactivateTool () { this.cursorPreview.remove(); - this.remove(); + this.cursorPreview = null; + this.tool.remove(); + this.tool = null; } } diff --git a/src/modes/broad-brush-helper.js b/src/modes/broad-brush-helper.js index 04acaa3d..464bc4a7 100644 --- a/src/modes/broad-brush-helper.js +++ b/src/modes/broad-brush-helper.js @@ -25,7 +25,7 @@ class BroadBrushHelper { if (event.event.button > 0) return; // only first mouse button this.finalPath = new paper.Path(); - stylePath(this.finalPath); + stylePath(this.finalPath, options.isEraser); this.finalPath.add(event.point); this.lastPoint = this.secondLastPoint = event.point; } @@ -77,7 +77,7 @@ class BroadBrushHelper { center: event.point, radius: options.brushSize / 2 }); - stylePath(this.finalPath); + stylePath(this.finalPath, options.isEraser); } else { const step = (event.point.subtract(this.lastPoint)).normalize(options.brushSize / 2); step.angle += 90; diff --git a/src/modes/segment-brush-helper.js b/src/modes/segment-brush-helper.js index 59396cb8..6a8837fe 100644 --- a/src/modes/segment-brush-helper.js +++ b/src/modes/segment-brush-helper.js @@ -30,7 +30,7 @@ class SegmentBrushHelper { center: event.point, radius: options.brushSize / 2 }); - stylePath(this.finalPath); + stylePath(this.finalPath, options.isEraser); this.lastPoint = event.point; } From a875bee81ce8fd432f8b05f370cc74e4275e5b89 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 17 Aug 2017 13:53:54 -0400 Subject: [PATCH 17/19] move blob helper files --- src/{modes => containers/blob}/blob.js | 2 +- src/{modes => containers/blob}/broad-brush-helper.js | 0 src/{modes => containers/blob}/segment-brush-helper.js | 0 src/{modes => containers/blob}/style-path.js | 0 src/containers/brush-mode.jsx | 2 +- src/containers/eraser-mode.jsx | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) rename src/{modes => containers/blob}/blob.js (99%) rename src/{modes => containers/blob}/broad-brush-helper.js (100%) rename src/{modes => containers/blob}/segment-brush-helper.js (100%) rename src/{modes => containers/blob}/style-path.js (100%) diff --git a/src/modes/blob.js b/src/containers/blob/blob.js similarity index 99% rename from src/modes/blob.js rename to src/containers/blob/blob.js index 6c095bd7..adbaedf8 100644 --- a/src/modes/blob.js +++ b/src/containers/blob/blob.js @@ -1,5 +1,5 @@ import paper from 'paper'; -import log from '../log/log'; +import log from '../../log/log'; import BroadBrushHelper from './broad-brush-helper'; import SegmentBrushHelper from './segment-brush-helper'; import {styleCursorPreview} from './style-path'; diff --git a/src/modes/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js similarity index 100% rename from src/modes/broad-brush-helper.js rename to src/containers/blob/broad-brush-helper.js diff --git a/src/modes/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js similarity index 100% rename from src/modes/segment-brush-helper.js rename to src/containers/blob/segment-brush-helper.js diff --git a/src/modes/style-path.js b/src/containers/blob/style-path.js similarity index 100% rename from src/modes/style-path.js rename to src/containers/blob/style-path.js diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index c1bbe6e6..e52b86db 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; -import Blobbiness from '../modes/blob'; +import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/brush-mode'; class BrushMode extends React.Component { diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx index 5782d7d4..9ca2f6ce 100644 --- a/src/containers/eraser-mode.jsx +++ b/src/containers/eraser-mode.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../modes/modes'; -import Blobbiness from '../modes/blob'; +import Blobbiness from './blob/blob'; import {changeBrushSize} from '../reducers/eraser-mode'; class EraserMode extends React.Component { From 6ede893585d5d44da4bf43182152dcbb61dbfc02 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 17 Aug 2017 16:50:30 -0400 Subject: [PATCH 18/19] Bring over new bug fixes from papergrapher --- src/containers/blob/blob.js | 36 ++++++++++++++++++--- src/containers/blob/broad-brush-helper.js | 2 +- src/containers/blob/segment-brush-helper.js | 17 ++++++++-- src/containers/brush-mode.jsx | 13 +++----- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index adbaedf8..c3c9c2ca 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -226,8 +226,37 @@ class Blobbiness { } for (let i = items.length - 1; i >= 0; i--) { + // TODO handle compound paths + if (items[i] instanceof paper.Path && (!items[i].fillColor || items[i].fillColor._alpha === 0)) { + // Gather path segments + const subpaths = []; + const firstSeg = items[i]; + const intersections = firstSeg.getIntersections(lastPath); + for (let j = intersections.length - 1; j >= 0; j--) { + const split = firstSeg.splitAt(intersections[j]); + if (split) { + split.insertAbove(firstSeg); + subpaths.push(split); + } + } + subpaths.push(firstSeg); + + // Remove the ones that are within the eraser stroke boundary + for (let k = subpaths.length - 1; k >= 0; k--) { + const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point; + if (lastPath.contains(segMidpoint)) { + subpaths[k].remove(); + subpaths.splice(k, 1); + } + } + lastPath.remove(); + // TODO add back undo + // pg.undo.snapshot('eraser'); + continue; + } // Erase const newPath = items[i].subtract(lastPath); + newPath.insertBelow(items[i]); // Gather path segments const subpaths = []; @@ -236,11 +265,10 @@ class Blobbiness { const firstSeg = items[i].clone(); const intersections = firstSeg.getIntersections(lastPath); // keep first and last segments - if (intersections.length === 0) { - continue; - } for (let j = intersections.length - 1; j >= 0; j--) { - subpaths.push(firstSeg.splitAt(intersections[j])); + const split = firstSeg.splitAt(intersections[j]); + split.insertAbove(firstSeg); + subpaths.push(split); } subpaths.push(firstSeg); } diff --git a/src/containers/blob/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js index 464bc4a7..f4cf735c 100644 --- a/src/containers/blob/broad-brush-helper.js +++ b/src/containers/blob/broad-brush-helper.js @@ -20,7 +20,7 @@ class BroadBrushHelper { } onBroadMouseDown (event, tool, options) { - tool.minDistance = 1; + tool.minDistance = options.brushSize / 2; tool.maxDistance = options.brushSize; if (event.event.button > 0) return; // only first mouse button diff --git a/src/containers/blob/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js index 6a8837fe..8b716d50 100644 --- a/src/containers/blob/segment-brush-helper.js +++ b/src/containers/blob/segment-brush-helper.js @@ -18,6 +18,7 @@ class SegmentBrushHelper { constructor () { this.lastPoint = null; this.finalPath = null; + this.firstCircle = null; } onSegmentMouseDown (event, tool, options) { @@ -26,10 +27,11 @@ class SegmentBrushHelper { tool.minDistance = 1; tool.maxDistance = options.brushSize; - this.finalPath = new paper.Path.Circle({ + this.firstCircle = new paper.Path.Circle({ center: event.point, radius: options.brushSize / 2 }); + this.finalPath = this.firstCircle; stylePath(this.finalPath, options.isEraser); this.lastPoint = event.point; } @@ -78,8 +80,17 @@ class SegmentBrushHelper { // 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? - // Smooth the path. - this.finalPath.simplify(2); + // Smooth the path. Make it unclosed first because smoothing of closed + // paths tends to cut off the path. + if (this.finalPath.segments && this.finalPath.segments.length > 4) { + this.finalPath.closed = false; + this.finalPath.simplify(2); + this.finalPath.closed = true; + // Merge again with the first point, since it gets distorted when we unclose the path. + const temp = this.finalPath.unite(this.firstCircle); + this.finalPath.remove(); + this.finalPath = temp; + } return this.finalPath; } } diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index e52b86db..b2549393 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -37,18 +37,13 @@ class BrushMode extends React.Component { return false; // Logic only component } activateTool () { + // TODO: Instead of clearing selection, consider a kind of "draw inside" + // analogous to how selection works with eraser + // pg.selection.clearSelection(); + // TODO: This is temporary until a component that provides the brush size is hooked up this.props.canvas.addEventListener('mousewheel', this.onScroll); this.blob.activateTool({isEraser: false, ...this.props.brushModeState}); - - // TODO Make sure a fill color is set on the brush - // if(!pg.stylebar.getFillColor()) { - // pg.stylebar.setFillColor(pg.stylebar.getStrokeColor()); - // pg.stylebar.setStrokeColor(null); - // } - - // TODO setup floating tool options panel in the editor - // pg.toolOptionPanel.setup(options, components, function() {}); } deactivateTool () { this.props.canvas.removeEventListener('mousewheel', this.onScroll); From b55b23e446e95c8623f7f73f08b7d86dc4b072e7 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Tue, 22 Aug 2017 15:57:12 -0400 Subject: [PATCH 19/19] make helper mouse functions have consistent interface --- src/containers/blob/blob.js | 6 +++--- src/containers/blob/broad-brush-helper.js | 2 +- src/containers/blob/segment-brush-helper.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js index c3c9c2ca..b06199b8 100644 --- a/src/containers/blob/blob.js +++ b/src/containers/blob/blob.js @@ -82,9 +82,9 @@ class Blobbiness { blob.resizeCursorIfNeeded(event.point); if (event.event.button > 0) return; // only first mouse button if (blob.brush === Blobbiness.BROAD) { - blob.broadBrushHelper.onBroadMouseDrag(event, blob.options); + blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options); } else if (blob.brush === Blobbiness.SEGMENT) { - blob.segmentBrushHelper.onSegmentMouseDrag(event, blob.options); + blob.segmentBrushHelper.onSegmentMouseDrag(event, blob.tool, blob.options); } else { log.warn(`Brush type does not exist: ${blob.brush}`); } @@ -102,7 +102,7 @@ class Blobbiness { if (blob.brush === Blobbiness.BROAD) { lastPath = blob.broadBrushHelper.onBroadMouseUp(event, blob.tool, blob.options); } else if (blob.brush === Blobbiness.SEGMENT) { - lastPath = blob.segmentBrushHelper.onSegmentMouseUp(event); + lastPath = blob.segmentBrushHelper.onSegmentMouseUp(event, blob.tool, blob.options); } else { log.warn(`Brush type does not exist: ${blob.brush}`); } diff --git a/src/containers/blob/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js index f4cf735c..b3bc1ca3 100644 --- a/src/containers/blob/broad-brush-helper.js +++ b/src/containers/blob/broad-brush-helper.js @@ -30,7 +30,7 @@ class BroadBrushHelper { this.lastPoint = this.secondLastPoint = event.point; } - onBroadMouseDrag (event, options) { + onBroadMouseDrag (event, tool, options) { const step = (event.delta).normalize(options.brushSize / 2); // Move the first point out away from the drag so that the end of the path is rounded diff --git a/src/containers/blob/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js index 8b716d50..88f7debd 100644 --- a/src/containers/blob/segment-brush-helper.js +++ b/src/containers/blob/segment-brush-helper.js @@ -36,7 +36,7 @@ class SegmentBrushHelper { this.lastPoint = event.point; } - onSegmentMouseDrag (event, options) { + onSegmentMouseDrag (event, tool, options) { if (event.event.button > 0) return; // only first mouse button const step = (event.delta).normalize(options.brushSize / 2);