Merge pull request #387 from fsih/raster

Bitmap brush
This commit is contained in:
DD Liu 2018-04-20 11:05:44 -04:00 committed by GitHub
commit 99534eb24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 493 additions and 38 deletions

View file

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
import brushIcon from './brush.svg';
const BitBrushModeComponent = props => (
<ToolSelectComponent
imgDescriptor={{
defaultMessage: 'Brush',
description: 'Label for the brush tool',
id: 'paint.brushMode.brush'
}}
imgSrc={brushIcon}
isSelected={props.isSelected}
onMouseDown={props.onMouseDown}
/>
);
BitBrushModeComponent.propTypes = {
isSelected: PropTypes.bool.isRequired,
onMouseDown: PropTypes.func.isRequired
};
export default BitBrushModeComponent;

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>brush</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="brush" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M9.50062663,11.4981734 L10.4995734,11.4981734 L10.4995734,14.4990134 L9.49962669,14.4990134 L9.49962669,11.4991734 L6.49978668,11.4991734 L6.49978668,10.4992267 L9.50062663,10.4992267 L9.50062663,11.4981734 Z M13.4992134,3.5 L16.5000533,3.5 L16.5000533,4.49994667 L17.5,4.49994667 L17.5,7.49978668 L16.5000533,7.49978668 L16.5000533,8.4987334 L15.4991067,8.4987334 L15.4991067,9.49868007 L14.49916,9.49868007 L14.49916,10.4986267 L12.4992667,10.4986267 L12.4992667,11.4985734 L11.49932,11.4985734 L11.49932,10.4986267 L10.4993734,10.4986267 L10.4993734,9.49868007 L9.4994267,9.49868007 L9.4994267,7.49978668 L10.4993734,7.49978668 L10.4993734,6.49984001 L11.49932,6.49984001 L11.49932,5.49989334 L12.4992667,5.49989334 L12.4992667,4.49994667 L13.4992134,4.49994667 L13.4992134,3.5 Z M5.49954002,11.4987734 L6.49948669,11.4987734 L6.49948669,12.49972 L7.50043331,12.49972 L7.50043331,13.4996667 L8.50037998,13.4996667 L8.50037998,14.4996134 L9.50032665,14.4996134 L9.50032665,15.49956 L8.50037998,15.49956 L8.50037998,16.4985067 L4.49959336,16.4985067 L4.49959336,15.49956 L3.49964669,15.49956 L3.49964669,13.4996667 L5.49954002,13.4996667 L5.49954002,11.4987734 Z M2.5,13.4990667 L2.5,12.49912 L3.49994667,12.49912 L3.49994667,13.4990667 L2.5,13.4990667 Z" id="Combined-Shape" fill="#575E75"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -6,6 +6,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 LiveInputHOC from '../forms/live-input-hoc.jsx';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
@ -13,12 +14,15 @@ import Input from '../forms/input.jsx';
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 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 curvedPointIcon from './icons/curved-point.svg';
import eraserIcon from '../eraser-mode/eraser.svg';
import flipHorizontalIcon from './icons/flip-horizontal.svg';
@ -74,6 +78,12 @@ const ModeToolsComponent = props => {
switch (props.mode) {
case Modes.BRUSH:
/* falls through */
case Modes.BIT_BRUSH:
{
const currentBrushIcon = isBitmap(props.format) ? bitBrushIcon : brushIcon;
const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue;
const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange;
return (
<div className={classNames(props.className, styles.modeTools)}>
<div>
@ -81,7 +91,7 @@ const ModeToolsComponent = props => {
alt={props.intl.formatMessage(messages.brushSize)}
className={styles.modeToolsIcon}
draggable={false}
src={brushIcon}
src={currentBrushIcon}
/>
</div>
<LiveInput
@ -90,11 +100,12 @@ const ModeToolsComponent = props => {
max={MAX_STROKE_WIDTH}
min="1"
type="number"
value={props.brushValue}
onSubmit={props.onBrushSliderChange}
value={currentBrushValue}
onSubmit={changeFunction}
/>
</div>
);
}
case Modes.ERASER:
return (
<div className={classNames(props.className, styles.modeTools)}>
@ -174,15 +185,18 @@ const ModeToolsComponent = props => {
};
ModeToolsComponent.propTypes = {
bitBrushSize: PropTypes.number,
brushValue: PropTypes.number,
className: PropTypes.string,
clipboardItems: PropTypes.arrayOf(PropTypes.array),
eraserValue: PropTypes.number,
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
hasSelectedUncurvedPoints: PropTypes.bool,
hasSelectedUnpointedPoints: PropTypes.bool,
intl: intlShape.isRequired,
mode: PropTypes.string.isRequired,
onBrushSliderChange: PropTypes.func,
onBitBrushSliderChange: PropTypes.func.isRequired,
onBrushSliderChange: PropTypes.func.isRequired,
onCopyToClipboard: PropTypes.func.isRequired,
onCurvePoints: PropTypes.func.isRequired,
onEraserSliderChange: PropTypes.func,
@ -195,6 +209,8 @@ ModeToolsComponent.propTypes = {
const mapStateToProps = state => ({
mode: state.scratchPaint.mode,
format: state.scratchPaint.format,
bitBrushSize: state.scratchPaint.bitBrushSize,
brushValue: state.scratchPaint.brushMode.brushSize,
clipboardItems: state.scratchPaint.clipboard.items,
eraserValue: state.scratchPaint.eraserMode.brushSize,
@ -204,6 +220,9 @@ const mapDispatchToProps = dispatch => ({
onBrushSliderChange: brushSize => {
dispatch(changeBrushSize(brushSize));
},
onBitBrushSliderChange: bitBrushSize => {
dispatch(changeBitBrushSize(bitBrushSize));
},
onEraserSliderChange: eraserSize => {
dispatch(changeEraserSize(eraserSize));
}

View file

@ -10,6 +10,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
import Box from '../box/box.jsx';
import Button from '../button/button.jsx';
import ButtonGroup from '../button-group/button-group.jsx';
@ -35,7 +36,7 @@ import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicat
import TextMode from '../../containers/text-mode.jsx';
import Formats from '../../lib/format';
import {isVector} from '../../lib/format';
import {isBitmap, isVector} from '../../lib/format';
import layout from '../../lib/layout-constants';
import styles from './paint-editor.css';
@ -309,34 +310,56 @@ const PaintEditorComponent = props => {
</div>
{/* Second Row */}
<div className={styles.row}>
<InputGroup
className={classNames(
styles.row,
styles.modDashedBorder,
styles.modLabeledIconHeight
)}
>
{/* fill */}
<FillColorIndicatorComponent
className={styles.modMarginRight}
onUpdateSvg={props.onUpdateSvg}
/>
{/* stroke */}
<StrokeColorIndicatorComponent
onUpdateSvg={props.onUpdateSvg}
/>
{/* stroke width */}
<StrokeWidthIndicatorComponent
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
<InputGroup className={styles.modModeTools}>
<ModeToolsContainer
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
</div>
{isVector(props.format) ?
<div className={styles.row}>
<InputGroup
className={classNames(
styles.row,
styles.modDashedBorder,
styles.modLabeledIconHeight
)}
>
{/* fill */}
<FillColorIndicatorComponent
className={styles.modMarginRight}
onUpdateSvg={props.onUpdateSvg}
/>
{/* stroke */}
<StrokeColorIndicatorComponent
onUpdateSvg={props.onUpdateSvg}
/>
{/* stroke width */}
<StrokeWidthIndicatorComponent
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
<InputGroup className={styles.modModeTools}>
<ModeToolsContainer
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
</div> :
<div className={styles.row}>
<InputGroup
className={classNames(
styles.row,
styles.modDashedBorder,
styles.modLabeledIconHeight
)}
>
{/* fill */}
<FillColorIndicatorComponent
className={styles.modMarginRight}
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
<InputGroup className={styles.modModeTools}>
<ModeToolsContainer
onUpdateSvg={props.onUpdateSvg}
/>
</InputGroup>
</div>
}
</div>
) : null}
@ -375,6 +398,14 @@ const PaintEditorComponent = props => {
</div>
) : null}
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
<div className={isBitmap(props.format) ? styles.modeSelector : styles.hidden}>
<BitBrushMode
onUpdateSvg={props.onUpdateSvg}
/>
</div>
) : null}
<div>
{/* Canvas */}
<div

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 BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx';
import BitBrushTool from '../helper/bit-tools/brush-tool';
class BitBrushMode extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'activateTool',
'deactivateTool'
]);
}
componentDidMount () {
if (this.props.isBitBrushModeActive) {
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.setBrushSize(nextProps.bitBrushSize);
}
if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) {
this.activateTool();
} else if (!nextProps.isBitBrushModeActive && this.props.isBitBrushModeActive) {
this.deactivateTool();
}
}
shouldComponentUpdate (nextProps) {
return nextProps.isBitBrushModeActive !== this.props.isBitBrushModeActive;
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
// Force the default brush 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 BitBrushTool(
this.props.onUpdateSvg
);
this.tool.setColor(color);
this.tool.setBrushSize(this.props.bitBrushSize);
this.tool.activate();
}
deactivateTool () {
this.tool.deactivateTool();
this.tool.remove();
this.tool = null;
}
render () {
return (
<BitBrushModeComponent
isSelected={this.props.isBitBrushModeActive}
onMouseDown={this.props.handleMouseDown}
/>
);
}
}
BitBrushMode.propTypes = {
bitBrushSize: PropTypes.number.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
isBitBrushModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onUpdateSvg: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
bitBrushSize: state.scratchPaint.bitBrushSize,
color: state.scratchPaint.color.fillColor,
isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH
});
const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_BRUSH));
},
onChangeFillColor: fillColor => {
dispatch(changeFillColor(fillColor));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BitBrushMode);

