mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
commit
06a3ab7390
15 changed files with 454 additions and 53 deletions
|
@ -1,27 +1,25 @@
|
||||||
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 selectIcon from './marquee.svg';
|
import selectIcon from './marquee.svg';
|
||||||
|
|
||||||
const BitSelectComponent = () => (
|
const BitSelectComponent = props => (
|
||||||
<ComingSoonTooltip
|
<ToolSelectComponent
|
||||||
place="right"
|
imgDescriptor={{
|
||||||
tooltipId="bit-select-mode"
|
defaultMessage: 'Select',
|
||||||
>
|
description: 'Label for the select tool, which allows selecting, moving, and resizing shapes',
|
||||||
<ToolSelectComponent
|
id: 'paint.selectMode.select'
|
||||||
disabled
|
}}
|
||||||
imgDescriptor={{
|
imgSrc={selectIcon}
|
||||||
defaultMessage: 'Select',
|
isSelected={props.isSelected}
|
||||||
description: 'Label for the select tool, which allows selecting, moving, and resizing shapes',
|
onMouseDown={props.onMouseDown}
|
||||||
id: 'paint.selectMode.select'
|
/>
|
||||||
}}
|
|
||||||
imgSrc={selectIcon}
|
|
||||||
isSelected={false}
|
|
||||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
/>
|
|
||||||
</ComingSoonTooltip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
BitSelectComponent.propTypes = {
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
onMouseDown: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default BitSelectComponent;
|
export default BitSelectComponent;
|
||||||
|
|
|
@ -165,6 +165,8 @@ const ModeToolsComponent = props => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case Modes.BIT_SELECT:
|
||||||
|
/* falls through */
|
||||||
case Modes.SELECT:
|
case Modes.SELECT:
|
||||||
return (
|
return (
|
||||||
<div className={classNames(props.className, styles.modeTools)}>
|
<div className={classNames(props.className, styles.modeTools)}>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import BitOvalMode from '../../containers/bit-oval-mode.jsx';
|
||||||
import BitRectMode from '../../containers/bit-rect-mode.jsx';
|
import BitRectMode from '../../containers/bit-rect-mode.jsx';
|
||||||
import BitFillMode from '../../containers/bit-fill-mode.jsx';
|
import BitFillMode from '../../containers/bit-fill-mode.jsx';
|
||||||
import BitEraserMode from '../../containers/bit-eraser-mode.jsx';
|
import BitEraserMode from '../../containers/bit-eraser-mode.jsx';
|
||||||
import BitSelectMode from '../../components/bit-select-mode/bit-select-mode.jsx';
|
import BitSelectMode from '../../containers/bit-select-mode.jsx';
|
||||||
import Box from '../box/box.jsx';
|
import Box from '../box/box.jsx';
|
||||||
import Button from '../button/button.jsx';
|
import Button from '../button/button.jsx';
|
||||||
import ButtonGroup from '../button-group/button-group.jsx';
|
import ButtonGroup from '../button-group/button-group.jsx';
|
||||||
|
@ -192,7 +192,9 @@ const PaintEditorComponent = props => (
|
||||||
<BitEraserMode
|
<BitEraserMode
|
||||||
onUpdateImage={props.onUpdateImage}
|
onUpdateImage={props.onUpdateImage}
|
||||||
/>
|
/>
|
||||||
<BitSelectMode />
|
<BitSelectMode
|
||||||
|
onUpdateImage={props.onUpdateImage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
92
src/containers/bit-select-mode.jsx
Normal file
92
src/containers/bit-select-mode.jsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
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 {changeMode} from '../reducers/modes';
|
||||||
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
|
import BitSelectTool from '../helper/bit-tools/select-tool';
|
||||||
|
import SelectModeComponent from '../components/bit-select-mode/bit-select-mode.jsx';
|
||||||
|
|
||||||
|
class BitSelectMode extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'activateTool',
|
||||||
|
'deactivateTool'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
if (this.props.isSelectModeActive) {
|
||||||
|
this.activateTool(this.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||||
|
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) {
|
||||||
|
this.activateTool();
|
||||||
|
} else if (!nextProps.isSelectModeActive && this.props.isSelectModeActive) {
|
||||||
|
this.deactivateTool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shouldComponentUpdate (nextProps) {
|
||||||
|
return nextProps.isSelectModeActive !== this.props.isSelectModeActive;
|
||||||
|
}
|
||||||
|
activateTool () {
|
||||||
|
this.tool = new BitSelectTool(
|
||||||
|
this.props.setSelectedItems,
|
||||||
|
this.props.clearSelectedItems,
|
||||||
|
this.props.onUpdateImage
|
||||||
|
);
|
||||||
|
this.tool.activate();
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.tool.deactivateTool();
|
||||||
|
this.tool.remove();
|
||||||
|
this.tool = null;
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<SelectModeComponent
|
||||||
|
isSelected={this.props.isSelectModeActive}
|
||||||
|
onMouseDown={this.props.handleMouseDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitSelectMode.propTypes = {
|
||||||
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
isSelectModeActive: PropTypes.bool.isRequired,
|
||||||
|
onUpdateImage: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||||
|
setSelectedItems: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isSelectModeActive: state.scratchPaint.mode === Modes.BIT_SELECT,
|
||||||
|
selectedItems: state.scratchPaint.selectedItems
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
clearSelectedItems: () => {
|
||||||
|
dispatch(clearSelectedItems());
|
||||||
|
},
|
||||||
|
setSelectedItems: () => {
|
||||||
|
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||||
|
},
|
||||||
|
handleMouseDown: () => {
|
||||||
|
dispatch(changeMode(Modes.BIT_SELECT));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BitSelectMode);
|
|
@ -9,6 +9,10 @@ import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
|
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
|
||||||
import {clearSelection, getSelectedLeafItems, getSelectedRootItems, getAllRootItems} from '../helper/selection';
|
import {clearSelection, getSelectedLeafItems, getSelectedRootItems, getAllRootItems} from '../helper/selection';
|
||||||
import {HANDLE_RATIO, ensureClockwise} from '../helper/math';
|
import {HANDLE_RATIO, ensureClockwise} from '../helper/math';
|
||||||
|
import {getRaster} from '../helper/layer';
|
||||||
|
import {flipBitmapHorizontal, flipBitmapVertical} from '../helper/bitmap';
|
||||||
|
import {isBitmap} from '../lib/format';
|
||||||
|
import Formats from '../lib/format';
|
||||||
|
|
||||||
class ModeTools extends React.Component {
|
class ModeTools extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
|
@ -136,8 +140,7 @@ class ModeTools extends React.Component {
|
||||||
this.props.onUpdateImage();
|
this.props.onUpdateImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_handleFlip (horizontalScale, verticalScale) {
|
_handleFlip (horizontalScale, verticalScale, selectedItems) {
|
||||||
let selectedItems = getSelectedRootItems();
|
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
// If nothing is selected, select everything
|
// If nothing is selected, select everything
|
||||||
selectedItems = getAllRootItems();
|
selectedItems = getAllRootItems();
|
||||||
|
@ -163,10 +166,22 @@ class ModeTools extends React.Component {
|
||||||
this.props.onUpdateImage();
|
this.props.onUpdateImage();
|
||||||
}
|
}
|
||||||
handleFlipHorizontal () {
|
handleFlipHorizontal () {
|
||||||
this._handleFlip(-1, 1);
|
const selectedItems = getSelectedRootItems();
|
||||||
|
if (isBitmap(this.props.format) && selectedItems.length === 0) {
|
||||||
|
getRaster().canvas = flipBitmapHorizontal(getRaster().canvas);
|
||||||
|
this.props.onUpdateImage();
|
||||||
|
} else {
|
||||||
|
this._handleFlip(-1, 1, selectedItems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleFlipVertical () {
|
handleFlipVertical () {
|
||||||
this._handleFlip(1, -1);
|
const selectedItems = getSelectedRootItems();
|
||||||
|
if (isBitmap(this.props.format) && selectedItems.length === 0) {
|
||||||
|
getRaster().canvas = flipBitmapVertical(getRaster().canvas);
|
||||||
|
this.props.onUpdateImage();
|
||||||
|
} else {
|
||||||
|
this._handleFlip(1, -1, selectedItems);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleCopyToClipboard () {
|
handleCopyToClipboard () {
|
||||||
const selectedItems = getSelectedRootItems();
|
const selectedItems = getSelectedRootItems();
|
||||||
|
@ -183,12 +198,23 @@ class ModeTools extends React.Component {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
|
|
||||||
if (this.props.clipboardItems.length > 0) {
|
if (this.props.clipboardItems.length > 0) {
|
||||||
|
let items = [];
|
||||||
for (let i = 0; i < this.props.clipboardItems.length; i++) {
|
for (let i = 0; i < this.props.clipboardItems.length; i++) {
|
||||||
const item = paper.Base.importJSON(this.props.clipboardItems[i]);
|
const item = paper.Base.importJSON(this.props.clipboardItems[i]);
|
||||||
if (item) {
|
if (item) {
|
||||||
item.selected = true;
|
items.push(item);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!items.length) return;
|
||||||
|
// If pasting a group or non-raster to bitmap, rasterize firsts
|
||||||
|
if (isBitmap(this.props.format) && !(items.length === 1 && items[0] instanceof paper.Raster)) {
|
||||||
|
const group = new paper.Group(items);
|
||||||
|
items = [group.rasterize()];
|
||||||
|
group.remove();
|
||||||
|
}
|
||||||
|
for (const item of items) {
|
||||||
const placedItem = paper.project.getActiveLayer().addChild(item);
|
const placedItem = paper.project.getActiveLayer().addChild(item);
|
||||||
|
placedItem.selected = true;
|
||||||
placedItem.position.x += 10 * this.props.pasteOffset;
|
placedItem.position.x += 10 * this.props.pasteOffset;
|
||||||
placedItem.position.y += 10 * this.props.pasteOffset;
|
placedItem.position.y += 10 * this.props.pasteOffset;
|
||||||
}
|
}
|
||||||
|
@ -217,6 +243,7 @@ class ModeTools extends React.Component {
|
||||||
ModeTools.propTypes = {
|
ModeTools.propTypes = {
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
incrementPasteOffset: PropTypes.func.isRequired,
|
incrementPasteOffset: PropTypes.func.isRequired,
|
||||||
onUpdateImage: PropTypes.func.isRequired,
|
onUpdateImage: PropTypes.func.isRequired,
|
||||||
pasteOffset: PropTypes.number,
|
pasteOffset: PropTypes.number,
|
||||||
|
@ -229,6 +256,7 @@ ModeTools.propTypes = {
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
clipboardItems: state.scratchPaint.clipboard.items,
|
clipboardItems: state.scratchPaint.clipboard.items,
|
||||||
|
format: state.scratchPaint.format,
|
||||||
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
||||||
selectedItems: state.scratchPaint.selectedItems
|
selectedItems: state.scratchPaint.selectedItems
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
@ -136,7 +137,11 @@ class PaintEditor extends React.Component {
|
||||||
case Modes.BIT_ERASER:
|
case Modes.BIT_ERASER:
|
||||||
this.props.changeMode(Modes.ERASER);
|
this.props.changeMode(Modes.ERASER);
|
||||||
break;
|
break;
|
||||||
|
case Modes.BIT_SELECT:
|
||||||
|
this.props.changeMode(Modes.SELECT);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
log.error(`Mode not handled: ${this.props.mode}`);
|
||||||
this.props.changeMode(Modes.BRUSH);
|
this.props.changeMode(Modes.BRUSH);
|
||||||
}
|
}
|
||||||
} else if (isBitmap(newFormat)) {
|
} else if (isBitmap(newFormat)) {
|
||||||
|
@ -162,7 +167,13 @@ class PaintEditor extends React.Component {
|
||||||
case Modes.ERASER:
|
case Modes.ERASER:
|
||||||
this.props.changeMode(Modes.BIT_ERASER);
|
this.props.changeMode(Modes.BIT_ERASER);
|
||||||
break;
|
break;
|
||||||
|
case Modes.RESHAPE:
|
||||||
|
/* falls through */
|
||||||
|
case Modes.SELECT:
|
||||||
|
this.props.changeMode(Modes.BIT_SELECT);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
log.error(`Mode not handled: ${this.props.mode}`);
|
||||||
this.props.changeMode(Modes.BIT_BRUSH);
|
this.props.changeMode(Modes.BIT_BRUSH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,7 +309,7 @@ class PaintEditor extends React.Component {
|
||||||
this.eyeDropper.pickX = -1;
|
this.eyeDropper.pickX = -1;
|
||||||
this.eyeDropper.pickY = -1;
|
this.eyeDropper.pickY = -1;
|
||||||
this.eyeDropper.activate();
|
this.eyeDropper.activate();
|
||||||
|
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
const colorInfo = this.eyeDropper.getColorInfo(
|
const colorInfo = this.eyeDropper.getColorInfo(
|
||||||
this.eyeDropper.pickX,
|
this.eyeDropper.pickX,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import {getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {forEachLinePoint, getBrushMark} from '../bitmap';
|
import {forEachLinePoint, getBrushMark} from '../bitmap';
|
||||||
import {getGuideLayer} from '../layer';
|
import {createCanvas, getGuideLayer} from '../layer';
|
||||||
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view';
|
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,9 +77,7 @@ class LineTool extends paper.Tool {
|
||||||
|
|
||||||
if (this.cursorPreview) this.cursorPreview.remove();
|
if (this.cursorPreview) this.cursorPreview.remove();
|
||||||
|
|
||||||
const tmpCanvas = document.createElement('canvas');
|
const tmpCanvas = createCanvas();
|
||||||
tmpCanvas.width = ART_BOARD_WIDTH;
|
|
||||||
tmpCanvas.height = ART_BOARD_HEIGHT;
|
|
||||||
this.drawTarget = new paper.Raster(tmpCanvas);
|
this.drawTarget = new paper.Raster(tmpCanvas);
|
||||||
this.drawTarget.parent = getGuideLayer();
|
this.drawTarget.parent = getGuideLayer();
|
||||||
this.drawTarget.guide = true;
|
this.drawTarget.guide = true;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
import {fillRect} from '../bitmap';
|
import {fillRect} from '../bitmap';
|
||||||
import {getRaster} from '../layer';
|
import {createCanvas, getRaster} from '../layer';
|
||||||
import {clearSelection} from '../selection';
|
import {clearSelection} from '../selection';
|
||||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||||
import NudgeTool from '../selection-tools/nudge-tool';
|
import NudgeTool from '../selection-tools/nudge-tool';
|
||||||
|
@ -131,9 +131,7 @@ class RectTool extends paper.Tool {
|
||||||
commitRect () {
|
commitRect () {
|
||||||
if (!this.rect || !this.rect.parent) return;
|
if (!this.rect || !this.rect.parent) return;
|
||||||
|
|
||||||
const tmpCanvas = document.createElement('canvas');
|
const tmpCanvas = createCanvas();
|
||||||
tmpCanvas.width = getRaster().width;
|
|
||||||
tmpCanvas.height = getRaster().height;
|
|
||||||
const context = tmpCanvas.getContext('2d');
|
const context = tmpCanvas.getContext('2d');
|
||||||
context.fillStyle = this.color;
|
context.fillStyle = this.color;
|
||||||
fillRect(this.rect, context);
|
fillRect(this.rect, context);
|
||||||
|
|
180
src/helper/bit-tools/select-tool.js
Normal file
180
src/helper/bit-tools/select-tool.js
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
|
import Modes from '../../lib/modes';
|
||||||
|
|
||||||
|
import {createCanvas, getRaster} from '../layer';
|
||||||
|
import {fillRect, scaleBitmap} from '../bitmap';
|
||||||
|
|
||||||
|
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||||
|
import NudgeTool from '../selection-tools/nudge-tool';
|
||||||
|
import SelectionBoxTool from '../selection-tools/selection-box-tool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* paper.Tool that handles select mode in bitmap. This is made up of 2 subtools.
|
||||||
|
* - The selection box tool is active when the user clicks an empty space and drags.
|
||||||
|
* It selects all items in the rectangle.
|
||||||
|
* - The bounding box tool is active if the user clicks on a non-empty space. It handles
|
||||||
|
* reshaping the selection.
|
||||||
|
*/
|
||||||
|
class SelectTool extends paper.Tool {
|
||||||
|
/** The distance within which mouse events count as a hit against an item */
|
||||||
|
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.onUpdateImage = onUpdateImage;
|
||||||
|
this.boundingBoxTool = new BoundingBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems, onUpdateImage);
|
||||||
|
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateImage);
|
||||||
|
this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT, setSelectedItems, clearSelectedItems);
|
||||||
|
this.selectionBoxMode = false;
|
||||||
|
this.selection = null;
|
||||||
|
this.active = false;
|
||||||
|
|
||||||
|
// 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.boundingBoxTool.setSelectionBounds();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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.selection && this.selection.parent && !this.selection.selected) {
|
||||||
|
// Selection got deselected
|
||||||
|
this.commitSelection();
|
||||||
|
}
|
||||||
|
if ((!this.selection || !this.selection.parent) &&
|
||||||
|
selectedItems && selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) {
|
||||||
|
// Track the new active selection. This may happen via undo or paste.
|
||||||
|
this.selection = selectedItems[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the hit options to use when conducting hit tests.
|
||||||
|
* @return {object} See paper.Item.hitTest for definition of options
|
||||||
|
*/
|
||||||
|
getHitOptions () {
|
||||||
|
// Tolerance needs to be scaled when the view is zoomed in in order to represent the same
|
||||||
|
// distance for the user to move the mouse.
|
||||||
|
return {
|
||||||
|
segments: true,
|
||||||
|
stroke: true,
|
||||||
|
curves: true,
|
||||||
|
fill: true,
|
||||||
|
guide: false,
|
||||||
|
tolerance: SelectTool.TOLERANCE / paper.view.zoom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleMouseDown (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
// If bounding box tool does not find an item that was hit, rasterize the old selection,
|
||||||
|
// then use selection box tool.
|
||||||
|
if (!this.boundingBoxTool
|
||||||
|
.onMouseDown(
|
||||||
|
event,
|
||||||
|
event.modifiers.alt,
|
||||||
|
event.modifiers.shift,
|
||||||
|
this.getHitOptions())) {
|
||||||
|
this.commitSelection();
|
||||||
|
this.selectionBoxMode = true;
|
||||||
|
this.selectionBoxTool.onMouseDown(event.modifiers.shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleMouseDrag (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
if (this.selectionBoxMode) {
|
||||||
|
this.selectionBoxTool.onMouseDrag(event);
|
||||||
|
} else {
|
||||||
|
this.boundingBoxTool.onMouseDrag(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleMouseUp (event) {
|
||||||
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
|
if (this.selectionBoxMode) {
|
||||||
|
this.selectionBoxTool.onMouseUpBitmap(event);
|
||||||
|
} else {
|
||||||
|
this.boundingBoxTool.onMouseUp(event);
|
||||||
|
}
|
||||||
|
this.selectionBoxMode = false;
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
commitSelection () {
|
||||||
|
if (!this.selection || !this.selection.parent) return;
|
||||||
|
|
||||||
|
this.maybeApplyScaleToCanvas(this.selection);
|
||||||
|
this.commitArbitraryTransformation(this.selection);
|
||||||
|
this.onUpdateImage();
|
||||||
|
}
|
||||||
|
maybeApplyScaleToCanvas (item) {
|
||||||
|
if (!item.matrix.isInvertible()) {
|
||||||
|
item.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// context.drawImage will anti-alias the image if both width and height are reduced.
|
||||||
|
// However, it will preserve pixel colors if only one or the other is reduced, and
|
||||||
|
// imageSmoothingEnabled is set to false. Therefore, we can avoid aliasing by scaling
|
||||||
|
// down images in a 2 step process.
|
||||||
|
const decomposed = item.matrix.decompose(); // Decomposition order: translate, rotate, scale, skew
|
||||||
|
if (Math.abs(decomposed.scaling.x) < 1 && Math.abs(decomposed.scaling.y) < 1 &&
|
||||||
|
decomposed.scaling.x !== 0 && decomposed.scaling.y !== 0) {
|
||||||
|
item.canvas = scaleBitmap(item.canvas, decomposed.scaling);
|
||||||
|
if (item.data && item.data.expanded) {
|
||||||
|
item.data.expanded.canvas = scaleBitmap(item.data.expanded.canvas, decomposed.scaling);
|
||||||
|
}
|
||||||
|
// Remove the scale from the item's matrix
|
||||||
|
item.matrix.append(
|
||||||
|
new paper.Matrix().scale(new paper.Point(1 / decomposed.scaling.x, 1 / decomposed.scaling.y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commitArbitraryTransformation (item) {
|
||||||
|
// Create a canvas to perform masking
|
||||||
|
const tmpCanvas = createCanvas();
|
||||||
|
const context = tmpCanvas.getContext('2d');
|
||||||
|
// Draw mask
|
||||||
|
const rect = new paper.Shape.Rectangle(new paper.Point(), item.size);
|
||||||
|
rect.matrix = item.matrix;
|
||||||
|
fillRect(rect, context);
|
||||||
|
rect.remove();
|
||||||
|
context.globalCompositeOperation = 'source-in';
|
||||||
|
|
||||||
|
// Draw image onto mask
|
||||||
|
const m = item.matrix;
|
||||||
|
context.transform(m.a, m.b, m.c, m.d, m.tx, m.ty);
|
||||||
|
let canvas = item.canvas;
|
||||||
|
if (item.data && item.data.expanded) {
|
||||||
|
canvas = item.data.expanded.canvas;
|
||||||
|
}
|
||||||
|
context.transform(1, 0, 0, 1, -canvas.width / 2, -canvas.height / 2);
|
||||||
|
context.drawImage(canvas, 0, 0);
|
||||||
|
|
||||||
|
// Draw temp canvas onto raster layer
|
||||||
|
getRaster().drawImage(tmpCanvas, new paper.Point());
|
||||||
|
item.remove();
|
||||||
|
this.selection = null;
|
||||||
|
}
|
||||||
|
deactivateTool () {
|
||||||
|
this.commitSelection();
|
||||||
|
this.boundingBoxTool.removeBoundsPath();
|
||||||
|
this.boundingBoxTool = null;
|
||||||
|
this.selectionBoxTool = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectTool;
|
|
@ -1,5 +1,5 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import {clearRaster, getRaster, hideGuideLayers, showGuideLayers} from './layer';
|
import {createCanvas, clearRaster, getRaster, hideGuideLayers, showGuideLayers} from './layer';
|
||||||
import {getGuideColor} from './guides';
|
import {getGuideColor} from './guides';
|
||||||
import {inlineSvgFonts} from 'scratch-svg-renderer';
|
import {inlineSvgFonts} from 'scratch-svg-renderer';
|
||||||
|
|
||||||
|
@ -558,6 +558,41 @@ const fillRect = function (rect, context) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flipBitmapHorizontal = function (canvas) {
|
||||||
|
const tmpCanvas = createCanvas(canvas.width, canvas.height);
|
||||||
|
const context = tmpCanvas.getContext('2d');
|
||||||
|
context.save();
|
||||||
|
context.scale(-1, 1);
|
||||||
|
context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height);
|
||||||
|
context.restore();
|
||||||
|
return tmpCanvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flipBitmapVertical = function (canvas) {
|
||||||
|
const tmpCanvas = createCanvas(canvas.width, canvas.height);
|
||||||
|
const context = tmpCanvas.getContext('2d');
|
||||||
|
context.save();
|
||||||
|
context.scale(1, -1);
|
||||||
|
context.drawImage(canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height);
|
||||||
|
context.restore();
|
||||||
|
return tmpCanvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scaleBitmap = function (canvas, scale) {
|
||||||
|
let tmpCanvas = createCanvas(Math.round(canvas.width * Math.abs(scale.x)), canvas.height);
|
||||||
|
if (scale.x < 0) {
|
||||||
|
canvas = flipBitmapHorizontal(canvas);
|
||||||
|
}
|
||||||
|
tmpCanvas.getContext('2d').drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height);
|
||||||
|
canvas = tmpCanvas;
|
||||||
|
tmpCanvas = createCanvas(canvas.width, Math.round(canvas.height * Math.abs(scale.y)));
|
||||||
|
if (scale.y < 0) {
|
||||||
|
canvas = flipBitmapVertical(canvas);
|
||||||
|
}
|
||||||
|
tmpCanvas.getContext('2d').drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height);
|
||||||
|
return tmpCanvas;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
convertToBitmap,
|
convertToBitmap,
|
||||||
convertToVector,
|
convertToVector,
|
||||||
|
@ -567,5 +602,8 @@ export {
|
||||||
getBrushMark,
|
getBrushMark,
|
||||||
getHitBounds,
|
getHitBounds,
|
||||||
drawEllipse,
|
drawEllipse,
|
||||||
forEachLinePoint
|
forEachLinePoint,
|
||||||
|
flipBitmapHorizontal,
|
||||||
|
flipBitmapVertical,
|
||||||
|
scaleBitmap
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,15 +14,27 @@ const _getPaintingLayer = function () {
|
||||||
return _getLayer('isPaintingLayer');
|
return _getLayer('isPaintingLayer');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a canvas with width and height matching the art board size.
|
||||||
|
* @param {?number} width Width of the canvas. Defaults to ART_BOARD_WIDTH.
|
||||||
|
* @param {?number} height Height of the canvas. Defaults to ART_BOARD_HEIGHT.
|
||||||
|
* @return {HTMLCanvasElement} the canvas
|
||||||
|
*/
|
||||||
|
const createCanvas = function (width, height) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width ? width : ART_BOARD_WIDTH;
|
||||||
|
canvas.height = height ? height : ART_BOARD_HEIGHT;
|
||||||
|
canvas.getContext('2d').imageSmoothingEnabled = false;
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
const clearRaster = function () {
|
const clearRaster = function () {
|
||||||
const layer = _getLayer('isRasterLayer');
|
const layer = _getLayer('isRasterLayer');
|
||||||
layer.removeChildren();
|
layer.removeChildren();
|
||||||
|
|
||||||
// Generate blank raster
|
// Generate blank raster
|
||||||
const tmpCanvas = document.createElement('canvas');
|
const raster = new paper.Raster(createCanvas());
|
||||||
tmpCanvas.width = ART_BOARD_WIDTH;
|
raster.canvas.getContext('2d').imageSmoothingEnabled = false;
|
||||||
tmpCanvas.height = ART_BOARD_HEIGHT;
|
|
||||||
const raster = new paper.Raster(tmpCanvas);
|
|
||||||
raster.parent = layer;
|
raster.parent = layer;
|
||||||
raster.guide = true;
|
raster.guide = true;
|
||||||
raster.locked = true;
|
raster.locked = true;
|
||||||
|
@ -197,6 +209,7 @@ const setupLayers = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
createCanvas,
|
||||||
hideGuideLayers,
|
hideGuideLayers,
|
||||||
showGuideLayers,
|
showGuideLayers,
|
||||||
getGuideLayer,
|
getGuideLayer,
|
||||||
|
|
|
@ -128,19 +128,24 @@ class BoundingBoxTool {
|
||||||
}
|
}
|
||||||
setSelectionBounds () {
|
setSelectionBounds () {
|
||||||
this.removeBoundsPath();
|
this.removeBoundsPath();
|
||||||
|
|
||||||
const items = getSelectedRootItems();
|
const items = getSelectedRootItems();
|
||||||
if (items.length <= 0) return;
|
if (items.length <= 0) return;
|
||||||
|
|
||||||
let rect = null;
|
let rect = null;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
if (item instanceof paper.Raster && item.loaded === false) {
|
||||||
|
item.onLoad = this.setSelectionBounds.bind(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (rect) {
|
if (rect) {
|
||||||
rect = rect.unite(item.bounds);
|
rect = rect.unite(item.bounds);
|
||||||
} else {
|
} else {
|
||||||
rect = item.bounds;
|
rect = item.bounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.boundsPath) {
|
if (!this.boundsPath) {
|
||||||
this.boundsPath = new paper.Path.Rectangle(rect);
|
this.boundsPath = new paper.Path.Rectangle(rect);
|
||||||
this.boundsPath.curves[0].divideAtTime(0.5);
|
this.boundsPath.curves[0].divideAtTime(0.5);
|
||||||
|
@ -156,7 +161,7 @@ class BoundingBoxTool {
|
||||||
this.boundsPath.parent = getGuideLayer();
|
this.boundsPath.parent = getGuideLayer();
|
||||||
this.boundsPath.strokeWidth = 1 / paper.view.zoom;
|
this.boundsPath.strokeWidth = 1 / paper.view.zoom;
|
||||||
this.boundsPath.strokeColor = getGuideColor();
|
this.boundsPath.strokeColor = getGuideColor();
|
||||||
|
|
||||||
// Make a template to copy
|
// Make a template to copy
|
||||||
const boundsScaleCircleShadow =
|
const boundsScaleCircleShadow =
|
||||||
new paper.Path.Circle({
|
new paper.Path.Circle({
|
||||||
|
@ -187,13 +192,13 @@ class BoundingBoxTool {
|
||||||
|
|
||||||
for (let index = 0; index < this.boundsPath.segments.length; index++) {
|
for (let index = 0; index < this.boundsPath.segments.length; index++) {
|
||||||
const segment = this.boundsPath.segments[index];
|
const segment = this.boundsPath.segments[index];
|
||||||
|
|
||||||
if (index === 7) {
|
if (index === 7) {
|
||||||
const offset = new paper.Point(0, 20);
|
const offset = new paper.Point(0, 20);
|
||||||
|
|
||||||
const arrows = new paper.Path(ARROW_PATH);
|
const arrows = new paper.Path(ARROW_PATH);
|
||||||
arrows.translate(segment.point.add(offset).add(-10.5, -5));
|
arrows.translate(segment.point.add(offset).add(-10.5, -5));
|
||||||
|
|
||||||
const line = new paper.Path.Rectangle(
|
const line = new paper.Path.Rectangle(
|
||||||
segment.point.add(offset).subtract(1, 0),
|
segment.point.add(offset).subtract(1, 0),
|
||||||
segment.point);
|
segment.point);
|
||||||
|
@ -213,7 +218,7 @@ class BoundingBoxTool {
|
||||||
rotHandle.parent = getGuideLayer();
|
rotHandle.parent = getGuideLayer();
|
||||||
this.boundsRotHandles[index] = rotHandle;
|
this.boundsRotHandles[index] = rotHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsScaleHandles[index] = boundsScaleHandle.clone();
|
this.boundsScaleHandles[index] = boundsScaleHandle.clone();
|
||||||
this.boundsScaleHandles[index].position = segment.point;
|
this.boundsScaleHandles[index].position = segment.point;
|
||||||
for (const child of this.boundsScaleHandles[index].children) {
|
for (const child of this.boundsScaleHandles[index].children) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ class SelectTool extends paper.Tool {
|
||||||
this.selectionBoxMode = false;
|
this.selectionBoxMode = false;
|
||||||
this.prevHoveredItemId = null;
|
this.prevHoveredItemId = null;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
// We have to set these functions instead of just declaring them because
|
// We have to set these functions instead of just declaring them because
|
||||||
// paper.js tools hook up the listeners in the setter functions.
|
// paper.js tools hook up the listeners in the setter functions.
|
||||||
this.onMouseDown = this.handleMouseDown;
|
this.onMouseDown = this.handleMouseDown;
|
||||||
|
@ -128,7 +128,7 @@ class SelectTool extends paper.Tool {
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
if (this.selectionBoxMode) {
|
if (this.selectionBoxMode) {
|
||||||
this.selectionBoxTool.onMouseUp(event);
|
this.selectionBoxTool.onMouseUpVector(event);
|
||||||
} else {
|
} else {
|
||||||
this.boundingBoxTool.onMouseUp(event);
|
this.boundingBoxTool.onMouseUp(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
import {rectSelect} from '../guides';
|
import {rectSelect} from '../guides';
|
||||||
import {clearSelection, processRectangularSelection} from '../selection';
|
import {clearSelection, processRectangularSelection} from '../selection';
|
||||||
|
import {getRaster} from '../layer';
|
||||||
|
|
||||||
/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */
|
/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */
|
||||||
class SelectionBoxTool {
|
class SelectionBoxTool {
|
||||||
|
@ -29,7 +31,7 @@ class SelectionBoxTool {
|
||||||
// Remove this rect on the next drag and up event
|
// Remove this rect on the next drag and up event
|
||||||
this.selectionRect.removeOnDrag();
|
this.selectionRect.removeOnDrag();
|
||||||
}
|
}
|
||||||
onMouseUp (event) {
|
onMouseUpVector (event) {
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
if (this.selectionRect) {
|
if (this.selectionRect) {
|
||||||
processRectangularSelection(event, this.selectionRect, this.mode);
|
processRectangularSelection(event, this.selectionRect, this.mode);
|
||||||
|
@ -38,6 +40,38 @@ class SelectionBoxTool {
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onMouseUpBitmap (event) {
|
||||||
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
if (this.selectionRect) {
|
||||||
|
const rect = new paper.Rectangle(
|
||||||
|
Math.round(this.selectionRect.bounds.x),
|
||||||
|
Math.round(this.selectionRect.bounds.y),
|
||||||
|
Math.round(this.selectionRect.bounds.width),
|
||||||
|
Math.round(this.selectionRect.bounds.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove dotted rectangle
|
||||||
|
this.selectionRect.remove();
|
||||||
|
this.selectionRect = null;
|
||||||
|
|
||||||
|
if (rect.area) {
|
||||||
|
// Pull selected raster to active layer
|
||||||
|
const raster = getRaster().getSubRaster(rect);
|
||||||
|
raster.parent = paper.project.activeLayer;
|
||||||
|
raster.canvas.getContext('2d').imageSmoothingEnabled = false;
|
||||||
|
raster.selected = true;
|
||||||
|
// Gather a bit of extra data so that we can avoid aliasing at edges
|
||||||
|
const expanded = getRaster().getSubRaster(rect.expand(4));
|
||||||
|
expanded.remove();
|
||||||
|
raster.data = {expanded: expanded};
|
||||||
|
|
||||||
|
// Clear area from raster layer
|
||||||
|
const context = getRaster().getContext(true /* modify */);
|
||||||
|
context.clearRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
this.setSelectedItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectionBoxTool;
|
export default SelectionBoxTool;
|
||||||
|
|
|
@ -8,6 +8,7 @@ const Modes = keyMirror({
|
||||||
BIT_TEXT: null,
|
BIT_TEXT: null,
|
||||||
BIT_FILL: null,
|
BIT_FILL: null,
|
||||||
BIT_ERASER: null,
|
BIT_ERASER: null,
|
||||||
|
BIT_SELECT: null,
|
||||||
BRUSH: null,
|
BRUSH: null,
|
||||||
ERASER: null,
|
ERASER: null,
|
||||||
LINE: null,
|
LINE: null,
|
||||||
|
@ -27,7 +28,8 @@ const BitmapModes = keyMirror({
|
||||||
BIT_RECT: null,
|
BIT_RECT: null,
|
||||||
BIT_TEXT: null,
|
BIT_TEXT: null,
|
||||||
BIT_FILL: null,
|
BIT_FILL: null,
|
||||||
BIT_ERASER: null
|
BIT_ERASER: null,
|
||||||
|
BIT_SELECT: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
Loading…
Reference in a new issue