Merge pull request #405 from fsih/lineTool

Add line tool
This commit is contained in:
DD Liu 2018-04-26 16:36:17 -04:00 committed by GitHub
commit af29e606d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 332 additions and 55 deletions

View file

@ -1,27 +1,25 @@
import React from 'react';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import PropTypes from 'prop-types';
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
import lineIcon from './line.svg';
const BitLineComponent = () => (
<ComingSoonTooltip
place="right"
tooltipId="bit-line-mode"
>
<ToolSelectComponent
disabled
imgDescriptor={{
defaultMessage: 'Line',
description: 'Label for the line tool, which draws straight line segments',
id: 'paint.lineMode.line'
}}
imgSrc={lineIcon}
isSelected={false}
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
/>
</ComingSoonTooltip>
const BitLineComponent = props => (
<ToolSelectComponent
imgDescriptor={{
defaultMessage: 'Line',
description: 'Label for the line tool, which draws straight line segments',
id: 'paint.lineMode.line'
}}
imgSrc={lineIcon}
isSelected={props.isSelected}
onMouseDown={props.onMouseDown}
/>
);
BitLineComponent.propTypes = {
isSelected: PropTypes.bool.isRequired,
onMouseDown: PropTypes.func.isRequired
};
export default BitLineComponent;

View file

@ -15,14 +15,15 @@ import InputGroup from '../input-group/input-group.jsx';
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import Modes from '../../lib/modes';
import Formats from '../../lib/format';
import {isBitmap} from '../../lib/format';
import {isBitmap, isVector} from '../../lib/format';
import styles from './mode-tools.css';
import copyIcon from './icons/copy.svg';
import pasteIcon from './icons/paste.svg';
import brushIcon from '../brush-mode/brush.svg';
import bitBrushIcon from '../bit-brush-mode/brush.svg';
import bitLineIcon from '../bit-line-mode/line.svg';
import brushIcon from '../brush-mode/brush.svg';
import curvedPointIcon from './icons/curved-point.svg';
import eraserIcon from '../eraser-mode/eraser.svg';
import flipHorizontalIcon from './icons/flip-horizontal.svg';
@ -39,6 +40,11 @@ const ModeToolsComponent = props => {
description: 'Label for the brush size input',
id: 'paint.modeTools.brushSize'
},
lineSize: {
defaultMessage: 'Line size',
description: 'Label for the line size input',
id: 'paint.modeTools.lineSize'
},
eraserSize: {
defaultMessage: 'Eraser size',
description: 'Label for the eraser size input',
@ -80,18 +86,22 @@ const ModeToolsComponent = props => {
case Modes.BRUSH:
/* falls through */
case Modes.BIT_BRUSH:
/* falls through */
case Modes.BIT_LINE:
{
const currentBrushIcon = isBitmap(props.format) ? bitBrushIcon : brushIcon;
const currentIcon = isVector(props.format) ? brushIcon :
props.mode === Modes.BIT_LINE ? bitLineIcon : bitBrushIcon;
const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue;
const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange;
const currentMessage = props.mode === Modes.BIT_LINE ? messages.lineSize : messages.brushSize;
return (
<div className={classNames(props.className, styles.modeTools)}>
<div>
<img
alt={props.intl.formatMessage(messages.brushSize)}
alt={props.intl.formatMessage(currentMessage)}
className={styles.modeToolsIcon}
draggable={false}
src={currentBrushIcon}
src={currentIcon}
/>
</div>
<LiveInput

View file

@ -11,7 +11,7 @@ import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
import BitLineMode from '../../components/bit-line-mode/bit-line-mode.jsx';
import BitLineMode from '../../containers/bit-line-mode.jsx';
import BitOvalMode from '../../components/bit-oval-mode/bit-oval-mode.jsx';
import BitRectMode from '../../components/bit-rect-mode/bit-rect-mode.jsx';
import BitTextMode from '../../components/bit-text-mode/bit-text-mode.jsx';
@ -410,7 +410,9 @@ const PaintEditorComponent = props => {
<BitBrushMode
onUpdateSvg={props.onUpdateSvg}
/>
<BitLineMode />
<BitLineMode
onUpdateSvg={props.onUpdateSvg}
/>
<BitOvalMode />
<BitRectMode />
<BitTextMode />

View file

@ -0,0 +1,107 @@
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
import {MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
import BitLineModeComponent from '../components/bit-line-mode/bit-line-mode.jsx';
import BitLineTool from '../helper/bit-tools/line-tool';
class BitLineMode extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'activateTool',
'deactivateTool'
]);
}
componentDidMount () {
if (this.props.isBitLineModeActive) {
this.activateTool(this.props);
}
}
componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.color !== this.props.color) {
this.tool.setColor(nextProps.color);
}
if (this.tool && nextProps.bitBrushSize !== this.props.bitBrushSize) {
this.tool.setLineSize(nextProps.bitBrushSize);
}
if (nextProps.isBitLineModeActive && !this.props.isBitLineModeActive) {
this.activateTool();
} else if (!nextProps.isBitLineModeActive && this.props.isBitLineModeActive) {
this.deactivateTool();
}
}
shouldComponentUpdate (nextProps) {
return nextProps.isBitLineModeActive !== this.props.isBitLineModeActive;
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
// Force the default line color if fill is MIXED or transparent
let color = this.props.color;
if (!color || color === MIXED) {
this.props.onChangeFillColor(DEFAULT_COLOR);
color = DEFAULT_COLOR;
}
this.tool = new BitLineTool(
this.props.onUpdateSvg
);
this.tool.setColor(color);
this.tool.setLineSize(this.props.bitBrushSize);
this.tool.activate();
}
deactivateTool () {
this.tool.deactivateTool();
this.tool.remove();
this.tool = null;
}
render () {
return (
<BitLineModeComponent
isSelected={this.props.isBitLineModeActive}
onMouseDown={this.props.handleMouseDown}
/>
);
}
}
BitLineMode.propTypes = {
bitBrushSize: PropTypes.number.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
isBitLineModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
bitBrushSize: state.scratchPaint.bitBrushSize,
color: state.scratchPaint.color.fillColor,
isBitLineModeActive: state.scratchPaint.mode === Modes.BIT_LINE
});
const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_LINE));
},
onChangeFillColor: fillColor => {
dispatch(changeFillColor(fillColor));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BitLineMode);