View file

@ -7,7 +7,6 @@ import Blobbiness from '../helper/blob-tools/blob';
import {MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeBrushSize} from '../reducers/brush-mode';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
@ -98,9 +97,6 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
changeBrushSize: brushSize => {
dispatch(changeBrushSize(brushSize));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BRUSH));
},

View file

@ -23,7 +23,7 @@ import EyeDropperTool from '../helper/tools/eye-dropper';
import Modes from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
import {isBitmap, isVector} from '../lib/format';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
@ -79,6 +79,13 @@ class PaintEditor extends React.Component {
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
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);
}
}
componentWillUnmount () {
document.removeEventListener('keydown', this.props.onKeyPress);
@ -280,6 +287,7 @@ class PaintEditor extends React.Component {
PaintEditor.propTypes = {
changeColorToEyeDropper: PropTypes.func,
changeMode: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
handleSwitchToBitmap: PropTypes.func.isRequired,
@ -344,6 +352,9 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeMode(Modes.RECT));
}
},
changeMode: mode => {
dispatch(changeMode(mode));
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},

View file

@ -0,0 +1,128 @@
import paper from '@scratch/paper';
import {getRaster} from '../layer';
import {forEachLinePoint, fillEllipse} from '../bitmap';
import {getGuideLayer} from '../layer';
/**
* Tool for drawing with the bitmap brush.
*/
class BrushTool 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.lastPoint = null;
this.cursorPreview = null;
}
setColor (color) {
this.color = color;
}
setBrushSize (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);
getRaster().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 = 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.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();
this.draw(event.point.x, event.point.y);
this.lastPoint = event.point;
}
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;
}
handleMouseUp (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
forEachLinePoint(this.lastPoint, event.point, this.draw.bind(this));
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 BrushTool;

View file

@ -1,5 +1,86 @@
import paper from '@scratch/paper';
const forEachLinePoint = function (point1, point2, callback) {
// Bresenham line algorithm
let x1 = ~~point1.x;
const x2 = ~~point2.x;
let y1 = ~~point1.y;
const y2 = ~~point2.y;
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
const sx = (x1 < x2) ? 1 : -1;
const sy = (y1 < y2) ? 1 : -1;
let err = dx - dy;
callback(x1, y1);
while (x1 !== x2 || y1 !== y2) {
const e2 = err * 2;
if (e2 > -dy) {
err -= dy;
x1 += sx;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
callback(x1, y1);
}
};
const fillEllipse = function (centerX, centerY, radiusX, radiusY, context) {
// Bresenham ellipse algorithm
centerX = ~~centerX;
centerY = ~~centerY;
radiusX = ~~radiusX;
radiusY = ~~radiusY;
const twoRadXSquared = 2 * radiusX * radiusX;
const twoRadYSquared = 2 * radiusY * radiusY;
let x = radiusX;
let y = 0;
let dx = radiusY * radiusY * (1 - (radiusX << 1));
let dy = radiusX * radiusX;
let error = 0;
let stoppingX = twoRadYSquared * radiusX;
let stoppingY = 0;
while (stoppingX >= stoppingY) {
context.fillRect(centerX - x, centerY - y, x << 1, y << 1);
y++;
stoppingY += twoRadXSquared;
error += dy;
dy += twoRadXSquared;
if ((error << 1) + dx > 0) {
x--;
stoppingX -= twoRadYSquared;
error += dx;
dx += twoRadYSquared;
}
}
x = 0;
y = radiusY;
dx = radiusY * radiusY;
dy = radiusX * radiusX * (1 - (radiusY << 1));
error = 0;
stoppingX = 0;
stoppingY = twoRadXSquared * radiusY;
while (stoppingX <= stoppingY) {
context.fillRect(centerX - x, centerY - y, x * 2, y * 2);
x++;
stoppingX += twoRadYSquared;
error += dx;
dx += twoRadYSquared;
if ((error << 1) + dy > 0) {
y--;
stoppingY -= twoRadXSquared;
error += dy;
dy += twoRadXSquared;
}
}
};
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;
@ -33,5 +114,7 @@ const trim = function (raster) {
};
export {
fillEllipse,
forEachLinePoint,
trim
};

View file

@ -27,6 +27,15 @@ const clearRaster = function () {
};
const getRaster = function () {
const layer = _getLayer('isRasterLayer');
// Generate blank raster
if (layer.children.length === 0) {
const raster = new paper.Raster(rasterSrc);
raster.parent = layer;
raster.guide = true;
raster.locked = true;
raster.position = paper.view.center;
}
return _getLayer('isRasterLayer').children[0];
};

View file

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

View file

@ -0,0 +1,33 @@
import log from '../log/log';
// Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width
// in the bitmap paint editor.
const CHANGE_BIT_BRUSH_SIZE = 'scratch-paint/brush-mode/CHANGE_BIT_BRUSH_SIZE';
const initialState = 10;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case CHANGE_BIT_BRUSH_SIZE:
if (isNaN(action.brushSize)) {
log.warn(`Invalid brush size: ${action.brushSize}`);
return state;
}
return Math.max(1, action.brushSize);
default:
return state;
}
};
// Action creators ==================================
const changeBitBrushSize = function (brushSize) {
return {
type: CHANGE_BIT_BRUSH_SIZE,
brushSize: brushSize
};
};
export {
reducer as default,
changeBitBrushSize
};

View file

@ -1,5 +1,6 @@
import {combineReducers} from 'redux';
import modeReducer from './modes';
import bitBrushSizeReducer from './bit-brush-size';
import brushModeReducer from './brush-mode';
import eraserModeReducer from './eraser-mode';
import colorReducer from './color';
@ -14,6 +15,7 @@ import undoReducer from './undo';
export default combineReducers({
mode: modeReducer,
bitBrushSize: bitBrushSizeReducer,
brushMode: brushModeReducer,
color: colorReducer,
clipboard: clipboardReducer,