From 4e4bb396a6631598c67c26fc955862e316257694 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 12 Jul 2018 15:48:30 -0400 Subject: [PATCH] Draw oval and rectangle outlines in bitmap (#550) --- .../bit-oval-mode/oval-outlined.svg | 19 +++ .../bit-rect-mode/rectangle-outlined.svg | 10 ++ src/components/button/button.css | 12 +- src/components/button/button.jsx | 5 +- .../labeled-icon-button.jsx | 1 + src/components/mode-tools/mode-tools.jsx | 82 ++++++++++- src/containers/bit-oval-mode.jsx | 33 ++++- src/containers/bit-rect-mode.jsx | 33 ++++- src/containers/fill-color-indicator.jsx | 6 +- src/containers/mode-tools.jsx | 12 +- src/containers/oval-mode.jsx | 2 +- src/containers/paint-editor.jsx | 22 +-- src/containers/paper-canvas.jsx | 10 +- src/containers/rect-mode.jsx | 2 +- src/containers/reshape-mode.jsx | 2 +- src/containers/rounded-rect-mode.jsx | 2 +- src/containers/select-mode.jsx | 2 +- src/containers/stroke-color-indicator.jsx | 7 +- src/containers/text-mode.jsx | 4 +- src/helper/bit-tools/oval-tool.js | 75 ++++++++-- src/helper/bit-tools/rect-tool.js | 57 +++++++- src/helper/bitmap.js | 137 ++++++++++++------ src/helper/style-path.js | 38 ++++- src/reducers/bit-brush-size.js | 16 ++ src/reducers/fill-bitmap-shapes.js | 25 ++++ src/reducers/fill-color.js | 2 +- src/reducers/scratch-paint-reducer.js | 2 + src/reducers/selected-items.js | 10 +- src/reducers/stroke-color.js | 6 +- src/reducers/stroke-width.js | 6 +- 30 files changed, 507 insertions(+), 133 deletions(-) create mode 100644 src/components/bit-oval-mode/oval-outlined.svg create mode 100644 src/components/bit-rect-mode/rectangle-outlined.svg create mode 100644 src/reducers/fill-bitmap-shapes.js diff --git a/src/components/bit-oval-mode/oval-outlined.svg b/src/components/bit-oval-mode/oval-outlined.svg new file mode 100644 index 00000000..9a6cd489 --- /dev/null +++ b/src/components/bit-oval-mode/oval-outlined.svg @@ -0,0 +1,19 @@ + + + + oval-outlined + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/bit-rect-mode/rectangle-outlined.svg b/src/components/bit-rect-mode/rectangle-outlined.svg new file mode 100644 index 00000000..36228546 --- /dev/null +++ b/src/components/bit-rect-mode/rectangle-outlined.svg @@ -0,0 +1,10 @@ + + + + rectange-outlined + Created with Sketch. + + + + + \ No newline at end of file diff --git a/src/components/button/button.css b/src/components/button/button.css index 9d331158..e807cd89 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -1,18 +1,20 @@ @import "../../css/colors.css"; -:local(.button) { +.button { background: none; cursor: pointer; user-select: none; } -:local(.button:active) { +.button:active { background-color: $motion-transparent; } - -:local(.mod-disabled) { +.highlighted.button { + background-color: $motion-transparent; +} +.mod-disabled { cursor: auto; opacity: .5; } -:local(.mod-disabled:active) { +.mod-disabled:active { background: none; } diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx index 091b58fe..7f1c1660 100644 --- a/src/components/button/button.jsx +++ b/src/components/button/button.jsx @@ -13,6 +13,7 @@ import styles from './button.css'; const ButtonComponent = ({ className, + highlighted, onClick, children, ...props @@ -29,7 +30,8 @@ const ButtonComponent = ({ styles.button, className, { - [styles.modDisabled]: disabled + [styles.modDisabled]: disabled, + [styles.highlighted]: highlighted } )} role="button" @@ -47,6 +49,7 @@ ButtonComponent.propTypes = { PropTypes.string, PropTypes.bool ]), + highlighted: PropTypes.bool, onClick: PropTypes.func.isRequired }; export default ButtonComponent; diff --git a/src/components/labeled-icon-button/labeled-icon-button.jsx b/src/components/labeled-icon-button/labeled-icon-button.jsx index 332f0dca..b4349747 100644 --- a/src/components/labeled-icon-button/labeled-icon-button.jsx +++ b/src/components/labeled-icon-button/labeled-icon-button.jsx @@ -35,6 +35,7 @@ const LabeledIconButton = ({ LabeledIconButton.propTypes = { className: PropTypes.string, + highlighted: PropTypes.bool, imgAlt: PropTypes.string, imgSrc: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, diff --git a/src/components/mode-tools/mode-tools.jsx b/src/components/mode-tools/mode-tools.jsx index b81c8678..94ee8dfa 100644 --- a/src/components/mode-tools/mode-tools.jsx +++ b/src/components/mode-tools/mode-tools.jsx @@ -8,9 +8,11 @@ import {changeBrushSize} from '../../reducers/brush-mode'; import {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode'; import {changeBitBrushSize} from '../../reducers/bit-brush-size'; import {changeBitEraserSize} from '../../reducers/bit-eraser-size'; +import {setShapesFilled} from '../../reducers/fill-bitmap-shapes'; import FontDropdown from '../../containers/font-dropdown.jsx'; import LiveInputHOC from '../forms/live-input-hoc.jsx'; +import Label from '../forms/label.jsx'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Input from '../forms/input.jsx'; import InputGroup from '../input-group/input-group.jsx'; @@ -33,6 +35,10 @@ import eraserIcon from '../eraser-mode/eraser.svg'; import flipHorizontalIcon from './icons/flip-horizontal.svg'; import flipVerticalIcon from './icons/flip-vertical.svg'; import straightPointIcon from './icons/straight-point.svg'; +import bitOvalIcon from '../bit-oval-mode/oval.svg'; +import bitRectIcon from '../bit-rect-mode/rectangle.svg'; +import bitOvalOutlinedIcon from '../bit-oval-mode/oval-outlined.svg'; +import bitRectOutlinedIcon from '../bit-rect-mode/rectangle-outlined.svg'; import {MAX_STROKE_WIDTH} from '../../reducers/stroke-width'; @@ -40,15 +46,10 @@ const LiveInput = LiveInputHOC(Input); const ModeToolsComponent = props => { const messages = defineMessages({ brushSize: { - defaultMessage: 'Brush size', + defaultMessage: 'Size', description: 'Label for the brush size input', id: 'paint.modeTools.brushSize' }, - lineSize: { - defaultMessage: 'Line size', - description: 'Label for the line size input', - id: 'paint.modeTools.lineSize' - }, eraserSize: { defaultMessage: 'Eraser size', description: 'Label for the eraser size input', @@ -79,6 +80,11 @@ const ModeToolsComponent = props => { description: 'Label for the button that converts selected points to sharp points', id: 'paint.modeTools.pointed' }, + thickness: { + defaultMessage: 'Thickness', + description: 'Label for the number input to choose the line thickness', + id: 'paint.modeTools.thickness' + }, flipHorizontal: { defaultMessage: 'Flip Horizontal', description: 'Label for the button to flip the image horizontally', @@ -88,6 +94,16 @@ const ModeToolsComponent = props => { defaultMessage: 'Flip Vertical', description: 'Label for the button to flip the image vertically', id: 'paint.modeTools.flipVertical' + }, + filled: { + defaultMessage: 'Filled', + description: 'Label for the button that sets the bitmap rectangle/oval mode to draw outlines', + id: 'paint.modeTools.filled' + }, + outlined: { + defaultMessage: 'Outlined', + description: 'Label for the button that sets the bitmap rectangle/oval mode to draw filled-in shapes', + id: 'paint.modeTools.outlined' } }); @@ -102,7 +118,7 @@ const ModeToolsComponent = props => { props.mode === Modes.BIT_LINE ? bitLineIcon : bitBrushIcon; const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue; const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange; - const currentMessage = props.mode === Modes.BIT_LINE ? messages.lineSize : messages.brushSize; + const currentMessage = props.mode === Modes.BIT_LINE ? messages.thickness : messages.brushSize; return (
@@ -234,6 +250,48 @@ const ModeToolsComponent = props => {
); + case Modes.BIT_RECT: + /* falls through */ + case Modes.BIT_OVAL: + { + const fillIcon = props.mode === Modes.BIT_RECT ? bitRectIcon : bitOvalIcon; + const outlineIcon = props.mode === Modes.BIT_RECT ? bitRectOutlinedIcon : bitOvalOutlinedIcon; + return ( +
+ + + + + + + {props.fillBitmapShapes ? null : ( + + + ) + } +
+ ); + } default: // Leave empty for now, if mode not supported return ( @@ -249,6 +307,7 @@ ModeToolsComponent.propTypes = { className: PropTypes.string, clipboardItems: PropTypes.arrayOf(PropTypes.array), eraserValue: PropTypes.number, + fillBitmapShapes: PropTypes.bool, format: PropTypes.oneOf(Object.keys(Formats)).isRequired, hasSelectedUncurvedPoints: PropTypes.bool, hasSelectedUnpointedPoints: PropTypes.bool, @@ -260,8 +319,10 @@ ModeToolsComponent.propTypes = { onCurvePoints: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onEraserSliderChange: PropTypes.func, + onFillShapes: PropTypes.func.isRequired, onFlipHorizontal: PropTypes.func.isRequired, onFlipVertical: PropTypes.func.isRequired, + onOutlineShapes: PropTypes.func.isRequired, onPasteFromClipboard: PropTypes.func.isRequired, onPointPoints: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, @@ -271,6 +332,7 @@ ModeToolsComponent.propTypes = { const mapStateToProps = state => ({ mode: state.scratchPaint.mode, format: state.scratchPaint.format, + fillBitmapShapes: state.scratchPaint.fillBitmapShapes, bitBrushSize: state.scratchPaint.bitBrushSize, bitEraserSize: state.scratchPaint.bitEraserSize, brushValue: state.scratchPaint.brushMode.brushSize, @@ -290,6 +352,12 @@ const mapDispatchToProps = dispatch => ({ }, onEraserSliderChange: eraserSize => { dispatch(changeEraserSize(eraserSize)); + }, + onFillShapes: () => { + dispatch(setShapesFilled(true)); + }, + onOutlineShapes: () => { + dispatch(setShapesFilled(false)); } }); diff --git a/src/containers/bit-oval-mode.jsx b/src/containers/bit-oval-mode.jsx index 9d367719..aed8231a 100644 --- a/src/containers/bit-oval-mode.jsx +++ b/src/containers/bit-oval-mode.jsx @@ -27,11 +27,20 @@ class BitOvalMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.color !== this.props.color) { - this.tool.setColor(nextProps.color); - } - if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { - this.tool.onSelectionChanged(nextProps.selectedItems); + if (this.tool) { + if (nextProps.color !== this.props.color) { + this.tool.setColor(nextProps.color); + } + if (nextProps.filled !== this.props.filled) { + this.tool.setFilled(nextProps.filled); + } + if (nextProps.thickness !== this.props.thickness || + nextProps.zoom !== this.props.zoom) { + this.tool.setThickness(nextProps.thickness); + } + if (nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } } if (nextProps.isOvalModeActive && !this.props.isOvalModeActive) { @@ -55,6 +64,8 @@ class BitOvalMode extends React.Component { this.props.clearSelectedItems, this.props.onUpdateImage); this.tool.setColor(this.props.color); + this.tool.setFilled(this.props.filled); + this.tool.setThickness(this.props.thickness); this.tool.activate(); } deactivateTool () { @@ -75,25 +86,31 @@ class BitOvalMode extends React.Component { BitOvalMode.propTypes = { clearSelectedItems: PropTypes.func.isRequired, color: PropTypes.string, + filled: PropTypes.bool, handleMouseDown: PropTypes.func.isRequired, isOvalModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + thickness: PropTypes.number.isRequired, + zoom: PropTypes.number.isRequired }; const mapStateToProps = state => ({ color: state.scratchPaint.color.fillColor, + filled: state.scratchPaint.fillBitmapShapes, isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL, - selectedItems: state.scratchPaint.selectedItems + selectedItems: state.scratchPaint.selectedItems, + thickness: state.scratchPaint.bitBrushSize, + zoom: state.scratchPaint.viewBounds.scaling.x }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_OVAL)); diff --git a/src/containers/bit-rect-mode.jsx b/src/containers/bit-rect-mode.jsx index ea46843e..dfbe2fec 100644 --- a/src/containers/bit-rect-mode.jsx +++ b/src/containers/bit-rect-mode.jsx @@ -27,11 +27,20 @@ class BitRectMode extends React.Component { } } componentWillReceiveProps (nextProps) { - if (this.tool && nextProps.color !== this.props.color) { - this.tool.setColor(nextProps.color); - } - if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { - this.tool.onSelectionChanged(nextProps.selectedItems); + if (this.tool) { + if (nextProps.color !== this.props.color) { + this.tool.setColor(nextProps.color); + } + if (nextProps.filled !== this.props.filled) { + this.tool.setFilled(nextProps.filled); + } + if (nextProps.thickness !== this.props.thickness || + nextProps.zoom !== this.props.zoom) { + this.tool.setThickness(nextProps.thickness); + } + if (nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } } if (nextProps.isRectModeActive && !this.props.isRectModeActive) { @@ -55,6 +64,8 @@ class BitRectMode extends React.Component { this.props.clearSelectedItems, this.props.onUpdateImage); this.tool.setColor(this.props.color); + this.tool.setFilled(this.props.filled); + this.tool.setThickness(this.props.thickness); this.tool.activate(); } deactivateTool () { @@ -75,25 +86,31 @@ class BitRectMode extends React.Component { BitRectMode.propTypes = { clearSelectedItems: PropTypes.func.isRequired, color: PropTypes.string, + filled: PropTypes.bool, handleMouseDown: PropTypes.func.isRequired, isRectModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + thickness: PropTypes.number.isRequired, + zoom: PropTypes.number.isRequired }; const mapStateToProps = state => ({ color: state.scratchPaint.color.fillColor, + filled: state.scratchPaint.fillBitmapShapes, isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT, - selectedItems: state.scratchPaint.selectedItems + selectedItems: state.scratchPaint.selectedItems, + thickness: state.scratchPaint.bitBrushSize, + zoom: state.scratchPaint.viewBounds.scaling.x }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_RECT)); diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 78d51d26..a79c1d37 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -5,6 +5,8 @@ import bindAll from 'lodash.bindall'; import {changeFillColor} from '../reducers/fill-color'; import {openFillColor, closeFillColor} from '../reducers/modals'; import Modes from '../lib/modes'; +import Formats from '../lib/format'; +import {isBitmap} from '../lib/format'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import {applyFillColorToSelection} from '../helper/style-path'; @@ -30,7 +32,7 @@ class FillColorIndicator extends React.Component { } handleChangeFillColor (newColor) { // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyFillColorToSelection(newColor, this.props.textEditTarget); + const isDifferent = applyFillColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; this.props.onChangeFillColor(newColor); } @@ -54,6 +56,7 @@ const mapStateToProps = state => ({ disabled: state.scratchPaint.mode === Modes.LINE, fillColor: state.scratchPaint.color.fillColor, fillColorModalVisible: state.scratchPaint.modals.fillColor, + format: state.scratchPaint.format, isEyeDropping: state.scratchPaint.color.eyeDropper.active, textEditTarget: state.scratchPaint.textEditTarget }); @@ -74,6 +77,7 @@ FillColorIndicator.propTypes = { disabled: PropTypes.bool.isRequired, fillColor: PropTypes.string, fillColorModalVisible: PropTypes.bool.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)), isEyeDropping: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, onCloseFillColor: PropTypes.func.isRequired, diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx index 5e7eaade..a1481b1e 100644 --- a/src/containers/mode-tools.jsx +++ b/src/containers/mode-tools.jsx @@ -128,7 +128,7 @@ class ModeTools extends React.Component { changed = true; } if (changed) { - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); this.props.onUpdateImage(); } } @@ -144,7 +144,7 @@ class ModeTools extends React.Component { } } if (changed) { - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); this.props.onUpdateImage(); } } @@ -193,7 +193,7 @@ class ModeTools extends React.Component { } handleDelete () { if (deleteSelection(this.props.mode, this.props.onUpdateImage)) { - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); } } handleCopyToClipboard () { @@ -232,7 +232,7 @@ class ModeTools extends React.Component { placedItem.position.y += 10 * this.props.pasteOffset; } this.props.incrementPasteOffset(); - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); this.props.onUpdateImage(); } } @@ -286,8 +286,8 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + setSelectedItems: format => { + dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format))); } }); diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index 40a407a1..6c6f9514 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -111,7 +111,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.OVAL)); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index f0799b05..70b9ff2e 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -45,6 +45,7 @@ class PaintEditor extends React.Component { 'handleSendForward', 'handleSendToBack', 'handleSendToFront', + 'handleSetSelectedItems', 'handleGroup', 'handleUngroup', 'handleZoomIn', @@ -218,16 +219,16 @@ class PaintEditor extends React.Component { } } handleUndo () { - performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateImage); + performUndo(this.props.undoState, this.props.onUndo, this.handleSetSelectedItems, this.handleUpdateImage); } handleRedo () { - performRedo(this.props.undoState, this.props.onRedo, this.props.setSelectedItems, this.handleUpdateImage); + performRedo(this.props.undoState, this.props.onRedo, this.handleSetSelectedItems, this.handleUpdateImage); } handleGroup () { - groupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateImage); + groupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage); } handleUngroup () { - ungroupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateImage); + ungroupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage); } handleSendBackward () { sendBackward(this.handleUpdateImage); @@ -241,6 +242,9 @@ class PaintEditor extends React.Component { handleSendToFront () { bringToFront(this.handleUpdateImage); } + handleSetSelectedItems () { + this.props.setSelectedItems(this.props.format); + } canUndo () { return shouldShowUndo(this.props.undoState); } @@ -250,17 +254,17 @@ class PaintEditor extends React.Component { handleZoomIn () { zoomOnSelection(PaintEditor.ZOOM_INCREMENT); this.props.updateViewBounds(paper.view.matrix); - this.props.setSelectedItems(); + this.handleSetSelectedItems(); } handleZoomOut () { zoomOnSelection(-PaintEditor.ZOOM_INCREMENT); this.props.updateViewBounds(paper.view.matrix); - this.props.setSelectedItems(); + this.handleSetSelectedItems(); } handleZoomReset () { resetZoom(); this.props.updateViewBounds(paper.view.matrix); - this.props.setSelectedItems(); + this.handleSetSelectedItems(); } setCanvas (canvas) { this.setState({canvas: canvas}); @@ -457,8 +461,8 @@ const mapDispatchToProps = dispatch => ({ removeTextEditTarget: () => { dispatch(setTextEditTarget()); }, - setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + setSelectedItems: format => { + dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format))); }, onDeactivateEyeDropper: () => { // set redux values to default for eye dropper reducer diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 38038ad7..944e4fb9 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -4,6 +4,7 @@ import React from 'react'; import {connect} from 'react-redux'; import paper from '@scratch/paper'; import Formats from '../lib/format'; +import {isBitmap} from '../lib/format'; import Modes from '../lib/modes'; import log from '../log/log'; @@ -68,7 +69,7 @@ class PaperCanvas extends React.Component { // Backspace, delete if (event.key === 'Delete' || event.key === 'Backspace') { if (deleteSelection(this.props.mode, this.props.onUpdateImage)) { - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); } } } @@ -229,7 +230,7 @@ class PaperCanvas extends React.Component { ); zoomOnFixedPoint(-deltaY / 100, fixedPoint); this.props.updateViewBounds(paper.view.matrix); - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); } else if (event.shiftKey && event.deltaX === 0) { // Scroll horizontally (based on vertical scroll delta) // This is needed as for some browser/system combinations which do not set deltaX. @@ -265,6 +266,7 @@ PaperCanvas.propTypes = { clearPasteOffset: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)), // Internal, up-to-date data format image: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(HTMLImageElement) @@ -290,8 +292,8 @@ const mapDispatchToProps = dispatch => ({ clearUndo: () => { dispatch(clearUndoState()); }, - setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + setSelectedItems: format => { + dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format))); }, clearSelectedItems: () => { dispatch(clearSelectedItems()); diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index ca936dfb..3788b5ba 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -111,7 +111,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.RECT)); diff --git a/src/containers/reshape-mode.jsx b/src/containers/reshape-mode.jsx index ff499cf9..ca83e4e6 100644 --- a/src/containers/reshape-mode.jsx +++ b/src/containers/reshape-mode.jsx @@ -92,7 +92,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.RESHAPE)); diff --git a/src/containers/rounded-rect-mode.jsx b/src/containers/rounded-rect-mode.jsx index bfee9b76..2ad730b9 100644 --- a/src/containers/rounded-rect-mode.jsx +++ b/src/containers/rounded-rect-mode.jsx @@ -90,7 +90,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.ROUNDED_RECT)); diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index e1a0ed47..ce43dc3b 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -96,7 +96,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, handleMouseDown: () => { dispatch(changeMode(Modes.SELECT)); diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index c752494c..4f559d91 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -5,6 +5,8 @@ import bindAll from 'lodash.bindall'; import {changeStrokeColor} from '../reducers/stroke-color'; import {openStrokeColor, closeStrokeColor} from '../reducers/modals'; import Modes from '../lib/modes'; +import Formats from '../lib/format'; +import {isBitmap} from '../lib/format'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import {applyStrokeColorToSelection} from '../helper/style-path'; @@ -30,7 +32,8 @@ class StrokeColorIndicator extends React.Component { } handleChangeStrokeColor (newColor) { // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyStrokeColorToSelection(newColor, this.props.textEditTarget); + const isDifferent = + applyStrokeColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; this.props.onChangeStrokeColor(newColor); } @@ -53,6 +56,7 @@ class StrokeColorIndicator extends React.Component { const mapStateToProps = state => ({ disabled: state.scratchPaint.mode === Modes.BRUSH || state.scratchPaint.mode === Modes.TEXT, + format: state.scratchPaint.format, isEyeDropping: state.scratchPaint.color.eyeDropper.active, strokeColor: state.scratchPaint.color.strokeColor, strokeColorModalVisible: state.scratchPaint.modals.strokeColor, @@ -73,6 +77,7 @@ const mapDispatchToProps = dispatch => ({ StrokeColorIndicator.propTypes = { disabled: PropTypes.bool.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)), isEyeDropping: PropTypes.bool.isRequired, onChangeStrokeColor: PropTypes.func.isRequired, onCloseStrokeColor: PropTypes.func.isRequired, diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index 7ab6f2e5..ad6baab7 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -148,7 +148,7 @@ const mapStateToProps = (state, ownProps) => ({ textEditTarget: state.scratchPaint.textEditTarget, viewBounds: state.scratchPaint.viewBounds }); -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch, ownProps) => ({ changeFont: font => { dispatch(changeFont(font)); }, @@ -162,7 +162,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.TEXT)); }, setSelectedItems: () => { - dispatch(setSelectedItems(getSelectedLeafItems())); + dispatch(setSelectedItems(getSelectedLeafItems(), ownProps.isBitmap)); }, setTextEditTarget: targetId => { dispatch(setTextEditTarget(targetId)); diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js index d202f3d5..3093f24a 100644 --- a/src/helper/bit-tools/oval-tool.js +++ b/src/helper/bit-tools/oval-tool.js @@ -57,17 +57,49 @@ class OvalTool extends paper.Tool { */ onSelectionChanged (selectedItems) { this.boundingBoxTool.onSelectionChanged(selectedItems); - if ((!this.oval || !this.oval.parent) && + if ((!this.oval || !this.oval.isInserted()) && selectedItems && selectedItems.length === 1 && selectedItems[0].shape === 'ellipse') { // Infer that an undo occurred and get back the active oval this.oval = selectedItems[0]; - } else if (this.oval && this.oval.parent && !this.oval.selected) { + if (this.oval.data.zoomLevel !== paper.view.zoom) { + this.oval.strokeWidth = this.oval.strokeWidth / this.oval.data.zoomLevel * paper.view.zoom; + this.oval.data.zoomLevel = paper.view.zoom; + } + } else if (this.oval && this.oval.isInserted() && !this.oval.selected) { // Oval got deselected this.commitOval(); } } setColor (color) { this.color = color; + if (this.oval) { + if (this.filled) { + this.oval.fillColor = this.color; + } else { + this.oval.strokeColor = this.color; + } + } + } + setFilled (filled) { + this.filled = filled; + if (this.oval) { + if (this.filled) { + this.oval.fillColor = this.color; + this.oval.strokeWidh = 0; + this.oval.strokeColor = null; + } else { + this.oval.fillColor = null; + this.oval.strokeWidth = this.thickness; + this.oval.strokeColor = this.color; + } + } + } + setThickness (thickness) { + this.thickness = thickness * paper.view.zoom; + if (this.oval && !this.filled) { + this.oval.strokeWidth = this.thickness; + } + if (this.oval) this.oval.data.zoomLevel = paper.view.zoom; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button @@ -79,11 +111,24 @@ class OvalTool extends paper.Tool { this.isBoundingBoxMode = false; clearSelection(this.clearSelectedItems); this.commitOval(); - this.oval = new paper.Shape.Ellipse({ - fillColor: this.color, - point: event.downPoint, - size: 0 - }); + if (this.filled) { + this.oval = new paper.Shape.Ellipse({ + fillColor: this.color, + point: event.downPoint, + strokeWidth: 0, + strokeScaling: false, + size: 0 + }); + } else { + this.oval = new paper.Shape.Ellipse({ + strokeColor: this.color, + strokeWidth: this.thickness, + point: event.downPoint, + strokeScaling: false, + size: 0 + }); + } + this.oval.data = {zoomLevel: paper.view.zoom}; } } handleMouseDrag (event) { @@ -132,19 +177,21 @@ class OvalTool extends paper.Tool { this.active = false; } commitOval () { - if (!this.oval || !this.oval.parent) return; + if (!this.oval || !this.oval.isInserted()) return; const radiusX = Math.abs(this.oval.size.width / 2); const radiusY = Math.abs(this.oval.size.height / 2); const context = getRaster().getContext('2d'); context.fillStyle = this.color; - const drew = drawEllipse( - this.oval.position.x, this.oval.position.y, - radiusX, radiusY, - this.oval.matrix, - true, /* isFilled */ - context); + const drew = drawEllipse({ + position: this.oval.position, + radiusX, + radiusY, + matrix: this.oval.matrix, + isFilled: this.filled, + thickness: this.thickness / paper.view.zoom + }, context); this.oval.remove(); this.oval = null; diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js index d5de5ebe..9041cda5 100644 --- a/src/helper/bit-tools/rect-tool.js +++ b/src/helper/bit-tools/rect-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; -import {fillRect} from '../bitmap'; +import {fillRect, outlineRect} from '../bitmap'; import {createCanvas, getRaster} from '../layer'; import {clearSelection} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; @@ -57,17 +57,49 @@ class RectTool extends paper.Tool { */ onSelectionChanged (selectedItems) { this.boundingBoxTool.onSelectionChanged(selectedItems); - if ((!this.rect || !this.rect.parent) && + if ((!this.rect || !this.rect.isInserted()) && selectedItems && selectedItems.length === 1 && selectedItems[0].shape === 'rectangle') { // Infer that an undo occurred and get back the active rect this.rect = selectedItems[0]; - } else if (this.rect && this.rect.parent && !this.rect.selected) { + if (this.rect.data.zoomLevel !== paper.view.zoom) { + this.rect.strokeWidth = this.rect.strokeWidth / this.rect.data.zoomLevel * paper.view.zoom; + this.rect.data.zoomLevel = paper.view.zoom; + } + } else if (this.rect && this.rect.isInserted() && !this.rect.selected) { // Rectangle got deselected this.commitRect(); } } setColor (color) { this.color = color; + if (this.rect) { + if (this.filled) { + this.rect.fillColor = this.color; + } else { + this.rect.strokeColor = this.color; + } + } + } + setFilled (filled) { + this.filled = filled; + if (this.rect) { + if (this.filled) { + this.rect.fillColor = this.color; + this.rect.strokeWidh = 0; + this.rect.strokeColor = null; + } else { + this.rect.fillColor = null; + this.rect.strokeWidth = this.thickness; + this.rect.strokeColor = this.color; + } + } + } + setThickness (thickness) { + this.thickness = thickness * paper.view.zoom; + if (this.rect && !this.filled) { + this.rect.strokeWidth = this.thickness; + } + if (this.rect) this.rect.data.zoomLevel = paper.view.zoom; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button @@ -97,7 +129,16 @@ class RectTool extends paper.Tool { } if (this.rect) this.rect.remove(); this.rect = new paper.Shape.Rectangle(baseRect); - this.rect.fillColor = this.color; + if (this.filled) { + this.rect.fillColor = this.color; + this.rect.strokeWidth = 0; + } else { + this.rect.strokeColor = this.color; + this.rect.strokeWidth = this.thickness; + } + this.rect.strokeJoin = 'round'; + this.rect.strokeScaling = false; + this.rect.data = {zoomLevel: paper.view.zoom}; if (event.modifiers.alt) { this.rect.position = event.downPoint; @@ -129,12 +170,16 @@ class RectTool extends paper.Tool { this.active = false; } commitRect () { - if (!this.rect || !this.rect.parent) return; + if (!this.rect || !this.rect.isInserted()) return; const tmpCanvas = createCanvas(); const context = tmpCanvas.getContext('2d'); context.fillStyle = this.color; - fillRect(this.rect, context); + if (this.filled) { + fillRect(this.rect, context); + } else { + outlineRect(this.rect, this.thickness / paper.view.zoom, context); + } getRaster().drawImage(tmpCanvas, new paper.Point()); this.rect.remove(); diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js index c13463c9..85df96d1 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -51,6 +51,8 @@ const solveQuadratic_ = function (a, b, c) { * @param {!number} options.radiusY minor radius of ellipse * @param {!number} options.shearSlope slope of the sheared x axis * @param {?boolean} options.isFilled true if isFilled + * @param {?function} options.drawFn The function called on each point in the outline, used only + * if isFilled is false. * @param {!CanvasRenderingContext2D} context for drawing * @return {boolean} true if anything was drawn, false if not */ @@ -61,6 +63,7 @@ const drawShearedEllipse_ = function (options, context) { const radiusY = ~~Math.abs(options.radiusY) - .5; const shearSlope = options.shearSlope; const isFilled = options.isFilled; + const drawFn = options.drawFn; if (shearSlope === Infinity || radiusX < 1 || radiusY < 1) { return false; } @@ -96,8 +99,8 @@ const drawShearedEllipse_ = function (options, context) { context.fillRect(centerX - pX1 - 1, centerY + pY, pX1 - pX2 + 1, 1); context.fillRect(centerX + pX2, centerY - pY - 1, pX1 - pX2 + 1, 1); } else { - context.fillRect(centerX - pX1 - 1, centerY + pY, 1, 1); - context.fillRect(centerX + pX1, centerY - pY - 1, 1, 1); + drawFn(centerX - pX1 - 1, centerY + pY); + drawFn(centerX + pX1, centerY - pY - 1); } y--; x = solveQuadratic_(A, B * y, (C * y * y) - 1); @@ -127,8 +130,8 @@ const drawShearedEllipse_ = function (options, context) { context.fillRect(centerX - pX - 1, centerY + pY2, 1, pY1 - pY2 + 1); context.fillRect(centerX + pX, centerY - pY1 - 1, 1, pY1 - pY2 + 1); } else { - context.fillRect(centerX - pX - 1, centerY + pY1, 1, 1); - context.fillRect(centerX + pX, centerY - pY1 - 1, 1, 1); + drawFn(centerX - pX - 1, centerY + pY1); + drawFn(centerX + pX, centerY - pY1 - 1); } x++; y = solveQuadratic_(C, B * x, (A * x * x) - 1); @@ -188,46 +191,6 @@ const drawShearedEllipse_ = function (options, context) { return true; }; -/** - * Draw an ellipse, given the original axis-aligned radii and - * an affine transformation. Returns false if the ellipse could - * not be drawn; for instance, the matrix is non-invertible. - * - * @param {!number} positionX Center of ellipse - * @param {!number} positionY Center of ellipse - * @param {!number} radiusX x-aligned radius of ellipse - * @param {!number} radiusY y-aligned radius of ellipse - * @param {!paper.Matrix} matrix affine transformation matrix - * @param {?boolean} isFilled true if isFilled - * @param {!CanvasRenderingContext2D} context for drawing - * @return {boolean} true if anything was drawn, false if not - */ -const drawEllipse = function (positionX, positionY, radiusX, radiusY, matrix, isFilled, context) { - if (!matrix.isInvertible()) return false; - const inverse = matrix.clone().invert(); - - // Calculate the ellipse formula - // A, B, and C represent Ax^2 + Bxy + Cy^2 = 1 coefficients in a transformed ellipse formula - const A = (inverse.a * inverse.a / radiusX / radiusX) + (inverse.b * inverse.b / radiusY / radiusY); - const B = (2 * inverse.a * inverse.c / radiusX / radiusX) + (2 * inverse.b * inverse.d / radiusY / radiusY); - const C = (inverse.c * inverse.c / radiusX / radiusX) + (inverse.d * inverse.d / radiusY / radiusY); - - // Convert to a sheared ellipse formula. All ellipses are equivalent to some sheared axis-aligned ellipse. - // radiusA, radiusB, and slope are parameters of a skewed ellipse with the above formula - const radiusB = 1 / Math.sqrt(C); - const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C))); - const slope = B / 2 / C; - - return drawShearedEllipse_({ - centerX: positionX, - centerY: positionY, - radiusX: radiusA, - radiusY: radiusB, - shearSlope: slope, - isFilled: isFilled - }, context); -}; - /** * @param {!number} size The diameter of the brush * @param {!string} color The css color of the brush @@ -273,13 +236,73 @@ const getBrushMark = function (size, color, isEraser) { radiusX: size / 2, radiusY: size / 2, shearSlope: 0, - isFilled: false + isFilled: false, + drawFn: (x, y) => context.fillRect(x, y, 1, 1) }, context); } } return canvas; }; +/** + * Draw an ellipse, given the original axis-aligned radii and + * an affine transformation. Returns false if the ellipse could + * not be drawn; for instance, the matrix is non-invertible. + * + * @param {!options} options Parameters for the ellipse + * @param {!paper.Point} options.position Center of ellipse + * @param {!number} options.radiusX x-aligned radius of ellipse + * @param {!number} options.radiusY y-aligned radius of ellipse + * @param {!paper.Matrix} options.matrix affine transformation matrix + * @param {?boolean} options.isFilled true if isFilled + * @param {?number} options.thickness Thickness of outline, used only if isFilled is false. + * @param {!CanvasRenderingContext2D} context for drawing + * @return {boolean} true if anything was drawn, false if not + */ +const drawEllipse = function (options, context) { + const positionX = options.position.x; + const positionY = options.position.y; + const radiusX = options.radiusX; + const radiusY = options.radiusY; + const matrix = options.matrix; + const isFilled = options.isFilled; + const thickness = options.thickness; + let drawFn = null; + + if (!matrix.isInvertible()) return false; + const inverse = matrix.clone().invert(); + + if (!isFilled) { + const brushMark = getBrushMark(thickness, context.fillStyle); + const roundedUpRadius = Math.ceil(thickness / 2); + drawFn = (x, y) => { + context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius); + }; + } + + // Calculate the ellipse formula + // A, B, and C represent Ax^2 + Bxy + Cy^2 = 1 coefficients in a transformed ellipse formula + const A = (inverse.a * inverse.a / radiusX / radiusX) + (inverse.b * inverse.b / radiusY / radiusY); + const B = (2 * inverse.a * inverse.c / radiusX / radiusX) + (2 * inverse.b * inverse.d / radiusY / radiusY); + const C = (inverse.c * inverse.c / radiusX / radiusX) + (inverse.d * inverse.d / radiusY / radiusY); + + // Convert to a sheared ellipse formula. All ellipses are equivalent to some sheared axis-aligned ellipse. + // radiusA, radiusB, and slope are parameters of a skewed ellipse with the above formula + const radiusB = 1 / Math.sqrt(C); + const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C))); + const slope = B / 2 / C; + + return drawShearedEllipse_({ + centerX: positionX, + centerY: positionY, + radiusX: radiusA, + radiusY: radiusB, + shearSlope: slope, + isFilled: isFilled, + drawFn: drawFn + }, context); +}; + 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; @@ -558,6 +581,29 @@ const fillRect = function (rect, context) { } }; +/** + * @param {!paper.Shape.Rectangle} rect The rectangle to draw to the canvas + * @param {!number} thickness The thickness of the outline + * @param {!HTMLCanvas2DContext} context The context in which to draw + */ +const outlineRect = function (rect, thickness, context) { + const brushMark = getBrushMark(thickness, context.fillStyle); + const roundedUpRadius = Math.ceil(thickness / 2); + const drawFn = (x, y) => { + context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius); + }; + + const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2)); + const widthPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, -rect.size.height / 2)); + const heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2)); + const endPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, rect.size.height / 2)); + + forEachLinePoint(startPoint, widthPoint, drawFn); + forEachLinePoint(startPoint, heightPoint, drawFn); + forEachLinePoint(endPoint, widthPoint, drawFn); + forEachLinePoint(endPoint, heightPoint, drawFn); +}; + const flipBitmapHorizontal = function (canvas) { const tmpCanvas = createCanvas(canvas.width, canvas.height); const context = tmpCanvas.getContext('2d'); @@ -597,6 +643,7 @@ export { convertToBitmap, convertToVector, fillRect, + outlineRect, floodFill, floodFillAll, getBrushMark, diff --git a/src/helper/style-path.js b/src/helper/style-path.js index eda8085a..9c7d829d 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -33,10 +33,11 @@ const _getColorStateListeners = function (textEditTargetId) { /** * Called when setting fill color * @param {string} colorString New color, css format + * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const applyFillColorToSelection = function (colorString, textEditTargetId) { +const applyFillColorToSelection = function (colorString, bitmapMode, textEditTargetId) { const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { @@ -45,7 +46,13 @@ const applyFillColorToSelection = function (colorString, textEditTargetId) { } else if (item.parent instanceof paper.CompoundPath) { item = item.parent; } - if (!_colorMatch(item.fillColor, colorString)) { + // In bitmap mode, fill color applies to the stroke if there is a stroke + if (bitmapMode && item.strokeColor !== null && item.strokeWidth !== 0) { + if (!_colorMatch(item.strokeColor, colorString)) { + changed = true; + item.strokeColor = colorString; + } + } else if (!_colorMatch(item.fillColor, colorString)) { changed = true; item.fillColor = colorString; } @@ -56,10 +63,14 @@ const applyFillColorToSelection = function (colorString, textEditTargetId) { /** * Called when setting stroke color * @param {string} colorString New color, css format + * @param {?boolean} bitmapMode True if the stroke color is being set in bitmap mode * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const applyStrokeColorToSelection = function (colorString, textEditTargetId) { +const applyStrokeColorToSelection = function (colorString, bitmapMode, textEditTargetId) { + // Bitmap mode doesn't have stroke color + if (bitmapMode) return false; + const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { @@ -125,14 +136,17 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) { /** * Get state of colors and stroke width for selection * @param {!Array} selectedItems Selected paper items - * @return {object} Object of strokeColor, strokeWidth, fillColor of the selection. + * @param {?boolean} bitmapMode True if the item is being selected in bitmap mode + * @return {?object} Object of strokeColor, strokeWidth, fillColor, thickness of the selection. * Gives MIXED when there are mixed values for a color, and null for transparent. * Gives null when there are mixed values for stroke width. + * Thickness is line thickness, used in the bitmap editor */ -const getColorsFromSelection = function (selectedItems) { +const getColorsFromSelection = function (selectedItems, bitmapMode) { let selectionFillColorString; let selectionStrokeColorString; let selectionStrokeWidth; + let selectionThickness; let firstChild = true; for (let item of selectedItems) { @@ -185,7 +199,10 @@ const getColorsFromSelection = function (selectedItems) { } } if (item.strokeColor) { - if (item.strokeColor.type === 'gradient') { + // Stroke color is fill color in bitmap + if (bitmapMode) { + itemFillColorString = item.strokeColor.toCSS(); + } else if (item.strokeColor.type === 'gradient') { itemStrokeColorString = MIXED; } else { itemStrokeColorString = item.strokeColor.toCSS(); @@ -197,6 +214,9 @@ const getColorsFromSelection = function (selectedItems) { selectionFillColorString = itemFillColorString; selectionStrokeColorString = itemStrokeColorString; selectionStrokeWidth = item.strokeWidth; + if (item.strokeWidth && item.data && item.data.zoomLevel) { + selectionThickness = item.strokeWidth / item.data.zoomLevel; + } } if (itemFillColorString !== selectionFillColorString) { selectionFillColorString = MIXED; @@ -209,6 +229,12 @@ const getColorsFromSelection = function (selectedItems) { } } } + if (bitmapMode) { + return { + fillColor: selectionFillColorString ? selectionFillColorString : null, + thickness: selectionThickness + }; + } return { fillColor: selectionFillColorString ? selectionFillColorString : null, strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, diff --git a/src/reducers/bit-brush-size.js b/src/reducers/bit-brush-size.js index d4754fd7..82a66990 100644 --- a/src/reducers/bit-brush-size.js +++ b/src/reducers/bit-brush-size.js @@ -1,4 +1,6 @@ import log from '../log/log'; +import {CHANGE_SELECTED_ITEMS} from './selected-items'; +import {getColorsFromSelection} from '../helper/style-path'; // Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width // in the bitmap paint editor. @@ -14,6 +16,20 @@ const reducer = function (state, action) { return state; } return Math.max(1, action.brushSize); + case CHANGE_SELECTED_ITEMS: + { + // Don't change state if no selection + if (!action.selectedItems || !action.selectedItems.length) { + return state; + } + // Vector mode doesn't have bit width + if (!action.bitmapMode) { + return state; + } + const colorState = getColorsFromSelection(action.selectedItems, action.bitmapMode); + if (colorState.thickness) return colorState.thickness; + return state; + } default: return state; } diff --git a/src/reducers/fill-bitmap-shapes.js b/src/reducers/fill-bitmap-shapes.js new file mode 100644 index 00000000..7ca44d70 --- /dev/null +++ b/src/reducers/fill-bitmap-shapes.js @@ -0,0 +1,25 @@ +const SET_FILLED = 'scratch-paint/fill-bitmap-shapes/SET_FILLED'; +const initialState = true; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_FILLED: + return action.filled; + default: + return state; + } +}; + +// Action creators ================================== +const setShapesFilled = function (filled) { + return { + type: SET_FILLED, + filled: filled + }; +}; + +export { + reducer as default, + setShapesFilled +}; diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js index a6cf48cf..ed79e689 100644 --- a/src/reducers/fill-color.js +++ b/src/reducers/fill-color.js @@ -22,7 +22,7 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection(action.selectedItems).fillColor; + return getColorsFromSelection(action.selectedItems, action.bitmapMode).fillColor; default: return state; } diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 3bb7c115..29b23218 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -6,6 +6,7 @@ import brushModeReducer from './brush-mode'; import eraserModeReducer from './eraser-mode'; import colorReducer from './color'; import clipboardReducer from './clipboard'; +import fillBitmapShapesReducer from './fill-bitmap-shapes'; import fontReducer from './font'; import formatReducer from './format'; import hoverReducer from './hover'; @@ -23,6 +24,7 @@ export default combineReducers({ color: colorReducer, clipboard: clipboardReducer, eraserMode: eraserModeReducer, + fillBitmapShapes: fillBitmapShapesReducer, font: fontReducer, format: formatReducer, hoveredItemId: hoverReducer, diff --git a/src/reducers/selected-items.js b/src/reducers/selected-items.js index c68dece7..d7bbd143 100644 --- a/src/reducers/selected-items.js +++ b/src/reducers/selected-items.js @@ -10,6 +10,10 @@ const reducer = function (state, action) { log.warn(`No selected items or wrong format provided: ${action.selectedItems}`); return state; } + if (action.selectedItems.length > 1 && action.bitmapMode) { + log.warn(`Multiselect should not be possible in bitmap mode: ${action.selectedItems}`); + return state; + } // If they are both empty, no change if (action.selectedItems.length === 0 && state.length === 0) { return state; @@ -24,12 +28,14 @@ const reducer = function (state, action) { /** * Set the selected item state to the given array of items * @param {Array} selectedItems from paper.project.selectedItems + * @param {?boolean} bitmapMode True if the items are being selected in bitmap mode * @return {object} Redux action to change the selected items. */ -const setSelectedItems = function (selectedItems) { +const setSelectedItems = function (selectedItems, bitmapMode) { return { type: CHANGE_SELECTED_ITEMS, - selectedItems: selectedItems + selectedItems: selectedItems, + bitmapMode: bitmapMode }; }; const clearSelectedItems = function () { diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js index 7bee304c..8e6d3a39 100644 --- a/src/reducers/stroke-color.js +++ b/src/reducers/stroke-color.js @@ -21,7 +21,11 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection(action.selectedItems).strokeColor; + // Bitmap mode doesn't have stroke color + if (action.bitmapMode) { + return state; + } + return getColorsFromSelection(action.selectedItems, action.bitmapMode).strokeColor; default: return state; } diff --git a/src/reducers/stroke-width.js b/src/reducers/stroke-width.js index 19564d2c..a1daae44 100644 --- a/src/reducers/stroke-width.js +++ b/src/reducers/stroke-width.js @@ -20,7 +20,11 @@ const reducer = function (state, action) { if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection(action.selectedItems).strokeWidth; + // Bitmap mode doesn't have stroke width + if (action.bitmapMode) { + return state; + } + return getColorsFromSelection(action.selectedItems, action.bitmapMode).strokeWidth; default: return state; }