From 4ea7d154eeba8436c6f88a6316b588eee4e798ef Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 16 Aug 2017 17:44:52 -0400 Subject: [PATCH] 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 +};