From 644655d25efef7c002162092993609147dd1e8d3 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 19 Jun 2018 22:14:15 -0400 Subject: [PATCH 01/13] Bitmap select tool --- .../bit-select-mode/bit-select-mode.jsx | 36 +++-- src/components/paint-editor/paint-editor.jsx | 6 +- src/containers/bit-select-mode.jsx | 92 ++++++++++++ src/containers/paint-editor.jsx | 13 +- src/helper/bit-tools/select-tool.js | 140 ++++++++++++++++++ src/helper/selection-tools/select-tool.js | 4 +- .../selection-tools/selection-box-tool.js | 32 +++- src/lib/modes.js | 4 +- 8 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 src/containers/bit-select-mode.jsx create mode 100644 src/helper/bit-tools/select-tool.js 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/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/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/select-tool.js b/src/helper/bit-tools/select-tool.js new file mode 100644 index 00000000..6f1f742a --- /dev/null +++ b/src/helper/bit-tools/select-tool.js @@ -0,0 +1,140 @@ +import Modes from '../../lib/modes'; + +import {getSelectedLeafItems} from '../selection'; +import {getRaster} from '../layer'; + +import BoundingBoxTool from '../selection-tools/bounding-box-tool'; +import NudgeTool from '../selection-tools/nudge-tool'; +import SelectionBoxTool from '../selection-tools/selection-box-tool'; +import paper from '@scratch/paper'; + +/** + * 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); + } + /** + * Returns the hit options to use when conducting hit tests. + * @param {boolean} preselectedOnly True if we should only return results that are already + * selected. + * @return {object} See paper.Item.hitTest for definition of options + */ + getHitOptions (preselectedOnly) { + // 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. + const hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + tolerance: SelectTool.TOLERANCE / paper.view.zoom + }; + if (preselectedOnly) { + hitOptions.selected = true; + } + return hitOptions; + } + 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(false /* preseelectedOnly */))) { + 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 () { + const selection = getSelectedLeafItems(); + if (selection.length) { + // @todo handle non-rasters? + for (const item of selection) { + if (item instanceof paper.Raster) { + // TODO image smoothing? + getRaster().canvas.drawImage( + item.canvas, + item.bounds.topLeft.x, + item.bounds.topLeft.y, + // Apply transform + ); + item.remove(); + } + } + } + } + deactivateTool () { + this.commitSelection(); + this.boundingBoxTool.removeBoundsPath(); + this.boundingBoxTool = null; + this.selectionBoxTool = null; + } +} + +export default SelectTool; 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..5e047458 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,34 @@ class SelectionBoxTool { this.setSelectedItems(); } } + onMouseUpBitmap (event) { + if (event.event.button > 0) return; // only first mouse button + if (this.selectionRect) { + const rect = new paper.Rectangle( + ~~this.selectionRect.bounds.x, + ~~this.selectionRect.bounds.y, + ~~this.selectionRect.bounds.width, + ~~this.selectionRect.bounds.height, + ); + + if (rect.area) { + // Pull selected raster to active layer + const raster = getRaster().getSubRaster(rect); + raster.parent = paper.project.activeLayer; + raster.selected = true; + this.setSelectedItems(); + + // Clear selection from raster layer + const context = getRaster().canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.clearRect(rect.x, rect.y, rect.width, rect.height); + } + + // Remove dotted rectangle + this.selectionRect.remove(); + this.selectionRect = null; + } + } } 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 { From 9c73d1a7d97ecce93cede07484d600beea486784 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 21 Jun 2018 10:22:24 -0400 Subject: [PATCH 02/13] Add files --- src/helper/bit-tools/line-tool.js | 6 +- src/helper/bit-tools/rect-tool.js | 6 +- src/helper/bit-tools/select-tool.js | 70 +++++++++++++++---- src/helper/bitmap.js | 9 +-- src/helper/layer.js | 21 ++++-- .../selection-tools/selection-box-tool.js | 15 ++-- 6 files changed, 91 insertions(+), 36 deletions(-) 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 49f0cfc6..cbaef41b 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 {drawRect} 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; drawRect(this.rect, context); diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 6f1f742a..0872ee07 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -1,12 +1,14 @@ +import paper from '@scratch/paper'; import Modes from '../../lib/modes'; import {getSelectedLeafItems} from '../selection'; -import {getRaster} from '../layer'; +import {createCanvas, getRaster} from '../layer'; +import {drawRect} from '../bitmap'; +import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; import SelectionBoxTool from '../selection-tools/selection-box-tool'; -import paper from '@scratch/paper'; /** * paper.Tool that handles select mode in bitmap. This is made up of 2 subtools. @@ -113,22 +115,62 @@ class SelectTool extends paper.Tool { } commitSelection () { const selection = getSelectedLeafItems(); - if (selection.length) { + + for (const item of selection) { // @todo handle non-rasters? - for (const item of selection) { - if (item instanceof paper.Raster) { - // TODO image smoothing? - getRaster().canvas.drawImage( - item.canvas, - item.bounds.topLeft.x, - item.bounds.topLeft.y, - // Apply transform - ); - item.remove(); - } + // @todo handle undo state + if (!(item instanceof paper.Raster) && item.data.expanded) continue; + // In the special case that there is no rotation + if (item.matrix.b === 0 && item.matrix.c === 0) { + this.commitScaleTransformation(item); + } else { + this.commitArbitraryTransformation(item); } } } + commitScaleTransformation (item) { + // 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. + + // @todo: Currently, we can't avoid anti-aliasing when the image is both scaled down on both axes and rotated. + let canvas = item.canvas; + if (item.matrix.a !== 1) { + const tmpCanvas = createCanvas(Math.round(item.size.width * item.matrix.a), canvas.height); + const context = tmpCanvas.getContext('2d'); + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + canvas = tmpCanvas; + } + if (item.matrix.d !== 1) { + const tmpCanvas = createCanvas(canvas.width, Math.round(item.size.height * item.matrix.d)); + const context = tmpCanvas.getContext('2d'); + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + canvas = context.canvas; + } + getRaster().drawImage(canvas, item.bounds.topLeft); + item.remove(); + } + 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; + drawRect(rect, context); + 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); + context.transform(1, 0, 0, 1, -item.data.expanded.canvas.width / 2, -item.data.expanded.canvas.height / 2); + context.drawImage(item.data.expanded.canvas, 0, 0); + + // Draw temp canvas onto raster layer + getRaster().canvas.getContext('2d').drawImage(tmpCanvas, 0, 0); + item.remove(); + } deactivateTool () { this.commitSelection(); this.boundingBoxTool.removeBoundsPath(); diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 87e38626..6af4d380 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -529,10 +529,11 @@ const drawRect = function (rect, context) { 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); + Math.round(rect.matrix.tx - (width / 2)), + Math.round(rect.matrix.ty - (height / 2)), + Math.round(width), + Math.round(height) + ); return; } const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2)); 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/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index 5e047458..f112177d 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -44,10 +44,10 @@ class SelectionBoxTool { if (event.event.button > 0) return; // only first mouse button if (this.selectionRect) { const rect = new paper.Rectangle( - ~~this.selectionRect.bounds.x, - ~~this.selectionRect.bounds.y, - ~~this.selectionRect.bounds.width, - ~~this.selectionRect.bounds.height, + Math.round(this.selectionRect.bounds.x), + Math.round(this.selectionRect.bounds.y), + Math.round(this.selectionRect.bounds.width), + Math.round(this.selectionRect.bounds.height), ); if (rect.area) { @@ -55,11 +55,14 @@ class SelectionBoxTool { const raster = getRaster().getSubRaster(rect); raster.parent = paper.project.activeLayer; 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}; this.setSelectedItems(); - // Clear selection from raster layer + // Clear area from raster layer const context = getRaster().canvas.getContext('2d'); - context.imageSmoothingEnabled = false; context.clearRect(rect.x, rect.y, rect.width, rect.height); } From a0faa1418e91395b07c89d36c8f9155e0702eb29 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Jun 2018 10:43:41 -0400 Subject: [PATCH 03/13] Fix flipped resizing --- src/helper/bit-tools/select-tool.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 0872ee07..658afffb 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -137,15 +137,29 @@ class SelectTool extends paper.Tool { // @todo: Currently, we can't avoid anti-aliasing when the image is both scaled down on both axes and rotated. let canvas = item.canvas; if (item.matrix.a !== 1) { - const tmpCanvas = createCanvas(Math.round(item.size.width * item.matrix.a), canvas.height); + const tmpCanvas = createCanvas(Math.round(item.size.width * Math.abs(item.matrix.a)), canvas.height); const context = tmpCanvas.getContext('2d'); - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + if (item.matrix.a < 0) { + context.save(); + context.scale(-1, 1); + context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); + context.restore(); + } else { + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + } canvas = tmpCanvas; } if (item.matrix.d !== 1) { - const tmpCanvas = createCanvas(canvas.width, Math.round(item.size.height * item.matrix.d)); + const tmpCanvas = createCanvas(canvas.width, Math.round(item.size.height * Math.abs(item.matrix.d))); const context = tmpCanvas.getContext('2d'); - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + if (item.matrix.d < 0) { + context.save(); + context.scale(1, -1); + context.drawImage(canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); + context.restore(); + } else { + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + } canvas = context.canvas; } getRaster().drawImage(canvas, item.bounds.topLeft); From 57e3f3c8cb40acc407abca3635a65c5c72e11b90 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 21 Jun 2018 21:04:36 -0400 Subject: [PATCH 04/13] Handle undo --- src/helper/bit-tools/select-tool.js | 10 +++++---- .../selection-tools/bounding-box-tool.js | 21 ++++++++++++------- .../selection-tools/selection-box-tool.js | 3 ++- src/helper/undo.js | 1 + 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 658afffb..a5d79bc9 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -4,7 +4,6 @@ import Modes from '../../lib/modes'; import {getSelectedLeafItems} from '../selection'; import {createCanvas, getRaster} from '../layer'; import {drawRect} from '../bitmap'; -import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; @@ -115,10 +114,9 @@ class SelectTool extends paper.Tool { } commitSelection () { const selection = getSelectedLeafItems(); - + let changed = false; for (const item of selection) { // @todo handle non-rasters? - // @todo handle undo state if (!(item instanceof paper.Raster) && item.data.expanded) continue; // In the special case that there is no rotation if (item.matrix.b === 0 && item.matrix.c === 0) { @@ -126,6 +124,10 @@ class SelectTool extends paper.Tool { } else { this.commitArbitraryTransformation(item); } + changed = true; + } + if (changed) { + this.onUpdateImage(); } } commitScaleTransformation (item) { @@ -182,7 +184,7 @@ class SelectTool extends paper.Tool { context.drawImage(item.data.expanded.canvas, 0, 0); // Draw temp canvas onto raster layer - getRaster().canvas.getContext('2d').drawImage(tmpCanvas, 0, 0); + getRaster().drawImage(tmpCanvas, new paper.Point()); item.remove(); } deactivateTool () { 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/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index f112177d..a89874cf 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -54,16 +54,17 @@ class SelectionBoxTool { // 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}; - this.setSelectedItems(); // Clear area from raster layer const context = getRaster().canvas.getContext('2d'); context.clearRect(rect.x, rect.y, rect.width, rect.height); + this.setSelectedItems(); } // Remove dotted rectangle diff --git a/src/helper/undo.js b/src/helper/undo.js index 671822cb..5ced2ba3 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -16,6 +16,7 @@ const performSnapshot = function (dispatchPerformSnapshot, format) { log.error('Format must be specified.'); } const guideLayers = hideGuideLayers(); + // TODO getRaster.getImageData is returning the correct updated image, but perform snapshot's exportSvg is getting the old raster dispatchPerformSnapshot({ json: paper.project.exportJSON({asString: false}), paintEditorFormat: format From f4df0e07af986d20718afe5c051aa55e650b43ff Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 25 Jun 2018 16:49:28 -0400 Subject: [PATCH 05/13] Redo fill logic --- src/helper/bit-tools/rect-tool.js | 10 +++++----- src/helper/bitmap.js | 32 ++++++++++++++++--------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js index cbaef41b..d5de5ebe 100644 --- a/src/helper/bit-tools/rect-tool.js +++ b/src/helper/bit-tools/rect-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; -import {drawRect} from '../bitmap'; +import {fillRect} from '../bitmap'; import {createCanvas, getRaster} from '../layer'; import {clearSelection} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; @@ -25,7 +25,7 @@ class RectTool extends paper.Tool { 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; @@ -98,7 +98,7 @@ class RectTool extends paper.Tool { 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 { @@ -107,7 +107,7 @@ class RectTool extends paper.Tool { } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - + if (this.isBoundingBoxMode) { this.boundingBoxTool.onMouseUp(event); this.isBoundingBoxMode = null; @@ -134,7 +134,7 @@ class RectTool extends paper.Tool { const tmpCanvas = createCanvas(); const context = tmpCanvas.getContext('2d'); context.fillStyle = this.color; - drawRect(this.rect, context); + fillRect(this.rect, context); getRaster().drawImage(tmpCanvas, new paper.Point()); this.rect.remove(); diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 6af4d380..22ef902b 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -523,7 +523,7 @@ const floodFillAll = function (x, y, color, context) { * @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) { +const fillRect = 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; @@ -541,25 +541,27 @@ const drawRect = function (rect, context) { 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.fillStyle, context); + const points = [startPoint, widthPoint, heightPoint, endPoint].sort((a, b) => a.x - b.x); + + const solveY = (point1, point2, x) => { + if (point2.x === point1.x) return center.x > point1.x ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY; + return ((point2.y - point1.y) / (point2.x - point1.x) * (x - point1.x)) + point1.y; + }; + for (let x = Math.round(points[0].x); x < Math.round(points[3].x); x++) { + const ys = [ + solveY(startPoint, widthPoint, x + .5), + solveY(startPoint, heightPoint, x + .5), + solveY(endPoint, widthPoint, x + .5), + solveY(endPoint, heightPoint, x + .5) + ].sort((a, b) => a - b); + context.fillRect(x, Math.round(ys[1]), 1, Math.max(1, Math.round(ys[2]) - Math.round(ys[1]))); + } }; export { convertToBitmap, convertToVector, - drawRect, + fillRect, floodFill, floodFillAll, getBrushMark, From 8759a3306f215798f43dd922395a8b0459b5ff92 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 25 Jun 2018 19:37:47 -0400 Subject: [PATCH 06/13] Keep scaled down images from becoming blurry --- src/helper/bit-tools/select-tool.js | 83 +++++++++++++++-------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index a5d79bc9..16630204 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -3,7 +3,7 @@ import Modes from '../../lib/modes'; import {getSelectedLeafItems} from '../selection'; import {createCanvas, getRaster} from '../layer'; -import {drawRect} from '../bitmap'; +import {fillRect} from '../bitmap'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; @@ -116,56 +116,61 @@ class SelectTool extends paper.Tool { const selection = getSelectedLeafItems(); let changed = false; for (const item of selection) { - // @todo handle non-rasters? + // @todo should we handle non-rasters (text?) if (!(item instanceof paper.Raster) && item.data.expanded) continue; - // In the special case that there is no rotation - if (item.matrix.b === 0 && item.matrix.c === 0) { - this.commitScaleTransformation(item); - } else { - this.commitArbitraryTransformation(item); - } + this.maybeApplyScaleToCanvas(item); + this.commitArbitraryTransformation(item); changed = true; } if (changed) { this.onUpdateImage(); } } - commitScaleTransformation (item) { + 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. - - // @todo: Currently, we can't avoid anti-aliasing when the image is both scaled down on both axes and rotated. - let canvas = item.canvas; - if (item.matrix.a !== 1) { - const tmpCanvas = createCanvas(Math.round(item.size.width * Math.abs(item.matrix.a)), canvas.height); - const context = tmpCanvas.getContext('2d'); - if (item.matrix.a < 0) { - context.save(); - context.scale(-1, 1); - context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); - context.restore(); - } else { - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); - } - canvas = tmpCanvas; + const decomposed = item.matrix.decompose(); + if (Math.abs(decomposed.scaling.x) < 1 && Math.abs(decomposed.scaling.y) < 1) { + this.scaleCanvas(item, decomposed.scaling); + this.scaleCanvas(item.data.expanded, decomposed.scaling); + const matrix = new paper.Matrix() + .translate(decomposed.translation) + .rotate(decomposed.rotation) + .skew(decomposed.skewing); + item.matrix = matrix; } - if (item.matrix.d !== 1) { - const tmpCanvas = createCanvas(canvas.width, Math.round(item.size.height * Math.abs(item.matrix.d))); - const context = tmpCanvas.getContext('2d'); - if (item.matrix.d < 0) { - context.save(); - context.scale(1, -1); - context.drawImage(canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); - context.restore(); - } else { - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); - } - canvas = context.canvas; + } + scaleCanvas (raster, scale) { + let canvas = raster.canvas; + let tmpCanvas = createCanvas(Math.round(raster.size.width * Math.abs(scale.x)), canvas.height); + let context = tmpCanvas.getContext('2d'); + if (scale.x < 0) { + context.save(); + context.scale(-1, 1); + context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); + context.restore(); + } else { + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); } - getRaster().drawImage(canvas, item.bounds.topLeft); - item.remove(); + canvas = tmpCanvas; + tmpCanvas = createCanvas(canvas.width, Math.round(raster.size.height * Math.abs(scale.y))); + context = tmpCanvas.getContext('2d'); + if (scale.y < 0) { + context.save(); + context.scale(1, -1); + context.drawImage(canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); + context.restore(); + } else { + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + } + raster.canvas = tmpCanvas; } commitArbitraryTransformation (item) { // Create a canvas to perform masking @@ -174,7 +179,7 @@ class SelectTool extends paper.Tool { // Draw mask const rect = new paper.Shape.Rectangle(new paper.Point(), item.size); rect.matrix = item.matrix; - drawRect(rect, context); + fillRect(rect, context); context.globalCompositeOperation = 'source-in'; // Draw image onto mask From 7aa74d7ffdcc51cfae59a563c0a9f3431508b36d Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 26 Jun 2018 16:04:38 -0400 Subject: [PATCH 07/13] wip --- src/helper/bit-tools/select-tool.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 16630204..c4866da6 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -142,8 +142,8 @@ class SelectTool extends paper.Tool { this.scaleCanvas(item.data.expanded, decomposed.scaling); const matrix = new paper.Matrix() .translate(decomposed.translation) - .rotate(decomposed.rotation) - .skew(decomposed.skewing); + .skew(decomposed.skewing) + .rotate(decomposed.rotation); item.matrix = matrix; } } @@ -193,7 +193,7 @@ class SelectTool extends paper.Tool { item.remove(); } deactivateTool () { - this.commitSelection(); + this.commitSelection(); // TODO this.boundingBoxTool.removeBoundsPath(); this.boundingBoxTool = null; this.selectionBoxTool = null; From ddca91a56744a4b67e5e8242dad78447ba70f4b4 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 27 Jun 2018 15:29:13 -0400 Subject: [PATCH 08/13] Fix the undo bug, but this time it's more real --- src/helper/selection-tools/selection-box-tool.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index a89874cf..3bcbf1b4 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -50,6 +50,10 @@ class SelectionBoxTool { 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); @@ -62,14 +66,10 @@ class SelectionBoxTool { raster.data = {expanded: expanded}; // Clear area from raster layer - const context = getRaster().canvas.getContext('2d'); + const context = getRaster().getContext(true /* modify */); context.clearRect(rect.x, rect.y, rect.width, rect.height); this.setSelectedItems(); } - - // Remove dotted rectangle - this.selectionRect.remove(); - this.selectionRect = null; } } } From a04088898e6c256e944f552945269b86bafc63f8 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 27 Jun 2018 23:42:29 -0400 Subject: [PATCH 09/13] keep reference to selection --- src/helper/bit-tools/select-tool.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index c4866da6..9a61a939 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -1,7 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; -import {getSelectedLeafItems} from '../selection'; import {createCanvas, getRaster} from '../layer'; import {fillRect} from '../bitmap'; @@ -52,6 +51,14 @@ class SelectTool extends paper.Tool { */ onSelectionChanged (selectedItems) { this.boundingBoxTool.onSelectionChanged(selectedItems); + if ((!this.selection || !this.selection.parent) && + selectedItems && selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) { + // Infer that an undo occurred and get back the active selection + this.selection = selectedItems[0]; + } else if (this.selection && this.selection.parent && !this.selection.selected) { + // Selection got deselected + this.commitSelection(); + } } /** * Returns the hit options to use when conducting hit tests. @@ -113,18 +120,11 @@ class SelectTool extends paper.Tool { this.active = false; } commitSelection () { - const selection = getSelectedLeafItems(); - let changed = false; - for (const item of selection) { - // @todo should we handle non-rasters (text?) - if (!(item instanceof paper.Raster) && item.data.expanded) continue; - this.maybeApplyScaleToCanvas(item); - this.commitArbitraryTransformation(item); - changed = true; - } - if (changed) { - this.onUpdateImage(); - } + if (!this.selection || !this.selection.parent) return; + + this.maybeApplyScaleToCanvas(this.selection); + this.commitArbitraryTransformation(this.selection); + this.onUpdateImage(); } maybeApplyScaleToCanvas (item) { if (!item.matrix.isInvertible()) { @@ -193,7 +193,7 @@ class SelectTool extends paper.Tool { item.remove(); } deactivateTool () { - this.commitSelection(); // TODO + this.commitSelection(); this.boundingBoxTool.removeBoundsPath(); this.boundingBoxTool = null; this.selectionBoxTool = null; From eed84d8cf2d40dc90a70afa60dfe7ee2adff11bb Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 28 Jun 2018 00:21:01 -0400 Subject: [PATCH 10/13] Handle flip in bitmap --- src/components/mode-tools/mode-tools.jsx | 2 ++ src/containers/mode-tools.jsx | 25 +++++++++++++++++++---- src/helper/bit-tools/select-tool.js | 25 +++++++++++------------ src/helper/bitmap.js | 26 ++++++++++++++++++++++-- 4 files changed, 59 insertions(+), 19 deletions(-) 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/containers/mode-tools.jsx b/src/containers/mode-tools.jsx index 3b9df8fe..1930c7a8 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()); + 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()); + this.props.onUpdateImage(); + } else { + this._handleFlip(1, -1, selectedItems); + } } handleCopyToClipboard () { const selectedItems = getSelectedRootItems(); @@ -217,6 +232,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 +245,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/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 9a61a939..f974f760 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -2,7 +2,7 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; import {createCanvas, getRaster} from '../layer'; -import {fillRect} from '../bitmap'; +import {fillRect, flipBitmapHorizontal, flipBitmapVertical} from '../bitmap'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; @@ -144,6 +144,13 @@ class SelectTool extends paper.Tool { .translate(decomposed.translation) .skew(decomposed.skewing) .rotate(decomposed.rotation); + console.log(item.matrix); + const composed = new paper.Matrix() + .scale(decomposed.scale) + .translate(decomposed.translation) + .skew(decomposed.skewing) + .rotate(decomposed.rotation); + console.log(composed); item.matrix = matrix; } } @@ -152,24 +159,16 @@ class SelectTool extends paper.Tool { let tmpCanvas = createCanvas(Math.round(raster.size.width * Math.abs(scale.x)), canvas.height); let context = tmpCanvas.getContext('2d'); if (scale.x < 0) { - context.save(); - context.scale(-1, 1); - context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); - context.restore(); - } else { - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + canvas = flipBitmapHorizontal(canvas); } + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); canvas = tmpCanvas; tmpCanvas = createCanvas(canvas.width, Math.round(raster.size.height * Math.abs(scale.y))); context = tmpCanvas.getContext('2d'); if (scale.y < 0) { - context.save(); - context.scale(1, -1); - context.drawImage(canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); - context.restore(); - } else { - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); + canvas = flipBitmapVertical(canvas); } + context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); raster.canvas = tmpCanvas; } commitArbitraryTransformation (item) { diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index 22ef902b..ac79b803 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,26 @@ const fillRect = function (rect, context) { } }; +const flipBitmapHorizontal = function (raster) { + const tmpCanvas = createCanvas(raster.size.width, raster.size.height); + const context = tmpCanvas.getContext('2d'); + context.save(); + context.scale(-1, 1); + context.drawImage(raster.canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); + context.restore(); + return tmpCanvas; +}; + +const flipBitmapVertical = function (raster) { + const tmpCanvas = createCanvas(raster.size.width, raster.size.height); + const context = tmpCanvas.getContext('2d'); + context.save(); + context.scale(1, -1); + context.drawImage(raster.canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); + context.restore(); + return tmpCanvas; +}; + export { convertToBitmap, convertToVector, @@ -567,5 +587,7 @@ export { getBrushMark, getHitBounds, drawEllipse, - forEachLinePoint + forEachLinePoint, + flipBitmapHorizontal, + flipBitmapVertical }; From 0483d654137bac508db98d487eb4538ac97ef5ed Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 28 Jun 2018 01:54:05 -0400 Subject: [PATCH 11/13] Fix paste and transform bugs --- src/containers/mode-tools.jsx | 4 +-- src/helper/bit-tools/select-tool.js | 55 +++++++++-------------------- src/helper/bitmap.js | 30 ++++++++++++---- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx index 1930c7a8..fddbcfa2 100644 --- a/src/containers/mode-tools.jsx +++ b/src/containers/mode-tools.jsx @@ -168,7 +168,7 @@ class ModeTools extends React.Component { handleFlipHorizontal () { const selectedItems = getSelectedRootItems(); if (isBitmap(this.props.format) && selectedItems.length === 0) { - getRaster().canvas = flipBitmapHorizontal(getRaster()); + getRaster().canvas = flipBitmapHorizontal(getRaster().canvas); this.props.onUpdateImage(); } else { this._handleFlip(-1, 1, selectedItems); @@ -177,7 +177,7 @@ class ModeTools extends React.Component { handleFlipVertical () { const selectedItems = getSelectedRootItems(); if (isBitmap(this.props.format) && selectedItems.length === 0) { - getRaster().canvas = flipBitmapVertical(getRaster()); + getRaster().canvas = flipBitmapVertical(getRaster().canvas); this.props.onUpdateImage(); } else { this._handleFlip(1, -1, selectedItems); diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index f974f760..22c1f7ec 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -2,7 +2,7 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; import {createCanvas, getRaster} from '../layer'; -import {fillRect, flipBitmapHorizontal, flipBitmapVertical} from '../bitmap'; +import {fillRect, scaleBitmap} from '../bitmap'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; @@ -51,14 +51,15 @@ class SelectTool extends paper.Tool { */ onSelectionChanged (selectedItems) { this.boundingBoxTool.onSelectionChanged(selectedItems); - if ((!this.selection || !this.selection.parent) && - selectedItems && selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) { - // Infer that an undo occurred and get back the active selection - this.selection = selectedItems[0]; - } else if (this.selection && this.selection.parent && !this.selection.selected) { + 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. @@ -136,41 +137,16 @@ class SelectTool extends paper.Tool { // 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(); - if (Math.abs(decomposed.scaling.x) < 1 && Math.abs(decomposed.scaling.y) < 1) { - this.scaleCanvas(item, decomposed.scaling); - this.scaleCanvas(item.data.expanded, decomposed.scaling); - const matrix = new paper.Matrix() - .translate(decomposed.translation) - .skew(decomposed.skewing) - .rotate(decomposed.rotation); - console.log(item.matrix); - const composed = new paper.Matrix() - .scale(decomposed.scale) - .translate(decomposed.translation) - .skew(decomposed.skewing) - .rotate(decomposed.rotation); - console.log(composed); - item.matrix = matrix; + 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); + 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))); } } - scaleCanvas (raster, scale) { - let canvas = raster.canvas; - let tmpCanvas = createCanvas(Math.round(raster.size.width * Math.abs(scale.x)), canvas.height); - let context = tmpCanvas.getContext('2d'); - if (scale.x < 0) { - canvas = flipBitmapHorizontal(canvas); - } - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); - canvas = tmpCanvas; - tmpCanvas = createCanvas(canvas.width, Math.round(raster.size.height * Math.abs(scale.y))); - context = tmpCanvas.getContext('2d'); - if (scale.y < 0) { - canvas = flipBitmapVertical(canvas); - } - context.drawImage(canvas, 0, 0, tmpCanvas.width, tmpCanvas.height); - raster.canvas = tmpCanvas; - } commitArbitraryTransformation (item) { // Create a canvas to perform masking const tmpCanvas = createCanvas(); @@ -190,6 +166,7 @@ class SelectTool extends paper.Tool { // Draw temp canvas onto raster layer getRaster().drawImage(tmpCanvas, new paper.Point()); item.remove(); + this.selection = null; } deactivateTool () { this.commitSelection(); diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index ac79b803..c13463c9 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -558,26 +558,41 @@ const fillRect = function (rect, context) { } }; -const flipBitmapHorizontal = function (raster) { - const tmpCanvas = createCanvas(raster.size.width, raster.size.height); +const flipBitmapHorizontal = function (canvas) { + const tmpCanvas = createCanvas(canvas.width, canvas.height); const context = tmpCanvas.getContext('2d'); context.save(); context.scale(-1, 1); - context.drawImage(raster.canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); + context.drawImage(canvas, 0, 0, -tmpCanvas.width, tmpCanvas.height); context.restore(); return tmpCanvas; }; -const flipBitmapVertical = function (raster) { - const tmpCanvas = createCanvas(raster.size.width, raster.size.height); +const flipBitmapVertical = function (canvas) { + const tmpCanvas = createCanvas(canvas.width, canvas.height); const context = tmpCanvas.getContext('2d'); context.save(); context.scale(1, -1); - context.drawImage(raster.canvas, 0, 0, tmpCanvas.width, -tmpCanvas.height); + 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, @@ -589,5 +604,6 @@ export { drawEllipse, forEachLinePoint, flipBitmapHorizontal, - flipBitmapVertical + flipBitmapVertical, + scaleBitmap }; From d7bd6280edc2ef01004ad92d96be24e43fe3f083 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 28 Jun 2018 02:04:24 -0400 Subject: [PATCH 12/13] clean up --- src/helper/bit-tools/select-tool.js | 12 +++--------- src/helper/undo.js | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 22c1f7ec..580ae578 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -63,14 +63,12 @@ class SelectTool extends paper.Tool { } /** * Returns the hit options to use when conducting hit tests. - * @param {boolean} preselectedOnly True if we should only return results that are already - * selected. * @return {object} See paper.Item.hitTest for definition of options */ - getHitOptions (preselectedOnly) { + 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. - const hitOptions = { + return { segments: true, stroke: true, curves: true, @@ -78,10 +76,6 @@ class SelectTool extends paper.Tool { guide: false, tolerance: SelectTool.TOLERANCE / paper.view.zoom }; - if (preselectedOnly) { - hitOptions.selected = true; - } - return hitOptions; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button @@ -94,7 +88,7 @@ class SelectTool extends paper.Tool { event, event.modifiers.alt, event.modifiers.shift, - this.getHitOptions(false /* preseelectedOnly */))) { + this.getHitOptions())) { this.commitSelection(); this.selectionBoxMode = true; this.selectionBoxTool.onMouseDown(event.modifiers.shift); diff --git a/src/helper/undo.js b/src/helper/undo.js index 5ced2ba3..671822cb 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -16,7 +16,6 @@ const performSnapshot = function (dispatchPerformSnapshot, format) { log.error('Format must be specified.'); } const guideLayers = hideGuideLayers(); - // TODO getRaster.getImageData is returning the correct updated image, but perform snapshot's exportSvg is getting the old raster dispatchPerformSnapshot({ json: paper.project.exportJSON({asString: false}), paintEditorFormat: format From e215b8a9b8e035701e38f245566b9311922e1d3a Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 28 Jun 2018 17:00:44 -0400 Subject: [PATCH 13/13] Fix issue with pasting vector into bitmap --- src/containers/mode-tools.jsx | 13 ++++++++++++- src/helper/bit-tools/select-tool.js | 12 +++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx index fddbcfa2..d252e38c 100644 --- a/src/containers/mode-tools.jsx +++ b/src/containers/mode-tools.jsx @@ -198,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; } diff --git a/src/helper/bit-tools/select-tool.js b/src/helper/bit-tools/select-tool.js index 73699799..b6e5d0be 100644 --- a/src/helper/bit-tools/select-tool.js +++ b/src/helper/bit-tools/select-tool.js @@ -135,7 +135,9 @@ class SelectTool extends paper.Tool { 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); - item.data.expanded.canvas = scaleBitmap(item.data.expanded.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))); @@ -155,8 +157,12 @@ class SelectTool extends paper.Tool { // Draw image onto mask const m = item.matrix; context.transform(m.a, m.b, m.c, m.d, m.tx, m.ty); - context.transform(1, 0, 0, 1, -item.data.expanded.canvas.width / 2, -item.data.expanded.canvas.height / 2); - context.drawImage(item.data.expanded.canvas, 0, 0); + 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());