-
+
diff --git a/src/components/paint-editor/paint-editor.css b/src/components/paint-editor/paint-editor.css
index 38ca5063..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;
@@ -179,13 +183,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..771bf79e 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';
@@ -35,6 +34,8 @@ 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 {isVector} from '../../lib/format';
import layout from '../../lib/layout-constants';
import styles from './paint-editor.css';
@@ -107,6 +108,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'
}
});
@@ -337,7 +343,7 @@ const PaintEditorComponent = props => {
{/* Modes */}
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
-
+
@@ -403,12 +409,11 @@ const PaintEditorComponent = props => {
}
-
-
+ {isVector(props.format) ?
+
-
+ :
+
+ }
{/* Zoom controls */}
@@ -469,6 +487,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,
@@ -478,6 +497,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..61feaf99 100644
--- a/src/containers/paint-editor.jsx
+++ b/src/containers/paint-editor.jsx
@@ -1,16 +1,19 @@
import paper from '@scratch/paper';
import PropTypes from 'prop-types';
+
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';
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';
@@ -19,6 +22,8 @@ import {resetZoom, zoomOnSelection} from '../helper/view';
import EyeDropperTool from '../helper/tools/eye-dropper';
import Modes from '../lib/modes';
+import Formats from '../lib/format';
+import {isBitmap} from '../lib/format';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
@@ -88,9 +93,20 @@ class PaintEditor extends React.Component {
const oldCenter = paper.project.view.center.clone();
resetZoom();
- const guideLayers = hideGuideLayers();
-
+ let raster;
+ if (isBitmap(this.props.format)) {
+ // @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,
@@ -101,9 +117,10 @@ class PaintEditor extends React.Component {
paper.project.view.center.y - bounds.y);
showGuideLayers(guideLayers);
+ if (raster) raster.remove();
if (!skipSnapshot) {
- performSnapshot(this.props.undoSnapshot);
+ performSnapshot(this.props.undoSnapshot, this.props.format);
}
// Restore old zoom
@@ -231,6 +248,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 +264,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 +281,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 +314,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 +347,12 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
+ handleSwitchToBitmap: () => {
+ dispatch(changeFormat(Formats.BITMAP));
+ },
+ handleSwitchToVector: () => {
+ dispatch(changeFormat(Formats.VECTOR));
+ },
removeTextEditTarget: () => {
dispatch(setTextEditTarget());
},
@@ -333,11 +363,11 @@ const mapDispatchToProps = dispatch => ({
// set redux values to default for eye dropper reducer
dispatch(deactivateEyeDropper());
},
- onUndo: () => {
- dispatch(undo());
+ onUndo: format => {
+ dispatch(undo(format));
},
- onRedo: () => {
- dispatch(redo());
+ onRedo: format => {
+ dispatch(redo(format));
},
undoSnapshot: snapshot => {
dispatch(undoSnapshot(snapshot));
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..5655eb85 100644
--- a/src/containers/paper-canvas.jsx
+++ b/src/containers/paper-canvas.jsx
@@ -3,13 +3,15 @@ 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 {trim} from '../helper/bitmap';
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';
@@ -17,6 +19,9 @@ import {ensureClockwise} from '../helper/math';
import {clearHoveredItem} from '../reducers/hover';
import {clearPasteOffset} from '../reducers/clipboard';
import {updateViewBounds} from '../reducers/view-bounds';
+import {changeFormat} from '../reducers/format';
+
+import {isVector, isBitmap} from '../lib/format';
import styles from './paper-canvas.css';
@@ -24,15 +29,23 @@ class PaperCanvas extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
+ 'convertToBitmap',
+ 'convertToVector',
'setCanvas',
'importSvg',
'handleKeyDown',
- 'handleWheel'
+ 'handleWheel',
+ 'switchCostume'
]);
}
componentDidMount () {
document.addEventListener('keydown', this.handleKeyDown);
paper.setup(this.canvas);
+
+ const context = this.canvas.getContext('2d');
+ context.webkitImageSmoothingEnabled = false;
+ context.imageSmoothingEnabled = false;
+
// Don't show handles by default
paper.settings.handleSize = 0;
// Make layers.
@@ -40,31 +53,16 @@ class PaperCanvas extends React.Component {
if (this.props.svg) {
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
} else {
- performSnapshot(this.props.undoSnapshot);
+ performSnapshot(this.props.undoSnapshot, this.props.format);
}
}
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 (isVector(this.props.format) && newProps.format === Formats.BITMAP) {
+ this.convertToBitmap();
+ } else if (isBitmap(this.props.format) && newProps.format === Formats.VECTOR) {
+ this.convertToVector();
}
}
componentWillUnmount () {
@@ -83,6 +81,53 @@ 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();
+ this.props.onUpdateSvg();
+ }.bind(this);
+ }
+ convertToVector () {
+ this.props.clearSelectedItems();
+ const raster = trim(getRaster());
+ if (raster.width === 0 || raster.height === 0) {
+ raster.remove();
+ } else {
+ paper.project.activeLayer.addChild(raster);
+ }
+ clearRaster();
+ this.props.onUpdateSvg();
+ }
+ 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) {
+ this.props.changeFormat(Formats.VECTOR_SKIP_CONVERT);
+ // 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, this.props.format);
+ }
+ }
importSvg (svg, rotationCenterX, rotationCenterY) {
const paperCanvas = this;
// Pre-process SVG to prevent parsing errors (discussion from #213)
@@ -115,7 +160,7 @@ class PaperCanvas extends React.Component {
if (!item) {
log.error('SVG import failed:');
log.info(svg);
- performSnapshot(paperCanvas.props.undoSnapshot);
+ performSnapshot(paperCanvas.props.undoSnapshot, paperCanvas.props.format);
return;
}
const itemWidth = item.bounds.width;
@@ -157,7 +202,7 @@ class PaperCanvas extends React.Component {
ungroupItems([item]);
}
- performSnapshot(paperCanvas.props.undoSnapshot);
+ performSnapshot(paperCanvas.props.undoSnapshot, paperCanvas.props.format);
}
});
}
@@ -209,10 +254,12 @@ class PaperCanvas extends React.Component {
PaperCanvas.propTypes = {
canvasRef: PropTypes.func,
+ changeFormat: PropTypes.func.isRequired,
clearHoveredItem: PropTypes.func.isRequired,
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,
@@ -224,7 +271,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 => {
@@ -245,6 +293,9 @@ const mapDispatchToProps = dispatch => ({
clearPasteOffset: () => {
dispatch(clearPasteOffset());
},
+ changeFormat: format => {
+ dispatch(changeFormat(format));
+ },
updateViewBounds: matrix => {
dispatch(updateViewBounds(matrix));
}
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
+};
diff --git a/src/helper/layer.js b/src/helper/layer.js
index 677dbae0..837d3747 100644
--- a/src/helper/layer.js
+++ b/src/helper/layer.js
@@ -1,4 +1,5 @@
import paper from '@scratch/paper';
+import rasterSrc from './transparent.png';
import log from '../log/log';
const _getLayer = function (layerString) {
@@ -7,33 +8,66 @@ const _getLayer = function (layerString) {
return layer;
}
}
- log.error(`Didn't find layer ${layerString}`);
};
const _getPaintingLayer = function () {
return _getLayer('isPaintingLayer');
};
+const clearRaster = function () {
+ const layer = _getLayer('isRasterLayer');
+ layer.removeChildren();
+
+ // Generate blank raster
+ 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];
+};
+
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
};
};
@@ -45,6 +79,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();
@@ -59,18 +98,19 @@ 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;
return paintingLayer;
};
+const _makeRasterLayer = function () {
+ const rasterLayer = new paper.Layer();
+ rasterLayer.data.isRasterLayer = true;
+ clearRaster();
+ return rasterLayer;
+};
+
const _makeBackgroundPaper = function (width, height, color) {
// creates a checkerboard path of width * height squares in color on white
let x = 0;
@@ -140,6 +180,7 @@ const _makeBackgroundGuideLayer = function () {
const setupLayers = function () {
const backgroundGuideLayer = _makeBackgroundGuideLayer();
+ _makeRasterLayer();
const paintLayer = _makePaintingLayer();
const guideLayer = _makeGuideLayer();
backgroundGuideLayer.sendToBack();
@@ -151,5 +192,7 @@ export {
hideGuideLayers,
showGuideLayers,
getGuideLayer,
+ clearRaster,
+ getRaster,
setupLayers
};
diff --git a/src/helper/transparent.png b/src/helper/transparent.png
new file mode 100644
index 00000000..ebfbd78d
Binary files /dev/null and b/src/helper/transparent.png differ
diff --git a/src/helper/undo.js b/src/helper/undo.js
index 3302e833..3f9592d2 100644
--- a/src/helper/undo.js
+++ b/src/helper/undo.js
@@ -1,18 +1,28 @@
// 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';
+import Formats from '../lib/format';
+import {isVector, isBitmap} from '../lib/format';
+import log from '../log/log';
-const performSnapshot = function (dispatchPerformSnapshot) {
+/**
+ * Take an undo snapshot
+ * @param {function} dispatchPerformSnapshot Callback to dispatch a state update
+ * @param {Formats} format Either Formats.BITMAP or Formats.VECTOR
+ */
+const performSnapshot = function (dispatchPerformSnapshot, format) {
const guideLayers = hideGuideLayers();
dispatchPerformSnapshot({
- json: paper.project.exportJSON({asString: false})
+ json: paper.project.exportJSON({asString: false}),
+ paintEditorFormat: format
});
showGuideLayers(guideLayers);
};
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,21 +31,38 @@ 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) {
if (undoState.pointer > 0) {
- _restore(undoState.stack[undoState.pointer - 1], setSelectedItems, onUpdateSvg);
- dispatchPerformUndo();
+ const state = undoState.stack[undoState.pointer - 1];
+ _restore(state, setSelectedItems, onUpdateSvg);
+ const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
+ isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
+ if (!format) {
+ log.error(`Invalid format: ${state.paintEditorFormat}`);
+ }
+ dispatchPerformUndo(format);
}
};
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateSvg) {
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
- _restore(undoState.stack[undoState.pointer + 1], setSelectedItems, onUpdateSvg);
- dispatchPerformRedo();
+ const state = undoState.stack[undoState.pointer + 1];
+ _restore(state, setSelectedItems, onUpdateSvg);
+ const format = isVector(state.paintEditorFormat) ? Formats.VECTOR_SKIP_CONVERT :
+ isBitmap(state.paintEditorFormat) ? Formats.BITMAP_SKIP_CONVERT : null;
+ if (!format) {
+ log.error(`Invalid format: ${state.paintEditorFormat}`);
+ }
+ dispatchPerformRedo(format);
}
};
diff --git a/src/lib/format.js b/src/lib/format.js
new file mode 100644
index 00000000..abad144f
--- /dev/null
+++ b/src/lib/format.js
@@ -0,0 +1,23 @@
+import keyMirror from 'keymirror';
+
+const Formats = keyMirror({
+ BITMAP: null,
+ VECTOR: null,
+ // Format changes which should not trigger conversions, for instance undo
+ BITMAP_SKIP_CONVERT: null,
+ VECTOR_SKIP_CONVERT: null
+});
+
+const isVector = function (format) {
+ return format === Formats.VECTOR || format === Formats.VECTOR_SKIP_CONVERT;
+};
+
+const isBitmap = function (format) {
+ return format === Formats.BITMAP || format === Formats.BITMAP_SKIP_CONVERT;
+};
+
+export {
+ Formats as default,
+ isVector,
+ isBitmap
+};
diff --git a/src/reducers/format.js b/src/reducers/format.js
new file mode 100644
index 00000000..9243e3fc
--- /dev/null
+++ b/src/reducers/format.js
@@ -0,0 +1,37 @@
+import Formats from '../lib/format';
+import log from '../log/log';
+import {UNDO, REDO} from './undo';
+
+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 UNDO:
+ /* falls through */
+ case REDO:
+ /* falls through */
+ 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/src/reducers/undo.js b/src/reducers/undo.js
index ce16c108..e372ffa2 100644
--- a/src/reducers/undo.js
+++ b/src/reducers/undo.js
@@ -63,14 +63,24 @@ const undoSnapshot = function (snapshot) {
snapshot: snapshot
};
};
-const undo = function () {
+/**
+ * @param {Format} format Either VECTOR_SKIP_CONVERT or BITMAP_SKIP_CONVERT
+ * @return {Action} undo action
+ */
+const undo = function (format) {
return {
- type: UNDO
+ type: UNDO,
+ format: format
};
};
-const redo = function () {
+/**
+ * @param {Format} format Either VECTOR_SKIP_CONVERT or BITMAP_SKIP_CONVERT
+ * @return {Action} undo action
+ */
+const redo = function (format) {
return {
- type: REDO
+ type: REDO,
+ format: format
};
};
const clearUndoState = function () {
@@ -85,5 +95,7 @@ export {
redo,
undoSnapshot,
clearUndoState,
- MAX_STACK_SIZE
+ MAX_STACK_SIZE,
+ UNDO,
+ REDO
};
diff --git a/test/unit/format-reducer.test.js b/test/unit/format-reducer.test.js
new file mode 100644
index 00000000..1941aed3
--- /dev/null
+++ b/test/unit/format-reducer.test.js
@@ -0,0 +1,35 @@
+/* eslint-env jest */
+import Formats from '../../src/lib/format';
+import reducer from '../../src/reducers/format';
+import {changeFormat} from '../../src/reducers/format';
+import {undo, redo} from '../../src/reducers/undo';
+
+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('undoRedoChangeFormat', () => {
+ let defaultState;
+ let reduxState = reducer(defaultState /* state */, changeFormat(Formats.BITMAP) /* action */);
+ expect(reduxState).toBe(Formats.BITMAP);
+ reduxState = reducer(reduxState /* state */, undo(Formats.BITMAP_SKIP_CONVERT) /* action */);
+ expect(reduxState).toBe(Formats.BITMAP_SKIP_CONVERT);
+ reduxState = reducer(reduxState /* state */, redo(Formats.VECTOR_SKIP_CONVERT) /* action */);
+ expect(reduxState).toBe(Formats.VECTOR_SKIP_CONVERT);
+});
+
+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);
+});