mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
Bitmap rectangle tool (#494)
This commit is contained in:
parent
760ddabfce
commit
389eba6284
8 changed files with 457 additions and 38 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 rectIcon from './rectangle.svg';
|
||||
|
||||
const BitRectComponent = () => (
|
||||
<ComingSoonTooltip
|
||||
place="right"
|
||||
tooltipId="bit-rect-mode"
|
||||
>
|
||||
const BitRectComponent = props => (
|
||||
<ToolSelectComponent
|
||||
disabled
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Rectangle',
|
||||
description: 'Label for the rectangle tool',
|
||||
id: 'paint.rectMode.rect'
|
||||
}}
|
||||
imgSrc={rectIcon}
|
||||
isSelected={false}
|
||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
||||
isSelected={props.isSelected}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
</ComingSoonTooltip>
|
||||
);
|
||||
|
||||
BitRectComponent.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BitRectComponent;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
<BitOvalMode />
|
||||
<BitRectMode />
|
||||
<BitRectMode
|
||||
onUpdateImage={props.onUpdateImage}
|
||||
/>
|
||||
<BitTextMode />
|
||||
<BitFillMode />
|
||||
<BitEraserMode />
|
||||
|
|
109
src/containers/bit-rect-mode.jsx
Normal file
109
src/containers/bit-rect-mode.jsx
Normal file
|
@ -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 (
|
||||
<RectModeComponent
|
||||
isSelected={this.props.isRectModeActive}
|
||||
onMouseDown={this.props.handleMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
152
src/helper/bit-tools/rect-tool.js
Normal file
152
src/helper/bit-tools/rect-tool.js
Normal file
|
@ -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<paper.Item>} 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;
|
|
@ -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<number>} newColor The color to replace with. A length 4 array [r, g, b, a].
|
||||
* @param {!Array<number>} oldColor The color to replace. A length 4 array [r, g, b, a].
|
||||
* This must be different from newColor.
|
||||
* @param {!Array<Array<int>>} 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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue