Change broad brush helper and segment brush helper into their own classes, instead of adding functions to tool

This commit is contained in:
DD Liu 2017-08-16 17:44:52 -04:00
parent 33a01c1396
commit 4ea7d154ee
7 changed files with 380 additions and 363 deletions

View file

@ -4,9 +4,7 @@ import {connect} from 'react-redux';
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import Blobbiness from '../modes/blob'; import Blobbiness from '../modes/blob';
import BrushModeReducer from '../reducers/brush-mode';
import {changeBrushSize} from '../reducers/brush-mode'; import {changeBrushSize} from '../reducers/brush-mode';
import paper from 'paper';
class BrushMode extends React.Component { class BrushMode extends React.Component {
static get MODE () { static get MODE () {

View file

@ -5,7 +5,6 @@ import bindAll from 'lodash.bindall';
import Modes from '../modes/modes'; import Modes from '../modes/modes';
import Blobbiness from '../modes/blob'; import Blobbiness from '../modes/blob';
import EraserModeReducer from '../reducers/eraser-mode'; import EraserModeReducer from '../reducers/eraser-mode';
import paper from 'paper';
class EraserMode extends React.Component { class EraserMode extends React.Component {
static get MODE () { static get MODE () {

View file

@ -1,7 +1,8 @@
import paper from 'paper'; import paper from 'paper';
import log from '../log/log'; import log from '../log/log';
import broadBrushHelper from './broad-brush-helper'; import BroadBrushHelper from './broad-brush-helper';
import segmentBrushHelper from './segment-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 * 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. * based on the brushSize in the state.
*/ */
class Blobbiness { class Blobbiness {
static get BROAD () { static get BROAD () {
return 'broadbrush'; return 'broadbrush';
} }
@ -23,321 +23,307 @@ class Blobbiness {
static get THRESHOLD () { static get THRESHOLD () {
return 9; return 9;
} }
constructor () {
this.broadBrushHelper = new BroadBrushHelper();
this.segmentBrushHelper = new SegmentBrushHelper();
}
setOptions (options) { setOptions (options) {
if (this.tool) { this.options = options;
this.tool.options = options; this.resizeCursorIfNeeded();
this.tool.resizeCursorIfNeeded();
}
} }
activateTool (isEraser, options) { activateTool (isEraser, options) {
this.tool = new paper.Tool(); this.tool = new paper.Tool();
this.isEraser = isEraser;
tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000); this.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); this.setOptions(options);
styleCursorPreview(this.cursorPreview);
this.tool.fixedDistance = 1;
tool.stylePath = function (path) { const blob = this;
if (isEraser) { this.tool.onMouseMove = function (event) {
path.fillColor = 'white'; blob.resizeCursorIfNeeded(event.point);
if (path === this.cursorPreview) { styleCursorPreview(blob.cursorPreview);
path.strokeColor = 'cornflowerblue'; blob.cursorPreview.bringToFront();
path.strokeWidth = 1; blob.cursorPreview.position = event.point;
}
} 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;
}; };
tool.onMouseDown = function (event) { this.tool.onMouseDown = function (event) {
tool.resizeCursorIfNeeded(event.point); blob.resizeCursorIfNeeded(event.point);
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
if (this.options.brushSize < Blobbiness.THRESHOLD) { if (blob.options.brushSize < Blobbiness.THRESHOLD) {
this.brush = Blobbiness.BROAD; blob.brush = Blobbiness.BROAD;
this.onBroadMouseDown(event); blob.broadBrushHelper.onBroadMouseDown(event, blob.tool, blob.options);
} else { } else {
this.brush = Blobbiness.SEGMENT; blob.brush = Blobbiness.SEGMENT;
this.onSegmentMouseDown(event); blob.segmentBrushHelper.onSegmentMouseDown(event, blob.tool, blob.options);
} }
this.cursorPreview.bringToFront(); blob.cursorPreview.bringToFront();
this.cursorPreview.position = event.point; blob.cursorPreview.position = event.point;
paper.view.draw(); paper.view.draw();
}; };
tool.onMouseDrag = function (event) { this.tool.onMouseDrag = function (event) {
tool.resizeCursorIfNeeded(event.point); blob.resizeCursorIfNeeded(event.point);
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
if (this.brush === Blobbiness.BROAD) { if (blob.brush === Blobbiness.BROAD) {
this.onBroadMouseDrag(event); blob.broadBrushHelper.onBroadMouseDrag(event, blob.options);
} else if (this.brush === Blobbiness.SEGMENT) { } else if (blob.brush === Blobbiness.SEGMENT) {
this.onSegmentMouseDrag(event); blob.segmentBrushHelper.onSegmentMouseDrag(event, blob.options);
} else { } else {
log.warn(`Brush type does not exist: ${this.brush}`); log.warn(`Brush type does not exist: ${blob.brush}`);
} }
this.cursorPreview.bringToFront(); blob.cursorPreview.bringToFront();
this.cursorPreview.position = event.point; blob.cursorPreview.position = event.point;
paper.view.draw(); paper.view.draw();
}; };
tool.onMouseUp = function (event) { this.tool.onMouseUp = function (event) {
tool.resizeCursorIfNeeded(event.point); blob.resizeCursorIfNeeded(event.point);
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
let lastPath; let lastPath;
if (this.brush === Blobbiness.BROAD) { if (blob.brush === Blobbiness.BROAD) {
lastPath = this.onBroadMouseUp(event); lastPath = blob.broadBrushHelper.onBroadMouseUp(event, blob.tool, blob.options);
} else if (this.brush === Blobbiness.SEGMENT) { } else if (blob.brush === Blobbiness.SEGMENT) {
lastPath = this.onSegmentMouseUp(event); lastPath = blob.segmentBrushHelper.onSegmentMouseUp(event);
} else { } else {
log.warn(`Brush type does not exist: ${this.brush}`); log.warn(`Brush type does not exist: ${blob.brush}`);
} }
if (isEraser) { if (isEraser) {
tool.mergeEraser(lastPath); blob.mergeEraser(lastPath);
} else { } else {
tool.mergeBrush(lastPath); blob.mergeBrush(lastPath);
} }
this.cursorPreview.bringToFront(); blob.cursorPreview.bringToFront();
this.cursorPreview.position = event.point; blob.cursorPreview.position = event.point;
// Reset // Reset
this.brush = null; blob.brush = null;
tool.fixedDistance = 1; this.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
}; };
} }
deactivateTool () { resizeCursorIfNeeded (point) {
if (this.tool) { if (!this.options) {
this.tool.cursorPreview.remove(); return;
this.tool.remove();
} }
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();
} }
} }

View file

@ -1,8 +1,9 @@
// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/ // Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/
import paper from 'paper'; 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. * to get the broad brush behavior.
* *
* Broad brush draws strokes by drawing points equidistant from the mouse event, perpendicular to the * 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 * @param {!Tool} tool paper.js mouse object
*/ */
const broadBrushHelper = function (tool) { class BroadBrushHelper {
let lastPoint; constructor () {
let secondLastPoint; this.lastPoint = null;
let finalPath; this.secondLastPoint = null;
this.finalPath = null;
}
tool.onBroadMouseDown = function (event) { onBroadMouseDown (event, tool, options) {
tool.minDistance = 1; tool.minDistance = 1;
tool.maxDistance = this.options.brushSize; tool.maxDistance = options.brushSize;
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
finalPath = new paper.Path(); this.finalPath = new paper.Path();
tool.stylePath(finalPath); stylePath(this.finalPath);
finalPath.add(event.point); this.finalPath.add(event.point);
lastPoint = secondLastPoint = event.point; this.lastPoint = this.secondLastPoint = event.point;
}; }
tool.onBroadMouseDrag = function (event) { onBroadMouseDrag (event, options) {
const step = (event.delta).normalize(this.options.brushSize / 2); 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 // 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) { if (this.finalPath.segments && this.finalPath.segments.length === 1) {
const removedPoint = finalPath.removeSegment(0).point; const removedPoint = this.finalPath.removeSegment(0).point;
// Add handles to round the end caps // Add handles to round the end caps
const handleVec = step.clone(); const handleVec = step.clone();
handleVec.length = this.options.brushSize / 2; handleVec.length = options.brushSize / 2;
handleVec.angle += 90; 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; step.angle += 90;
const top = event.middlePoint.add(step); const top = event.middlePoint.add(step);
const bottom = event.middlePoint.subtract(step); const bottom = event.middlePoint.subtract(step);
if (finalPath.segments.length > 3) { if (this.finalPath.segments.length > 3) {
finalPath.removeSegment(finalPath.segments.length - 1); this.finalPath.removeSegment(this.finalPath.segments.length - 1);
finalPath.removeSegment(0); this.finalPath.removeSegment(0);
} }
finalPath.add(top); this.finalPath.add(top);
finalPath.add(event.point.add(step)); this.finalPath.add(event.point.add(step));
finalPath.insert(0, bottom); this.finalPath.insert(0, bottom);
finalPath.insert(0, event.point.subtract(step)); this.finalPath.insert(0, event.point.subtract(step));
if (finalPath.segments.length === 5) { if (this.finalPath.segments.length === 5) {
// Flatten is necessary to prevent smooth from getting rid of the effect // Flatten is necessary to prevent smooth from getting rid of the effect
// of the handles on the first point. // 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(); this.finalPath.smooth();
lastPoint = event.point; this.lastPoint = event.point;
secondLastPoint = event.lastPoint; 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 // 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 // the second to last point to get the right direction vector for the end cap
if (event.point.equals(lastPoint)) { if (event.point.equals(this.lastPoint)) {
lastPoint = secondLastPoint; this.lastPoint = this.secondLastPoint;
} }
// If the points are still equal, then there was no drag, so just draw a circle. // If the points are still equal, then there was no drag, so just draw a circle.
if (event.point.equals(lastPoint)) { if (event.point.equals(this.lastPoint)) {
finalPath.remove(); this.finalPath.remove();
finalPath = new paper.Path.Circle({ this.finalPath = new paper.Path.Circle({
center: event.point, center: event.point,
radius: this.options.brushSize / 2 radius: options.brushSize / 2
}); });
tool.stylePath(finalPath); stylePath(this.finalPath);
} else { } 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; step.angle += 90;
const handleVec = step.clone(); const handleVec = step.clone();
handleVec.length = this.options.brushSize / 2; handleVec.length = options.brushSize / 2;
const top = event.point.add(step); const top = event.point.add(step);
const bottom = event.point.subtract(step); const bottom = event.point.subtract(step);
finalPath.add(top); this.finalPath.add(top);
finalPath.insert(0, bottom); this.finalPath.insert(0, bottom);
// Simplify before adding end cap so cap doesn't get warped // Simplify before adding end cap so cap doesn't get warped
finalPath.simplify(1); this.finalPath.simplify(1);
// Add end cap // Add end cap
step.angle -= 90; step.angle -= 90;
finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec)); this.finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec));
finalPath.closed = true; this.finalPath.closed = true;
} }
// Resolve self-crossings // Resolve self-crossings
const newPath = const newPath =
finalPath this.finalPath
.resolveCrossings() .resolveCrossings()
.reorient(true /* nonZero */, true /* clockwise */) .reorient(true /* nonZero */, true /* clockwise */)
.reduce({simplify: true}); .reduce({simplify: true});
newPath.copyAttributes(finalPath); newPath.copyAttributes(this.finalPath);
newPath.fillColor = finalPath.fillColor; newPath.fillColor = this.finalPath.fillColor;
finalPath = newPath; this.finalPath = newPath;
return finalPath; return this.finalPath;
}; }
}; }
export default broadBrushHelper; export default BroadBrushHelper;

View file

@ -1,7 +1,8 @@
import paper from 'paper'; 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. * to get the broad brush behavior.
* *
* Segment brush draws by creating a rounded rectangle for each mouse move event and merging all of * 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 * @param {!Tool} tool paper.js mouse object
*/ */
const segmentBrushHelper = function (tool) { class SegmentBrushHelper {
let lastPoint; constructor () {
let finalPath; this.lastPoint = null;
this.finalPath = null;
}
tool.onSegmentMouseDown = function (event) { onSegmentMouseDown (event, tool, options) {
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
tool.minDistance = 1; tool.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, center: event.point,
radius: this.options.brushSize / 2 radius: options.brushSize / 2
}); });
tool.stylePath(finalPath); stylePath(this.finalPath);
lastPoint = event.point; this.lastPoint = event.point;
}; }
tool.onSegmentMouseDrag = function (event) { onSegmentMouseDrag (event, options) {
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
const step = (event.delta).normalize(this.options.brushSize / 2); const step = (event.delta).normalize(options.brushSize / 2);
const handleVec = step.clone(); const handleVec = step.clone();
handleVec.length = this.options.brushSize / 2; handleVec.length = options.brushSize / 2;
handleVec.angle += 90; handleVec.angle += 90;
const path = new paper.Path(); const path = new paper.Path();
@ -46,7 +49,7 @@ const segmentBrushHelper = function (tool) {
path.fillColor = 'black'; path.fillColor = 'black';
// Add handles to round the end caps // 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; step.angle += 90;
path.add(event.lastPoint.add(step)); path.add(event.lastPoint.add(step));
@ -60,25 +63,25 @@ const segmentBrushHelper = function (tool) {
path.closed = true; path.closed = true;
// The unite function on curved paths does not always work (sometimes deletes half the path) // The unite function on curved paths does not always work (sometimes deletes half the path)
// so we have to flatten. // 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; this.lastPoint = event.point;
const newPath = finalPath.unite(path); const newPath = this.finalPath.unite(path);
path.remove(); path.remove();
finalPath.remove(); this.finalPath.remove();
finalPath = newPath; this.finalPath = newPath;
}; }
tool.onSegmentMouseUp = function (event) { onSegmentMouseUp (event) {
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
// TODO: This smoothing tends to cut off large portions of the path! Would like to eventually // 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? // add back smoothing, maybe a custom implementation that only applies to a subset of the line?
// Smooth the path. // Smooth the path.
finalPath.simplify(2); this.finalPath.simplify(2);
return finalPath; return this.finalPath;
}; }
}; }
export default segmentBrushHelper; export default SegmentBrushHelper;

28
src/modes/style-path.js Normal file
View file

@ -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
};

View file

@ -28,4 +28,4 @@ const changeBrushSize = function (brushSize) {
export { export {
reducer as default, reducer as default,
changeBrushSize changeBrushSize
}; };