diff --git a/src/components/bit-select-mode/bit-select-mode.jsx b/src/components/bit-select-mode/bit-select-mode.jsx
index 96db583b..ec214e33 100644
--- a/src/components/bit-select-mode/bit-select-mode.jsx
+++ b/src/components/bit-select-mode/bit-select-mode.jsx
@@ -1,27 +1,25 @@
import React from 'react';
-
-import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
+import PropTypes from 'prop-types';
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
import selectIcon from './marquee.svg';
-const BitSelectComponent = () => (
-
-
-
+const BitSelectComponent = props => (
+
);
+BitSelectComponent.propTypes = {
+ isSelected: PropTypes.bool.isRequired,
+ onMouseDown: PropTypes.func.isRequired
+};
+
export default BitSelectComponent;
diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx
index dd4b8c7c..25348719 100644
--- a/src/components/mode-tools/mode-tools.jsx
+++ b/src/components/mode-tools/mode-tools.jsx
@@ -165,6 +165,8 @@ const ModeToolsComponent = props => {
/>
);
+ case Modes.BIT_SELECT:
+ /* falls through */
case Modes.SELECT:
return (
diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx
index 8435bbaf..3c58aedd 100644
--- a/src/components/paint-editor/paint-editor.jsx
+++ b/src/components/paint-editor/paint-editor.jsx
@@ -12,7 +12,7 @@ import BitOvalMode from '../../containers/bit-oval-mode.jsx';
import BitRectMode from '../../containers/bit-rect-mode.jsx';
import BitFillMode from '../../containers/bit-fill-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 Button from '../button/button.jsx';
import ButtonGroup from '../button-group/button-group.jsx';
@@ -192,7 +192,9 @@ const PaintEditorComponent = props => (
-
+
) : null}
diff --git a/src/containers/bit-select-mode.jsx b/src/containers/bit-select-mode.jsx
new file mode 100644
index 00000000..d51beb25
--- /dev/null
+++ b/src/containers/bit-select-mode.jsx
@@ -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 (
+
+ );
+ }
+}
+
+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);
diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx
index 3b9df8fe..d252e38c 100644
--- a/src/containers/mode-tools.jsx
+++ b/src/containers/mode-tools.jsx
@@ -9,6 +9,10 @@ import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
import {clearSelection, getSelectedLeafItems, getSelectedRootItems, getAllRootItems} from '../helper/selection';
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 {
constructor (props) {
@@ -136,8 +140,7 @@ class ModeTools extends React.Component {
this.props.onUpdateImage();
}
}
- _handleFlip (horizontalScale, verticalScale) {
- let selectedItems = getSelectedRootItems();
+ _handleFlip (horizontalScale, verticalScale, selectedItems) {
if (selectedItems.length === 0) {
// If nothing is selected, select everything
selectedItems = getAllRootItems();
@@ -163,10 +166,22 @@ class ModeTools extends React.Component {
this.props.onUpdateImage();
}
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 () {
- 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 () {
const selectedItems = getSelectedRootItems();
@@ -183,12 +198,23 @@ class ModeTools extends React.Component {
clearSelection(this.props.clearSelectedItems);
if (this.props.clipboardItems.length > 0) {
+ let items = [];
for (let i = 0; i < this.props.clipboardItems.length; i++) {
const item = paper.Base.importJSON(this.props.clipboardItems[i]);
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);
+ placedItem.selected = true;
placedItem.position.x += 10 * this.props.pasteOffset;
placedItem.position.y += 10 * this.props.pasteOffset;
}
@@ -217,6 +243,7 @@ class ModeTools extends React.Component {
ModeTools.propTypes = {
clearSelectedItems: PropTypes.func.isRequired,
clipboardItems: PropTypes.arrayOf(PropTypes.array),
+ format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
incrementPasteOffset: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
pasteOffset: PropTypes.number,
@@ -229,6 +256,7 @@ ModeTools.propTypes = {
const mapStateToProps = state => ({
clipboardItems: state.scratchPaint.clipboard.items,
+ format: state.scratchPaint.format,
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
selectedItems: state.scratchPaint.selectedItems
});
diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx
index c65ccef9..f0799b05 100644
--- a/src/containers/paint-editor.jsx
+++ b/src/containers/paint-editor.jsx
@@ -1,5 +1,6 @@
import paper from '@scratch/paper';
import PropTypes from 'prop-types';
+import log from '../log/log';
import React from 'react';
import {connect} from 'react-redux';
@@ -136,7 +137,11 @@ class PaintEditor extends React.Component {
case Modes.BIT_ERASER:
this.props.changeMode(Modes.ERASER);
break;
+ case Modes.BIT_SELECT:
+ this.props.changeMode(Modes.SELECT);
+ break;
default:
+ log.error(`Mode not handled: ${this.props.mode}`);
this.props.changeMode(Modes.BRUSH);
}
} else if (isBitmap(newFormat)) {
@@ -162,7 +167,13 @@ class PaintEditor extends React.Component {
case Modes.ERASER:
this.props.changeMode(Modes.BIT_ERASER);
break;
+ case Modes.RESHAPE:
+ /* falls through */
+ case Modes.SELECT:
+ this.props.changeMode(Modes.BIT_SELECT);
+ break;
default:
+ log.error(`Mode not handled: ${this.props.mode}`);
this.props.changeMode(Modes.BIT_BRUSH);
}
}
@@ -298,7 +309,7 @@ class PaintEditor extends React.Component {
this.eyeDropper.pickX = -1;
this.eyeDropper.pickY = -1;
this.eyeDropper.activate();
-
+
this.intervalId = setInterval(() => {
const colorInfo = this.eyeDropper.getColorInfo(
this.eyeDropper.pickX,
diff --git a/src/helper/bit-tools/line-tool.js b/src/helper/bit-tools/line-tool.js
index b9abf012..3a2f42d2 100644
--- a/src/helper/bit-tools/line-tool.js
+++ b/src/helper/bit-tools/line-tool.js
@@ -1,7 +1,7 @@
import paper from '@scratch/paper';
import {getRaster} from '../layer';
import {forEachLinePoint, getBrushMark} from '../bitmap';
-import {getGuideLayer} from '../layer';
+import {createCanvas, getGuideLayer} from '../layer';
import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view';
/**
@@ -77,9 +77,7 @@ class LineTool extends paper.Tool {
if (this.cursorPreview) this.cursorPreview.remove();
- const tmpCanvas = document.createElement('canvas');
- tmpCanvas.width = ART_BOARD_WIDTH;
- tmpCanvas.height = ART_BOARD_HEIGHT;
+ const tmpCanvas = createCanvas();
this.drawTarget = new paper.Raster(tmpCanvas);
this.drawTarget.parent = getGuideLayer();
this.drawTarget.guide = true;
diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js
index ef0a89cb..d5de5ebe 100644
--- a/src/helper/bit-tools/rect-tool.js
+++ b/src/helper/bit-tools/rect-tool.js
@@ -1,7 +1,7 @@
import paper from '@scratch/paper';
import Modes from '../../lib/modes';
import {fillRect} from '../bitmap';
-import {getRaster} from '../layer';
+import {createCanvas, getRaster} from '../layer';
import {clearSelection} from '../selection';
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
import NudgeTool from '../selection-tools/nudge-tool';
@@ -131,9 +131,7 @@ class RectTool extends paper.Tool {
commitRect () {
if (!this.rect || !this.rect.parent) return;
- const tmpCanvas = document.createElement('canvas');
- tmpCanvas.width = getRaster().width;
- tmpCanvas.height = getRaster().height;
+ const tmpCanvas = createCanvas();
const context = tmpCanvas.getContext('2d');
context.fillStyle = this.color;
fillRect(this.rect, context);
diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js
new file mode 100644
index 00000000..b6e5d0be
--- /dev/null
+++ b/src/helper/bit-tools/select-tool.js
@@ -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} 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;
diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js
index 22ef902b..c13463c9 100644
--- a/src/helper/bitmap.js
+++ b/src/helper/bitmap.js
@@ -1,5 +1,5 @@
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 {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 {
convertToBitmap,
convertToVector,
@@ -567,5 +602,8 @@ export {
getBrushMark,
getHitBounds,
drawEllipse,
- forEachLinePoint
+ forEachLinePoint,
+ flipBitmapHorizontal,
+ flipBitmapVertical,
+ scaleBitmap
};
diff --git a/src/helper/layer.js b/src/helper/layer.js
index a0d6d462..b89b67ec 100644
--- a/src/helper/layer.js
+++ b/src/helper/layer.js
@@ -14,15 +14,27 @@ const _getPaintingLayer = function () {
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 layer = _getLayer('isRasterLayer');
layer.removeChildren();
// Generate blank raster
- const tmpCanvas = document.createElement('canvas');
- tmpCanvas.width = ART_BOARD_WIDTH;
- tmpCanvas.height = ART_BOARD_HEIGHT;
- const raster = new paper.Raster(tmpCanvas);
+ const raster = new paper.Raster(createCanvas());
+ raster.canvas.getContext('2d').imageSmoothingEnabled = false;
raster.parent = layer;
raster.guide = true;
raster.locked = true;
@@ -197,6 +209,7 @@ const setupLayers = function () {
};
export {
+ createCanvas,
hideGuideLayers,
showGuideLayers,
getGuideLayer,
diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js
index b0e2a9eb..e11ec1b7 100644
--- a/src/helper/selection-tools/bounding-box-tool.js
+++ b/src/helper/selection-tools/bounding-box-tool.js
@@ -128,19 +128,24 @@ class BoundingBoxTool {
}
setSelectionBounds () {
this.removeBoundsPath();
-
+
const items = getSelectedRootItems();
if (items.length <= 0) return;
-
+
let rect = null;
for (const item of items) {
+ if (item instanceof paper.Raster && item.loaded === false) {
+ item.onLoad = this.setSelectionBounds.bind(this);
+ return;
+ }
+
if (rect) {
rect = rect.unite(item.bounds);
} else {
rect = item.bounds;
}
}
-
+
if (!this.boundsPath) {
this.boundsPath = new paper.Path.Rectangle(rect);
this.boundsPath.curves[0].divideAtTime(0.5);
@@ -156,7 +161,7 @@ class BoundingBoxTool {
this.boundsPath.parent = getGuideLayer();
this.boundsPath.strokeWidth = 1 / paper.view.zoom;
this.boundsPath.strokeColor = getGuideColor();
-
+
// Make a template to copy
const boundsScaleCircleShadow =
new paper.Path.Circle({
@@ -187,13 +192,13 @@ class BoundingBoxTool {
for (let index = 0; index < this.boundsPath.segments.length; index++) {
const segment = this.boundsPath.segments[index];
-
+
if (index === 7) {
const offset = new paper.Point(0, 20);
-
+
const arrows = new paper.Path(ARROW_PATH);
arrows.translate(segment.point.add(offset).add(-10.5, -5));
-
+
const line = new paper.Path.Rectangle(
segment.point.add(offset).subtract(1, 0),
segment.point);
@@ -213,7 +218,7 @@ class BoundingBoxTool {
rotHandle.parent = getGuideLayer();
this.boundsRotHandles[index] = rotHandle;
}
-
+
this.boundsScaleHandles[index] = boundsScaleHandle.clone();
this.boundsScaleHandles[index].position = segment.point;
for (const child of this.boundsScaleHandles[index].children) {
diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js
index c069892f..db1a3377 100644
--- a/src/helper/selection-tools/select-tool.js
+++ b/src/helper/selection-tools/select-tool.js
@@ -37,7 +37,7 @@ class SelectTool extends paper.Tool {
this.selectionBoxMode = false;
this.prevHoveredItemId = 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;
@@ -128,7 +128,7 @@ class SelectTool extends paper.Tool {
if (event.event.button > 0 || !this.active) return; // only first mouse button
if (this.selectionBoxMode) {
- this.selectionBoxTool.onMouseUp(event);
+ this.selectionBoxTool.onMouseUpVector(event);
} else {
this.boundingBoxTool.onMouseUp(event);
}
diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js
index e059b7f6..3bcbf1b4 100644
--- a/src/helper/selection-tools/selection-box-tool.js
+++ b/src/helper/selection-tools/selection-box-tool.js
@@ -1,5 +1,7 @@
+import paper from '@scratch/paper';
import {rectSelect} from '../guides';
import {clearSelection, processRectangularSelection} from '../selection';
+import {getRaster} from '../layer';
/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */
class SelectionBoxTool {
@@ -29,7 +31,7 @@ class SelectionBoxTool {
// Remove this rect on the next drag and up event
this.selectionRect.removeOnDrag();
}
- onMouseUp (event) {
+ onMouseUpVector (event) {
if (event.event.button > 0) return; // only first mouse button
if (this.selectionRect) {
processRectangularSelection(event, this.selectionRect, this.mode);
@@ -38,6 +40,38 @@ class SelectionBoxTool {
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;
diff --git a/src/lib/modes.js b/src/lib/modes.js
index 1f41016e..5319c0e7 100644
--- a/src/lib/modes.js
+++ b/src/lib/modes.js
@@ -8,6 +8,7 @@ const Modes = keyMirror({
BIT_TEXT: null,
BIT_FILL: null,
BIT_ERASER: null,
+ BIT_SELECT: null,
BRUSH: null,
ERASER: null,
LINE: null,
@@ -27,7 +28,8 @@ const BitmapModes = keyMirror({
BIT_RECT: null,
BIT_TEXT: null,
BIT_FILL: null,
- BIT_ERASER: null
+ BIT_ERASER: null,
+ BIT_SELECT: null
});
export {