mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Change broad brush helper and segment brush helper into their own classes, instead of adding functions to tool
This commit is contained in:
parent
33a01c1396
commit
4ea7d154ee
7 changed files with 380 additions and 363 deletions
|
@ -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 () {
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -24,18 +24,97 @@ class Blobbiness {
|
||||||
return 9;
|
return 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions (options) {
|
constructor () {
|
||||||
if (this.tool) {
|
this.broadBrushHelper = new BroadBrushHelper();
|
||||||
this.tool.options = options;
|
this.segmentBrushHelper = new SegmentBrushHelper();
|
||||||
this.tool.resizeCursorIfNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOptions (options) {
|
||||||
|
this.options = options;
|
||||||
|
this.resizeCursorIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
activateTool (isEraser, options) {
|
activateTool (isEraser, options) {
|
||||||
this.tool = new paper.Tool();
|
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);
|
||||||
|
blob.cursorPreview.bringToFront();
|
||||||
|
blob.cursorPreview.position = event.point;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tool.onMouseDown = function (event) {
|
||||||
|
blob.resizeCursorIfNeeded(event.point);
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
if (blob.options.brushSize < Blobbiness.THRESHOLD) {
|
||||||
|
blob.brush = Blobbiness.BROAD;
|
||||||
|
blob.broadBrushHelper.onBroadMouseDown(event, blob.tool, blob.options);
|
||||||
|
} else {
|
||||||
|
blob.brush = Blobbiness.SEGMENT;
|
||||||
|
blob.segmentBrushHelper.onSegmentMouseDown(event, blob.tool, blob.options);
|
||||||
|
}
|
||||||
|
blob.cursorPreview.bringToFront();
|
||||||
|
blob.cursorPreview.position = event.point;
|
||||||
|
paper.view.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tool.onMouseDrag = function (event) {
|
||||||
|
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);
|
||||||
|
} else if (blob.brush === Blobbiness.SEGMENT) {
|
||||||
|
blob.segmentBrushHelper.onSegmentMouseDrag(event, blob.options);
|
||||||
|
} else {
|
||||||
|
log.warn(`Brush type does not exist: ${blob.brush}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
blob.cursorPreview.bringToFront();
|
||||||
|
blob.cursorPreview.position = event.point;
|
||||||
|
paper.view.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tool.onMouseUp = function (event) {
|
||||||
|
blob.resizeCursorIfNeeded(event.point);
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
let lastPath;
|
||||||
|
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: ${blob.brush}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEraser) {
|
||||||
|
blob.mergeEraser(lastPath);
|
||||||
|
} else {
|
||||||
|
blob.mergeBrush(lastPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
blob.cursorPreview.bringToFront();
|
||||||
|
blob.cursorPreview.position = event.point;
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
blob.brush = null;
|
||||||
|
this.fixedDistance = 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCursorIfNeeded (point) {
|
||||||
|
if (!this.options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tool.cursorPreviewLastPoint = new paper.Point(-10000, -10000);
|
|
||||||
tool.resizeCursorIfNeeded = function (point) {
|
|
||||||
if (typeof point === 'undefined') {
|
if (typeof point === 'undefined') {
|
||||||
point = this.cursorPreviewLastPoint;
|
point = this.cursorPreviewLastPoint;
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,129 +135,37 @@ class Blobbiness {
|
||||||
this.cursorPreview = newPreview;
|
this.cursorPreview = newPreview;
|
||||||
}
|
}
|
||||||
this.brushSize = this.options.brushSize;
|
this.brushSize = this.options.brushSize;
|
||||||
};
|
|
||||||
|
|
||||||
this.setOptions(options);
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
tool.onMouseDown = function (event) {
|
|
||||||
tool.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);
|
|
||||||
} else {
|
|
||||||
this.brush = Blobbiness.SEGMENT;
|
|
||||||
this.onSegmentMouseDown(event);
|
|
||||||
}
|
|
||||||
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 === Blobbiness.BROAD) {
|
|
||||||
this.onBroadMouseDrag(event);
|
|
||||||
} else if (this.brush === Blobbiness.SEGMENT) {
|
|
||||||
this.onSegmentMouseDrag(event);
|
|
||||||
} else {
|
|
||||||
log.warn(`Brush type does not exist: ${this.brush}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cursorPreview.bringToFront();
|
mergeBrush (lastPath) {
|
||||||
this.cursorPreview.position = event.point;
|
const blob = this;
|
||||||
paper.view.draw();
|
|
||||||
};
|
|
||||||
|
|
||||||
tool.onMouseUp = function (event) {
|
|
||||||
tool.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);
|
|
||||||
} else {
|
|
||||||
log.warn(`Brush type does not exist: ${this.brush}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEraser) {
|
|
||||||
tool.mergeEraser(lastPath);
|
|
||||||
} else {
|
|
||||||
tool.mergeBrush(lastPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cursorPreview.bringToFront();
|
|
||||||
this.cursorPreview.position = event.point;
|
|
||||||
|
|
||||||
// Reset
|
|
||||||
this.brush = null;
|
|
||||||
tool.fixedDistance = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
tool.mergeBrush = function (lastPath) {
|
|
||||||
// Get all path items to merge with
|
// Get all path items to merge with
|
||||||
const paths = paper.project.getItems({
|
const paths = paper.project.getItems({
|
||||||
match: function (item) {
|
match: function (item) {
|
||||||
return tool.isMergeable(lastPath, item);
|
return blob.isMergeable(lastPath, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mergedPath = lastPath;
|
let mergedPath = lastPath;
|
||||||
let i;
|
let i;
|
||||||
// Move down z order to first overlapping item
|
// Move down z order to first overlapping item
|
||||||
for (i = paths.length - 1; i >= 0 && !tool.touches(paths[i], lastPath); i--) {
|
for (i = paths.length - 1; i >= 0 && !this.touches(paths[i], lastPath); i--) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mergedPathIndex = i;
|
let mergedPathIndex = i;
|
||||||
for (; i >= 0; i--) {
|
for (; i >= 0; i--) {
|
||||||
if (!tool.touches(paths[i], lastPath)) {
|
if (!this.touches(paths[i], lastPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!paths[i].getFillColor()) {
|
if (!paths[i].getFillColor()) {
|
||||||
// Ignore for merge. Paths without fill need to be in paths though,
|
// Ignore for merge. Paths without fill need to be in paths though,
|
||||||
// since they can visibly change if z order changes
|
// since they can visibly change if z order changes
|
||||||
} else if (tool.colorMatch(paths[i], lastPath)) {
|
} else if (this.colorMatch(paths[i], lastPath)) {
|
||||||
// Make sure the new shape isn't overlapped by anything that would
|
// Make sure the new shape isn't overlapped by anything that would
|
||||||
// visibly change if we change its z order
|
// visibly change if we change its z order
|
||||||
for (let j = mergedPathIndex; j > i; j--) {
|
for (let j = mergedPathIndex; j > i; j--) {
|
||||||
if (tool.touches(paths[j], paths[i])) {
|
if (this.touches(paths[j], paths[i])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,14 +187,16 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
// TODO: Add back undo
|
// TODO: Add back undo
|
||||||
// pg.undo.snapshot('broadbrush');
|
// pg.undo.snapshot('broadbrush');
|
||||||
};
|
}
|
||||||
|
|
||||||
|
mergeEraser (lastPath) {
|
||||||
|
const blob = this;
|
||||||
|
|
||||||
tool.mergeEraser = function (lastPath) {
|
|
||||||
// Get all path items to merge with
|
// Get all path items to merge with
|
||||||
// If there are selected items, try to erase from amongst those.
|
// If there are selected items, try to erase from amongst those.
|
||||||
let items = paper.project.getItems({
|
let items = paper.project.getItems({
|
||||||
match: function (item) {
|
match: function (item) {
|
||||||
return item.selected && tool.isMergeable(lastPath, item) && tool.touches(lastPath, 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
|
// Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset
|
||||||
|
@ -217,7 +206,7 @@ class Blobbiness {
|
||||||
// pg.selection.clearSelection();
|
// pg.selection.clearSelection();
|
||||||
items = paper.project.getItems({
|
items = paper.project.getItems({
|
||||||
match: function (item) {
|
match: function (item) {
|
||||||
return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item);
|
return blob.isMergeable(lastPath, item) && blob.touches(lastPath, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -278,7 +267,7 @@ class Blobbiness {
|
||||||
let newCw = cw;
|
let newCw = cw;
|
||||||
for (let k = ccwChildren.length - 1; k >= 0; k--) {
|
for (let k = ccwChildren.length - 1; k >= 0; k--) {
|
||||||
const ccw = ccwChildren[k];
|
const ccw = ccwChildren[k];
|
||||||
if (tool.firstEnclosesSecond(ccw, cw) || tool.firstEnclosesSecond(cw, ccw)) {
|
if (this.firstEnclosesSecond(ccw, cw) || this.firstEnclosesSecond(cw, ccw)) {
|
||||||
const temp = newCw.subtract(ccw);
|
const temp = newCw.subtract(ccw);
|
||||||
temp.insertAbove(newCw);
|
temp.insertAbove(newCw);
|
||||||
newCw.remove();
|
newCw.remove();
|
||||||
|
@ -295,27 +284,27 @@ class Blobbiness {
|
||||||
lastPath.remove();
|
lastPath.remove();
|
||||||
// TODO: Add back undo handling
|
// TODO: Add back undo handling
|
||||||
// pg.undo.snapshot('eraser');
|
// pg.undo.snapshot('eraser');
|
||||||
};
|
}
|
||||||
|
|
||||||
tool.colorMatch = function (existingPath, addedPath) {
|
colorMatch (existingPath, addedPath) {
|
||||||
// Note: transparent fill colors do notdetect as touching
|
// Note: transparent fill colors do notdetect as touching
|
||||||
return existingPath.getFillColor().equals(addedPath.getFillColor()) &&
|
return existingPath.getFillColor().equals(addedPath.getFillColor()) &&
|
||||||
(addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null
|
(addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null
|
||||||
(addedPath.getStrokeColor() &&
|
(addedPath.getStrokeColor() &&
|
||||||
addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) &&
|
addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) &&
|
||||||
addedPath.getStrokeWidth() === existingPath.getStrokeWidth() &&
|
addedPath.getStrokeWidth() === existingPath.getStrokeWidth() &&
|
||||||
tool.touches(existingPath, addedPath);
|
this.touches(existingPath, addedPath);
|
||||||
};
|
}
|
||||||
|
|
||||||
tool.touches = function (path1, path2) {
|
touches (path1, path2) {
|
||||||
// Two shapes are touching if their paths intersect
|
// Two shapes are touching if their paths intersect
|
||||||
if (path1 && path2 && path1.intersects(path2)) {
|
if (path1 && path2 && path1.intersects(path2)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return tool.firstEnclosesSecond(path1, path2) || tool.firstEnclosesSecond(path2, path1);
|
return this.firstEnclosesSecond(path1, path2) || this.firstEnclosesSecond(path2, path1);
|
||||||
};
|
}
|
||||||
|
|
||||||
tool.firstEnclosesSecond = function (path1, path2) {
|
firstEnclosesSecond (path1, path2) {
|
||||||
// Two shapes are also touching if one is completely inside the other
|
// Two shapes are also touching if one is completely inside the other
|
||||||
if (path1 && path2 && path2.firstSegment && path2.firstSegment.point &&
|
if (path1 && path2 && path2.firstSegment && path2.firstSegment.point &&
|
||||||
path1.hitTest(path2.firstSegment.point)) {
|
path1.hitTest(path2.firstSegment.point)) {
|
||||||
|
@ -323,21 +312,18 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
// TODO: clean up these no point paths
|
// TODO: clean up these no point paths
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
tool.isMergeable = function (newPath, existingPath) {
|
isMergeable (newPath, existingPath) {
|
||||||
return existingPath instanceof paper.PathItem && // path or compound path
|
return existingPath instanceof paper.PathItem && // path or compound path
|
||||||
existingPath !== this.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 !== newPath && // don't merge with self
|
||||||
existingPath.parent instanceof paper.Layer; // don't merge with nested in group
|
existingPath.parent instanceof paper.Layer; // don't merge with nested in group
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
if (this.tool) {
|
this.cursorPreview.remove();
|
||||||
this.tool.cursorPreview.remove();
|
this.remove();
|
||||||
this.tool.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
this.finalPath.smooth();
|
||||||
|
this.lastPoint = event.point;
|
||||||
|
this.secondLastPoint = event.lastPoint;
|
||||||
}
|
}
|
||||||
finalPath.smooth();
|
|
||||||
lastPoint = event.point;
|
|
||||||
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;
|
||||||
|
|
|
@ -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
28
src/modes/style-path.js
Normal 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
|
||||||
|
};
|
Loading…
Reference in a new issue