From 0bf2355cbc43bb51b4eb2408c0721595d7dfc32a Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 4 Apr 2018 18:27:55 -0400 Subject: [PATCH 01/14] Make raster not blurry on zoom --- src/containers/paper-canvas.css | 3 --- src/containers/paper-canvas.jsx | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index 65a26ef0..aa352c6a 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -4,7 +4,4 @@ margin: auto; position: absolute; background-color: #fff; - /* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches - back and forth from aliased to unaliased and that looks bad */ - image-rendering: pixelated; } diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index bac7ed42..026f446c 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -33,6 +33,12 @@ class PaperCanvas extends React.Component { componentDidMount () { document.addEventListener('keydown', this.handleKeyDown); paper.setup(this.canvas); + + const context = this.canvas.getContext('2d'); + context.webkitImageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + context.imageSmoothingEnabled = false; + // Don't show handles by default paper.settings.handleSize = 0; // Make layers. From 82d2e30cae8dbceb840013f83be80170a14ee499 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Apr 2018 17:37:40 -0400 Subject: [PATCH 02/14] Add raster layer --- src/helper/layer.js | 22 ++++++++++++++++++++++ src/helper/transparent.png | Bin 0 -> 414 bytes 2 files changed, 22 insertions(+) create mode 100644 src/helper/transparent.png diff --git a/src/helper/layer.js b/src/helper/layer.js index 61473824..e665e9b4 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -1,5 +1,6 @@ import paper from '@scratch/paper'; import canvasBg from './background.png'; +import rasterSrc from './transparent.png'; import log from '../log/log'; const _getLayer = function (layerString) { @@ -15,6 +16,19 @@ const _getPaintingLayer = function () { return _getLayer('isPaintingLayer'); }; +const getRaster = function () { + const layer = _getLayer('isRasterLayer'); + // Generate blank raster + if (layer.children.length === 0) { + const raster = new paper.Raster(rasterSrc); + raster.parent = layer; + raster.guide = true; + raster.locked = true; + raster.position = paper.view.center; + } + return _getLayer('isRasterLayer').children[0]; +}; + const _getBackgroundGuideLayer = function () { return _getLayer('isBackgroundGuideLayer'); }; @@ -72,6 +86,12 @@ const _makePaintingLayer = function () { return paintingLayer; }; +const _makeRasterLayer = function () { + const rasterLayer = new paper.Layer(); + rasterLayer.data.isRasterLayer = true; + return rasterLayer; +}; + const _makeBackgroundGuideLayer = function () { const guideLayer = new paper.Layer(); guideLayer.locked = true; @@ -113,6 +133,7 @@ const _makeBackgroundGuideLayer = function () { const setupLayers = function () { const backgroundGuideLayer = _makeBackgroundGuideLayer(); + _makeRasterLayer(); const paintLayer = _makePaintingLayer(); const guideLayer = _makeGuideLayer(); backgroundGuideLayer.sendToBack(); @@ -124,5 +145,6 @@ export { hideGuideLayers, showGuideLayers, getGuideLayer, + getRaster, setupLayers }; diff --git a/src/helper/transparent.png b/src/helper/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..ebfbd78d7bb185a8b821b5242930fcff0712ca04 GIT binary patch literal 414 zcmeAS@N?(olHy`uVBq!ia0y~yV0-|?85}G?5mmbftAU(UPZ!6Kid%0FGBN@Mn-u=9 j&+~8qF-HN_LZC*{eKP~&`EKLuAR|3p{an^LB{Ts54Luau literal 0 HcmV?d00001 From c6d886ff1e421df1d76cb3a8456c1e190a32d4cb Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Apr 2018 17:51:10 -0400 Subject: [PATCH 03/14] Re-enable convert to bitmap button --- src/components/paint-editor/icons/bitmap.svg | 2 +- src/components/paint-editor/paint-editor.css | 10 ++++++-- src/components/paint-editor/paint-editor.jsx | 27 +++++++++----------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/paint-editor/icons/bitmap.svg b/src/components/paint-editor/icons/bitmap.svg index 8b07f54e..47832b0c 100644 --- a/src/components/paint-editor/icons/bitmap.svg +++ b/src/components/paint-editor/icons/bitmap.svg @@ -5,7 +5,7 @@ Created with Sketch. - + diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index 38ca5063..087c1e82 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -179,13 +179,19 @@ $border-radius: 0.25rem; .bitmap-button { display: flex; border-radius: 5px; - background-color: hsla(0, 0%, 0%, .25); + background-color: $motion-primary; padding: calc(2 * $grid-unit); line-height: 1.5rem; font-size: calc(3 * $grid-unit); font-weight: bold; + color: white; justify-content: center; - opacity: .5; + opacity: .75; +} + +.bitmap-button:active { + background-color: $motion-primary; + opacity: 1; } .bitmap-button-icon { diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 72297011..534000d0 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -403,22 +403,19 @@ const PaintEditorComponent = props => { }
- -
- - - {props.intl.formatMessage(messages.bitmap)} - -
-
+ + + {props.intl.formatMessage(messages.bitmap)} + + {/* Zoom controls */} From e6b151c41fba3554172886feaa3b022c04f82cf6 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Apr 2018 17:51:10 -0400 Subject: [PATCH 04/14] Re-enable convert to bitmap button --- src/components/paint-editor/icons/bitmap.svg | 2 +- src/components/paint-editor/paint-editor.css | 10 +++++-- src/components/paint-editor/paint-editor.jsx | 28 +++++++++----------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/paint-editor/icons/bitmap.svg b/src/components/paint-editor/icons/bitmap.svg index 8b07f54e..47832b0c 100644 --- a/src/components/paint-editor/icons/bitmap.svg +++ b/src/components/paint-editor/icons/bitmap.svg @@ -5,7 +5,7 @@ Created with Sketch. - + diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index 38ca5063..087c1e82 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -179,13 +179,19 @@ $border-radius: 0.25rem; .bitmap-button { display: flex; border-radius: 5px; - background-color: hsla(0, 0%, 0%, .25); + background-color: $motion-primary; padding: calc(2 * $grid-unit); line-height: 1.5rem; font-size: calc(3 * $grid-unit); font-weight: bold; + color: white; justify-content: center; - opacity: .5; + opacity: .75; +} + +.bitmap-button:active { + background-color: $motion-primary; + opacity: 1; } .bitmap-button-icon { diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 72297011..a323debd 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -15,7 +15,6 @@ import Button from '../button/button.jsx'; import ButtonGroup from '../button-group/button-group.jsx'; import BrushMode from '../../containers/brush-mode.jsx'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; -import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Dropdown from '../dropdown/dropdown.jsx'; import EraserMode from '../../containers/eraser-mode.jsx'; import FillColorIndicatorComponent from '../../containers/fill-color-indicator.jsx'; @@ -403,22 +402,19 @@ const PaintEditorComponent = props => { }
- -
- - - {props.intl.formatMessage(messages.bitmap)} - -
-
+ + + {props.intl.formatMessage(messages.bitmap)} + + {/* Zoom controls */} From 2b8c29176543763a866c723e3e96e6eb847eae7d Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 5 Apr 2018 18:06:37 -0400 Subject: [PATCH 05/14] Add format reducer --- src/lib/format.js | 8 +++++++ src/reducers/format.js | 32 +++++++++++++++++++++++++++ src/reducers/scratch-paint-reducer.js | 2 ++ test/unit/format-reducer.test.js | 24 ++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 src/lib/format.js create mode 100644 src/reducers/format.js create mode 100644 test/unit/format-reducer.test.js diff --git a/src/lib/format.js b/src/lib/format.js new file mode 100644 index 00000000..6748d0da --- /dev/null +++ b/src/lib/format.js @@ -0,0 +1,8 @@ +import keyMirror from 'keymirror'; + +const Formats = keyMirror({ + BITMAP: null, + VECTOR: null +}); + +export default Formats; diff --git a/src/reducers/format.js b/src/reducers/format.js new file mode 100644 index 00000000..a8a2f7c4 --- /dev/null +++ b/src/reducers/format.js @@ -0,0 +1,32 @@ +import Formats from '../lib/format'; +import log from '../log/log'; + +const CHANGE_FORMAT = 'scratch-paint/formats/CHANGE_FORMAT'; +const initialState = Formats.VECTOR; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_FORMAT: + if (action.format in Formats) { + return action.format; + } + log.warn(`Format does not exist: ${action.format}`); + /* falls through */ + default: + return state; + } +}; + +// Action creators ================================== +const changeFormat = function (format) { + return { + type: CHANGE_FORMAT, + format: format + }; +}; + +export { + reducer as default, + changeFormat +}; diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 258dea9e..224dcad7 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -4,6 +4,7 @@ import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; import clipboardReducer from './clipboard'; +import formatReducer from './format'; import hoverReducer from './hover'; import modalsReducer from './modals'; import selectedItemReducer from './selected-items'; @@ -17,6 +18,7 @@ export default combineReducers({ color: colorReducer, clipboard: clipboardReducer, eraserMode: eraserModeReducer, + format: formatReducer, hoveredItemId: hoverReducer, modals: modalsReducer, selectedItems: selectedItemReducer, diff --git a/test/unit/format-reducer.test.js b/test/unit/format-reducer.test.js new file mode 100644 index 00000000..5b12ca43 --- /dev/null +++ b/test/unit/format-reducer.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ +import Formats from '../../src/lib/format'; +import reducer from '../../src/reducers/format'; +import {changeFormat} from '../../src/reducers/format'; + +test('initialState', () => { + let defaultState; + expect(reducer(defaultState /* state */, {type: 'anything'} /* action */) in Formats).toBeTruthy(); +}); + +test('changeFormat', () => { + let defaultState; + expect(reducer(defaultState /* state */, changeFormat(Formats.BITMAP) /* action */)).toBe(Formats.BITMAP); + expect(reducer(Formats.BITMAP /* state */, changeFormat(Formats.BITMAP) /* action */)) + .toBe(Formats.BITMAP); + expect(reducer(Formats.BITMAP /* state */, changeFormat(Formats.VECTOR) /* action */)) + .toBe(Formats.VECTOR); +}); + +test('invalidChangeMode', () => { + expect(reducer(Formats.BITMAP /* state */, changeFormat('non-existant mode') /* action */)) + .toBe(Formats.BITMAP); + expect(reducer(Formats.BITMAP /* state */, changeFormat() /* action */)).toBe(Formats.BITMAP); +}); From f995f30e370c0eff71a00eeba9abdc4ea663b5ff Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 6 Apr 2018 10:35:01 -0400 Subject: [PATCH 06/14] Button toggles on switch format --- src/components/paint-editor/paint-editor.jsx | 50 +++++++++++++++----- src/containers/paint-editor.jsx | 15 ++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index a323debd..ed373319 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -34,6 +34,7 @@ import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicat import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx'; import TextMode from '../../containers/text-mode.jsx'; +import Formats from '../../lib/format'; import layout from '../../lib/layout-constants'; import styles from './paint-editor.css'; @@ -106,6 +107,11 @@ const messages = defineMessages({ defaultMessage: 'Convert to Bitmap', description: 'Label for button that converts the paint editor to bitmap mode', id: 'paint.paintEditor.bitmap' + }, + vector: { + defaultMessage: 'Convert to Vector', + description: 'Label for button that converts the paint editor to vector mode', + id: 'paint.paintEditor.vector' } }); @@ -402,19 +408,34 @@ const PaintEditorComponent = props => { }
- + {props.format === Formats.VECTOR ? + : + + } {/* Zoom controls */} @@ -465,6 +486,7 @@ PaintEditorComponent.propTypes = { canUndo: PropTypes.func.isRequired, canvas: PropTypes.instanceOf(Element), colorInfo: Loupe.propTypes.colorInfo, + format: PropTypes.oneOf(Object.keys(Formats)).isRequired, intl: intlShape, isEyeDropping: PropTypes.bool, name: PropTypes.string, @@ -474,6 +496,8 @@ PaintEditorComponent.propTypes = { onSendForward: PropTypes.func.isRequired, onSendToBack: PropTypes.func.isRequired, onSendToFront: PropTypes.func.isRequired, + onSwitchToBitmap: PropTypes.func.isRequired, + onSwitchToVector: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired, onUngroup: PropTypes.func.isRequired, onUpdateName: PropTypes.func.isRequired, diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 11f9e267..932293b7 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -4,6 +4,7 @@ import React from 'react'; import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; +import {changeFormat} from '../reducers/format'; import {undo, redo, undoSnapshot} from '../reducers/undo'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {deactivateEyeDropper} from '../reducers/eye-dropper'; @@ -19,6 +20,7 @@ import {resetZoom, zoomOnSelection} from '../helper/view'; import EyeDropperTool from '../helper/tools/eye-dropper'; import Modes from '../lib/modes'; +import Formats from '../lib/format'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; @@ -231,6 +233,7 @@ class PaintEditor extends React.Component { canUndo={this.canUndo} canvas={this.state.canvas} colorInfo={this.state.colorInfo} + format={this.props.format} isEyeDropping={this.props.isEyeDropping} name={this.props.name} rotationCenterX={this.props.rotationCenterX} @@ -246,6 +249,8 @@ class PaintEditor extends React.Component { onSendForward={this.handleSendForward} onSendToBack={this.handleSendToBack} onSendToFront={this.handleSendToFront} + onSwitchToBitmap={this.props.handleSwitchToBitmap} + onSwitchToVector={this.props.handleSwitchToVector} onUndo={this.handleUndo} onUngroup={this.handleUngroup} onUpdateName={this.props.onUpdateName} @@ -261,6 +266,9 @@ class PaintEditor extends React.Component { PaintEditor.propTypes = { changeColorToEyeDropper: PropTypes.func, clearSelectedItems: PropTypes.func.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)).isRequired, + handleSwitchToBitmap: PropTypes.func.isRequired, + handleSwitchToVector: PropTypes.func.isRequired, isEyeDropping: PropTypes.bool, name: PropTypes.string, onDeactivateEyeDropper: PropTypes.func.isRequired, @@ -291,6 +299,7 @@ PaintEditor.propTypes = { const mapStateToProps = state => ({ changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback, clipboardItems: state.scratchPaint.clipboard.items, + format: state.scratchPaint.format, isEyeDropping: state.scratchPaint.color.eyeDropper.active, pasteOffset: state.scratchPaint.clipboard.pasteOffset, previousTool: state.scratchPaint.color.eyeDropper.previousTool, @@ -323,6 +332,12 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, + handleSwitchToBitmap: () => { + dispatch(changeFormat(Formats.BITMAP)); + }, + handleSwitchToVector: () => { + dispatch(changeFormat(Formats.VECTOR)); + }, removeTextEditTarget: () => { dispatch(setTextEditTarget()); }, From 94ae07f1f207659b7719b22b78b4306128e2786f Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 6 Apr 2018 14:41:30 -0400 Subject: [PATCH 07/14] Bitmap button rasterizes --- src/containers/paper-canvas.jsx | 73 ++++++++++++++++++++++----------- src/helper/layer.js | 21 ++++++---- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 026f446c..4da7a5b6 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -3,13 +3,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import paper from '@scratch/paper'; +import Formats from '../lib/format'; import Modes from '../lib/modes'; import log from '../log/log'; import {performSnapshot} from '../helper/undo'; import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; -import {setupLayers} from '../helper/layer'; +import {clearRaster, getRaster, setupLayers} from '../helper/layer'; import {deleteSelection, getSelectedLeafItems} from '../helper/selection'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view'; @@ -24,10 +25,12 @@ class PaperCanvas extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'convertToBitmap', 'setCanvas', 'importSvg', 'handleKeyDown', - 'handleWheel' + 'handleWheel', + 'switchCostume' ]); } componentDidMount () { @@ -36,7 +39,6 @@ class PaperCanvas extends React.Component { const context = this.canvas.getContext('2d'); context.webkitImageSmoothingEnabled = false; - context.mozImageSmoothingEnabled = false; context.imageSmoothingEnabled = false; // Don't show handles by default @@ -50,27 +52,12 @@ class PaperCanvas extends React.Component { } } componentWillReceiveProps (newProps) { - if (this.props.svgId === newProps.svgId) return; - for (const layer of paper.project.layers) { - if (!layer.data.isBackgroundGuideLayer) { - layer.removeChildren(); - } - } - this.props.clearUndo(); - this.props.clearSelectedItems(); - this.props.clearHoveredItem(); - this.props.clearPasteOffset(); - if (newProps.svg) { - // Store the zoom/pan and restore it after importing a new SVG - const oldZoom = paper.project.view.zoom; - const oldCenter = paper.project.view.center.clone(); - resetZoom(); - this.props.updateViewBounds(paper.view.matrix); - this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY); - paper.project.view.zoom = oldZoom; - paper.project.view.center = oldCenter; - } else { - performSnapshot(this.props.undoSnapshot); + if (this.props.svgId !== newProps.svgId) { + this.switchCostume(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY); + } else if (this.props.format === Formats.VECTOR && newProps.format === Formats.BITMAP) { + this.convertToBitmap(); + } else if (this.props.format === Formats.BITMAP && newProps.format === Formats.VECTOR) { + // do vector conversion } } componentWillUnmount () { @@ -89,6 +76,40 @@ class PaperCanvas extends React.Component { } } } + convertToBitmap () { + const raster = paper.project.activeLayer.rasterize(72, false /* insert */); + raster.onLoad = function () { + const subCanvas = raster.canvas; + getRaster().drawImage(subCanvas, raster.bounds.topLeft); + }; + paper.project.activeLayer.removeChildren(); + performSnapshot(this.props.undoSnapshot); + } + switchCostume (svg, rotationCenterX, rotationCenterY) { + for (const layer of paper.project.layers) { + if (layer.data.isRasterLayer) { + clearRaster(); + } else if (!layer.data.isBackgroundGuideLayer) { + layer.removeChildren(); + } + } + this.props.clearUndo(); + this.props.clearSelectedItems(); + this.props.clearHoveredItem(); + this.props.clearPasteOffset(); + if (svg) { + // Store the zoom/pan and restore it after importing a new SVG + const oldZoom = paper.project.view.zoom; + const oldCenter = paper.project.view.center.clone(); + resetZoom(); + this.props.updateViewBounds(paper.view.matrix); + this.importSvg(svg, rotationCenterX, rotationCenterY); + paper.project.view.zoom = oldZoom; + paper.project.view.center = oldCenter; + } else { + performSnapshot(this.props.undoSnapshot); + } + } importSvg (svg, rotationCenterX, rotationCenterY) { const paperCanvas = this; // Pre-process SVG to prevent parsing errors (discussion from #213) @@ -219,6 +240,7 @@ PaperCanvas.propTypes = { clearPasteOffset: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)).isRequired, mode: PropTypes.oneOf(Object.keys(Modes)), onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, @@ -230,7 +252,8 @@ PaperCanvas.propTypes = { updateViewBounds: PropTypes.func.isRequired }; const mapStateToProps = state => ({ - mode: state.scratchPaint.mode + mode: state.scratchPaint.mode, + format: state.scratchPaint.format }); const mapDispatchToProps = dispatch => ({ undoSnapshot: snapshot => { diff --git a/src/helper/layer.js b/src/helper/layer.js index e665e9b4..cd206c07 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -16,16 +16,19 @@ const _getPaintingLayer = function () { return _getLayer('isPaintingLayer'); }; -const getRaster = function () { +const clearRaster = function () { const layer = _getLayer('isRasterLayer'); + layer.removeChildren(); + // Generate blank raster - if (layer.children.length === 0) { - const raster = new paper.Raster(rasterSrc); - raster.parent = layer; - raster.guide = true; - raster.locked = true; - raster.position = paper.view.center; - } + const raster = new paper.Raster(rasterSrc); + raster.parent = layer; + raster.guide = true; + raster.locked = true; + raster.position = paper.view.center; +}; + +const getRaster = function () { return _getLayer('isRasterLayer').children[0]; }; @@ -89,6 +92,7 @@ const _makePaintingLayer = function () { const _makeRasterLayer = function () { const rasterLayer = new paper.Layer(); rasterLayer.data.isRasterLayer = true; + clearRaster(); return rasterLayer; }; @@ -145,6 +149,7 @@ export { hideGuideLayers, showGuideLayers, getGuideLayer, + clearRaster, getRaster, setupLayers }; From 15053d15f03f7c93eeb85af62b3ffde3c5ca00c4 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 6 Apr 2018 16:53:09 -0400 Subject: [PATCH 08/14] convert to vector on button press --- src/containers/paper-canvas.jsx | 14 ++++++++++++- src/helper/bitmap.js | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/helper/bitmap.js diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 4da7a5b6..79592495 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -7,6 +7,7 @@ import Formats from '../lib/format'; import Modes from '../lib/modes'; import log from '../log/log'; +import {trim} from '../helper/bitmap'; import {performSnapshot} from '../helper/undo'; import {undoSnapshot, clearUndoState} from '../reducers/undo'; import {isGroup, ungroupItems} from '../helper/group'; @@ -26,6 +27,7 @@ class PaperCanvas extends React.Component { super(props); bindAll(this, [ 'convertToBitmap', + 'convertToVector', 'setCanvas', 'importSvg', 'handleKeyDown', @@ -57,7 +59,7 @@ class PaperCanvas extends React.Component { } else if (this.props.format === Formats.VECTOR && newProps.format === Formats.BITMAP) { this.convertToBitmap(); } else if (this.props.format === Formats.BITMAP && newProps.format === Formats.VECTOR) { - // do vector conversion + this.convertToVector(); } } componentWillUnmount () { @@ -85,6 +87,16 @@ class PaperCanvas extends React.Component { paper.project.activeLayer.removeChildren(); performSnapshot(this.props.undoSnapshot); } + convertToVector () { + const raster = trim(getRaster()); + if (raster.width === 0 || raster.height === 0) { + raster.remove(); + } else { + paper.project.activeLayer.addChild(raster); + } + clearRaster(); + performSnapshot(this.props.undoSnapshot); + } switchCostume (svg, rotationCenterX, rotationCenterY) { for (const layer of paper.project.layers) { if (layer.data.isRasterLayer) { diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js new file mode 100644 index 00000000..5c1db739 --- /dev/null +++ b/src/helper/bitmap.js @@ -0,0 +1,37 @@ +import paper from '@scratch/paper'; + +const rowBlank_ = function (imageData, width, y) { + for (let x = 0; x < width; ++x) { + if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false; + } + return true; +}; + +const columnBlank_ = function (imageData, width, x, top, bottom) { + for (let y = top; y < bottom; ++y) { + if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false; + } + return true; +}; + +// Adapted from Tim Down's https://gist.github.com/timdown/021d9c8f2aabc7092df564996f5afbbf +// Trims transparent pixels from edges. +const trim = function (raster) { + const width = raster.width; + const imageData = raster.getImageData(raster.bounds); + let top = 0; + let bottom = imageData.height; + let left = 0; + let right = imageData.width; + + while (top < bottom && rowBlank_(imageData, width, top)) ++top; + while (bottom - 1 > top && rowBlank_(imageData, width, bottom - 1)) --bottom; + while (left < right && columnBlank_(imageData, width, left, top, bottom)) ++left; + while (right - 1 > left && columnBlank_(imageData, width, right - 1, top, bottom)) --right; + + return raster.getSubRaster(new paper.Rectangle(left, top, right - left, bottom - top)); +}; + +export { + trim +}; From 6e4ab3191af1fa117037803490727f66773532ad Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 9 Apr 2018 17:47:11 -0400 Subject: [PATCH 09/14] Get undo/redo working --- src/containers/paint-editor.jsx | 21 +++++++++-- src/containers/paper-canvas.jsx | 10 +++--- src/helper/layer.js | 36 +++++++++++++------ .../selection-tools/bounding-box-tool.js | 2 +- src/helper/undo.js | 12 +++++-- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 932293b7..fe486300 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 React from 'react'; import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; @@ -11,7 +12,8 @@ import {deactivateEyeDropper} from '../reducers/eye-dropper'; import {setTextEditTarget} from '../reducers/text-edit-target'; import {updateViewBounds} from '../reducers/view-bounds'; -import {hideGuideLayers, showGuideLayers} from '../helper/layer'; +import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer'; +import {trim} from '../helper/bitmap'; import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; import {groupSelection, ungroupSelection} from '../helper/group'; @@ -90,9 +92,20 @@ class PaintEditor extends React.Component { const oldCenter = paper.project.view.center.clone(); resetZoom(); - const guideLayers = hideGuideLayers(); - + let raster; + if (this.props.format === Formats.BITMAP) { + // @todo export bitmap here + raster = trim(getRaster()); + if (raster.width === 0 || raster.height === 0) { + raster.remove(); + } else { + paper.project.activeLayer.addChild(raster); + } + } + + const guideLayers = hideGuideLayers(true /* includeRaster */); const bounds = paper.project.activeLayer.bounds; + this.props.onUpdateSvg( paper.project.exportSVG({ asString: true, @@ -108,6 +121,8 @@ class PaintEditor extends React.Component { performSnapshot(this.props.undoSnapshot); } + if (raster) raster.remove(); + // Restore old zoom paper.project.view.zoom = oldZoom; paper.project.view.center = oldCenter; diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 79592495..c0c05b93 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -79,15 +79,17 @@ class PaperCanvas extends React.Component { } } convertToBitmap () { + this.props.clearSelectedItems(); const raster = paper.project.activeLayer.rasterize(72, false /* insert */); raster.onLoad = function () { const subCanvas = raster.canvas; getRaster().drawImage(subCanvas, raster.bounds.topLeft); - }; - paper.project.activeLayer.removeChildren(); - performSnapshot(this.props.undoSnapshot); + paper.project.activeLayer.removeChildren(); + this.props.onUpdateSvg(); + }.bind(this); } convertToVector () { + this.props.clearSelectedItems(); const raster = trim(getRaster()); if (raster.width === 0 || raster.height === 0) { raster.remove(); @@ -95,7 +97,7 @@ class PaperCanvas extends React.Component { paper.project.activeLayer.addChild(raster); } clearRaster(); - performSnapshot(this.props.undoSnapshot); + this.props.onUpdateSvg(); } switchCostume (svg, rotationCenterX, rotationCenterY) { for (const layer of paper.project.layers) { diff --git a/src/helper/layer.js b/src/helper/layer.js index cd206c07..15a05178 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -9,7 +9,6 @@ const _getLayer = function (layerString) { return layer; } } - log.error(`Didn't find layer ${layerString}`); }; const _getPaintingLayer = function () { @@ -36,22 +35,40 @@ const _getBackgroundGuideLayer = function () { return _getLayer('isBackgroundGuideLayer'); }; +const _makeGuideLayer = function () { + const guideLayer = new paper.Layer(); + guideLayer.data.isGuideLayer = true; + return guideLayer; +}; + const getGuideLayer = function () { - return _getLayer('isGuideLayer'); + let layer = _getLayer('isGuideLayer'); + if (!layer) { + layer = _makeGuideLayer(); + _getPaintingLayer().activate(); + } + return layer; }; /** * Removes the guide layers, e.g. for purposes of exporting the image. Must call showGuideLayers to re-add them. + * @param {boolean} includeRaster true if the raster layer should also be hidden * @return {object} an object of the removed layers, which should be passed to showGuideLayers to re-add them. */ -const hideGuideLayers = function () { +const hideGuideLayers = function (includeRaster) { const backgroundGuideLayer = _getBackgroundGuideLayer(); const guideLayer = getGuideLayer(); guideLayer.remove(); backgroundGuideLayer.remove(); + let rasterLayer; + if (includeRaster) { + rasterLayer = _getLayer('isRasterLayer'); + rasterLayer.remove(); + } return { guideLayer: guideLayer, - backgroundGuideLayer: backgroundGuideLayer + backgroundGuideLayer: backgroundGuideLayer, + rasterLayer: rasterLayer }; }; @@ -63,6 +80,11 @@ const hideGuideLayers = function () { const showGuideLayers = function (guideLayers) { const backgroundGuideLayer = guideLayers.backgroundGuideLayer; const guideLayer = guideLayers.guideLayer; + const rasterLayer = guideLayers.rasterLayer; + if (rasterLayer && !rasterLayer.index) { + paper.project.addLayer(rasterLayer); + rasterLayer.sendToBack(); + } if (!backgroundGuideLayer.index) { paper.project.addLayer(backgroundGuideLayer); backgroundGuideLayer.sendToBack(); @@ -77,12 +99,6 @@ const showGuideLayers = function (guideLayers) { } }; -const _makeGuideLayer = function () { - const guideLayer = new paper.Layer(); - guideLayer.data.isGuideLayer = true; - return guideLayer; -}; - const _makePaintingLayer = function () { const paintingLayer = new paper.Layer(); paintingLayer.data.isPaintingLayer = true; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index f9035aab..b751b630 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -54,7 +54,7 @@ class BoundingBoxTool { * @param {Array} selectedItems Array of selected items. */ onSelectionChanged (selectedItems) { - if (selectedItems) { + if (selectedItems && selectedItems.length) { this.setSelectionBounds(); } else { this.removeBoundsPath(); diff --git a/src/helper/undo.js b/src/helper/undo.js index 3302e833..fde36c26 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -1,7 +1,7 @@ // undo functionality // modifed from https://github.com/memononen/stylii import paper from '@scratch/paper'; -import {hideGuideLayers, showGuideLayers} from '../helper/layer'; +import {hideGuideLayers, showGuideLayers, getRaster} from '../helper/layer'; const performSnapshot = function (dispatchPerformSnapshot) { const guideLayers = hideGuideLayers(); @@ -12,7 +12,8 @@ const performSnapshot = function (dispatchPerformSnapshot) { }; const _restore = function (entry, setSelectedItems, onUpdateSvg) { - for (const layer of paper.project.layers) { + for (let i = paper.project.layers.length - 1; i >= 0; i--) { + const layer = paper.project.layers[i]; if (!layer.data.isBackgroundGuideLayer) { layer.removeChildren(); layer.remove(); @@ -21,7 +22,12 @@ const _restore = function (entry, setSelectedItems, onUpdateSvg) { paper.project.importJSON(entry.json); setSelectedItems(); - onUpdateSvg(true /* skipSnapshot */); + getRaster().onLoad = function () { + onUpdateSvg(true /* skipSnapshot */); + }; + if (getRaster().loaded) { + getRaster().onLoad(); + } }; const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateSvg) { From a6e7fb42516067d6045df9128e8a42630b5dbf75 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 9 Apr 2018 20:10:53 -0400 Subject: [PATCH 10/14] update button state when undoing between modes, hide vector tools --- src/components/paint-editor/paint-editor.css | 4 +++ src/components/paint-editor/paint-editor.jsx | 5 +-- src/containers/paint-editor.jsx | 16 +++++----- src/containers/paper-canvas.jsx | 14 +++++---- src/helper/undo.js | 33 ++++++++++++++++---- src/lib/format.js | 19 +++++++++-- src/reducers/format.js | 5 +++ src/reducers/undo.js | 22 ++++++++++--- test/unit/format-reducer.test.js | 11 +++++++ 9 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css index 087c1e82..f5500253 100644 --- a/src/components/paint-editor/paint-editor.css +++ b/src/components/paint-editor/paint-editor.css @@ -150,6 +150,10 @@ $border-radius: 0.25rem; justify-content: space-between; } +.hidden { + display: none; +} + .zoom-controls { display: flex; flex-direction: row-reverse; diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index ed373319..771bf79e 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -35,6 +35,7 @@ import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicat import TextMode from '../../containers/text-mode.jsx'; import Formats from '../../lib/format'; +import {isVector} from '../../lib/format'; import layout from '../../lib/layout-constants'; import styles from './paint-editor.css'; @@ -342,7 +343,7 @@ const PaintEditorComponent = props => {
{/* Modes */} {props.canvas !== null ? ( // eslint-disable-line no-negated-condition -
+
@@ -408,7 +409,7 @@ const PaintEditorComponent = props => { }
- {props.format === Formats.VECTOR ? + {isVector(props.format) ?