From 389eba628461ba3f34de0f0ef2c1a36f2088cbfb Mon Sep 17 00:00:00 2001 From: DD Liu Date: Mon, 11 Jun 2018 11:48:35 -0400 Subject: [PATCH] Bitmap rectangle tool (#494) --- .../bit-rect-mode/bit-rect-mode.jsx | 35 ++-- src/components/paint-editor/paint-editor.jsx | 6 +- src/containers/bit-rect-mode.jsx | 109 +++++++++++++ src/containers/paint-editor.jsx | 45 +++++- src/containers/paper-canvas.jsx | 10 +- src/helper/bit-tools/rect-tool.js | 152 ++++++++++++++++++ src/helper/bitmap.js | 126 +++++++++++++++ src/lib/modes.js | 12 +- 8 files changed, 457 insertions(+), 38 deletions(-) create mode 100644 src/containers/bit-rect-mode.jsx create mode 100644 src/helper/bit-tools/rect-tool.js diff --git a/src/components/bit-rect-mode/bit-rect-mode.jsx b/src/components/bit-rect-mode/bit-rect-mode.jsx index 42c58c33..c69c4ddf 100644 --- a/src/components/bit-rect-mode/bit-rect-mode.jsx +++ b/src/components/bit-rect-mode/bit-rect-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 rectIcon from './rectangle.svg'; -const BitRectComponent = () => ( - - - +const BitRectComponent = props => ( + ); +BitRectComponent.propTypes = { + isSelected: PropTypes.bool.isRequired, + onMouseDown: PropTypes.func.isRequired +}; + export default BitRectComponent; diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 387e2cb6..f2ad64f6 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -9,7 +9,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx'; import BitBrushMode from '../../containers/bit-brush-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 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'; @@ -177,7 +177,9 @@ const PaintEditorComponent = props => ( onUpdateImage={props.onUpdateImage} /> - + diff --git a/src/containers/bit-rect-mode.jsx b/src/containers/bit-rect-mode.jsx new file mode 100644 index 00000000..ea46843e --- /dev/null +++ b/src/containers/bit-rect-mode.jsx @@ -0,0 +1,109 @@ +import paper from '@scratch/paper'; +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, setSelectedItems} from '../reducers/selected-items'; +import {clearSelection, getSelectedLeafItems} from '../helper/selection'; +import RectTool from '../helper/bit-tools/rect-tool'; +import RectModeComponent from '../components/bit-rect-mode/bit-rect-mode.jsx'; + +class BitRectMode extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'activateTool', + 'deactivateTool' + ]); + } + componentDidMount () { + if (this.props.isRectModeActive) { + this.activateTool(this.props); + } + } + componentWillReceiveProps (nextProps) { + if (this.tool && nextProps.color !== this.props.color) { + this.tool.setColor(nextProps.color); + } + if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } + + if (nextProps.isRectModeActive && !this.props.isRectModeActive) { + this.activateTool(); + } else if (!nextProps.isRectModeActive && this.props.isRectModeActive) { + this.deactivateTool(); + } + } + shouldComponentUpdate (nextProps) { + return nextProps.isRectModeActive !== this.props.isRectModeActive; + } + activateTool () { + clearSelection(this.props.clearSelectedItems); + // Force the default brush color if fill is MIXED or transparent + const fillColorPresent = this.props.color !== MIXED && this.props.color !== null; + if (!fillColorPresent) { + this.props.onChangeFillColor(DEFAULT_COLOR); + } + this.tool = new RectTool( + this.props.setSelectedItems, + this.props.clearSelectedItems, + this.props.onUpdateImage); + this.tool.setColor(this.props.color); + this.tool.activate(); + } + deactivateTool () { + this.tool.deactivateTool(); + this.tool.remove(); + this.tool = null; + } + render () { + return ( + + ); + } +} + +BitRectMode.propTypes = { + clearSelectedItems: PropTypes.func.isRequired, + color: PropTypes.string, + handleMouseDown: PropTypes.func.isRequired, + isRectModeActive: PropTypes.bool.isRequired, + onChangeFillColor: PropTypes.func.isRequired, + onUpdateImage: PropTypes.func.isRequired, + selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), + setSelectedItems: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + color: state.scratchPaint.color.fillColor, + isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT, + selectedItems: state.scratchPaint.selectedItems +}); +const mapDispatchToProps = dispatch => ({ + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + setSelectedItems: () => { + dispatch(setSelectedItems(getSelectedLeafItems())); + }, + handleMouseDown: () => { + dispatch(changeMode(Modes.BIT_RECT)); + }, + onChangeFillColor: fillColor => { + dispatch(changeFillColor(fillColor)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BitRectMode); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 8460845c..c5b6745e 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -2,6 +2,7 @@ import paper from '@scratch/paper'; import PropTypes from 'prop-types'; import React from 'react'; +import {connect} from 'react-redux'; import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; @@ -13,7 +14,7 @@ import {setTextEditTarget} from '../reducers/text-edit-target'; import {updateViewBounds} from '../reducers/view-bounds'; import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer'; -import {getHitBounds} from '../helper/bitmap'; +import {convertToBitmap, convertToVector, getHitBounds} from '../helper/bitmap'; import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {groupSelection, ungroupSelection} from '../helper/group'; @@ -24,9 +25,9 @@ import {resetZoom, zoomOnSelection} from '../helper/view'; import EyeDropperTool from '../helper/tools/eye-dropper'; import Modes from '../lib/modes'; +import {BitmapModes} from '../lib/modes'; import Formats from '../lib/format'; import {isBitmap, isVector} from '../lib/format'; -import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; class PaintEditor extends React.Component { @@ -61,6 +62,9 @@ class PaintEditor extends React.Component { canvas: null, colorInfo: null }; + // When isSwitchingFormats is true, the format is about to switch, but isn't done switching. + // This gives currently active tools a chance to finish what they were doing. + this.isSwitchingFormats = false; } componentDidMount () { document.addEventListener('keydown', (/* event */) => { @@ -76,6 +80,17 @@ class PaintEditor extends React.Component { document.addEventListener('mousedown', this.onMouseDown); document.addEventListener('touchstart', this.onMouseDown); } + componentWillReceiveProps (newProps) { + if ((isVector(this.props.format) && newProps.format === Formats.BITMAP) || + (isBitmap(this.props.format) && newProps.format === Formats.VECTOR)) { + this.isSwitchingFormats = true; + } + if (isVector(this.props.format) && isBitmap(newProps.format)) { + this.switchMode(Formats.BITMAP); + } else if (isVector(newProps.format) && isBitmap(this.props.format)) { + this.switchMode(Formats.VECTOR); + } + } componentDidUpdate (prevProps) { if (this.props.isEyeDropping && !prevProps.isEyeDropping) { this.startEyeDroppingLoop(); @@ -83,9 +98,12 @@ class PaintEditor extends React.Component { this.stopEyeDroppingLoop(); } - if ((isVector(this.props.format) && isBitmap(prevProps.format)) || - (isVector(prevProps.format) && isBitmap(this.props.format))) { - this.switchMode(this.props.format); + if (this.props.format === Formats.VECTOR && isBitmap(prevProps.format)) { + this.isSwitchingFormats = false; + convertToVector(this.props.clearSelectedItems, this.handleUpdateImage); + } else if (isVector(prevProps.format) && this.props.format === Formats.BITMAP) { + this.isSwitchingFormats = false; + convertToBitmap(this.props.clearSelectedItems, this.handleUpdateImage); } } componentWillUnmount () { @@ -103,6 +121,9 @@ class PaintEditor extends React.Component { case Modes.BIT_LINE: this.props.changeMode(Modes.LINE); break; + case Modes.BIT_RECT: + this.props.changeMode(Modes.RECT); + break; default: this.props.changeMode(Modes.BRUSH); } @@ -114,20 +135,28 @@ class PaintEditor extends React.Component { case Modes.LINE: this.props.changeMode(Modes.BIT_LINE); break; + case Modes.RECT: + this.props.changeMode(Modes.BIT_RECT); + break; default: this.props.changeMode(Modes.BIT_BRUSH); } } } handleUpdateImage (skipSnapshot) { - if (isBitmap(this.props.format)) { + // If in the middle of switching formats, rely on the current mode instead of format. + let actualFormat = this.props.format; + if (this.isSwitchingFormats) { + actualFormat = BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR; + } + if (isBitmap(actualFormat)) { const rect = getHitBounds(getRaster()); this.props.onUpdateImage( false /* isVector */, getRaster().getImageData(rect), (ART_BOARD_WIDTH / 2) - rect.x, (ART_BOARD_HEIGHT / 2) - rect.y); - } else if (isVector(this.props.format)) { + } else if (isVector(actualFormat)) { const guideLayers = hideGuideLayers(true /* includeRaster */); // Export at 0.5x @@ -150,7 +179,7 @@ class PaintEditor extends React.Component { } if (!skipSnapshot) { - performSnapshot(this.props.undoSnapshot, this.props.format); + performSnapshot(this.props.undoSnapshot, actualFormat); } } handleUndo () { diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 0f5075e6..8df84571 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -7,7 +7,6 @@ import Formats from '../lib/format'; import Modes from '../lib/modes'; import log from '../log/log'; -import {convertToBitmap, convertToVector} from '../helper/bitmap'; import {performSnapshot} from '../helper/undo'; import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; @@ -21,8 +20,6 @@ import {clearPasteOffset} from '../reducers/clipboard'; import {updateViewBounds} from '../reducers/view-bounds'; import {changeFormat} from '../reducers/format'; -import {isVector, isBitmap} from '../lib/format'; - import styles from './paper-canvas.css'; class PaperCanvas extends React.Component { @@ -56,10 +53,6 @@ class PaperCanvas extends React.Component { if (this.props.imageId !== newProps.imageId) { this.switchCostume( newProps.imageFormat, newProps.image, newProps.rotationCenterX, newProps.rotationCenterY); - } else if (isVector(this.props.format) && newProps.format === Formats.BITMAP) { - convertToBitmap(this.props.clearSelectedItems, this.props.onUpdateImage); - } else if (isBitmap(this.props.format) && newProps.format === Formats.VECTOR) { - convertToVector(this.props.clearSelectedItems, this.props.onUpdateImage); } } componentWillUnmount () { @@ -260,12 +253,11 @@ PaperCanvas.propTypes = { clearPasteOffset: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired, - format: PropTypes.oneOf(Object.keys(Formats)), // Internal, up-to-date data format image: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(HTMLImageElement) ]), - imageFormat: PropTypes.string, // The incoming image's data format, used during import + imageFormat: PropTypes.string, // The incoming image's data format, used during import. The user could switch this. imageId: PropTypes.string, mode: PropTypes.oneOf(Object.keys(Modes)), onUpdateImage: PropTypes.func.isRequired, diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js new file mode 100644 index 00000000..49f0cfc6 --- /dev/null +++ b/src/helper/bit-tools/rect-tool.js @@ -0,0 +1,152 @@ +import paper from '@scratch/paper'; +import Modes from '../../lib/modes'; +import {drawRect} from '../bitmap'; +import {getRaster} from '../layer'; +import {clearSelection} from '../selection'; +import BoundingBoxTool from '../selection-tools/bounding-box-tool'; +import NudgeTool from '../selection-tools/nudge-tool'; + +/** + * Tool for drawing rects. + */ +class RectTool extends paper.Tool { + static get TOLERANCE () { + return 6; + } + /** + * @param {function} setSelectedItems Callback to set the set of selected items in the Redux state + * @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state + * @param {!function} onUpdateImage A callback to call when the image visibly changes + */ + constructor (setSelectedItems, clearSelectedItems, onUpdateImage) { + super(); + this.setSelectedItems = setSelectedItems; + this.clearSelectedItems = clearSelectedItems; + this.onUpdateImage = onUpdateImage; + this.boundingBoxTool = new BoundingBoxTool(Modes.BIT_RECT, setSelectedItems, clearSelectedItems, onUpdateImage); + const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage); + + // We have to set these functions instead of just declaring them because + // paper.js tools hook up the listeners in the setter functions. + this.onMouseDown = this.handleMouseDown; + this.onMouseDrag = this.handleMouseDrag; + this.onMouseUp = this.handleMouseUp; + this.onKeyUp = nudgeTool.onKeyUp; + this.onKeyDown = nudgeTool.onKeyDown; + + this.rect = null; + this.color = null; + this.active = false; + } + getHitOptions () { + return { + segments: false, + stroke: true, + curves: false, + fill: true, + guide: false, + match: hitResult => + (hitResult.item.data && hitResult.item.data.isHelperItem) || + hitResult.item === this.rect, // Allow hits on bounding box and rect only + tolerance: RectTool.TOLERANCE / paper.view.zoom + }; + } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + if ((!this.rect || !this.rect.parent) && + selectedItems && selectedItems.length === 1 && selectedItems[0].shape === 'rectangle') { + // Infer that an undo occurred and get back the active rect + this.rect = selectedItems[0]; + } else if (this.rect && this.rect.parent && !this.rect.selected) { + // Rectangle got deselected + this.commitRect(); + } + } + setColor (color) { + this.color = color; + } + handleMouseDown (event) { + if (event.event.button > 0) return; // only first mouse button + this.active = true; + + if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { + this.isBoundingBoxMode = true; + } else { + this.isBoundingBoxMode = false; + clearSelection(this.clearSelectedItems); + this.commitRect(); + } + } + handleMouseDrag (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseDrag(event); + return; + } + + const dimensions = event.point.subtract(event.downPoint); + const baseRect = new paper.Rectangle(event.downPoint, event.point); + if (event.modifiers.shift) { + baseRect.height = baseRect.width; + dimensions.y = event.downPoint.y > event.point.y ? -Math.abs(baseRect.width) : Math.abs(baseRect.width); + } + if (this.rect) this.rect.remove(); + this.rect = new paper.Shape.Rectangle(baseRect); + this.rect.fillColor = this.color; + + if (event.modifiers.alt) { + this.rect.position = event.downPoint; + } else { + this.rect.position = event.downPoint.add(dimensions.multiply(.5)); + } + } + handleMouseUp (event) { + if (event.event.button > 0 || !this.active) return; // only first mouse button + + if (this.isBoundingBoxMode) { + this.boundingBoxTool.onMouseUp(event); + this.isBoundingBoxMode = null; + return; + } + + if (this.rect) { + if (Math.abs(this.rect.size.width * this.rect.size.height) < RectTool.TOLERANCE / paper.view.zoom) { + // Tiny shape created unintentionally? + this.rect.remove(); + this.rect = null; + } else { + // Hit testing does not work correctly unless the width and height are positive + this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height)); + this.rect.selected = true; + this.setSelectedItems(); + } + } + this.active = false; + } + commitRect () { + if (!this.rect || !this.rect.parent) return; + + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = getRaster().width; + tmpCanvas.height = getRaster().height; + const context = tmpCanvas.getContext('2d'); + context.fillStyle = this.color; + drawRect(this.rect, context); + getRaster().drawImage(tmpCanvas, new paper.Point()); + + this.rect.remove(); + this.rect = null; + this.onUpdateImage(); + } + deactivateTool () { + this.commitRect(); + this.boundingBoxTool.removeBoundsPath(); + } +} + +export default RectTool; diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 1aac8241..f4eca707 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -209,9 +209,135 @@ const convertToVector = function (clearSelectedItems, onUpdateImage) { onUpdateImage(); }; +const getColor_ = function (x, y, context) { + return context.getImageData(x, y, 1, 1).data; +}; + +const matchesColor_ = function (x, y, imageData, oldColor) { + const index = ((y * imageData.width) + x) * 4; + return ( + imageData.data[index + 0] === oldColor[0] && + imageData.data[index + 1] === oldColor[1] && + imageData.data[index + 2] === oldColor[2] && + imageData.data[index + 3 ] === oldColor[3] + ); +}; + +const colorPixel_ = function (x, y, imageData, newColor) { + const index = ((y * imageData.width) + x) * 4; + imageData.data[index + 0] = newColor[0]; + imageData.data[index + 1] = newColor[1]; + imageData.data[index + 2] = newColor[2]; + imageData.data[index + 3] = newColor[3]; +}; + +/** + * Flood fill beginning at the given point. + * Based on http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/ + * + * @param {!int} x The x coordinate on the context at which to begin + * @param {!int} y The y coordinate on the context at which to begin + * @param {!ImageData} imageData The image data to edit + * @param {!Array} newColor The color to replace with. A length 4 array [r, g, b, a]. + * @param {!Array} oldColor The color to replace. A length 4 array [r, g, b, a]. + * This must be different from newColor. + * @param {!Array>} stack The stack of pixels we need to look at + */ +const floodFillInternal_ = function (x, y, imageData, newColor, oldColor, stack) { + while (y > 0 && matchesColor_(x, y - 1, imageData, oldColor)) { + y--; + } + let lastLeftMatchedColor = false; + let lastRightMatchedColor = false; + for (; y < imageData.height; y++) { + if (!matchesColor_(x, y, imageData, oldColor)) break; + colorPixel_(x, y, imageData, newColor); + if (x > 0) { + if (matchesColor_(x - 1, y, imageData, oldColor)) { + if (!lastLeftMatchedColor) { + stack.push([x - 1, y]); + lastLeftMatchedColor = true; + } + } else { + lastLeftMatchedColor = false; + } + } + if (x < imageData.width - 1) { + if (matchesColor_(x + 1, y, imageData, oldColor)) { + if (!lastRightMatchedColor) { + stack.push([x + 1, y]); + lastRightMatchedColor = true; + } + } else { + lastRightMatchedColor = false; + } + } + } +}; + +/** + * Flood fill beginning at the given point + * @param {!int} x The x coordinate on the context at which to begin + * @param {!int} y The y coordinate on the context at which to begin + * @param {!HTMLCanvas2DContext} context The context in which to draw + */ +const floodFill = function (x, y, context) { + const oldColor = getColor_(x, y, context); + context.fillRect(x, y, 1, 1); + const newColor = getColor_(x, y, context); + const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height); + if (matchesColor_(x, y, imageData, oldColor)) { // no-op + return; + } + colorPixel_(x, y, imageData, newColor); // Restore old color to avoid affecting result + const stack = [[x, y]]; + while (stack.length) { + const pop = stack.pop(); + floodFillInternal_(pop[0], pop[1], imageData, newColor, oldColor, stack); + } + context.putImageData(imageData, 0, 0); +}; + +/** + * @param {!paper.Shape.Rectangle} rect The rectangle to draw to the canvas + * @param {!HTMLCanvas2DContext} context The context in which to draw + */ +const drawRect = function (rect, context) { + // No rotation component to matrix + if (rect.matrix.b === 0 && rect.matrix.c === 0) { + const width = rect.size.width * rect.matrix.a; + const height = rect.size.height * rect.matrix.d; + context.fillRect( + ~~(rect.matrix.tx - (width / 2)), + ~~(rect.matrix.ty - (height / 2)), + ~~width, + ~~height); + return; + } + const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2)); + const widthPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, -rect.size.height / 2)); + const heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2)); + const endPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, rect.size.height / 2)); + const center = rect.matrix.transform(new paper.Point()); + forEachLinePoint(startPoint, widthPoint, (x, y) => { + context.fillRect(x, y, 1, 1); + }); + forEachLinePoint(startPoint, heightPoint, (x, y) => { + context.fillRect(x, y, 1, 1); + }); + forEachLinePoint(endPoint, widthPoint, (x, y) => { + context.fillRect(x, y, 1, 1); + }); + forEachLinePoint(endPoint, heightPoint, (x, y) => { + context.fillRect(x, y, 1, 1); + }); + floodFill(~~center.x, ~~center.y, context); +}; + export { convertToBitmap, convertToVector, + drawRect, getBrushMark, getHitBounds, fillEllipse, diff --git a/src/lib/modes.js b/src/lib/modes.js index 332558cd..e0e25b2a 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -3,6 +3,7 @@ import keyMirror from 'keymirror'; const Modes = keyMirror({ BIT_BRUSH: null, BIT_LINE: null, + BIT_RECT: null, BRUSH: null, ERASER: null, LINE: null, @@ -15,4 +16,13 @@ const Modes = keyMirror({ TEXT: null }); -export default Modes; +const BitmapModes = keyMirror({ + BIT_BRUSH: null, + BIT_LINE: null, + BIT_RECT: null +}); + +export { + Modes as default, + BitmapModes +};