mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
commit
bf1d4ca82a
15 changed files with 400 additions and 75 deletions
|
@ -5,7 +5,7 @@
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="bitmap" fill="#575E75">
|
<g id="bitmap" fill="#FFFFFF">
|
||||||
<path d="M4,3 L16,3 L16,4 L4,4 L4,3 Z M2,5 L3,5 L3,13 L2,13 L2,5 Z M17,5 L18,5 L18,13 L17,13 L17,5 Z M2,13 L18,13 L18,15 L2,15 L2,13 Z M4,12 L16,12 L16,13 L4,13 L4,12 Z M5,11 L8,11 L8,12 L5,12 L5,11 Z M6,10 L7,10 L7,11 L6,11 L6,10 Z M9,11 L16,11 L16,12 L9,12 L9,11 Z M10,10 L15,10 L15,11 L10,11 L10,10 Z M11,9 L14,9 L14,10 L11,10 L11,9 Z M12,8 L13,8 L13,9 L12,9 L12,8 Z M16,12 L17,12 L17,13 L16,13 L16,12 Z M3,15 L17,15 L17,16 L3,16 L3,15 Z M3,4 L4,4 L4,5 L3,5 L3,4 Z M16,4 L17,4 L17,5 L16,5 L16,4 Z M4,16 L16,16 L16,17 L4,17 L4,16 Z" id="Combined-Shape"></path>
|
<path d="M4,3 L16,3 L16,4 L4,4 L4,3 Z M2,5 L3,5 L3,13 L2,13 L2,5 Z M17,5 L18,5 L18,13 L17,13 L17,5 Z M2,13 L18,13 L18,15 L2,15 L2,13 Z M4,12 L16,12 L16,13 L4,13 L4,12 Z M5,11 L8,11 L8,12 L5,12 L5,11 Z M6,10 L7,10 L7,11 L6,11 L6,10 Z M9,11 L16,11 L16,12 L9,12 L9,11 Z M10,10 L15,10 L15,11 L10,11 L10,10 Z M11,9 L14,9 L14,10 L11,10 L11,9 Z M12,8 L13,8 L13,9 L12,9 L12,8 Z M16,12 L17,12 L17,13 L16,13 L16,12 Z M3,15 L17,15 L17,16 L3,16 L3,15 Z M3,4 L4,4 L4,5 L3,5 L3,4 Z M16,4 L17,4 L17,5 L16,5 L16,4 Z M4,16 L16,16 L16,17 L4,17 L4,16 Z" id="Combined-Shape"></path>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -150,6 +150,10 @@ $border-radius: 0.25rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.zoom-controls {
|
.zoom-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
@ -179,13 +183,19 @@ $border-radius: 0.25rem;
|
||||||
.bitmap-button {
|
.bitmap-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: hsla(0, 0%, 0%, .25);
|
background-color: $motion-primary;
|
||||||
padding: calc(2 * $grid-unit);
|
padding: calc(2 * $grid-unit);
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
font-size: calc(3 * $grid-unit);
|
font-size: calc(3 * $grid-unit);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: .5;
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bitmap-button:active {
|
||||||
|
background-color: $motion-primary;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bitmap-button-icon {
|
.bitmap-button-icon {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import Button from '../button/button.jsx';
|
||||||
import ButtonGroup from '../button-group/button-group.jsx';
|
import ButtonGroup from '../button-group/button-group.jsx';
|
||||||
import BrushMode from '../../containers/brush-mode.jsx';
|
import BrushMode from '../../containers/brush-mode.jsx';
|
||||||
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
|
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
|
||||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
|
||||||
import Dropdown from '../dropdown/dropdown.jsx';
|
import Dropdown from '../dropdown/dropdown.jsx';
|
||||||
import EraserMode from '../../containers/eraser-mode.jsx';
|
import EraserMode from '../../containers/eraser-mode.jsx';
|
||||||
import FillColorIndicatorComponent from '../../containers/fill-color-indicator.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 StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx';
|
||||||
import TextMode from '../../containers/text-mode.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 layout from '../../lib/layout-constants';
|
||||||
import styles from './paint-editor.css';
|
import styles from './paint-editor.css';
|
||||||
|
|
||||||
|
@ -107,6 +108,11 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Convert to Bitmap',
|
defaultMessage: 'Convert to Bitmap',
|
||||||
description: 'Label for button that converts the paint editor to bitmap mode',
|
description: 'Label for button that converts the paint editor to bitmap mode',
|
||||||
id: 'paint.paintEditor.bitmap'
|
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 => {
|
||||||
<div className={styles.topAlignRow}>
|
<div className={styles.topAlignRow}>
|
||||||
{/* Modes */}
|
{/* Modes */}
|
||||||
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
|
{props.canvas !== null ? ( // eslint-disable-line no-negated-condition
|
||||||
<div className={styles.modeSelector}>
|
<div className={isVector(props.format) ? styles.modeSelector : styles.hidden}>
|
||||||
<SelectMode
|
<SelectMode
|
||||||
onUpdateSvg={props.onUpdateSvg}
|
onUpdateSvg={props.onUpdateSvg}
|
||||||
/>
|
/>
|
||||||
|
@ -403,12 +409,11 @@ const PaintEditorComponent = props => {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.canvasControls}>
|
<div className={styles.canvasControls}>
|
||||||
<ComingSoonTooltip
|
{isVector(props.format) ?
|
||||||
className={styles.bitmapTooltip}
|
<Button
|
||||||
place="top"
|
className={styles.bitmapButton}
|
||||||
tooltipId="bitmap-converter"
|
onClick={props.onSwitchToBitmap}
|
||||||
>
|
>
|
||||||
<div className={styles.bitmapButton}>
|
|
||||||
<img
|
<img
|
||||||
className={styles.bitmapButtonIcon}
|
className={styles.bitmapButtonIcon}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
@ -417,8 +422,21 @@ const PaintEditorComponent = props => {
|
||||||
<span>
|
<span>
|
||||||
{props.intl.formatMessage(messages.bitmap)}
|
{props.intl.formatMessage(messages.bitmap)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Button> :
|
||||||
</ComingSoonTooltip>
|
<Button
|
||||||
|
className={styles.bitmapButton}
|
||||||
|
onClick={props.onSwitchToVector}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={styles.bitmapButtonIcon}
|
||||||
|
draggable={false}
|
||||||
|
src={bitmapIcon}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{props.intl.formatMessage(messages.vector)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
{/* Zoom controls */}
|
{/* Zoom controls */}
|
||||||
<InputGroup className={styles.zoomControls}>
|
<InputGroup className={styles.zoomControls}>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
@ -469,6 +487,7 @@ PaintEditorComponent.propTypes = {
|
||||||
canUndo: PropTypes.func.isRequired,
|
canUndo: PropTypes.func.isRequired,
|
||||||
canvas: PropTypes.instanceOf(Element),
|
canvas: PropTypes.instanceOf(Element),
|
||||||
colorInfo: Loupe.propTypes.colorInfo,
|
colorInfo: Loupe.propTypes.colorInfo,
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
isEyeDropping: PropTypes.bool,
|
isEyeDropping: PropTypes.bool,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
@ -478,6 +497,8 @@ PaintEditorComponent.propTypes = {
|
||||||
onSendForward: PropTypes.func.isRequired,
|
onSendForward: PropTypes.func.isRequired,
|
||||||
onSendToBack: PropTypes.func.isRequired,
|
onSendToBack: PropTypes.func.isRequired,
|
||||||
onSendToFront: PropTypes.func.isRequired,
|
onSendToFront: PropTypes.func.isRequired,
|
||||||
|
onSwitchToBitmap: PropTypes.func.isRequired,
|
||||||
|
onSwitchToVector: PropTypes.func.isRequired,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onUngroup: PropTypes.func.isRequired,
|
onUngroup: PropTypes.func.isRequired,
|
||||||
onUpdateName: PropTypes.func.isRequired,
|
onUpdateName: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
||||||
|
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
|
import {changeFormat} from '../reducers/format';
|
||||||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
||||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||||
import {updateViewBounds} from '../reducers/view-bounds';
|
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 {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||||
|
@ -19,6 +22,8 @@ import {resetZoom, zoomOnSelection} from '../helper/view';
|
||||||
import EyeDropperTool from '../helper/tools/eye-dropper';
|
import EyeDropperTool from '../helper/tools/eye-dropper';
|
||||||
|
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import Formats from '../lib/format';
|
||||||
|
import {isBitmap} from '../lib/format';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
|
@ -88,9 +93,20 @@ class PaintEditor extends React.Component {
|
||||||
const oldCenter = paper.project.view.center.clone();
|
const oldCenter = paper.project.view.center.clone();
|
||||||
resetZoom();
|
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;
|
const bounds = paper.project.activeLayer.bounds;
|
||||||
|
|
||||||
this.props.onUpdateSvg(
|
this.props.onUpdateSvg(
|
||||||
paper.project.exportSVG({
|
paper.project.exportSVG({
|
||||||
asString: true,
|
asString: true,
|
||||||
|
@ -101,9 +117,10 @@ class PaintEditor extends React.Component {
|
||||||
paper.project.view.center.y - bounds.y);
|
paper.project.view.center.y - bounds.y);
|
||||||
|
|
||||||
showGuideLayers(guideLayers);
|
showGuideLayers(guideLayers);
|
||||||
|
if (raster) raster.remove();
|
||||||
|
|
||||||
if (!skipSnapshot) {
|
if (!skipSnapshot) {
|
||||||
performSnapshot(this.props.undoSnapshot);
|
performSnapshot(this.props.undoSnapshot, this.props.format);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore old zoom
|
// Restore old zoom
|
||||||
|
@ -231,6 +248,7 @@ class PaintEditor extends React.Component {
|
||||||
canUndo={this.canUndo}
|
canUndo={this.canUndo}
|
||||||
canvas={this.state.canvas}
|
canvas={this.state.canvas}
|
||||||
colorInfo={this.state.colorInfo}
|
colorInfo={this.state.colorInfo}
|
||||||
|
format={this.props.format}
|
||||||
isEyeDropping={this.props.isEyeDropping}
|
isEyeDropping={this.props.isEyeDropping}
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
rotationCenterX={this.props.rotationCenterX}
|
rotationCenterX={this.props.rotationCenterX}
|
||||||
|
@ -246,6 +264,8 @@ class PaintEditor extends React.Component {
|
||||||
onSendForward={this.handleSendForward}
|
onSendForward={this.handleSendForward}
|
||||||
onSendToBack={this.handleSendToBack}
|
onSendToBack={this.handleSendToBack}
|
||||||
onSendToFront={this.handleSendToFront}
|
onSendToFront={this.handleSendToFront}
|
||||||
|
onSwitchToBitmap={this.props.handleSwitchToBitmap}
|
||||||
|
onSwitchToVector={this.props.handleSwitchToVector}
|
||||||
onUndo={this.handleUndo}
|
onUndo={this.handleUndo}
|
||||||
onUngroup={this.handleUngroup}
|
onUngroup={this.handleUngroup}
|
||||||
onUpdateName={this.props.onUpdateName}
|
onUpdateName={this.props.onUpdateName}
|
||||||
|
@ -261,6 +281,9 @@ class PaintEditor extends React.Component {
|
||||||
PaintEditor.propTypes = {
|
PaintEditor.propTypes = {
|
||||||
changeColorToEyeDropper: PropTypes.func,
|
changeColorToEyeDropper: PropTypes.func,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
|
handleSwitchToBitmap: PropTypes.func.isRequired,
|
||||||
|
handleSwitchToVector: PropTypes.func.isRequired,
|
||||||
isEyeDropping: PropTypes.bool,
|
isEyeDropping: PropTypes.bool,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
onDeactivateEyeDropper: PropTypes.func.isRequired,
|
onDeactivateEyeDropper: PropTypes.func.isRequired,
|
||||||
|
@ -291,6 +314,7 @@ PaintEditor.propTypes = {
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback,
|
changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback,
|
||||||
clipboardItems: state.scratchPaint.clipboard.items,
|
clipboardItems: state.scratchPaint.clipboard.items,
|
||||||
|
format: state.scratchPaint.format,
|
||||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||||
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
||||||
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
|
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
|
||||||
|
@ -323,6 +347,12 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
|
handleSwitchToBitmap: () => {
|
||||||
|
dispatch(changeFormat(Formats.BITMAP));
|
||||||
|
},
|
||||||
|
handleSwitchToVector: () => {
|
||||||
|
dispatch(changeFormat(Formats.VECTOR));
|
||||||
|
},
|
||||||
removeTextEditTarget: () => {
|
removeTextEditTarget: () => {
|
||||||
dispatch(setTextEditTarget());
|
dispatch(setTextEditTarget());
|
||||||
},
|
},
|
||||||
|
@ -333,11 +363,11 @@ const mapDispatchToProps = dispatch => ({
|
||||||
// set redux values to default for eye dropper reducer
|
// set redux values to default for eye dropper reducer
|
||||||
dispatch(deactivateEyeDropper());
|
dispatch(deactivateEyeDropper());
|
||||||
},
|
},
|
||||||
onUndo: () => {
|
onUndo: format => {
|
||||||
dispatch(undo());
|
dispatch(undo(format));
|
||||||
},
|
},
|
||||||
onRedo: () => {
|
onRedo: format => {
|
||||||
dispatch(redo());
|
dispatch(redo(format));
|
||||||
},
|
},
|
||||||
undoSnapshot: snapshot => {
|
undoSnapshot: snapshot => {
|
||||||
dispatch(undoSnapshot(snapshot));
|
dispatch(undoSnapshot(snapshot));
|
||||||
|
|
|
@ -4,7 +4,4 @@
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #fff;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,15 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
|
import Formats from '../lib/format';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
import log from '../log/log';
|
import log from '../log/log';
|
||||||
|
|
||||||
|
import {trim} from '../helper/bitmap';
|
||||||
import {performSnapshot} from '../helper/undo';
|
import {performSnapshot} from '../helper/undo';
|
||||||
import {undoSnapshot, clearUndoState} from '../reducers/undo';
|
import {undoSnapshot, clearUndoState} from '../reducers/undo';
|
||||||
import {isGroup, ungroupItems} from '../helper/group';
|
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 {deleteSelection, getSelectedLeafItems} from '../helper/selection';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
|
import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
|
||||||
|
@ -17,6 +19,9 @@ import {ensureClockwise} from '../helper/math';
|
||||||
import {clearHoveredItem} from '../reducers/hover';
|
import {clearHoveredItem} from '../reducers/hover';
|
||||||
import {clearPasteOffset} from '../reducers/clipboard';
|
import {clearPasteOffset} from '../reducers/clipboard';
|
||||||
import {updateViewBounds} from '../reducers/view-bounds';
|
import {updateViewBounds} from '../reducers/view-bounds';
|
||||||
|
import {changeFormat} from '../reducers/format';
|
||||||
|
|
||||||
|
import {isVector, isBitmap} from '../lib/format';
|
||||||
|
|
||||||
import styles from './paper-canvas.css';
|
import styles from './paper-canvas.css';
|
||||||
|
|
||||||
|
@ -24,15 +29,23 @@ class PaperCanvas extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
|
'convertToBitmap',
|
||||||
|
'convertToVector',
|
||||||
'setCanvas',
|
'setCanvas',
|
||||||
'importSvg',
|
'importSvg',
|
||||||
'handleKeyDown',
|
'handleKeyDown',
|
||||||
'handleWheel'
|
'handleWheel',
|
||||||
|
'switchCostume'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
document.addEventListener('keydown', this.handleKeyDown);
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
paper.setup(this.canvas);
|
paper.setup(this.canvas);
|
||||||
|
|
||||||
|
const context = this.canvas.getContext('2d');
|
||||||
|
context.webkitImageSmoothingEnabled = false;
|
||||||
|
context.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
// Don't show handles by default
|
// Don't show handles by default
|
||||||
paper.settings.handleSize = 0;
|
paper.settings.handleSize = 0;
|
||||||
// Make layers.
|
// Make layers.
|
||||||
|
@ -40,31 +53,16 @@ class PaperCanvas extends React.Component {
|
||||||
if (this.props.svg) {
|
if (this.props.svg) {
|
||||||
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
this.importSvg(this.props.svg, this.props.rotationCenterX, this.props.rotationCenterY);
|
||||||
} else {
|
} else {
|
||||||
performSnapshot(this.props.undoSnapshot);
|
performSnapshot(this.props.undoSnapshot, this.props.format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
if (this.props.svgId === newProps.svgId) return;
|
if (this.props.svgId !== newProps.svgId) {
|
||||||
for (const layer of paper.project.layers) {
|
this.switchCostume(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
|
||||||
if (!layer.data.isBackgroundGuideLayer) {
|
} else if (isVector(this.props.format) && newProps.format === Formats.BITMAP) {
|
||||||
layer.removeChildren();
|
this.convertToBitmap();
|
||||||
}
|
} else if (isBitmap(this.props.format) && newProps.format === Formats.VECTOR) {
|
||||||
}
|
this.convertToVector();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
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) {
|
importSvg (svg, rotationCenterX, rotationCenterY) {
|
||||||
const paperCanvas = this;
|
const paperCanvas = this;
|
||||||
// Pre-process SVG to prevent parsing errors (discussion from #213)
|
// Pre-process SVG to prevent parsing errors (discussion from #213)
|
||||||
|
@ -115,7 +160,7 @@ class PaperCanvas extends React.Component {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
log.error('SVG import failed:');
|
log.error('SVG import failed:');
|
||||||
log.info(svg);
|
log.info(svg);
|
||||||
performSnapshot(paperCanvas.props.undoSnapshot);
|
performSnapshot(paperCanvas.props.undoSnapshot, paperCanvas.props.format);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const itemWidth = item.bounds.width;
|
const itemWidth = item.bounds.width;
|
||||||
|
@ -157,7 +202,7 @@ class PaperCanvas extends React.Component {
|
||||||
ungroupItems([item]);
|
ungroupItems([item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
performSnapshot(paperCanvas.props.undoSnapshot);
|
performSnapshot(paperCanvas.props.undoSnapshot, paperCanvas.props.format);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -209,10 +254,12 @@ class PaperCanvas extends React.Component {
|
||||||
|
|
||||||
PaperCanvas.propTypes = {
|
PaperCanvas.propTypes = {
|
||||||
canvasRef: PropTypes.func,
|
canvasRef: PropTypes.func,
|
||||||
|
changeFormat: PropTypes.func.isRequired,
|
||||||
clearHoveredItem: PropTypes.func.isRequired,
|
clearHoveredItem: PropTypes.func.isRequired,
|
||||||
clearPasteOffset: PropTypes.func.isRequired,
|
clearPasteOffset: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
clearUndo: PropTypes.func.isRequired,
|
clearUndo: PropTypes.func.isRequired,
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
|
||||||
mode: PropTypes.oneOf(Object.keys(Modes)),
|
mode: PropTypes.oneOf(Object.keys(Modes)),
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
|
@ -224,7 +271,8 @@ PaperCanvas.propTypes = {
|
||||||
updateViewBounds: PropTypes.func.isRequired
|
updateViewBounds: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
mode: state.scratchPaint.mode
|
mode: state.scratchPaint.mode,
|
||||||
|
format: state.scratchPaint.format
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
undoSnapshot: snapshot => {
|
undoSnapshot: snapshot => {
|
||||||
|
@ -245,6 +293,9 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearPasteOffset: () => {
|
clearPasteOffset: () => {
|
||||||
dispatch(clearPasteOffset());
|
dispatch(clearPasteOffset());
|
||||||
},
|
},
|
||||||
|
changeFormat: format => {
|
||||||
|
dispatch(changeFormat(format));
|
||||||
|
},
|
||||||
updateViewBounds: matrix => {
|
updateViewBounds: matrix => {
|
||||||
dispatch(updateViewBounds(matrix));
|
dispatch(updateViewBounds(matrix));
|
||||||
}
|
}
|
||||||
|
|
37
src/helper/bitmap.js
Normal file
37
src/helper/bitmap.js
Normal file
|
@ -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
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
|
import rasterSrc from './transparent.png';
|
||||||
import log from '../log/log';
|
import log from '../log/log';
|
||||||
|
|
||||||
const _getLayer = function (layerString) {
|
const _getLayer = function (layerString) {
|
||||||
|
@ -7,33 +8,66 @@ const _getLayer = function (layerString) {
|
||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.error(`Didn't find layer ${layerString}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getPaintingLayer = function () {
|
const _getPaintingLayer = function () {
|
||||||
return _getLayer('isPaintingLayer');
|
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 () {
|
const _getBackgroundGuideLayer = function () {
|
||||||
return _getLayer('isBackgroundGuideLayer');
|
return _getLayer('isBackgroundGuideLayer');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _makeGuideLayer = function () {
|
||||||
|
const guideLayer = new paper.Layer();
|
||||||
|
guideLayer.data.isGuideLayer = true;
|
||||||
|
return guideLayer;
|
||||||
|
};
|
||||||
|
|
||||||
const getGuideLayer = function () {
|
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.
|
* 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.
|
* @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 backgroundGuideLayer = _getBackgroundGuideLayer();
|
||||||
const guideLayer = getGuideLayer();
|
const guideLayer = getGuideLayer();
|
||||||
guideLayer.remove();
|
guideLayer.remove();
|
||||||
backgroundGuideLayer.remove();
|
backgroundGuideLayer.remove();
|
||||||
|
let rasterLayer;
|
||||||
|
if (includeRaster) {
|
||||||
|
rasterLayer = _getLayer('isRasterLayer');
|
||||||
|
rasterLayer.remove();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
guideLayer: guideLayer,
|
guideLayer: guideLayer,
|
||||||
backgroundGuideLayer: backgroundGuideLayer
|
backgroundGuideLayer: backgroundGuideLayer,
|
||||||
|
rasterLayer: rasterLayer
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,6 +79,11 @@ const hideGuideLayers = function () {
|
||||||
const showGuideLayers = function (guideLayers) {
|
const showGuideLayers = function (guideLayers) {
|
||||||
const backgroundGuideLayer = guideLayers.backgroundGuideLayer;
|
const backgroundGuideLayer = guideLayers.backgroundGuideLayer;
|
||||||
const guideLayer = guideLayers.guideLayer;
|
const guideLayer = guideLayers.guideLayer;
|
||||||
|
const rasterLayer = guideLayers.rasterLayer;
|
||||||
|
if (rasterLayer && !rasterLayer.index) {
|
||||||
|
paper.project.addLayer(rasterLayer);
|
||||||
|
rasterLayer.sendToBack();
|
||||||
|
}
|
||||||
if (!backgroundGuideLayer.index) {
|
if (!backgroundGuideLayer.index) {
|
||||||
paper.project.addLayer(backgroundGuideLayer);
|
paper.project.addLayer(backgroundGuideLayer);
|
||||||
backgroundGuideLayer.sendToBack();
|
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 _makePaintingLayer = function () {
|
||||||
const paintingLayer = new paper.Layer();
|
const paintingLayer = new paper.Layer();
|
||||||
paintingLayer.data.isPaintingLayer = true;
|
paintingLayer.data.isPaintingLayer = true;
|
||||||
return paintingLayer;
|
return paintingLayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _makeRasterLayer = function () {
|
||||||
|
const rasterLayer = new paper.Layer();
|
||||||
|
rasterLayer.data.isRasterLayer = true;
|
||||||
|
clearRaster();
|
||||||
|
return rasterLayer;
|
||||||
|
};
|
||||||
|
|
||||||
const _makeBackgroundPaper = function (width, height, color) {
|
const _makeBackgroundPaper = function (width, height, color) {
|
||||||
// creates a checkerboard path of width * height squares in color on white
|
// creates a checkerboard path of width * height squares in color on white
|
||||||
let x = 0;
|
let x = 0;
|
||||||
|
@ -140,6 +180,7 @@ const _makeBackgroundGuideLayer = function () {
|
||||||
|
|
||||||
const setupLayers = function () {
|
const setupLayers = function () {
|
||||||
const backgroundGuideLayer = _makeBackgroundGuideLayer();
|
const backgroundGuideLayer = _makeBackgroundGuideLayer();
|
||||||
|
_makeRasterLayer();
|
||||||
const paintLayer = _makePaintingLayer();
|
const paintLayer = _makePaintingLayer();
|
||||||
const guideLayer = _makeGuideLayer();
|
const guideLayer = _makeGuideLayer();
|
||||||
backgroundGuideLayer.sendToBack();
|
backgroundGuideLayer.sendToBack();
|
||||||
|
@ -151,5 +192,7 @@ export {
|
||||||
hideGuideLayers,
|
hideGuideLayers,
|
||||||
showGuideLayers,
|
showGuideLayers,
|
||||||
getGuideLayer,
|
getGuideLayer,
|
||||||
|
clearRaster,
|
||||||
|
getRaster,
|
||||||
setupLayers
|
setupLayers
|
||||||
};
|
};
|
||||||
|
|
BIN
src/helper/transparent.png
Normal file
BIN
src/helper/transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 414 B |
|
@ -1,18 +1,28 @@
|
||||||
// undo functionality
|
// undo functionality
|
||||||
// modifed from https://github.com/memononen/stylii
|
// modifed from https://github.com/memononen/stylii
|
||||||
import paper from '@scratch/paper';
|
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();
|
const guideLayers = hideGuideLayers();
|
||||||
dispatchPerformSnapshot({
|
dispatchPerformSnapshot({
|
||||||
json: paper.project.exportJSON({asString: false})
|
json: paper.project.exportJSON({asString: false}),
|
||||||
|
paintEditorFormat: format
|
||||||
});
|
});
|
||||||
showGuideLayers(guideLayers);
|
showGuideLayers(guideLayers);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _restore = function (entry, setSelectedItems, onUpdateSvg) {
|
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) {
|
if (!layer.data.isBackgroundGuideLayer) {
|
||||||
layer.removeChildren();
|
layer.removeChildren();
|
||||||
layer.remove();
|
layer.remove();
|
||||||
|
@ -21,21 +31,38 @@ const _restore = function (entry, setSelectedItems, onUpdateSvg) {
|
||||||
paper.project.importJSON(entry.json);
|
paper.project.importJSON(entry.json);
|
||||||
|
|
||||||
setSelectedItems();
|
setSelectedItems();
|
||||||
|
getRaster().onLoad = function () {
|
||||||
onUpdateSvg(true /* skipSnapshot */);
|
onUpdateSvg(true /* skipSnapshot */);
|
||||||
|
};
|
||||||
|
if (getRaster().loaded) {
|
||||||
|
getRaster().onLoad();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateSvg) {
|
const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateSvg) {
|
||||||
if (undoState.pointer > 0) {
|
if (undoState.pointer > 0) {
|
||||||
_restore(undoState.stack[undoState.pointer - 1], setSelectedItems, onUpdateSvg);
|
const state = undoState.stack[undoState.pointer - 1];
|
||||||
dispatchPerformUndo();
|
_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) {
|
const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateSvg) {
|
||||||
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) {
|
||||||
_restore(undoState.stack[undoState.pointer + 1], setSelectedItems, onUpdateSvg);
|
const state = undoState.stack[undoState.pointer + 1];
|
||||||
dispatchPerformRedo();
|
_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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
23
src/lib/format.js
Normal file
23
src/lib/format.js
Normal file
|
@ -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
|
||||||
|
};
|
37
src/reducers/format.js
Normal file
37
src/reducers/format.js
Normal file
|
@ -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
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ import brushModeReducer from './brush-mode';
|
||||||
import eraserModeReducer from './eraser-mode';
|
import eraserModeReducer from './eraser-mode';
|
||||||
import colorReducer from './color';
|
import colorReducer from './color';
|
||||||
import clipboardReducer from './clipboard';
|
import clipboardReducer from './clipboard';
|
||||||
|
import formatReducer from './format';
|
||||||
import hoverReducer from './hover';
|
import hoverReducer from './hover';
|
||||||
import modalsReducer from './modals';
|
import modalsReducer from './modals';
|
||||||
import selectedItemReducer from './selected-items';
|
import selectedItemReducer from './selected-items';
|
||||||
|
@ -17,6 +18,7 @@ export default combineReducers({
|
||||||
color: colorReducer,
|
color: colorReducer,
|
||||||
clipboard: clipboardReducer,
|
clipboard: clipboardReducer,
|
||||||
eraserMode: eraserModeReducer,
|
eraserMode: eraserModeReducer,
|
||||||
|
format: formatReducer,
|
||||||
hoveredItemId: hoverReducer,
|
hoveredItemId: hoverReducer,
|
||||||
modals: modalsReducer,
|
modals: modalsReducer,
|
||||||
selectedItems: selectedItemReducer,
|
selectedItems: selectedItemReducer,
|
||||||
|
|
|
@ -63,14 +63,24 @@ const undoSnapshot = function (snapshot) {
|
||||||
snapshot: 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 {
|
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 {
|
return {
|
||||||
type: REDO
|
type: REDO,
|
||||||
|
format: format
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const clearUndoState = function () {
|
const clearUndoState = function () {
|
||||||
|
@ -85,5 +95,7 @@ export {
|
||||||
redo,
|
redo,
|
||||||
undoSnapshot,
|
undoSnapshot,
|
||||||
clearUndoState,
|
clearUndoState,
|
||||||
MAX_STACK_SIZE
|
MAX_STACK_SIZE,
|
||||||
|
UNDO,
|
||||||
|
REDO
|
||||||
};
|
};
|
||||||
|
|
35
test/unit/format-reducer.test.js
Normal file
35
test/unit/format-reducer.test.js
Normal file
|
@ -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);
|
||||||
|
});
|
Loading…
Reference in a new issue