View file

@ -49,6 +49,7 @@ class PaintEditor extends React.Component {
'handleZoomReset',
'canRedo',
'canUndo',
'switchMode',
'onMouseDown',
'setCanvas',
'setTextArea',
@ -81,11 +82,9 @@ class PaintEditor extends React.Component {
this.stopEyeDroppingLoop();
}
// @todo move to correct corresponding tool
if (isVector(this.props.format) && isBitmap(prevProps.format)) {
this.props.changeMode(Modes.BRUSH);
} else if (isVector(prevProps.format) && isBitmap(this.props.format)) {
this.props.changeMode(Modes.BIT_BRUSH);
if ((isVector(this.props.format) && isBitmap(prevProps.format)) ||
(isVector(prevProps.format) && isBitmap(this.props.format))) {
this.switchMode(this.props.format);
}
}
componentWillUnmount () {
@ -94,6 +93,31 @@ class PaintEditor extends React.Component {
document.removeEventListener('mousedown', this.onMouseDown);
document.removeEventListener('touchstart', this.onMouseDown);
}
switchMode (newFormat) {
if (isVector(newFormat)) {
switch (this.props.mode) {
case Modes.BIT_BRUSH:
this.props.changeMode(Modes.BRUSH);
break;
case Modes.BIT_LINE:
this.props.changeMode(Modes.LINE);
break;
default:
this.props.changeMode(Modes.BRUSH);
}
} else if (isBitmap(newFormat)) {
switch (this.props.mode) {
case Modes.BRUSH:
this.props.changeMode(Modes.BIT_BRUSH);
break;
case Modes.LINE:
this.props.changeMode(Modes.BIT_LINE);
break;
default:
this.props.changeMode(Modes.BIT_BRUSH);
}
}
}
handleUpdateSvg (skipSnapshot) {
// Store the zoom/pan and restore it after snapshotting
// TODO Only doing this because snapshotting at zoom/pan makes export wrong
@ -300,6 +324,7 @@ PaintEditor.propTypes = {
handleSwitchToBitmap: PropTypes.func.isRequired,
handleSwitchToVector: PropTypes.func.isRequired,
isEyeDropping: PropTypes.bool,
mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
name: PropTypes.string,
onDeactivateEyeDropper: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
@ -331,6 +356,7 @@ const mapStateToProps = state => ({
clipboardItems: state.scratchPaint.clipboard.items,
format: state.scratchPaint.format,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
mode: state.scratchPaint.mode,
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
selectedItems: state.scratchPaint.selectedItems,

View file

@ -1,6 +1,6 @@
import paper from '@scratch/paper';
import {getRaster} from '../layer';
import {forEachLinePoint, fillEllipse} from '../bitmap';
import {forEachLinePoint, getBrushMark} from '../bitmap';
import {getGuideLayer} from '../layer';
/**
@ -42,6 +42,7 @@ class BrushTool extends paper.Tool {
if (!this.size) {
return;
}
// The cursor preview was unattached from the view by an outside process,
// such as changing costumes or undo.
if (this.cursorPreview && !this.cursorPreview.parent) {
@ -53,30 +54,13 @@ class BrushTool extends paper.Tool {
this.cursorPreview.remove();
}
this.tmpCanvas = document.createElement('canvas');
const roundedUpRadius = Math.ceil(this.size / 2);
this.tmpCanvas.width = roundedUpRadius * 2;
this.tmpCanvas.height = roundedUpRadius * 2;
const context = this.tmpCanvas.getContext('2d');
context.imageSmoothingEnabled = false;
context.fillStyle = this.color;
// Small squares for pixel artists
if (this.size <= 5) {
if (this.size % 2) {
context.fillRect(1, 1, this.size, this.size);
} else {
context.fillRect(0, 0, this.size, this.size);
}
} else {
const roundedDownRadius = ~~(this.size / 2);
fillEllipse(roundedDownRadius, roundedDownRadius, roundedDownRadius, roundedDownRadius, context);
}
this.tmpCanvas = getBrushMark(this.size, this.color);
this.cursorPreview = new paper.Raster(this.tmpCanvas);
this.cursorPreview.guide = true;
this.cursorPreview.parent = getGuideLayer();
this.cursorPreview.data.isHelperItem = true;
}
this.lastSize = this.size;
this.lastColor = this.color;
}
@ -96,10 +80,6 @@ class BrushTool extends paper.Tool {
handleMouseDrag (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
if (this.isBoundingBoxMode) {
this.boundingBoxTool.onMouseDrag(event);
return;
}
forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this));
this.lastPoint = event.point;
}

View file

@ -0,0 +1,124 @@
import paper from '@scratch/paper';
import {getRaster} from '../layer';
import {forEachLinePoint, getBrushMark} from '../bitmap';
import {getGuideLayer} from '../layer';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view';
/**
* Tool for drawing lines with the bitmap brush.
*/
class LineTool extends paper.Tool {
/**
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
*/
constructor (onUpdateSvg) {
super();
this.onUpdateSvg = onUpdateSvg;
// We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions.
this.onMouseMove = this.handleMouseMove;
this.onMouseDown = this.handleMouseDown;
this.onMouseDrag = this.handleMouseDrag;
this.onMouseUp = this.handleMouseUp;
this.colorState = null;
this.active = false;
this.startPoint = null;
this.cursorPreview = null;
// Raster to which to draw
this.drawTarget = null;
}
setColor (color) {
this.color = color;
}
setLineSize (size) {
// For performance, make sure this is an integer
this.size = Math.max(1, ~~size);
}
// Draw a brush mark at the given point
draw (x, y) {
const roundedUpRadius = Math.ceil(this.size / 2);
this.drawTarget.drawImage(this.tmpCanvas, new paper.Point(~~x - roundedUpRadius, ~~y - roundedUpRadius));
}
updateCursorIfNeeded () {
if (!this.size) {
return;
}
// The cursor preview was unattached from the view by an outside process,
// such as changing costumes or undo.
if (this.cursorPreview && !this.cursorPreview.parent) {
this.cursorPreview = null;
}
if (!this.cursorPreview || !(this.lastSize === this.size && this.lastColor === this.color)) {
if (this.cursorPreview) {
this.cursorPreview.remove();
}
this.tmpCanvas = getBrushMark(this.size, this.color);
this.cursorPreview = new paper.Raster(this.tmpCanvas);
this.cursorPreview.guide = true;
this.cursorPreview.parent = getGuideLayer();
this.cursorPreview.data.isHelperItem = true;
}
this.lastSize = this.size;
this.lastColor = this.color;
}
handleMouseMove (event) {
this.updateCursorIfNeeded();
this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y);
}
handleMouseDown (event) {
if (event.event.button > 0) return; // only first mouse button
this.active = true;
this.cursorPreview.remove();
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = ART_BOARD_WIDTH;
tmpCanvas.height = ART_BOARD_HEIGHT;
this.drawTarget = new paper.Raster(tmpCanvas);
this.drawTarget.parent = getGuideLayer();
this.drawTarget.guide = true;
this.drawTarget.locked = true;
this.drawTarget.position = getRaster().position;
this.draw(event.point.x, event.point.y);
this.startPoint = event.point;
}
handleMouseDrag (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
// Clear
const context = this.drawTarget.canvas.getContext('2d');
context.clearRect(0, 0, ART_BOARD_WIDTH, ART_BOARD_HEIGHT);
forEachLinePoint(this.startPoint, event.point, this.draw.bind(this));
}
handleMouseUp (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
this.drawTarget.remove();
this.drawTarget = getRaster();
forEachLinePoint(this.startPoint, event.point, this.draw.bind(this));
this.drawTarget = null;
this.onUpdateSvg();
this.lastPoint = null;
this.active = false;
this.updateCursorIfNeeded();
this.cursorPreview.position = new paper.Point(~~event.point.x, ~~event.point.y);
}
deactivateTool () {
this.active = false;
this.tmpCanvas = null;
if (this.cursorPreview) {
this.cursorPreview.remove();
this.cursorPreview = null;
}
}
}
export default LineTool;

View file

@ -81,6 +81,34 @@ const fillEllipse = function (centerX, centerY, radiusX, radiusY, context) {
}
};
/**
* @param {!number} size The diameter of the brush
* @param {!string} color The css color of the brush
* @return {HTMLCanvasElement} a canvas with the brush mark printed on it
*/
const getBrushMark = function (size, color) {
size = ~~size;
const canvas = document.createElement('canvas');
const roundedUpRadius = Math.ceil(size / 2);
canvas.width = roundedUpRadius * 2;
canvas.height = roundedUpRadius * 2;
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;
context.fillStyle = color;
// Small squares for pixel artists
if (size <= 5) {
if (size % 2) {
context.fillRect(1, 1, size, size);
} else {
context.fillRect(0, 0, size, size);
}
} else {
const roundedDownRadius = ~~(size / 2);
fillEllipse(roundedDownRadius, roundedDownRadius, roundedDownRadius, roundedDownRadius, context);
}
return canvas;
};
const rowBlank_ = function (imageData, width, y) {
for (let x = 0; x < width; ++x) {
if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false;
@ -114,6 +142,7 @@ const trim = function (raster) {
};
export {
getBrushMark,
fillEllipse,
forEachLinePoint,
trim

View file

@ -2,6 +2,7 @@ import keyMirror from 'keymirror';
const Modes = keyMirror({
BIT_BRUSH: null,
BIT_LINE: null,
BRUSH: null,
ERASER: null,
LINE: null,