From 4cadcb3da37ea8d1efa857517985eefa912e6d71 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 14 Jun 2018 10:35:02 -0400 Subject: [PATCH] Bitmap eraser tool (#507) --- .../bit-eraser-mode/bit-eraser-mode.jsx | 35 ++++---- src/components/mode-tools/mode-tools.jsx | 20 ++++- src/components/paint-editor/paint-editor.jsx | 6 +- src/containers/bit-eraser-mode.jsx | 90 +++++++++++++++++++ src/containers/paint-editor.jsx | 6 ++ src/helper/bit-tools/brush-tool.js | 15 +++- src/helper/bitmap.js | 6 +- src/lib/modes.js | 4 +- src/reducers/bit-eraser-size.js | 31 +++++++ src/reducers/scratch-paint-reducer.js | 2 + 10 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 src/containers/bit-eraser-mode.jsx create mode 100644 src/reducers/bit-eraser-size.js diff --git a/src/components/bit-eraser-mode/bit-eraser-mode.jsx b/src/components/bit-eraser-mode/bit-eraser-mode.jsx index ef367fff..c018a04e 100644 --- a/src/components/bit-eraser-mode/bit-eraser-mode.jsx +++ b/src/components/bit-eraser-mode/bit-eraser-mode.jsx @@ -1,27 +1,26 @@ import React from 'react'; +import PropTypes from 'prop-types'; -import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx'; import eraserIcon from './eraser.svg'; -const BitEraserComponent = () => ( - - - +const BitEraserComponent = props => ( + ); +BitEraserComponent.propTypes = { + isSelected: PropTypes.bool.isRequired, + onMouseDown: PropTypes.func.isRequired +}; + export default BitEraserComponent; diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index 8ccaa600..e0edbf53 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -7,6 +7,7 @@ import React from 'react'; import {changeBrushSize} from '../../reducers/brush-mode'; import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode'; import {changeBitBrushSize} from '../../reducers/bit-brush-size'; +import {changeBitEraserSize} from '../../reducers/bit-eraser-size'; import FontDropdown from '../../containers/font-dropdown.jsx'; import LiveInputHOC from '../forms/live-input-hoc.jsx'; @@ -23,6 +24,7 @@ import copyIcon from './icons/copy.svg'; import pasteIcon from './icons/paste.svg'; import bitBrushIcon from '../bit-brush-mode/brush.svg'; +import bitEraserIcon from '../bit-eraser-mode/eraser.svg'; import bitLineIcon from '../bit-line-mode/line.svg'; import brushIcon from '../brush-mode/brush.svg'; import curvedPointIcon from './icons/curved-point.svg'; @@ -117,7 +119,13 @@ const ModeToolsComponent = props => { ); } + case Modes.BIT_ERASER: + /* falls through */ case Modes.ERASER: + { + const currentIcon = isVector(props.format) ? eraserIcon : bitEraserIcon; + const currentEraserValue = isBitmap(props.format) ? props.bitEraserSize : props.eraserValue; + const changeFunction = isBitmap(props.format) ? props.onBitEraserSliderChange : props.onEraserSliderChange; return (
@@ -125,7 +133,7 @@ const ModeToolsComponent = props => { alt={props.intl.formatMessage(messages.eraserSize)} className={styles.modeToolsIcon} draggable={false} - src={eraserIcon} + src={currentIcon} />
{ max={MAX_STROKE_WIDTH} min="1" type="number" - value={props.eraserValue} - onSubmit={props.onEraserSliderChange} + value={currentEraserValue} + onSubmit={changeFunction} />
); + } case Modes.RESHAPE: return (
@@ -207,6 +216,7 @@ const ModeToolsComponent = props => { ModeToolsComponent.propTypes = { bitBrushSize: PropTypes.number, + bitEraserSize: PropTypes.number, brushValue: PropTypes.number, className: PropTypes.string, clipboardItems: PropTypes.arrayOf(PropTypes.array), @@ -233,6 +243,7 @@ const mapStateToProps = state => ({ mode: state.scratchPaint.mode, format: state.scratchPaint.format, bitBrushSize: state.scratchPaint.bitBrushSize, + bitEraserSize: state.scratchPaint.bitEraserSize, brushValue: state.scratchPaint.brushMode.brushSize, clipboardItems: state.scratchPaint.clipboard.items, eraserValue: state.scratchPaint.eraserMode.brushSize, @@ -245,6 +256,9 @@ const mapDispatchToProps = dispatch => ({ onBitBrushSliderChange: bitBrushSize => { dispatch(changeBitBrushSize(bitBrushSize)); }, + onBitEraserSliderChange: eraserSize => { + dispatch(changeBitEraserSize(eraserSize)); + }, onEraserSliderChange: eraserSize => { dispatch(changeEraserSize(eraserSize)); } diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index f2ad64f6..2eef5d98 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -12,7 +12,7 @@ import BitOvalMode from '../../components/bit-oval-mode/bit-oval-mode.jsx'; import BitRectMode from '../../containers/bit-rect-mode.jsx'; import BitTextMode from '../../components/bit-text-mode/bit-text-mode.jsx'; import BitFillMode from '../../components/bit-fill-mode/bit-fill-mode.jsx'; -import BitEraserMode from '../../components/bit-eraser-mode/bit-eraser-mode.jsx'; +import BitEraserMode from '../../containers/bit-eraser-mode.jsx'; import BitSelectMode from '../../components/bit-select-mode/bit-select-mode.jsx'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; @@ -182,7 +182,9 @@ const PaintEditorComponent = props => ( /> - +
) : null} diff --git a/src/containers/bit-eraser-mode.jsx b/src/containers/bit-eraser-mode.jsx new file mode 100644 index 00000000..8ea97e9d --- /dev/null +++ b/src/containers/bit-eraser-mode.jsx @@ -0,0 +1,90 @@ +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 {changeMode} from '../reducers/modes'; +import {clearSelectedItems} from '../reducers/selected-items'; +import {clearSelection} from '../helper/selection'; + +import BitEraserModeComponent from '../components/bit-eraser-mode/bit-eraser-mode.jsx'; +import BitBrushTool from '../helper/bit-tools/brush-tool'; + +class BitEraserMode extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool' + ]); + } + componentDidMount () { + if (this.props.isBitEraserModeActive) { + this.activateTool(this.props); + } + } + componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.bitEraserSize !== this.props.bitEraserSize) { + this.tool.setBrushSize(nextProps.bitEraserSize); + } + + if (nextProps.isBitEraserModeActive && !this.props.isBitEraserModeActive) { + this.activateTool(); + } else if (!nextProps.isBitEraserModeActive && this.props.isBitEraserModeActive) { + this.deactivateTool(); + } + } + shouldComponentUpdate (nextProps) { + return nextProps.isBitEraserModeActive !== this.props.isBitEraserModeActive; + } + activateTool () { + clearSelection(this.props.clearSelectedItems); + this.tool = new BitBrushTool( + this.props.onUpdateImage, + true /* isEraser */ + ); + this.tool.setBrushSize(this.props.bitEraserSize); + + this.tool.activate(); + } + deactivateTool () { + this.tool.deactivateTool(); + this.tool.remove(); + this.tool = null; + } + render () { + return ( + + ); + } +} + +BitEraserMode.propTypes = { + bitEraserSize: PropTypes.number.isRequired, + clearSelectedItems: PropTypes.func.isRequired, + handleMouseDown: PropTypes.func.isRequired, + isBitEraserModeActive: PropTypes.bool.isRequired, + onUpdateImage: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + bitEraserSize: state.scratchPaint.bitEraserSize, + isBitEraserModeActive: state.scratchPaint.mode === Modes.BIT_ERASER +}); +const mapDispatchToProps = dispatch => ({ + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + handleMouseDown: () => { + dispatch(changeMode(Modes.BIT_ERASER)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BitEraserMode); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index c5b6745e..f32879a3 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -124,6 +124,9 @@ class PaintEditor extends React.Component { case Modes.BIT_RECT: this.props.changeMode(Modes.RECT); break; + case Modes.BIT_ERASER: + this.props.changeMode(Modes.ERASER); + break; default: this.props.changeMode(Modes.BRUSH); } @@ -138,6 +141,9 @@ class PaintEditor extends React.Component { case Modes.RECT: this.props.changeMode(Modes.BIT_RECT); break; + case Modes.ERASER: + this.props.changeMode(Modes.BIT_ERASER); + break; default: this.props.changeMode(Modes.BIT_BRUSH); } diff --git a/src/helper/bit-tools/brush-tool.js b/src/helper/bit-tools/brush-tool.js index 5417e686..2da576aa 100644 --- a/src/helper/bit-tools/brush-tool.js +++ b/src/helper/bit-tools/brush-tool.js @@ -4,15 +4,17 @@ import {forEachLinePoint, getBrushMark} from '../bitmap'; import {getGuideLayer} from '../layer'; /** - * Tool for drawing with the bitmap brush. + * Tool for drawing with the bitmap brush and eraser */ class BrushTool extends paper.Tool { /** * @param {!function} onUpdateImage A callback to call when the image visibly changes + * @param {boolean} isEraser True if brush should erase */ - constructor (onUpdateImage) { + constructor (onUpdateImage, isEraser) { super(); this.onUpdateImage = onUpdateImage; + this.isEraser = isEraser; // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -39,7 +41,14 @@ class BrushTool extends paper.Tool { this.tmpCanvas = getBrushMark(this.size, this.color); } const roundedUpRadius = Math.ceil(this.size / 2); + const context = getRaster().getContext('2d'); + if (this.isEraser) { + context.globalCompositeOperation = 'destination-out'; + } getRaster().drawImage(this.tmpCanvas, new paper.Point(~~x - roundedUpRadius, ~~y - roundedUpRadius)); + if (this.isEraser) { + context.globalCompositeOperation = 'source-over'; + } } updateCursorIfNeeded () { if (!this.size) { @@ -57,7 +66,7 @@ class BrushTool extends paper.Tool { this.cursorPreview.remove(); } - this.tmpCanvas = getBrushMark(this.size, this.color); + this.tmpCanvas = getBrushMark(this.size, this.color, this.isEraser); this.cursorPreview = new paper.Raster(this.tmpCanvas); this.cursorPreview.guide = true; this.cursorPreview.parent = getGuideLayer(); diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index f4eca707..5603e6f9 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -86,9 +86,10 @@ 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 + * @param {?boolean} isEraser True if we want the brush mark for the eraser * @return {HTMLCanvasElement} a canvas with the brush mark printed on it */ -const getBrushMark = function (size, color) { +const getBrushMark = function (size, color, isEraser) { size = ~~size; const canvas = document.createElement('canvas'); const roundedUpRadius = Math.ceil(size / 2); @@ -96,7 +97,8 @@ const getBrushMark = function (size, color) { canvas.height = roundedUpRadius * 2; const context = canvas.getContext('2d'); context.imageSmoothingEnabled = false; - context.fillStyle = color; + context.fillStyle = isEraser ? 'white' : color; + // @todo add outline for erasers // Small squares for pixel artists if (size <= 5) { if (size % 2) { diff --git a/src/lib/modes.js b/src/lib/modes.js index e0e25b2a..83041da0 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -4,6 +4,7 @@ const Modes = keyMirror({ BIT_BRUSH: null, BIT_LINE: null, BIT_RECT: null, + BIT_ERASER: null, BRUSH: null, ERASER: null, LINE: null, @@ -19,7 +20,8 @@ const Modes = keyMirror({ const BitmapModes = keyMirror({ BIT_BRUSH: null, BIT_LINE: null, - BIT_RECT: null + BIT_RECT: null, + BIT_ERASER: null }); export { diff --git a/src/reducers/bit-eraser-size.js b/src/reducers/bit-eraser-size.js new file mode 100644 index 00000000..2d436a4e --- /dev/null +++ b/src/reducers/bit-eraser-size.js @@ -0,0 +1,31 @@ +import log from '../log/log'; + +const CHANGE_BIT_ERASER_SIZE = 'scratch-paint/eraser-mode/CHANGE_BIT_ERASER_SIZE'; +const initialState = 40; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_BIT_ERASER_SIZE: + if (isNaN(action.eraserSize)) { + log.warn(`Invalid eraser size: ${action.eraserSize}`); + return state; + } + return Math.max(1, action.eraserSize); + default: + return state; + } +}; + +// Action creators ================================== +const changeBitEraserSize = function (eraserSize) { + return { + type: CHANGE_BIT_ERASER_SIZE, + eraserSize: eraserSize + }; +}; + +export { + reducer as default, + changeBitEraserSize +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 31dcb476..3bb7c115 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -1,6 +1,7 @@ import {combineReducers} from 'redux'; import modeReducer from './modes'; import bitBrushSizeReducer from './bit-brush-size'; +import bitEraserSizeReducer from './bit-eraser-size'; import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; @@ -17,6 +18,7 @@ import undoReducer from './undo'; export default combineReducers({ mode: modeReducer, bitBrushSize: bitBrushSizeReducer, + bitEraserSize: bitEraserSizeReducer, brushMode: brushModeReducer, color: colorReducer, clipboard: clipboardReducer,