mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 22:47:03 -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 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 ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||||
|
|
||||||
import rectIcon from './rectangle.svg';
|
import rectIcon from './rectangle.svg';
|
||||||
|
|
||||||
const BitRectComponent = () => (
|
const BitRectComponent = props => (
|
||||||
<ComingSoonTooltip
|
<ToolSelectComponent
|
||||||
place="right"
|
imgDescriptor={{
|
||||||
tooltipId="bit-rect-mode"
|
defaultMessage: 'Rectangle',
|
||||||
>
|
description: 'Label for the rectangle tool',
|
||||||
<ToolSelectComponent
|
id: 'paint.rectMode.rect'
|
||||||
disabled
|
}}
|
||||||
imgDescriptor={{
|
imgSrc={rectIcon}
|
||||||
defaultMessage: 'Rectangle',
|
isSelected={props.isSelected}
|
||||||
description: 'Label for the rectangle tool',
|
onMouseDown={props.onMouseDown}
|
||||||
id: 'paint.rectMode.rect'
|
/>
|
||||||
}}
|
|
||||||
imgSrc={rectIcon}
|
|
||||||
isSelected={false}
|
|
||||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
/>
|
|
||||||
</ComingSoonTooltip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
BitRectComponent.propTypes = {
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
onMouseDown: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default BitRectComponent;
|
export default BitRectComponent;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
|
||||||
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
|
import BitBrushMode from '../../containers/bit-brush-mode.jsx';
|
||||||
import BitLineMode from '../../containers/bit-line-mode.jsx';
|
import BitLineMode from '../../containers/bit-line-mode.jsx';
|
||||||
import BitOvalMode from '../../components/bit-oval-mode/bit-oval-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 BitTextMode from '../../components/bit-text-mode/bit-text-mode.jsx';
|
||||||
import BitFillMode from '../../components/bit-fill-mode/bit-fill-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 '../../components/bit-eraser-mode/bit-eraser-mode.jsx';
|
||||||
|
@ -177,7 +177,9 @@ const PaintEditorComponent = props => (
|
||||||
onUpdateImage={props.onUpdateImage}
|
onUpdateImage={props.onUpdateImage}
|
||||||
/>
|
/>
|
||||||
<BitOvalMode />
|
<BitOvalMode />
|
||||||
<BitRectMode />
|
<BitRectMode
|
||||||
|
onUpdateImage={props.onUpdateImage}
|
||||||
|
/>
|
||||||
<BitTextMode />
|
<BitTextMode />
|
||||||
<BitFillMode />
|
<BitFillMode />
|
||||||
<BitEraserMode />
|
<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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
||||||
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
|
@ -13,7 +14,7 @@ import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||||
import {updateViewBounds} from '../reducers/view-bounds';
|
import {updateViewBounds} from '../reducers/view-bounds';
|
||||||
|
|
||||||
import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
|
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 {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||||
|
@ -24,9 +25,9 @@ import {resetZoom, zoomOnSelection} from '../helper/view';
|
||||||
import EyeDropperTool from '../helper/tools/eye-dropper';
|
import EyeDropperTool from '../helper/tools/eye-dropper';
|
||||||
|
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import {BitmapModes} from '../lib/modes';
|
||||||
import Formats from '../lib/format';
|
import Formats from '../lib/format';
|
||||||
import {isBitmap, isVector} from '../lib/format';
|
import {isBitmap, isVector} from '../lib/format';
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
class PaintEditor extends React.Component {
|
class PaintEditor extends React.Component {
|
||||||
|
@ -61,6 +62,9 @@ class PaintEditor extends React.Component {
|
||||||
canvas: null,
|
canvas: null,
|
||||||
colorInfo: 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 () {
|
componentDidMount () {
|
||||||
document.addEventListener('keydown', (/* event */) => {
|
document.addEventListener('keydown', (/* event */) => {
|
||||||
|
@ -76,6 +80,17 @@ class PaintEditor extends React.Component {
|
||||||
document.addEventListener('mousedown', this.onMouseDown);
|
document.addEventListener('mousedown', this.onMouseDown);
|
||||||
document.addEventListener('touchstart', 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) {
|
componentDidUpdate (prevProps) {
|
||||||
if (this.props.isEyeDropping && !prevProps.isEyeDropping) {
|
if (this.props.isEyeDropping && !prevProps.isEyeDropping) {
|
||||||
this.startEyeDroppingLoop();
|
this.startEyeDroppingLoop();
|
||||||
|
@ -83,9 +98,12 @@ class PaintEditor extends React.Component {
|
||||||
this.stopEyeDroppingLoop();
|
this.stopEyeDroppingLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isVector(this.props.format) && isBitmap(prevProps.format)) ||
|
if (this.props.format === Formats.VECTOR && isBitmap(prevProps.format)) {
|
||||||
(isVector(prevProps.format) && isBitmap(this.props.format))) {
|
this.isSwitchingFormats = false;
|
||||||
this.switchMode(this.props.format);
|
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 () {
|
componentWillUnmount () {
|
||||||
|
@ -103,6 +121,9 @@ class PaintEditor extends React.Component {
|
||||||
case Modes.BIT_LINE:
|
case Modes.BIT_LINE:
|
||||||
this.props.changeMode(Modes.LINE);
|
this.props.changeMode(Modes.LINE);
|
||||||
break;
|
break;
|
||||||
|
case Modes.BIT_RECT:
|
||||||
|
this.props.changeMode(Modes.RECT);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.props.changeMode(Modes.BRUSH);
|
this.props.changeMode(Modes.BRUSH);
|
||||||
}
|
}
|
||||||
|
@ -114,20 +135,28 @@ class PaintEditor extends React.Component {
|
||||||
case Modes.LINE:
|
case Modes.LINE:
|
||||||
this.props.changeMode(Modes.BIT_LINE);
|
this.props.changeMode(Modes.BIT_LINE);
|
||||||
break;
|
break;
|
||||||
|
case Modes.RECT:
|
||||||
|
this.props.changeMode(Modes.BIT_RECT);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.props.changeMode(Modes.BIT_BRUSH);
|
this.props.changeMode(Modes.BIT_BRUSH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleUpdateImage (skipSnapshot) {
|
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());
|
const rect = getHitBounds(getRaster());
|
||||||
this.props.onUpdateImage(
|
this.props.onUpdateImage(
|
||||||
false /* isVector */,
|
false /* isVector */,
|
||||||
getRaster().getImageData(rect),
|
getRaster().getImageData(rect),
|
||||||
(ART_BOARD_WIDTH / 2) - rect.x,
|
(ART_BOARD_WIDTH / 2) - rect.x,
|
||||||
(ART_BOARD_HEIGHT / 2) - rect.y);
|
(ART_BOARD_HEIGHT / 2) - rect.y);
|
||||||
} else if (isVector(this.props.format)) {
|
} else if (isVector(actualFormat)) {
|
||||||
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
const guideLayers = hideGuideLayers(true /* includeRaster */);
|
||||||
|
|
||||||
// Export at 0.5x
|
// Export at 0.5x
|
||||||
|
@ -150,7 +179,7 @@ class PaintEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipSnapshot) {
|
if (!skipSnapshot) {
|
||||||
performSnapshot(this.props.undoSnapshot, this.props.format);
|
performSnapshot(this.props.undoSnapshot, actualFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleUndo () {
|
handleUndo () {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import Formats from '../lib/format';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
import log from '../log/log';
|
import log from '../log/log';
|
||||||
|
|
||||||
import {convertToBitmap, convertToVector} from '../helper/bitmap';
|
|
||||||
import {performSnapshot} from '../helper/undo';
|
import {performSnapshot} from '../helper/undo';
|
||||||
import {undoSnapshot, clearUndoState} from '../reducers/undo';
|
import {undoSnapshot, clearUndoState} from '../reducers/undo';
|
||||||
import {isGroup, ungroupItems} from '../helper/group';
|
import {isGroup, ungroupItems} from '../helper/group';
|
||||||
|
@ -21,8 +20,6 @@ import {clearPasteOffset} from '../reducers/clipboard';
|
||||||
import {updateViewBounds} from '../reducers/view-bounds';
|
import {updateViewBounds} from '../reducers/view-bounds';
|
||||||
import {changeFormat} from '../reducers/format';
|
import {changeFormat} from '../reducers/format';
|
||||||
|
|
||||||
import {isVector, isBitmap} from '../lib/format';
|
|
||||||
|
|
||||||
import styles from './paper-canvas.css';
|
import styles from './paper-canvas.css';
|
||||||
|
|
||||||
class PaperCanvas extends React.Component {
|
class PaperCanvas extends React.Component {
|
||||||
|
@ -56,10 +53,6 @@ class PaperCanvas extends React.Component {
|
||||||
if (this.props.imageId !== newProps.imageId) {
|
if (this.props.imageId !== newProps.imageId) {
|
||||||
this.switchCostume(
|
this.switchCostume(
|
||||||
newProps.imageFormat, newProps.image, newProps.rotationCenterX, newProps.rotationCenterY);
|
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 () {
|
componentWillUnmount () {
|
||||||
|
@ -260,12 +253,11 @@ PaperCanvas.propTypes = {
|
||||||
clearPasteOffset: PropTypes.func.isRequired,
|
clearPasteOffset: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
clearUndo: PropTypes.func.isRequired,
|
clearUndo: PropTypes.func.isRequired,
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)), // Internal, up-to-date data format
|
|
||||||
image: PropTypes.oneOfType([
|
image: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.instanceOf(HTMLImageElement)
|
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,
|
imageId: PropTypes.string,
|
||||||
mode: PropTypes.oneOf(Object.keys(Modes)),
|
mode: PropTypes.oneOf(Object.keys(Modes)),
|
||||||
onUpdateImage: PropTypes.func.isRequired,
|
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();
|
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 {
|
export {
|
||||||
convertToBitmap,
|
convertToBitmap,
|
||||||
convertToVector,
|
convertToVector,
|
||||||
|
drawRect,
|
||||||
getBrushMark,
|
getBrushMark,
|
||||||
getHitBounds,
|
getHitBounds,
|
||||||
fillEllipse,
|
fillEllipse,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import keyMirror from 'keymirror';
|
||||||
const Modes = keyMirror({
|
const Modes = keyMirror({
|
||||||
BIT_BRUSH: null,
|
BIT_BRUSH: null,
|
||||||
BIT_LINE: null,
|
BIT_LINE: null,
|
||||||
|
BIT_RECT: null,
|
||||||
BRUSH: null,
|
BRUSH: null,
|
||||||
ERASER: null,
|
ERASER: null,
|
||||||
LINE: null,
|
LINE: null,
|
||||||
|
@ -15,4 +16,13 @@ const Modes = keyMirror({
|
||||||
TEXT: null
|
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