mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 13:32:28 -05:00
Merge branch 'develop' into structure
This commit is contained in:
commit
09dad3df7d
18 changed files with 552 additions and 44 deletions
19
src/components/line-mode.jsx
Normal file
19
src/components/line-mode.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const LineModeComponent = props => (
|
||||
<button onClick={props.onMouseDown}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Line"
|
||||
description="Label for the line tool, which draws straight line segments"
|
||||
id="paint.lineMode.line"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
LineModeComponent.propTypes = {
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LineModeComponent;
|
|
@ -3,6 +3,8 @@ import React from 'react';
|
|||
import PaperCanvas from '../containers/paper-canvas.jsx';
|
||||
import BrushMode from '../containers/brush-mode.jsx';
|
||||
import EraserMode from '../containers/eraser-mode.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import LineMode from '../containers/line-mode.jsx';
|
||||
|
||||
import styles from './paint-editor.css';
|
||||
|
||||
|
@ -26,7 +28,7 @@ class PaintEditorComponent extends React.Component {
|
|||
<div className={styles.inputGroup}>
|
||||
{/* Todo use Label and BufferedInput from Gui */}
|
||||
<label>Costume
|
||||
<input value="meow"/>
|
||||
<input value="meow" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -103,14 +105,29 @@ class PaintEditorComponent extends React.Component {
|
|||
{/* Modes */}
|
||||
{this.state.canvas ? (
|
||||
<div className={styles.modeSelector}>
|
||||
<BrushMode canvas={this.state.canvas} />
|
||||
<EraserMode canvas={this.state.canvas} />
|
||||
<BrushMode
|
||||
canvas={this.state.canvas}
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
<EraserMode
|
||||
canvas={this.state.canvas}
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
<LineMode
|
||||
canvas={this.state.canvas}
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Canvas */}
|
||||
<div className={styles.canvasContainer}>
|
||||
<PaperCanvas canvasRef={this.setCanvas} />
|
||||
<PaperCanvas
|
||||
canvasRef={this.setCanvas}
|
||||
rotationCenterX={this.props.rotationCenterX}
|
||||
rotationCenterY={this.props.rotationCenterY}
|
||||
svg={this.props.svg}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,4 +135,11 @@ class PaintEditorComponent extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
PaintEditorComponent.propTypes = {
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
svg: PropTypes.string
|
||||
};
|
||||
|
||||
export default PaintEditorComponent;
|
||||
|
|
|
@ -24,9 +24,13 @@ class Blobbiness {
|
|||
return 9;
|
||||
}
|
||||
|
||||
constructor () {
|
||||
/**
|
||||
* @param {function} updateCallback call when the drawing has changed to let listeners know
|
||||
*/
|
||||
constructor (updateCallback) {
|
||||
this.broadBrushHelper = new BroadBrushHelper();
|
||||
this.segmentBrushHelper = new SegmentBrushHelper();
|
||||
this.updateCallback = updateCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +68,7 @@ class Blobbiness {
|
|||
|
||||
this.tool.onMouseDown = function (event) {
|
||||
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 (blob.options.brushSize < Blobbiness.THRESHOLD) {
|
||||
blob.brush = Blobbiness.BROAD;
|
||||
|
@ -80,7 +84,7 @@ class Blobbiness {
|
|||
|
||||
this.tool.onMouseDrag = function (event) {
|
||||
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 (blob.brush === Blobbiness.BROAD) {
|
||||
blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options);
|
||||
} else if (blob.brush === Blobbiness.SEGMENT) {
|
||||
|
@ -96,7 +100,7 @@ class Blobbiness {
|
|||
|
||||
this.tool.onMouseUp = function (event) {
|
||||
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;
|
||||
if (blob.brush === Blobbiness.BROAD) {
|
||||
|
@ -113,6 +117,9 @@ class Blobbiness {
|
|||
blob.mergeBrush(lastPath);
|
||||
}
|
||||
|
||||
blob.cursorPreview.visible = false;
|
||||
blob.updateCallback();
|
||||
blob.cursorPreview.visible = true;
|
||||
blob.cursorPreview.bringToFront();
|
||||
blob.cursorPreview.position = event.point;
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class BroadBrushHelper {
|
|||
onBroadMouseDown (event, tool, options) {
|
||||
tool.minDistance = options.brushSize / 2;
|
||||
tool.maxDistance = options.brushSize;
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
|
||||
this.finalPath = new paper.Path();
|
||||
stylePath(this.finalPath, options.isEraser);
|
||||
|
|
|
@ -22,7 +22,7 @@ class SegmentBrushHelper {
|
|||
}
|
||||
|
||||
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.maxDistance = options.brushSize;
|
||||
|
@ -37,7 +37,7 @@ class SegmentBrushHelper {
|
|||
}
|
||||
|
||||
onSegmentMouseDrag (event, tool, 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(options.brushSize / 2);
|
||||
const handleVec = step.clone();
|
||||
|
@ -75,7 +75,7 @@ class SegmentBrushHelper {
|
|||
}
|
||||
|
||||
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
|
||||
// add back smoothing, maybe a custom implementation that only applies to a subset of the line?
|
||||
|
|
|
@ -16,7 +16,7 @@ class BrushMode extends React.Component {
|
|||
'deactivateTool',
|
||||
'onScroll'
|
||||
]);
|
||||
this.blob = new Blobbiness();
|
||||
this.blob = new Blobbiness(this.props.onUpdateSvg);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isBrushModeActive) {
|
||||
|
@ -70,7 +70,8 @@ BrushMode.propTypes = {
|
|||
canvas: PropTypes.instanceOf(Element).isRequired,
|
||||
changeBrushSize: PropTypes.func.isRequired,
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isBrushModeActive: PropTypes.bool.isRequired
|
||||
isBrushModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
|
@ -16,7 +16,7 @@ class EraserMode extends React.Component {
|
|||
'deactivateTool',
|
||||
'onScroll'
|
||||
]);
|
||||
this.blob = new Blobbiness();
|
||||
this.blob = new Blobbiness(this.props.onUpdateSvg);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isEraserModeActive) {
|
||||
|
@ -66,7 +66,8 @@ EraserMode.propTypes = {
|
|||
brushSize: PropTypes.number.isRequired
|
||||
}),
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isEraserModeActive: PropTypes.bool.isRequired
|
||||
isEraserModeActive: PropTypes.bool.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
304
src/containers/line-mode.jsx
Normal file
304
src/containers/line-mode.jsx
Normal file
|
@ -0,0 +1,304 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import Modes from '../modes/modes';
|
||||
import {changeLineWidth} from '../reducers/line-mode';
|
||||
import LineModeComponent from '../components/line-mode.jsx';
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import paper from 'paper';
|
||||
|
||||
class LineMode extends React.Component {
|
||||
static get SNAP_TOLERANCE () {
|
||||
return 6;
|
||||
}
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'activateTool',
|
||||
'deactivateTool',
|
||||
'onMouseDown',
|
||||
'onMouseMove',
|
||||
'onMouseDrag',
|
||||
'onMouseUp',
|
||||
'toleranceSquared',
|
||||
'findLineEnd',
|
||||
'onScroll'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isLineModeActive) {
|
||||
this.activateTool();
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.isLineModeActive && !this.props.isLineModeActive) {
|
||||
this.activateTool();
|
||||
} else if (!nextProps.isLineModeActive && this.props.isLineModeActive) {
|
||||
this.deactivateTool();
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate () {
|
||||
return false; // Static component, for now
|
||||
}
|
||||
activateTool () {
|
||||
// TODO add back selection
|
||||
// pg.selection.clearSelection();
|
||||
this.props.canvas.addEventListener('mousewheel', this.onScroll);
|
||||
this.tool = new paper.Tool();
|
||||
|
||||
this.path = null;
|
||||
this.hitResult = null;
|
||||
|
||||
// TODO add back colors
|
||||
// Make sure a stroke color is set on the line tool
|
||||
// if(!pg.stylebar.getStrokeColor()) {
|
||||
// pg.stylebar.setStrokeColor(pg.stylebar.getFillColor());
|
||||
// pg.stylebar.setFillColor(null);
|
||||
// }
|
||||
|
||||
const lineMode = this;
|
||||
this.tool.onMouseDown = function (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
lineMode.onMouseDown(event);
|
||||
};
|
||||
this.tool.onMouseMove = function (event) {
|
||||
lineMode.onMouseMove(event);
|
||||
};
|
||||
this.tool.onMouseDrag = function (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
lineMode.onMouseDrag(event);
|
||||
};
|
||||
this.tool.onMouseUp = function (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
lineMode.onMouseUp(event);
|
||||
};
|
||||
|
||||
this.tool.activate();
|
||||
}
|
||||
onMouseDown (event) {
|
||||
// Deselect old path
|
||||
if (this.path) {
|
||||
this.path.setSelected(false);
|
||||
this.path = null;
|
||||
}
|
||||
|
||||
// If you click near a point, continue that line instead of making a new line
|
||||
this.hitResult = this.findLineEnd(event.point);
|
||||
if (this.hitResult) {
|
||||
this.path = this.hitResult.path;
|
||||
if (this.hitResult.isFirst) {
|
||||
this.path.reverse();
|
||||
}
|
||||
this.path.lastSegment.setSelected(true);
|
||||
this.path.add(this.hitResult.segment); // Add second point, which is what will move when dragged
|
||||
this.path.lastSegment.handleOut = null; // Make sure line isn't curvy
|
||||
this.path.lastSegment.handleIn = null;
|
||||
}
|
||||
|
||||
// If not near other path, start a new path
|
||||
if (!this.path) {
|
||||
this.path = new paper.Path();
|
||||
|
||||
// TODO add back style
|
||||
// this.path = pg.stylebar.applyActiveToolbarStyle(path);
|
||||
this.path.setStrokeColor('black');
|
||||
this.path.setStrokeWidth(this.props.lineModeState.lineWidth);
|
||||
|
||||
this.path.setSelected(true);
|
||||
this.path.add(event.point);
|
||||
this.path.add(event.point); // Add second point, which is what will move when dragged
|
||||
paper.view.draw();
|
||||
}
|
||||
}
|
||||
onMouseMove (event) {
|
||||
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
||||
// joining/closing the paths.
|
||||
if (this.hitResult) {
|
||||
this.hitResult.path.setSelected(false);
|
||||
this.hitResult = null;
|
||||
}
|
||||
|
||||
if (this.path &&
|
||||
!this.path.closed &&
|
||||
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) {
|
||||
this.hitResult = {
|
||||
path: this.path,
|
||||
segment: this.path.firstSegment,
|
||||
isFirst: true
|
||||
};
|
||||
} else {
|
||||
this.hitResult = this.findLineEnd(event.point);
|
||||
}
|
||||
|
||||
if (this.hitResult) {
|
||||
const hitPath = this.hitResult.path;
|
||||
hitPath.setSelected(true);
|
||||
if (this.hitResult.isFirst) {
|
||||
hitPath.firstSegment.setSelected(true);
|
||||
} else {
|
||||
hitPath.lastSegment.setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
onMouseDrag (event) {
|
||||
// If near another path's endpoint, or this path's beginpoint, clip to it to suggest
|
||||
// joining/closing the paths.
|
||||
if (this.hitResult && this.hitResult.path !== this.path) this.hitResult.path.setSelected(false);
|
||||
this.hitResult = null;
|
||||
|
||||
if (this.path &&
|
||||
this.path.segments.length > 3 &&
|
||||
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared()) {
|
||||
this.hitResult = {
|
||||
path: this.path,
|
||||
segment: this.path.firstSegment,
|
||||
isFirst: true
|
||||
};
|
||||
} else {
|
||||
this.hitResult = this.findLineEnd(event.point, this.path);
|
||||
if (this.hitResult) {
|
||||
const hitPath = this.hitResult.path;
|
||||
hitPath.setSelected(true);
|
||||
if (this.hitResult.isFirst) {
|
||||
hitPath.firstSegment.setSelected(true);
|
||||
} else {
|
||||
hitPath.lastSegment.setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// snapping
|
||||
if (this.path) {
|
||||
if (this.hitResult) {
|
||||
this.path.lastSegment.point = this.hitResult.segment.point;
|
||||
} else {
|
||||
this.path.lastSegment.point = event.point;
|
||||
}
|
||||
}
|
||||
}
|
||||
onMouseUp (event) {
|
||||
// If I single clicked, don't do anything
|
||||
if (this.path.segments.length < 2 ||
|
||||
(this.path.segments.length === 2 &&
|
||||
this.path.firstSegment.point.getDistance(event.point, true) < this.toleranceSquared())) {
|
||||
this.path.remove();
|
||||
this.path = null;
|
||||
// TODO don't erase the line if both ends are snapped to different points
|
||||
return;
|
||||
} else if (
|
||||
this.path.lastSegment.point.getDistance(this.path.segments[this.path.segments.length - 2].point, true) <
|
||||
this.toleranceSquared()) {
|
||||
this.path.removeSegment(this.path.segments.length - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// If I intersect other line end points, join or close
|
||||
if (this.hitResult) {
|
||||
this.path.removeSegment(this.path.segments.length - 1);
|
||||
if (this.path.firstSegment === this.hitResult.segment) {
|
||||
// close path
|
||||
this.path.closed = true;
|
||||
this.path.setSelected(false);
|
||||
} else {
|
||||
// joining two paths
|
||||
if (!this.hitResult.isFirst) {
|
||||
this.hitResult.path.reverse();
|
||||
}
|
||||
this.path.join(this.hitResult.path);
|
||||
}
|
||||
this.hitResult = null;
|
||||
}
|
||||
this.props.onUpdateSvg();
|
||||
|
||||
// TODO add back undo
|
||||
// if (this.path) {
|
||||
// pg.undo.snapshot('line');
|
||||
// }
|
||||
}
|
||||
toleranceSquared () {
|
||||
return Math.pow(LineMode.SNAP_TOLERANCE / paper.view.zoom, 2);
|
||||
}
|
||||
findLineEnd (point, excludePath) {
|
||||
const lines = paper.project.getItems({
|
||||
class: paper.Path
|
||||
});
|
||||
// Prefer more recent lines
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].closed) {
|
||||
continue;
|
||||
}
|
||||
if (excludePath && lines[i] === excludePath) {
|
||||
continue;
|
||||
}
|
||||
if (lines[i].firstSegment &&
|
||||
lines[i].firstSegment.point.getDistance(point, true) < this.toleranceSquared()) {
|
||||
return {
|
||||
path: lines[i],
|
||||
segment: lines[i].firstSegment,
|
||||
isFirst: true
|
||||
};
|
||||
}
|
||||
if (lines[i].lastSegment && lines[i].lastSegment.point.getDistance(point, true) < this.toleranceSquared()) {
|
||||
return {
|
||||
path: lines[i],
|
||||
segment: lines[i].lastSegment,
|
||||
isFirst: false
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
deactivateTool () {
|
||||
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
|
||||
this.tool.remove();
|
||||
this.tool = null;
|
||||
this.hitResult = null;
|
||||
if (this.path) {
|
||||
this.path.setSelected(false);
|
||||
this.path = null;
|
||||
}
|
||||
}
|
||||
onScroll (event) {
|
||||
if (event.deltaY < 0) {
|
||||
this.props.changeLineWidth(this.props.lineModeState.lineWidth + 1);
|
||||
} else if (event.deltaY > 0 && this.props.lineModeState.lineWidth > 1) {
|
||||
this.props.changeLineWidth(this.props.lineModeState.lineWidth - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<LineModeComponent onMouseDown={this.props.handleMouseDown} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineMode.propTypes = {
|
||||
canvas: PropTypes.instanceOf(Element).isRequired,
|
||||
changeLineWidth: PropTypes.func.isRequired,
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isLineModeActive: PropTypes.bool.isRequired,
|
||||
lineModeState: PropTypes.shape({
|
||||
lineWidth: PropTypes.number.isRequired
|
||||
}),
|
||||
onUpdateSvg: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lineModeState: state.scratchPaint.lineMode,
|
||||
isLineModeActive: state.scratchPaint.mode === Modes.LINE
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
changeLineWidth: lineWidth => {
|
||||
dispatch(changeLineWidth(lineWidth));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.LINE));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LineMode);
|
|
@ -4,23 +4,50 @@ import PaintEditorComponent from '../components/paint-editor.jsx';
|
|||
import {changeMode} from '../reducers/modes';
|
||||
import Modes from '../modes/modes';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import paper from 'paper';
|
||||
|
||||
class PaintEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleUpdateSvg'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('keydown', this.props.onKeyPress);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||
}
|
||||
handleUpdateSvg () {
|
||||
const bounds = paper.project.activeLayer.bounds;
|
||||
this.props.onUpdateSvg(
|
||||
paper.project.exportSVG({
|
||||
asString: true,
|
||||
matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
|
||||
}),
|
||||
paper.project.view.center.x - bounds.x,
|
||||
paper.project.view.center.y - bounds.y);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<PaintEditorComponent />
|
||||
<PaintEditorComponent
|
||||
rotationCenterX={this.props.rotationCenterX}
|
||||
rotationCenterY={this.props.rotationCenterY}
|
||||
svg={this.props.svg}
|
||||
onUpdateSvg={this.handleUpdateSvg}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PaintEditor.propTypes = {
|
||||
onKeyPress: PropTypes.func.isRequired
|
||||
onKeyPress: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
svg: PropTypes.string
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -29,6 +56,8 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeMode(Modes.ERASER));
|
||||
} else if (event.key === 'b') {
|
||||
dispatch(changeMode(Modes.BRUSH));
|
||||
} else if (event.key === 'l') {
|
||||
dispatch(changeMode(Modes.LINE));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,27 +7,50 @@ class PaperCanvas extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'setCanvas'
|
||||
'setCanvas',
|
||||
'importSvg'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
paper.setup(this.canvas);
|
||||
// Create a Paper.js Path to draw a line into it:
|
||||
const path = new paper.Path();
|
||||
// Give the stroke a color
|
||||
path.strokeColor = 'black';
|
||||
const start = new paper.Point(100, 100);
|
||||
// Move to start and draw a line from there
|
||||
path.moveTo(start);
|
||||
// Note that the plus operator on Point objects does not work
|
||||
// in JavaScript. Instead, we need to call the add() function:
|
||||
path.lineTo(start.add([200, -50]));
|
||||
// Draw the view now:
|
||||
paper.view.draw();
|
||||
if (this.props.svg) {
|
||||
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
paper.project.activeLayer.removeChildren();
|
||||
this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
paper.remove();
|
||||
}
|
||||
importSvg (svg, rotationCenterX, rotationCenterY) {
|
||||
const imported = paper.project.importSVG(svg,
|
||||
{
|
||||
expandShapes: true,
|
||||
onLoad: function (item) {
|
||||
// Remove viewbox
|
||||
if (item.clipped) {
|
||||
item.clipped = false;
|
||||
// Consider removing clip mask here?
|
||||
}
|
||||
while (item.reduce() !== item) {
|
||||
item = item.reduce();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (typeof rotationCenterX !== 'undefined' && typeof rotationCenterY !== 'undefined') {
|
||||
imported.position =
|
||||
paper.project.view.center
|
||||
.add(imported.bounds.width / 2, imported.bounds.height / 2)
|
||||
.subtract(rotationCenterX, rotationCenterY);
|
||||
} else {
|
||||
// Center
|
||||
imported.position = paper.project.view.center;
|
||||
}
|
||||
|
||||
paper.project.view.update();
|
||||
}
|
||||
setCanvas (canvas) {
|
||||
this.canvas = canvas;
|
||||
if (this.props.canvasRef) {
|
||||
|
@ -44,7 +67,10 @@ class PaperCanvas extends React.Component {
|
|||
}
|
||||
|
||||
PaperCanvas.propTypes = {
|
||||
canvasRef: PropTypes.func
|
||||
canvasRef: PropTypes.func,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
svg: PropTypes.string
|
||||
};
|
||||
|
||||
export default PaperCanvas;
|
||||
|
|
|
@ -2,7 +2,8 @@ import keyMirror from 'keymirror';
|
|||
|
||||
const Modes = keyMirror({
|
||||
BRUSH: null,
|
||||
ERASER: null
|
||||
ERASER: null,
|
||||
LINE: null
|
||||
});
|
||||
|
||||
export default Modes;
|
||||
|
|
|
@ -13,10 +13,28 @@ const store = createStore(
|
|||
intlInitialState,
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
);
|
||||
const svgString =
|
||||
'<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' +
|
||||
' x="0px" y="0px" width="32px" height="32px" viewBox="0.5 384.5 32 32"' +
|
||||
' enable-background="new 0.5 384.5 32 32" xml:space="preserve">' +
|
||||
'<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M7.5,392.241h7.269' +
|
||||
'c4.571,0,8.231,5.555,8.231,10.123v7.377"/>' +
|
||||
'<polyline points="10.689,399.492 3.193,391.997 10.689,384.5 "/>' +
|
||||
'<polyline points="30.185,405.995 22.689,413.491 15.192,405.995 "/>' +
|
||||
'</svg>';
|
||||
const onUpdateSvg = function (newSvgString, rotationCenterX, rotationCenterY) {
|
||||
console.log(newSvgString);
|
||||
console.log(`rotationCenterX: ${rotationCenterX} rotationCenterY: ${rotationCenterY}`);
|
||||
};
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<IntlProvider>
|
||||
<PaintEditor />
|
||||
<PaintEditor
|
||||
rotationCenterX={0}
|
||||
rotationCenterY={0}
|
||||
svg={svgString}
|
||||
onUpdateSvg={onUpdateSvg}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</Provider>
|
||||
), appTarget);
|
||||
|
|
31
src/reducers/line-mode.js
Normal file
31
src/reducers/line-mode.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import log from '../log/log';
|
||||
|
||||
const CHANGE_LINE_WIDTH = 'scratch-paint/line-mode/CHANGE_LINE_WIDTH';
|
||||
const initialState = {lineWidth: 2};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case CHANGE_LINE_WIDTH:
|
||||
if (isNaN(action.lineWidth)) {
|
||||
log.warn(`Invalid line width: ${action.lineWidth}`);
|
||||
return state;
|
||||
}
|
||||
return {lineWidth: Math.max(1, action.lineWidth)};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
const changeLineWidth = function (lineWidth) {
|
||||
return {
|
||||
type: CHANGE_LINE_WIDTH,
|
||||
lineWidth: lineWidth
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
changeLineWidth
|
||||
};
|
|
@ -2,9 +2,11 @@ import {combineReducers} from 'redux';
|
|||
import modeReducer from './modes';
|
||||
import brushModeReducer from './brush-mode';
|
||||
import eraserModeReducer from './eraser-mode';
|
||||
import lineModeReducer from './line-mode';
|
||||
|
||||
export default combineReducers({
|
||||
mode: modeReducer,
|
||||
brushMode: brushModeReducer,
|
||||
eraserMode: eraserModeReducer
|
||||
eraserMode: eraserModeReducer,
|
||||
lineMode: lineModeReducer
|
||||
});
|
||||
|
|
15
test/unit/components/line-mode.test.jsx
Normal file
15
test/unit/components/line-mode.test.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* eslint-env jest */
|
||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import {shallow} from 'enzyme';
|
||||
import LineModeComponent from '../../../src/components/line-mode.jsx'; // eslint-disable-line no-unused-vars
|
||||
|
||||
describe('LineModeComponent', () => {
|
||||
test('triggers callback when clicked', () => {
|
||||
const onClick = jest.fn();
|
||||
const componentShallowWrapper = shallow(
|
||||
<LineModeComponent onMouseDown={onClick}/>
|
||||
);
|
||||
componentShallowWrapper.simulate('click');
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
31
test/unit/line-mode-reducer.test.js
Normal file
31
test/unit/line-mode-reducer.test.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* eslint-env jest */
|
||||
import lineReducer from '../../src/reducers/line-mode';
|
||||
import {changeLineWidth} from '../../src/reducers/line-mode';
|
||||
|
||||
test('initialState', () => {
|
||||
let defaultState;
|
||||
|
||||
expect(lineReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
||||
expect(lineReducer(defaultState /* state */, {type: 'anything'} /* action */).lineWidth).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('changeLineWidth', () => {
|
||||
let defaultState;
|
||||
const newLineWidth = 8078;
|
||||
|
||||
expect(lineReducer(defaultState /* state */, changeLineWidth(newLineWidth) /* action */))
|
||||
.toEqual({lineWidth: newLineWidth});
|
||||
expect(lineReducer(2 /* state */, changeLineWidth(newLineWidth) /* action */))
|
||||
.toEqual({lineWidth: newLineWidth});
|
||||
expect(lineReducer(2 /* state */, changeLineWidth(-1) /* action */))
|
||||
.toEqual({lineWidth: 1});
|
||||
});
|
||||
|
||||
test('invalidChangeLineWidth', () => {
|
||||
const origState = {lineWidth: 2};
|
||||
|
||||
expect(lineReducer(origState /* state */, changeLineWidth('invalid argument') /* action */))
|
||||
.toBe(origState);
|
||||
expect(lineReducer(origState /* state */, changeLineWidth() /* action */))
|
||||
.toBe(origState);
|
||||
});
|
|
@ -19,6 +19,6 @@ test('changeMode', () => {
|
|||
|
||||
test('invalidChangeMode', () => {
|
||||
expect(reducer(Modes.BRUSH /* state */, changeMode('non-existant mode') /* action */))
|
||||
.toBe(Modes.BRUSH);
|
||||
.toBe(Modes.BRUSH);
|
||||
expect(reducer(Modes.BRUSH /* state */, changeMode() /* action */)).toBe(Modes.BRUSH);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
const defaultsDeep = require('lodash.defaultsdeep');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
@ -53,12 +52,12 @@ const base = {
|
|||
}]
|
||||
},
|
||||
plugins: []
|
||||
.concat(process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
include: /\.min\.js$/,
|
||||
minimize: true
|
||||
})
|
||||
] : [])
|
||||
.concat(process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
include: /\.min\.js$/,
|
||||
minimize: true
|
||||
})
|
||||
] : [])
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
|
@ -96,7 +95,7 @@ module.exports = [
|
|||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'commonjs2',
|
||||
libraryTarget: 'commonjs2'
|
||||
}
|
||||
})
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue