mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Pen tool
This commit is contained in:
parent
b79bb8174f
commit
131193ef73
2 changed files with 164 additions and 18 deletions
|
@ -6,9 +6,9 @@ import Modes from '../modes/modes';
|
||||||
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearHoveredItem, setHoveredItem} from '../reducers/hover';
|
import {clearHoveredItem, setHoveredItem} from '../reducers/hover';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
|
|
||||||
import {getSelectedLeafItems} from '../helper/selection';
|
import {clearSelection} from '../helper/selection';
|
||||||
import PenTool from '../helper/tools/pen-tool';
|
import PenTool from '../helper/tools/pen-tool';
|
||||||
import PenModeComponent from '../components/pen-mode.jsx';
|
import PenModeComponent from '../components/pen-mode.jsx';
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@ class PenMode extends React.Component {
|
||||||
if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
|
if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
|
||||||
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
|
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
|
||||||
}
|
}
|
||||||
|
if (this.tool &&
|
||||||
|
(nextProps.colorState.strokeColor !== this.props.colorState.strokeColor ||
|
||||||
|
nextProps.colorState.strokeWidth !== this.props.colorState.strokeWidth)) {
|
||||||
|
this.tool.setColorState(nextProps.colorState);
|
||||||
|
}
|
||||||
|
|
||||||
if (nextProps.isPenModeActive && !this.props.isPenModeActive) {
|
if (nextProps.isPenModeActive && !this.props.isPenModeActive) {
|
||||||
this.activateTool();
|
this.activateTool();
|
||||||
|
@ -40,13 +45,15 @@ class PenMode extends React.Component {
|
||||||
return false; // Static component, for now
|
return false; // Static component, for now
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.tool = new PenTool(
|
this.tool = new PenTool(
|
||||||
this.props.setHoveredItem,
|
this.props.setHoveredItem,
|
||||||
this.props.clearHoveredItem,
|
this.props.clearHoveredItem,
|
||||||
this.props.setSelectedItems,
|
|
||||||
this.props.clearSelectedItems,
|
this.props.clearSelectedItems,
|
||||||
this.props.onUpdateSvg
|
this.props.onUpdateSvg
|
||||||
);
|
);
|
||||||
|
this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
|
||||||
|
this.tool.setColorState(this.props.colorState);
|
||||||
this.tool.activate();
|
this.tool.activate();
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
@ -64,17 +71,23 @@ class PenMode extends React.Component {
|
||||||
PenMode.propTypes = {
|
PenMode.propTypes = {
|
||||||
clearHoveredItem: PropTypes.func.isRequired,
|
clearHoveredItem: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
colorState: PropTypes.shape({
|
||||||
|
fillColor: PropTypes.string,
|
||||||
|
strokeColor: PropTypes.string,
|
||||||
|
strokeWidth: PropTypes.number
|
||||||
|
}).isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
hoveredItemId: PropTypes.number,
|
hoveredItemId: PropTypes.number,
|
||||||
isPenModeActive: PropTypes.bool.isRequired,
|
isPenModeActive: PropTypes.bool.isRequired,
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
setHoveredItem: PropTypes.func.isRequired,
|
setHoveredItem: PropTypes.func.isRequired
|
||||||
setSelectedItems: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
colorState: state.scratchPaint.color,
|
||||||
isPenModeActive: state.scratchPaint.mode === Modes.PEN,
|
isPenModeActive: state.scratchPaint.mode === Modes.PEN,
|
||||||
hoveredItemId: state.scratchPaint.hoveredItemId
|
hoveredItemId: state.scratchPaint.hoveredItemId
|
||||||
|
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
setHoveredItem: hoveredItemId => {
|
setHoveredItem: hoveredItemId => {
|
||||||
|
@ -86,9 +99,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
setSelectedItems: () => {
|
|
||||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
|
||||||
},
|
|
||||||
handleMouseDown: () => {
|
handleMouseDown: () => {
|
||||||
dispatch(changeMode(Modes.PEN));
|
dispatch(changeMode(Modes.PEN));
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,32 +1,49 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import log from '../../log/log';
|
import {stylePath} from '../style-path';
|
||||||
|
import {endPointHit, touching} from '../snapping';
|
||||||
|
import {drawHitPoint, removeHitPoint} from '../guides';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool to handle freehand drawing of lines.
|
* Tool to handle freehand drawing of lines.
|
||||||
*/
|
*/
|
||||||
class PenTool extends paper.Tool {
|
class PenTool extends paper.Tool {
|
||||||
|
static get SNAP_TOLERANCE () {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
/** Smaller numbers match the line more closely, larger numbers for smoother curves */
|
||||||
|
static get SMOOTHING () {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @param {function} setHoveredItem Callback to set the hovered item
|
* @param {function} setHoveredItem Callback to set the hovered item
|
||||||
* @param {function} clearHoveredItem Callback to clear the hovered item
|
* @param {function} clearHoveredItem Callback to clear the hovered item
|
||||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
|
||||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||||
*/
|
*/
|
||||||
constructor (setHoveredItem, clearHoveredItem, setSelectedItems, clearSelectedItems, onUpdateSvg) {
|
constructor (setHoveredItem, clearHoveredItem, clearSelectedItems, onUpdateSvg) {
|
||||||
super();
|
super();
|
||||||
this.setHoveredItem = setHoveredItem;
|
this.setHoveredItem = setHoveredItem;
|
||||||
this.clearHoveredItem = clearHoveredItem;
|
this.clearHoveredItem = clearHoveredItem;
|
||||||
this.setSelectedItems = setSelectedItems;
|
|
||||||
this.clearSelectedItems = clearSelectedItems;
|
this.clearSelectedItems = clearSelectedItems;
|
||||||
this.onUpdateSvg = onUpdateSvg;
|
this.onUpdateSvg = onUpdateSvg;
|
||||||
|
|
||||||
this.prevHoveredItemId = null;
|
this.prevHoveredItemId = null;
|
||||||
|
this.colorState = null;
|
||||||
|
this.path = null;
|
||||||
|
this.hitResult = null;
|
||||||
|
|
||||||
|
// Piece of whole path that was added by last stroke. Used to smooth just the added part.
|
||||||
|
this.subpath = null;
|
||||||
|
this.subpathIndex = 0;
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
// We have to set these functions instead of just declaring them because
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
// paper.js tools hook up the listeners in the setter functions.
|
||||||
this.onMouseDown = this.handleMouseDown;
|
this.onMouseDown = this.handleMouseDown;
|
||||||
this.onMouseMove = this.handleMouseMove;
|
this.onMouseMove = this.handleMouseMove;
|
||||||
this.onMouseDrag = this.handleMouseDrag;
|
this.onMouseDrag = this.handleMouseDrag;
|
||||||
this.onMouseUp = this.handleMouseUp;
|
this.onMouseUp = this.handleMouseUp;
|
||||||
|
|
||||||
|
this.fixedDistance = 2;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* To be called when the hovered item changes. When the select tool hovers over a
|
* To be called when the hovered item changes. When the select tool hovers over a
|
||||||
|
@ -38,16 +55,135 @@ class PenTool extends paper.Tool {
|
||||||
setPrevHoveredItemId (prevHoveredItemId) {
|
setPrevHoveredItemId (prevHoveredItemId) {
|
||||||
this.prevHoveredItemId = prevHoveredItemId;
|
this.prevHoveredItemId = prevHoveredItemId;
|
||||||
}
|
}
|
||||||
handleMouseDown () {
|
setColorState (colorState) {
|
||||||
log.warn('Pen not yet implemented');
|
this.colorState = colorState;
|
||||||
}
|
}
|
||||||
handleMouseMove () {
|
drawHitPoint (hitResult) {
|
||||||
|
// If near another path's endpoint, draw hit point to indicate that paths would merge
|
||||||
|
if (hitResult) {
|
||||||
|
const hitPath = hitResult.path;
|
||||||
|
if (hitResult.isFirst) {
|
||||||
|
drawHitPoint(hitPath.firstSegment.point);
|
||||||
|
} else {
|
||||||
|
drawHitPoint(hitPath.lastSegment.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleMouseDrag () {
|
handleMouseDown (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
this.subpath = new paper.Path({insert: false});
|
||||||
|
|
||||||
|
// If you click near a point, continue that line instead of making a new line
|
||||||
|
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE);
|
||||||
|
if (this.hitResult) {
|
||||||
|
this.path = this.hitResult.path;
|
||||||
|
stylePath(this.path, this.colorState.strokeColor, this.colorState.strokeWidth);
|
||||||
|
if (this.hitResult.isFirst) {
|
||||||
|
this.path.reverse();
|
||||||
|
}
|
||||||
|
this.subpathIndex = this.path.segments.length;
|
||||||
|
this.path.lastSegment.handleOut = null; // Don't interfere with the curvature of the existing path
|
||||||
|
this.path.lastSegment.handleIn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not near other path, start a new path
|
||||||
|
if (!this.path) {
|
||||||
|
this.path = new paper.Path();
|
||||||
|
stylePath(this.path, this.colorState.strokeColor, this.colorState.strokeWidth);
|
||||||
|
this.path.add(event.point);
|
||||||
|
this.subpath.add(event.point);
|
||||||
|
paper.view.draw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleMouseUp () {
|
handleMouseMove (event) {
|
||||||
|
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
||||||
|
// joining/closing the paths.
|
||||||
|
if (this.hitResult) {
|
||||||
|
removeHitPoint();
|
||||||
|
}
|
||||||
|
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE);
|
||||||
|
this.drawHitPoint(this.hitResult);
|
||||||
|
}
|
||||||
|
handleMouseDrag (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
// If near another path's endpoint, or this path's beginpoint, highlight it to suggest
|
||||||
|
// joining/closing the paths.
|
||||||
|
if (this.hitResult) {
|
||||||
|
removeHitPoint();
|
||||||
|
this.hitResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path &&
|
||||||
|
!this.path.closed &&
|
||||||
|
this.path.segments.length > 3 &&
|
||||||
|
touching(this.path.firstSegment.point, event.point, PenTool.SNAP_TOLERANCE)) {
|
||||||
|
this.hitResult = {
|
||||||
|
path: this.path,
|
||||||
|
segment: this.path.firstSegment,
|
||||||
|
isFirst: true
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.hitResult = endPointHit(event.point, PenTool.SNAP_TOLERANCE, this.path);
|
||||||
|
}
|
||||||
|
if (this.hitResult) {
|
||||||
|
this.drawHitPoint(this.hitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.path.add(event.point);
|
||||||
|
this.subpath.add(event.point);
|
||||||
|
}
|
||||||
|
handleMouseUp (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
|
// If I single clicked, don't do anything
|
||||||
|
if (!this.hitResult && // Might be connecting 2 points that are very close
|
||||||
|
(this.path.segments.length < 2 ||
|
||||||
|
(this.path.segments.length === 2 &&
|
||||||
|
touching(this.path.firstSegment.point, event.point, PenTool.SNAP_TOLERANCE)))) {
|
||||||
|
this.path.remove();
|
||||||
|
this.path = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth only the added portion
|
||||||
|
const hasStartConnection = this.subpathIndex > 0;
|
||||||
|
const hasEndConnection = !!this.hitResult;
|
||||||
|
this.path.removeSegments(this.subpathIndex);
|
||||||
|
this.subpath.simplify(this.SMOOTHING);
|
||||||
|
if (hasStartConnection && this.subpath.length > 0) {
|
||||||
|
this.subpath.removeSegment(0);
|
||||||
|
}
|
||||||
|
if (hasEndConnection && this.subpath.length > 0) {
|
||||||
|
this.subpath.removeSegment(this.subpath.length - 1);
|
||||||
|
}
|
||||||
|
this.path.insertSegments(this.subpathIndex, this.subpath.segments);
|
||||||
|
this.subpath = null;
|
||||||
|
this.subpathIndex = 0;
|
||||||
|
|
||||||
|
// If I intersect other line end points, join or close
|
||||||
|
if (this.hitResult) {
|
||||||
|
if (touching(this.path.firstSegment.point, this.hitResult.segment.point, PenTool.SNAP_TOLERANCE)) {
|
||||||
|
// close path
|
||||||
|
this.path.closed = true;
|
||||||
|
} else {
|
||||||
|
// joining two paths
|
||||||
|
if (!this.hitResult.isFirst) {
|
||||||
|
this.hitResult.path.reverse();
|
||||||
|
}
|
||||||
|
this.path.join(this.hitResult.path);
|
||||||
|
}
|
||||||
|
removeHitPoint();
|
||||||
|
this.hitResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path) {
|
||||||
|
this.onUpdateSvg();
|
||||||
|
this.path = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
|
this.fixedDistance = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue