mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
get broad brush working
This commit is contained in:
parent
2846dcebc8
commit
014907ba98
9 changed files with 622 additions and 8 deletions
|
@ -1,14 +1,20 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PaperCanvas from '../containers/paper-canvas.jsx';
|
import PaperCanvas from '../containers/paper-canvas.jsx';
|
||||||
|
import BrushTool from '../containers/tools/brush-tool.jsx';
|
||||||
|
|
||||||
const PaintEditorComponent = props => (
|
const PaintEditorComponent = props => (
|
||||||
<PaperCanvas
|
<div>
|
||||||
tool={props.tool}
|
<PaperCanvas
|
||||||
/>
|
canvasId={props.canvasId}
|
||||||
|
tool={props.tool}
|
||||||
|
/>
|
||||||
|
<BrushTool canvasId={props.canvasId} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
PaintEditorComponent.propTypes = {
|
PaintEditorComponent.propTypes = {
|
||||||
|
canvasId: PropTypes.string.isRequired,
|
||||||
tool: PropTypes.shape({
|
tool: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PaintEditorComponent from '../components/paint-editor.jsx';
|
import PaintEditorComponent from '../components/paint-editor.jsx';
|
||||||
import tools from '../reducers/tools';
|
import ToolsReducer from '../reducers/tools';
|
||||||
import ToolTypes from '../tools/tool-types.js';
|
import ToolTypes from '../tools/tool-types.js';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ class PaintEditor extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<PaintEditorComponent
|
<PaintEditorComponent
|
||||||
|
canvasId="paper-canvas"
|
||||||
tool={this.props.tool}
|
tool={this.props.tool}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -35,9 +36,9 @@ const mapStateToProps = state => ({
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onKeyPress: e => {
|
onKeyPress: e => {
|
||||||
if (e.key === 'e') {
|
if (e.key === 'e') {
|
||||||
dispatch(tools.changeTool(ToolTypes.ERASER));
|
dispatch(ToolsReducer.changeTool(ToolTypes.ERASER));
|
||||||
} else if (e.key === 'b') {
|
} else if (e.key === 'b') {
|
||||||
dispatch(tools.changeTool(ToolTypes.BRUSH));
|
dispatch(ToolsReducer.changeTool(ToolTypes.BRUSH));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,12 +33,13 @@ class PaperCanvas extends React.Component {
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<canvas id="paper-canvas" />
|
<canvas id={this.props.canvasId} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaperCanvas.propTypes = {
|
PaperCanvas.propTypes = {
|
||||||
|
canvasId: PropTypes.string.isRequired,
|
||||||
tool: PropTypes.shape({
|
tool: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired
|
||||||
})
|
})
|
||||||
|
|
102
src/containers/tools/brush-tool.jsx
Normal file
102
src/containers/tools/brush-tool.jsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
import ToolTypes from '../../tools/tool-types.js';
|
||||||
|
import BlobTool from '../../tools/blob.js';
|
||||||
|
import BrushToolReducer from '../../reducers/brush-tool';
|
||||||
|
import paper from 'paper';
|
||||||
|
|
||||||
|
class BrushTool extends React.Component {
|
||||||
|
static get TOOL_TYPE () {
|
||||||
|
return ToolTypes.BRUSH;
|
||||||
|
}
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'activateTool',
|
||||||
|
'deactivateTool',
|
||||||
|
'onScroll'
|
||||||
|
]);
|
||||||
|
this.blob = new BlobTool();
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
if (this.props.tool === BrushTool.TOOL_TYPE) {
|
||||||
|
this.activateTool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool !== BrushTool.TOOL_TYPE) {
|
||||||
|
this.activateTool();
|
||||||
|
} else if (nextProps.tool !== BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) {
|
||||||
|
this.deactivateTool();
|
||||||
|
} else if (nextProps.tool === BrushTool.TOOL_TYPE && this.props.tool === BrushTool.TOOL_TYPE) {
|
||||||
|
this.blob.setOptions(nextProps.brushToolState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldComponentUpdate () {
|
||||||
|
return false; // Logic only component
|
||||||
|
}
|
||||||
|
activateTool () {
|
||||||
|
document.getElementById(this.props.canvasId)
|
||||||
|
.addEventListener('mousewheel', this.onScroll);
|
||||||
|
|
||||||
|
const tool = new paper.Tool();
|
||||||
|
this.blob.activateTool(false /* isEraser */, tool, this.props.brushToolState);
|
||||||
|
|
||||||
|
// // Make sure a fill color is set on the brush
|
||||||
|
// if(!pg.stylebar.getFillColor()) {
|
||||||
|
// pg.stylebar.setFillColor(pg.stylebar.getStrokeColor());
|
||||||
|
// pg.stylebar.setStrokeColor(null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // setup floating tool options panel in the editor
|
||||||
|
// pg.toolOptionPanel.setup(options, components, function() {});
|
||||||
|
|
||||||
|
tool.activate();
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
document.getElementById(this.props.canvasId)
|
||||||
|
.removeEventListener('mousewheel', this.onScroll);
|
||||||
|
this.blob.deactivateTool();
|
||||||
|
}
|
||||||
|
onScroll (event) {
|
||||||
|
if (event.deltaY < 0) {
|
||||||
|
this.props.changeBrushSize(this.props.brushToolState.brushSize + 1);
|
||||||
|
} else if (event.deltaY > 0 && this.props.brushToolState.brushSize > 1) {
|
||||||
|
this.props.changeBrushSize(this.props.brushToolState.brushSize - 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BrushTool.propTypes = {
|
||||||
|
brushToolState: PropTypes.shape({
|
||||||
|
brushSize: PropTypes.number.isRequired
|
||||||
|
}),
|
||||||
|
canvasId: PropTypes.string.isRequired,
|
||||||
|
changeBrushSize: PropTypes.func.isRequired,
|
||||||
|
tool: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
brushToolState: state.brushTool,
|
||||||
|
tool: state.tool
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
changeBrushSize: brushSize => {
|
||||||
|
dispatch(BrushToolReducer.changeBrushSize(brushSize));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BrushTool);
|
25
src/reducers/brush-tool.js
Normal file
25
src/reducers/brush-tool.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
const CHANGE_BRUSH_SIZE = 'scratch-paint/tools/CHANGE_BRUSH_SIZE';
|
||||||
|
const initialState = {brushSize: 5};
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case CHANGE_BRUSH_SIZE:
|
||||||
|
return {brushSize: Math.max(1, action.brushSize)};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
reducer.changeBrushSize = function (brushSize) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_BRUSH_SIZE,
|
||||||
|
brushSize: brushSize,
|
||||||
|
meta: {
|
||||||
|
throttle: 30
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = reducer;
|
|
@ -1,5 +1,6 @@
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
|
|
||||||
module.exports = combineReducers({
|
module.exports = combineReducers({
|
||||||
tool: require('./tools')
|
tool: require('./tools'),
|
||||||
|
brushTool: require('./brush-tool')
|
||||||
});
|
});
|
||||||
|
|
327
src/tools/blob.js
Normal file
327
src/tools/blob.js
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
const paper = require('paper');
|
||||||
|
const log = require('../log/log');
|
||||||
|
const broadBrushHelper = require('./broad-brush-helper');
|
||||||
|
|
||||||
|
class BlobTool {
|
||||||
|
|
||||||
|
static get BROAD () {
|
||||||
|
return 'broadbrush';
|
||||||
|
}
|
||||||
|
static get SEGMENT () {
|
||||||
|
return 'segmentbrush';
|
||||||
|
}
|
||||||
|
|
||||||
|
// brush size >= threshold use segment brush, else use broadbrush
|
||||||
|
// Segment brush has performance issues at low threshold, but broad brush has weird corners
|
||||||
|
// which are more obvious the bigger it is
|
||||||
|
static get THRESHOLD () {
|
||||||
|
return 100000;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions (options) {
|
||||||
|
console.log('setOptions');
|
||||||
|
this.options = options;
|
||||||
|
if (this.cursorPreview) {
|
||||||
|
this.cursorPreview = new paper.Path.Circle({
|
||||||
|
center: [this.cursorPreview.center.x, this.cursorPreview.center.y],
|
||||||
|
radius: options.brushSize / 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activateTool (isEraser, tool, options) {
|
||||||
|
console.log('activateTool isEraser?'+isEraser);
|
||||||
|
this.tool = tool;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
let cursorPreview = this.cursorPreview = new paper.Path.Circle({
|
||||||
|
center: [-10000, -10000],
|
||||||
|
radius: options.brushSize / 2
|
||||||
|
});
|
||||||
|
this.brushSize = options.brushSize;
|
||||||
|
|
||||||
|
tool.stylePath = function (path) {
|
||||||
|
if (isEraser) {
|
||||||
|
path.fillColor = 'white';
|
||||||
|
if (path === cursorPreview) {
|
||||||
|
path.strokeColor = 'cornflowerblue';
|
||||||
|
path.strokeWidth = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO keep a separate active toolbar style for brush vs pen?
|
||||||
|
//path = pg.stylebar.applyActiveToolbarStyle(path);
|
||||||
|
|
||||||
|
//TODO FIX
|
||||||
|
|
||||||
|
path.fillColor = 'black';
|
||||||
|
if (path === cursorPreview) {
|
||||||
|
path.strokeColor = 'cornflowerblue';
|
||||||
|
path.strokeWidth = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.stylePath(cursorPreview);
|
||||||
|
|
||||||
|
tool.fixedDistance = 1;
|
||||||
|
|
||||||
|
broadBrushHelper(tool, options);
|
||||||
|
// TODO add
|
||||||
|
//pg.segmentbrushhelper(tool, options);
|
||||||
|
tool.onMouseMove = function (event) {
|
||||||
|
if (this.brushSize !== options.brushSize) {
|
||||||
|
cursorPreview.remove();
|
||||||
|
cursorPreview = new paper.Path.Circle({
|
||||||
|
center: event.point,
|
||||||
|
radius: options.brushSize / 2
|
||||||
|
});
|
||||||
|
this.brushSize = options.brushSize;
|
||||||
|
}
|
||||||
|
tool.stylePath(cursorPreview);
|
||||||
|
cursorPreview.bringToFront();
|
||||||
|
cursorPreview.position = event.point;
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.onMouseDown = function (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
if (options.brushSize < BlobTool.THRESHOLD) {
|
||||||
|
this.brush = BlobTool.BROAD;
|
||||||
|
this.onBroadMouseDown(event);
|
||||||
|
} else {
|
||||||
|
this.brush = BlobTool.SEGMENT;
|
||||||
|
this.onSegmentMouseDown(event);
|
||||||
|
}
|
||||||
|
cursorPreview.bringToFront();
|
||||||
|
cursorPreview.position = event.point;
|
||||||
|
paper.view.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.onMouseDrag = function (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
if (this.brush === BlobTool.BROAD) {
|
||||||
|
this.onBroadMouseDrag(event);
|
||||||
|
} else if (this.brush === Blob.SEGMENT) {
|
||||||
|
this.onSegmentMouseDrag(event);
|
||||||
|
} else {
|
||||||
|
log.warning(`Brush type does not exist: ${this.brush}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorPreview.bringToFront();
|
||||||
|
cursorPreview.position = event.point;
|
||||||
|
paper.view.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.onMouseUp = function (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
let lastPath;
|
||||||
|
if (this.brush === BlobTool.BROAD) {
|
||||||
|
lastPath = this.onBroadMouseUp(event);
|
||||||
|
} else if (this.brush === BlobTool.SEGMENT) {
|
||||||
|
lastPath = this.onSegmentMouseUp(event);
|
||||||
|
} else {
|
||||||
|
log.warning(`Brush type does not exist: ${this.brush}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEraser) {
|
||||||
|
tool.mergeEraser(lastPath);
|
||||||
|
} else {
|
||||||
|
tool.mergeBrush(lastPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorPreview.bringToFront();
|
||||||
|
cursorPreview.position = event.point;
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
this.brush = null;
|
||||||
|
tool.fixedDistance = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.mergeBrush = function (lastPath) {
|
||||||
|
// Get all path items to merge with
|
||||||
|
const paths = paper.project.getItems({
|
||||||
|
match: function (item) {
|
||||||
|
return tool.isMergeable(lastPath, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mergedPath = lastPath;
|
||||||
|
let i;
|
||||||
|
// Move down z order to first overlapping item
|
||||||
|
for (i = paths.length - 1; i >= 0 && !tool.touches(paths[i], lastPath); i--) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mergedPathIndex = i;
|
||||||
|
for (; i >= 0; i--) {
|
||||||
|
if (!tool.touches(paths[i], lastPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!paths[i].getFillColor()) {
|
||||||
|
// Ignore for merge. Paths without fill need to be in paths though,
|
||||||
|
// since they can visibly change if z order changes
|
||||||
|
} else if (tool.colorMatch(paths[i], lastPath)) {
|
||||||
|
// Make sure the new shape isn't overlapped by anything that would
|
||||||
|
// visibly change if we change its z order
|
||||||
|
for (let j = mergedPathIndex; j > i; j--) {
|
||||||
|
if (tool.touches(paths[j], paths[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Merge same fill color
|
||||||
|
const tempPath = mergedPath.unite(paths[i]);
|
||||||
|
tempPath.strokeColor = paths[i].strokeColor;
|
||||||
|
tempPath.strokeWidth = paths[i].strokeWidth;
|
||||||
|
if (mergedPath === lastPath) {
|
||||||
|
tempPath.insertAbove(paths[i]); // First intersected path determines z position of the new path
|
||||||
|
} else {
|
||||||
|
tempPath.insertAbove(mergedPath); // Rest of merges join z index of merged path
|
||||||
|
mergedPathIndex--; // Removed an item, so the merged path index decreases
|
||||||
|
}
|
||||||
|
mergedPath.remove();
|
||||||
|
mergedPath = tempPath;
|
||||||
|
paths[i].remove();
|
||||||
|
paths.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO FIX
|
||||||
|
//pg.undo.snapshot('broadbrush');
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.mergeEraser = function (lastPath) {
|
||||||
|
// Get all path items to merge with
|
||||||
|
// If there are selected items, try to erase from amongst those.
|
||||||
|
let items = paper.project.getItems({
|
||||||
|
match: function (item) {
|
||||||
|
return item.selected && tool.isMergeable(lastPath, item) && tool.touches(lastPath, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Eraser didn't hit anything selected, so assume they meant to erase from all instead of from subset
|
||||||
|
// and deselect the selection
|
||||||
|
if (items.length === 0) {
|
||||||
|
// TODO FIX
|
||||||
|
//pg.selection.clearSelection();
|
||||||
|
items = paper.project.getItems({
|
||||||
|
match: function (item) {
|
||||||
|
return tool.isMergeable(lastPath, item) && tool.touches(lastPath, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = items.length - 1; i >= 0; i--) {
|
||||||
|
// Erase
|
||||||
|
const newPath = items[i].subtract(lastPath);
|
||||||
|
|
||||||
|
// Gather path segments
|
||||||
|
const subpaths = [];
|
||||||
|
if (items[i] instanceof paper.PathItem && !items[i].closed) {
|
||||||
|
const firstSeg = items[i].clone();
|
||||||
|
const intersections = firstSeg.getIntersections(lastPath);
|
||||||
|
// keep first and last segments
|
||||||
|
if (intersections.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let j = intersections.length - 1; j >= 0; j--) {
|
||||||
|
subpaths.push(firstSeg.splitAt(intersections[j]));
|
||||||
|
}
|
||||||
|
subpaths.push(firstSeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the ones that are within the eraser stroke boundary, or are already part of new path.
|
||||||
|
// This way subpaths only remain if they didn't get turned into a shape by subtract.
|
||||||
|
for (let k = subpaths.length - 1; k >= 0; k--) {
|
||||||
|
const segMidpoint = subpaths[k].getLocationAt(subpaths[k].length / 2).point;
|
||||||
|
if (lastPath.contains(segMidpoint) || newPath.contains(segMidpoint)) {
|
||||||
|
subpaths[k].remove();
|
||||||
|
subpaths.splice(k, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide topologically separate shapes into their own compound paths, instead of
|
||||||
|
// everything being stuck together.
|
||||||
|
// Assume that result of erase operation returns clockwise paths for positive shapes
|
||||||
|
const clockwiseChildren = [];
|
||||||
|
const ccwChildren = [];
|
||||||
|
if (newPath.children) {
|
||||||
|
for (let j = newPath.children.length - 1; j >= 0; j--) {
|
||||||
|
const child = newPath.children[j];
|
||||||
|
if (child.isClockwise()) {
|
||||||
|
clockwiseChildren.push(child);
|
||||||
|
} else {
|
||||||
|
ccwChildren.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let j = 0; j < clockwiseChildren.length; j++) {
|
||||||
|
const cw = clockwiseChildren[j];
|
||||||
|
cw.copyAttributes(newPath);
|
||||||
|
cw.fillColor = newPath.fillColor;
|
||||||
|
cw.strokeColor = newPath.strokeColor;
|
||||||
|
cw.strokeWidth = newPath.strokeWidth;
|
||||||
|
cw.insertAbove(items[i]);
|
||||||
|
|
||||||
|
// Go backward since we are deleting elements
|
||||||
|
let newCw = cw;
|
||||||
|
for (let k = ccwChildren.length - 1; k >= 0; k--) {
|
||||||
|
const ccw = ccwChildren[k];
|
||||||
|
if (tool.firstEnclosesSecond(ccw, cw) || tool.firstEnclosesSecond(cw, ccw)) {
|
||||||
|
const temp = newCw.subtract(ccw);
|
||||||
|
temp.insertAbove(newCw);
|
||||||
|
newCw.remove();
|
||||||
|
newCw = temp;
|
||||||
|
ccw.remove();
|
||||||
|
ccwChildren.splice(k, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newPath.remove();
|
||||||
|
}
|
||||||
|
items[i].remove();
|
||||||
|
}
|
||||||
|
lastPath.remove();
|
||||||
|
// TODO FIX
|
||||||
|
//pg.undo.snapshot('eraser');
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.colorMatch = function (existingPath, addedPath) {
|
||||||
|
// Note: transparent fill colors do notdetect as touching
|
||||||
|
return existingPath.getFillColor().equals(addedPath.getFillColor()) &&
|
||||||
|
(addedPath.getStrokeColor() === existingPath.getStrokeColor() || // both null
|
||||||
|
(addedPath.getStrokeColor() &&
|
||||||
|
addedPath.getStrokeColor().equals(existingPath.getStrokeColor()))) &&
|
||||||
|
addedPath.getStrokeWidth() === existingPath.getStrokeWidth() &&
|
||||||
|
tool.touches(existingPath, addedPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.touches = function (path1, path2) {
|
||||||
|
// Two shapes are touching if their paths intersect
|
||||||
|
if (path1 && path2 && path1.intersects(path2)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return tool.firstEnclosesSecond(path1, path2) || tool.firstEnclosesSecond(path2, path1);
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.firstEnclosesSecond = function (path1, path2) {
|
||||||
|
// Two shapes are also touching if one is completely inside the other
|
||||||
|
if (path1 && path2 && path2.firstSegment && path2.firstSegment.point &&
|
||||||
|
path1.hitTest(path2.firstSegment.point)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// TODO clean up these no point paths
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.isMergeable = function (newPath, existingPath) {
|
||||||
|
return existingPath instanceof paper.PathItem && // path or compound path
|
||||||
|
existingPath !== cursorPreview && // don't merge with the mouse preview
|
||||||
|
existingPath !== newPath && // don't merge with self
|
||||||
|
existingPath.parent instanceof paper.Layer; // don't merge with nested in group
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivateTool () {
|
||||||
|
console.log('deactivateTool');
|
||||||
|
this.cursorPreview.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BlobTool;
|
107
src/tools/broad-brush-helper.js
Normal file
107
src/tools/broad-brush-helper.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/
|
||||||
|
const paper = require('paper');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies segment brush functions to the tool.
|
||||||
|
* @param {!Tool} tool paper.js mouse object
|
||||||
|
* @param {!options} options brush tool state object
|
||||||
|
* @param {!options.brushSize} brush tool diameter
|
||||||
|
*/
|
||||||
|
const broadBrushHelper = function (tool, options) {
|
||||||
|
let lastPoint;
|
||||||
|
let secondLastPoint;
|
||||||
|
let finalPath;
|
||||||
|
|
||||||
|
tool.onBroadMouseDown = function (event) {
|
||||||
|
tool.minDistance = options.brushSize / 4;
|
||||||
|
tool.maxDistance = options.brushSize;
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
finalPath = new paper.Path();
|
||||||
|
tool.stylePath(finalPath);
|
||||||
|
finalPath.add(event.point);
|
||||||
|
lastPoint = secondLastPoint = event.point;
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.onBroadMouseDrag = function (event) {
|
||||||
|
const step = (event.delta).normalize(options.brushSize / 2);
|
||||||
|
|
||||||
|
// Move the first point out away from the drag so that the end of the path is rounded
|
||||||
|
if (finalPath.segments && finalPath.segments.length === 1) {
|
||||||
|
const removedPoint = finalPath.removeSegment(0).point;
|
||||||
|
// Add handles to round the end caps
|
||||||
|
const handleVec = step.clone();
|
||||||
|
handleVec.length = options.brushSize / 2;
|
||||||
|
handleVec.angle += 90;
|
||||||
|
finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec));
|
||||||
|
}
|
||||||
|
step.angle += 90;
|
||||||
|
const top = event.middlePoint.add(step);
|
||||||
|
const bottom = event.middlePoint.subtract(step);
|
||||||
|
|
||||||
|
if (finalPath.segments.length > 3) {
|
||||||
|
finalPath.removeSegment(finalPath.segments.length - 1);
|
||||||
|
finalPath.removeSegment(0);
|
||||||
|
}
|
||||||
|
finalPath.add(top);
|
||||||
|
finalPath.add(event.point.add(step));
|
||||||
|
finalPath.insert(0, bottom);
|
||||||
|
finalPath.insert(0, event.point.subtract(step));
|
||||||
|
if (finalPath.segments.length === 5) {
|
||||||
|
// Flatten is necessary to prevent smooth from getting rid of the effect
|
||||||
|
// of the handles on the first point.
|
||||||
|
finalPath.flatten(options.brushSize / 5);
|
||||||
|
}
|
||||||
|
finalPath.smooth();
|
||||||
|
lastPoint = event.point;
|
||||||
|
secondLastPoint = event.lastPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
tool.onBroadMouseUp = function (event) {
|
||||||
|
// If the mouse up is at the same point as the mouse drag event then we need
|
||||||
|
// the second to last point to get the right direction vector for the end cap
|
||||||
|
if (event.point.equals(lastPoint)) {
|
||||||
|
lastPoint = secondLastPoint;
|
||||||
|
}
|
||||||
|
// If the points are still equal, then there was no drag, so just draw a circle.
|
||||||
|
if (event.point.equals(lastPoint)) {
|
||||||
|
finalPath.remove();
|
||||||
|
finalPath = new paper.Path.Circle({
|
||||||
|
center: event.point,
|
||||||
|
radius: options.brushSize / 2
|
||||||
|
});
|
||||||
|
tool.stylePath(finalPath);
|
||||||
|
} else {
|
||||||
|
const step = (event.point.subtract(lastPoint)).normalize(options.brushSize / 2);
|
||||||
|
step.angle += 90;
|
||||||
|
const handleVec = step.clone();
|
||||||
|
handleVec.length = options.brushSize / 2;
|
||||||
|
|
||||||
|
const top = event.point.add(step);
|
||||||
|
const bottom = event.point.subtract(step);
|
||||||
|
finalPath.add(top);
|
||||||
|
finalPath.insert(0, bottom);
|
||||||
|
|
||||||
|
// Simplify before adding end cap so cap doesn't get warped
|
||||||
|
finalPath.simplify(1);
|
||||||
|
|
||||||
|
// Add end cap
|
||||||
|
step.angle -= 90;
|
||||||
|
finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec));
|
||||||
|
finalPath.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve self-crossings
|
||||||
|
const newPath =
|
||||||
|
finalPath
|
||||||
|
.resolveCrossings()
|
||||||
|
.reorient(true /* nonZero */, true /* clockwise */)
|
||||||
|
.reduce({simplify: true});
|
||||||
|
newPath.copyAttributes(finalPath);
|
||||||
|
newPath.fillColor = finalPath.fillColor;
|
||||||
|
finalPath = newPath;
|
||||||
|
return finalPath;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = broadBrushHelper;
|
44
src/tools/eraser.js
Normal file
44
src/tools/eraser.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// TODO share code with brush
|
||||||
|
|
||||||
|
pg.tools.registerTool({
|
||||||
|
id: 'eraser',
|
||||||
|
name: 'Eraser'
|
||||||
|
});
|
||||||
|
|
||||||
|
pg.tools.eraser = function() {
|
||||||
|
var blob = new pg.blob();
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
brushWidth: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
var components = {
|
||||||
|
brushWidth: {
|
||||||
|
type: 'float',
|
||||||
|
label: 'Eraser width',
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var activateTool = function() {
|
||||||
|
// get options from local storage if present
|
||||||
|
options = pg.tools.getLocalOptions(options);
|
||||||
|
var tool = new Tool();
|
||||||
|
blob.activateTool(true /* isEraser */, tool, options);
|
||||||
|
|
||||||
|
// setup floating tool options panel in the editor
|
||||||
|
pg.toolOptionPanel.setup(options, components, function() {});
|
||||||
|
|
||||||
|
tool.activate();
|
||||||
|
};
|
||||||
|
|
||||||
|
var deactivateTool = function() {
|
||||||
|
blob.deactivateTool();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: options,
|
||||||
|
activateTool : activateTool,
|
||||||
|
deactivateTool : deactivateTool
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in a new issue