mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Bitmap eraser tool (#507)
This commit is contained in:
parent
689d4fb0a7
commit
4cadcb3da3
10 changed files with 186 additions and 29 deletions
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
90
src/containers/bit-eraser-mode.jsx
Normal file
90
src/containers/bit-eraser-mode.jsx
Normal 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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
31
src/reducers/bit-eraser-size.js
Normal file
31
src/reducers/bit-eraser-size.js
Normal 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
|
||||
};
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue