Draw oval and rectangle outlines in bitmap (#550)

This commit is contained in:
DD Liu 2018-07-12 15:48:30 -04:00 committed by GitHub
parent 11bab6ebe2
commit 4e4bb396a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 507 additions and 133 deletions

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>oval-outlined</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="oval-outlined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(4.000000, 4.000000)" fill="#575E75">
<polygon id="Fill-1" points="0 9.33333333 1.33333333 9.33333333 1.33333333 2.66666667 0 2.66666667"></polygon>
<polygon id="Fill-2" points="1.33333333 2.66666667 2.66666667 2.66666667 2.66666667 1.33333333 1.33333333 1.33333333"></polygon>
<polygon id="Fill-3" points="1.33333333 10.6666667 2.66666667 10.6666667 2.66666667 9.33333333 1.33333333 9.33333333"></polygon>
<polygon id="Fill-4" points="2.66666667 1.33333333 9.33333333 1.33333333 9.33333333 0 2.66666667 0"></polygon>
<polygon id="Fill-5" points="9.33333333 2.66666667 10.6666667 2.66666667 10.6666667 1.33333333 9.33333333 1.33333333"></polygon>
<polygon id="Fill-6" points="10.6666667 9.33333333 12 9.33333333 12 2.66666667 10.6666667 2.66666667"></polygon>
<polygon id="Fill-7" points="9.33333333 10.6666667 10.6666667 10.6666667 10.6666667 9.33333333 9.33333333 9.33333333"></polygon>
<polygon id="Fill-8" points="2.66666667 12 9.33333333 12 9.33333333 10.6666667 2.66666667 10.6666667"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>rectange-outlined</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="rectange-outlined" stroke="none" stroke-width="1.33333333" fill="none" fill-rule="evenodd">
<rect id="rectangle-icon" stroke="#575E75" x="4.5" y="4.5" width="11" height="11"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 576 B

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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 (
<div className={classNames(props.className, styles.modeTools)}>
<div>
@ -234,6 +250,48 @@ const ModeToolsComponent = props => {
</InputGroup>
</div>
);
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 (
<div className={classNames(props.className, styles.modeTools)}>
<InputGroup>
<LabeledIconButton
highlighted={props.fillBitmapShapes}
imgSrc={fillIcon}
title={props.intl.formatMessage(messages.filled)}
onClick={props.onFillShapes}
/>
</InputGroup>
<InputGroup>
<LabeledIconButton
highlighted={!props.fillBitmapShapes}
imgSrc={outlineIcon}
title={props.intl.formatMessage(messages.outlined)}
onClick={props.onOutlineShapes}
/>
</InputGroup>
{props.fillBitmapShapes ? null : (
<InputGroup>
<Label text={props.intl.formatMessage(messages.thickness)}>
<LiveInput
range
small
max={MAX_STROKE_WIDTH}
min="1"
type="number"
value={props.bitBrushSize}
onSubmit={props.onBitBrushSliderChange}
/>
</Label>
</InputGroup>)
}
</div>
);
}
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));
}
});

View file

@ -27,12 +27,21 @@ class BitOvalMode extends React.Component {
}
}
componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.color !== this.props.color) {
if (this.tool) {
if (nextProps.color !== this.props.color) {
this.tool.setColor(nextProps.color);
}
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
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) {
this.activateTool();
@ -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));

View file

@ -27,12 +27,21 @@ class BitRectMode extends React.Component {
}
}
componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.color !== this.props.color) {
if (this.tool) {
if (nextProps.color !== this.props.color) {
this.tool.setColor(nextProps.color);
}
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
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) {
this.activateTool();
@ -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));

View file

@ -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,

View file

@ -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)));
}
});

View file

@ -111,7 +111,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.OVAL));

View file

@ -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

View file

@ -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());

View file

@ -111,7 +111,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.RECT));

View file

@ -92,7 +92,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.RESHAPE));

View file

@ -90,7 +90,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.ROUNDED_RECT));

View file

@ -96,7 +96,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems()));
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.SELECT));

View file

@ -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,

View file

@ -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));

View file

@ -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();
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;

View file

@ -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);
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;
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();

View file

@ -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,

View file

@ -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<paper.Item>} 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,

View file

@ -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;
}

View file

@ -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
};

View file

@ -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;
}

View file

@ -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,

View file

@ -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<paper.Item>} 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 () {

View file

@ -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;
}

View file

@ -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;
}