Bitmap eraser tool (#507)

This commit is contained in:
DD Liu 2018-06-14 10:35:02 -04:00 committed by GitHub
parent 689d4fb0a7
commit 4cadcb3da3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 29 deletions

View file

@ -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 = () => (
<ComingSoonTooltip
place="right"
tooltipId="bit-eraser-mode"
>
<ToolSelectComponent
disabled
imgDescriptor={{
defaultMessage: 'Eraser',
description: 'Label for the eraser tool',
id: 'paint.eraserMode.eraser'
}}
imgSrc={eraserIcon}
isSelected={false}
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
/>
</ComingSoonTooltip>
const BitEraserComponent = props => (
<ToolSelectComponent
imgDescriptor={{
defaultMessage: 'Eraser',
description: 'Label for the eraser tool',
id: 'paint.eraserMode.eraser'
}}
imgSrc={eraserIcon}
isSelected={props.isSelected}
onMouseDown={props.onMouseDown}
/>
);
BitEraserComponent.propTypes = {
isSelected: PropTypes.bool.isRequired,
onMouseDown: PropTypes.func.isRequired
};
export default BitEraserComponent;

View file

@ -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 => {
</div>
);
}
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 (
<div className={classNames(props.className, styles.modeTools)}>
<div>
@ -125,7 +133,7 @@ const ModeToolsComponent = props => {
alt={props.intl.formatMessage(messages.eraserSize)}
className={styles.modeToolsIcon}
draggable={false}
src={eraserIcon}
src={currentIcon}
/>
</div>
<LiveInput
@ -134,11 +142,12 @@ const ModeToolsComponent = props => {
max={MAX_STROKE_WIDTH}
min="1"
type="number"
value={props.eraserValue}
onSubmit={props.onEraserSliderChange}
value={currentEraserValue}
onSubmit={changeFunction}
/>
</div>
);
}
case Modes.RESHAPE:
return (
<div className={classNames(props.className, styles.modeTools)}>
@ -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));
}

View file

@ -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 => (
/>
<BitTextMode />
<BitFillMode />
<BitEraserMode />
<BitEraserMode
onUpdateImage={props.onUpdateImage}
/>
<BitSelectMode />
</div>
) : null}

View file

@ -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 (
<BitEraserModeComponent
isSelected={this.props.isBitEraserModeActive}
onMouseDown={this.props.handleMouseDown}
/>
);
}
}
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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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) {

View file

@ -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 {

View file

@ -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
};

View file

@ -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,