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"; @import "../../css/colors.css";
:local(.button) { .button {
background: none; background: none;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
:local(.button:active) { .button:active {
background-color: $motion-transparent; background-color: $motion-transparent;
} }
.highlighted.button {
:local(.mod-disabled) { background-color: $motion-transparent;
}
.mod-disabled {
cursor: auto; cursor: auto;
opacity: .5; opacity: .5;
} }
:local(.mod-disabled:active) { .mod-disabled:active {
background: none; background: none;
} }

View file

@ -13,6 +13,7 @@ import styles from './button.css';
const ButtonComponent = ({ const ButtonComponent = ({
className, className,
highlighted,
onClick, onClick,
children, children,
...props ...props
@ -29,7 +30,8 @@ const ButtonComponent = ({
styles.button, styles.button,
className, className,
{ {
[styles.modDisabled]: disabled [styles.modDisabled]: disabled,
[styles.highlighted]: highlighted
} }
)} )}
role="button" role="button"
@ -47,6 +49,7 @@ ButtonComponent.propTypes = {
PropTypes.string, PropTypes.string,
PropTypes.bool PropTypes.bool
]), ]),
highlighted: PropTypes.bool,
onClick: PropTypes.func.isRequired onClick: PropTypes.func.isRequired
}; };
export default ButtonComponent; export default ButtonComponent;

View file

@ -35,6 +35,7 @@ const LabeledIconButton = ({
LabeledIconButton.propTypes = { LabeledIconButton.propTypes = {
className: PropTypes.string, className: PropTypes.string,
highlighted: PropTypes.bool,
imgAlt: PropTypes.string, imgAlt: PropTypes.string,
imgSrc: PropTypes.string.isRequired, imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func.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 {changeBrushSize as changeEraserSize} from '../../reducers/eraser-mode';
import {changeBitBrushSize} from '../../reducers/bit-brush-size'; import {changeBitBrushSize} from '../../reducers/bit-brush-size';
import {changeBitEraserSize} from '../../reducers/bit-eraser-size'; import {changeBitEraserSize} from '../../reducers/bit-eraser-size';
import {setShapesFilled} from '../../reducers/fill-bitmap-shapes';
import FontDropdown from '../../containers/font-dropdown.jsx'; import FontDropdown from '../../containers/font-dropdown.jsx';
import LiveInputHOC from '../forms/live-input-hoc.jsx'; import LiveInputHOC from '../forms/live-input-hoc.jsx';
import Label from '../forms/label.jsx';
import {defineMessages, injectIntl, intlShape} from 'react-intl'; import {defineMessages, injectIntl, intlShape} from 'react-intl';
import Input from '../forms/input.jsx'; import Input from '../forms/input.jsx';
import InputGroup from '../input-group/input-group.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 flipHorizontalIcon from './icons/flip-horizontal.svg';
import flipVerticalIcon from './icons/flip-vertical.svg'; import flipVerticalIcon from './icons/flip-vertical.svg';
import straightPointIcon from './icons/straight-point.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'; import {MAX_STROKE_WIDTH} from '../../reducers/stroke-width';
@ -40,15 +46,10 @@ const LiveInput = LiveInputHOC(Input);
const ModeToolsComponent = props => { const ModeToolsComponent = props => {
const messages = defineMessages({ const messages = defineMessages({
brushSize: { brushSize: {
defaultMessage: 'Brush size', defaultMessage: 'Size',
description: 'Label for the brush size input', description: 'Label for the brush size input',
id: 'paint.modeTools.brushSize' id: 'paint.modeTools.brushSize'
}, },
lineSize: {
defaultMessage: 'Line size',
description: 'Label for the line size input',
id: 'paint.modeTools.lineSize'
},
eraserSize: { eraserSize: {
defaultMessage: 'Eraser size', defaultMessage: 'Eraser size',
description: 'Label for the eraser size input', 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', description: 'Label for the button that converts selected points to sharp points',
id: 'paint.modeTools.pointed' id: 'paint.modeTools.pointed'
}, },
thickness: {
defaultMessage: 'Thickness',
description: 'Label for the number input to choose the line thickness',
id: 'paint.modeTools.thickness'
},
flipHorizontal: { flipHorizontal: {
defaultMessage: 'Flip Horizontal', defaultMessage: 'Flip Horizontal',
description: 'Label for the button to flip the image horizontally', description: 'Label for the button to flip the image horizontally',
@ -88,6 +94,16 @@ const ModeToolsComponent = props => {
defaultMessage: 'Flip Vertical', defaultMessage: 'Flip Vertical',
description: 'Label for the button to flip the image vertically', description: 'Label for the button to flip the image vertically',
id: 'paint.modeTools.flipVertical' 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; props.mode === Modes.BIT_LINE ? bitLineIcon : bitBrushIcon;
const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue; const currentBrushValue = isBitmap(props.format) ? props.bitBrushSize : props.brushValue;
const changeFunction = isBitmap(props.format) ? props.onBitBrushSliderChange : props.onBrushSliderChange; 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 ( return (
<div className={classNames(props.className, styles.modeTools)}> <div className={classNames(props.className, styles.modeTools)}>
<div> <div>
@ -234,6 +250,48 @@ const ModeToolsComponent = props => {
</InputGroup> </InputGroup>
</div> </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: default:
// Leave empty for now, if mode not supported // Leave empty for now, if mode not supported
return ( return (
@ -249,6 +307,7 @@ ModeToolsComponent.propTypes = {
className: PropTypes.string, className: PropTypes.string,
clipboardItems: PropTypes.arrayOf(PropTypes.array), clipboardItems: PropTypes.arrayOf(PropTypes.array),
eraserValue: PropTypes.number, eraserValue: PropTypes.number,
fillBitmapShapes: PropTypes.bool,
format: PropTypes.oneOf(Object.keys(Formats)).isRequired, format: PropTypes.oneOf(Object.keys(Formats)).isRequired,
hasSelectedUncurvedPoints: PropTypes.bool, hasSelectedUncurvedPoints: PropTypes.bool,
hasSelectedUnpointedPoints: PropTypes.bool, hasSelectedUnpointedPoints: PropTypes.bool,
@ -260,8 +319,10 @@ ModeToolsComponent.propTypes = {
onCurvePoints: PropTypes.func.isRequired, onCurvePoints: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onEraserSliderChange: PropTypes.func, onEraserSliderChange: PropTypes.func,
onFillShapes: PropTypes.func.isRequired,
onFlipHorizontal: PropTypes.func.isRequired, onFlipHorizontal: PropTypes.func.isRequired,
onFlipVertical: PropTypes.func.isRequired, onFlipVertical: PropTypes.func.isRequired,
onOutlineShapes: PropTypes.func.isRequired,
onPasteFromClipboard: PropTypes.func.isRequired, onPasteFromClipboard: PropTypes.func.isRequired,
onPointPoints: PropTypes.func.isRequired, onPointPoints: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired,
@ -271,6 +332,7 @@ ModeToolsComponent.propTypes = {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
mode: state.scratchPaint.mode, mode: state.scratchPaint.mode,
format: state.scratchPaint.format, format: state.scratchPaint.format,
fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
bitBrushSize: state.scratchPaint.bitBrushSize, bitBrushSize: state.scratchPaint.bitBrushSize,
bitEraserSize: state.scratchPaint.bitEraserSize, bitEraserSize: state.scratchPaint.bitEraserSize,
brushValue: state.scratchPaint.brushMode.brushSize, brushValue: state.scratchPaint.brushMode.brushSize,
@ -290,6 +352,12 @@ const mapDispatchToProps = dispatch => ({
}, },
onEraserSliderChange: eraserSize => { onEraserSliderChange: eraserSize => {
dispatch(changeEraserSize(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) { componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.color !== this.props.color) { if (this.tool) {
if (nextProps.color !== this.props.color) {
this.tool.setColor(nextProps.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); this.tool.onSelectionChanged(nextProps.selectedItems);
} }
}
if (nextProps.isOvalModeActive && !this.props.isOvalModeActive) { if (nextProps.isOvalModeActive && !this.props.isOvalModeActive) {
this.activateTool(); this.activateTool();
@ -55,6 +64,8 @@ class BitOvalMode extends React.Component {
this.props.clearSelectedItems, this.props.clearSelectedItems,
this.props.onUpdateImage); this.props.onUpdateImage);
this.tool.setColor(this.props.color); this.tool.setColor(this.props.color);
this.tool.setFilled(this.props.filled);
this.tool.setThickness(this.props.thickness);
this.tool.activate(); this.tool.activate();
} }
deactivateTool () { deactivateTool () {
@ -75,25 +86,31 @@ class BitOvalMode extends React.Component {
BitOvalMode.propTypes = { BitOvalMode.propTypes = {
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string, color: PropTypes.string,
filled: PropTypes.bool,
handleMouseDown: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired,
isOvalModeActive: PropTypes.bool.isRequired, isOvalModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired, onChangeFillColor: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), 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 => ({ const mapStateToProps = state => ({
color: state.scratchPaint.color.fillColor, color: state.scratchPaint.color.fillColor,
filled: state.scratchPaint.fillBitmapShapes,
isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL, 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 => ({ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => { clearSelectedItems: () => {
dispatch(clearSelectedItems()); dispatch(clearSelectedItems());
}, },
setSelectedItems: () => { setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */));
}, },
handleMouseDown: () => { handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_OVAL)); dispatch(changeMode(Modes.BIT_OVAL));

View file

@ -27,12 +27,21 @@ class BitRectMode extends React.Component {
} }
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.color !== this.props.color) { if (this.tool) {
if (nextProps.color !== this.props.color) {
this.tool.setColor(nextProps.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); this.tool.onSelectionChanged(nextProps.selectedItems);
} }
}
if (nextProps.isRectModeActive && !this.props.isRectModeActive) { if (nextProps.isRectModeActive && !this.props.isRectModeActive) {
this.activateTool(); this.activateTool();
@ -55,6 +64,8 @@ class BitRectMode extends React.Component {
this.props.clearSelectedItems, this.props.clearSelectedItems,
this.props.onUpdateImage); this.props.onUpdateImage);
this.tool.setColor(this.props.color); this.tool.setColor(this.props.color);
this.tool.setFilled(this.props.filled);
this.tool.setThickness(this.props.thickness);
this.tool.activate(); this.tool.activate();
} }
deactivateTool () { deactivateTool () {
@ -75,25 +86,31 @@ class BitRectMode extends React.Component {
BitRectMode.propTypes = { BitRectMode.propTypes = {
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string, color: PropTypes.string,
filled: PropTypes.bool,
handleMouseDown: PropTypes.func.isRequired, handleMouseDown: PropTypes.func.isRequired,
isRectModeActive: PropTypes.bool.isRequired, isRectModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired, onChangeFillColor: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), 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 => ({ const mapStateToProps = state => ({
color: state.scratchPaint.color.fillColor, color: state.scratchPaint.color.fillColor,
filled: state.scratchPaint.fillBitmapShapes,
isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT, 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 => ({ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => { clearSelectedItems: () => {
dispatch(clearSelectedItems()); dispatch(clearSelectedItems());
}, },
setSelectedItems: () => { setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */));
}, },
handleMouseDown: () => { handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_RECT)); dispatch(changeMode(Modes.BIT_RECT));

View file

@ -5,6 +5,8 @@ import bindAll from 'lodash.bindall';
import {changeFillColor} from '../reducers/fill-color'; import {changeFillColor} from '../reducers/fill-color';
import {openFillColor, closeFillColor} from '../reducers/modals'; import {openFillColor, closeFillColor} from '../reducers/modals';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
import {applyFillColorToSelection} from '../helper/style-path'; import {applyFillColorToSelection} from '../helper/style-path';
@ -30,7 +32,7 @@ class FillColorIndicator extends React.Component {
} }
handleChangeFillColor (newColor) { handleChangeFillColor (newColor) {
// Apply color and update redux, but do not update svg until picker closes. // 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._hasChanged = this._hasChanged || isDifferent;
this.props.onChangeFillColor(newColor); this.props.onChangeFillColor(newColor);
} }
@ -54,6 +56,7 @@ const mapStateToProps = state => ({
disabled: state.scratchPaint.mode === Modes.LINE, disabled: state.scratchPaint.mode === Modes.LINE,
fillColor: state.scratchPaint.color.fillColor, fillColor: state.scratchPaint.color.fillColor,
fillColorModalVisible: state.scratchPaint.modals.fillColor, fillColorModalVisible: state.scratchPaint.modals.fillColor,
format: state.scratchPaint.format,
isEyeDropping: state.scratchPaint.color.eyeDropper.active, isEyeDropping: state.scratchPaint.color.eyeDropper.active,
textEditTarget: state.scratchPaint.textEditTarget textEditTarget: state.scratchPaint.textEditTarget
}); });
@ -74,6 +77,7 @@ FillColorIndicator.propTypes = {
disabled: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired,
fillColor: PropTypes.string, fillColor: PropTypes.string,
fillColorModalVisible: PropTypes.bool.isRequired, fillColorModalVisible: PropTypes.bool.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)),
isEyeDropping: PropTypes.bool.isRequired, isEyeDropping: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired, onChangeFillColor: PropTypes.func.isRequired,
onCloseFillColor: PropTypes.func.isRequired, onCloseFillColor: PropTypes.func.isRequired,

View file

@ -128,7 +128,7 @@ class ModeTools extends React.Component {
changed = true; changed = true;
} }
if (changed) { if (changed) {
this.props.setSelectedItems(); this.props.setSelectedItems(this.props.format);
this.props.onUpdateImage(); this.props.onUpdateImage();
} }
} }
@ -144,7 +144,7 @@ class ModeTools extends React.Component {
} }
} }
if (changed) { if (changed) {
this.props.setSelectedItems(); this.props.setSelectedItems(this.props.format);
this.props.onUpdateImage(); this.props.onUpdateImage();
} }
} }
@ -193,7 +193,7 @@ class ModeTools extends React.Component {
} }
handleDelete () { handleDelete () {
if (deleteSelection(this.props.mode, this.props.onUpdateImage)) { if (deleteSelection(this.props.mode, this.props.onUpdateImage)) {
this.props.setSelectedItems(); this.props.setSelectedItems(this.props.format);
} }
} }
handleCopyToClipboard () { handleCopyToClipboard () {
@ -232,7 +232,7 @@ class ModeTools extends React.Component {
placedItem.position.y += 10 * this.props.pasteOffset; placedItem.position.y += 10 * this.props.pasteOffset;
} }
this.props.incrementPasteOffset(); this.props.incrementPasteOffset();
this.props.setSelectedItems(); this.props.setSelectedItems(this.props.format);
this.props.onUpdateImage(); this.props.onUpdateImage();
} }
} }
@ -286,8 +286,8 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => { clearSelectedItems: () => {
dispatch(clearSelectedItems()); dispatch(clearSelectedItems());
}, },
setSelectedItems: () => { setSelectedItems: format => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
} }
}); });

View file

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

View file

@ -45,6 +45,7 @@ class PaintEditor extends React.Component {
'handleSendForward', 'handleSendForward',
'handleSendToBack', 'handleSendToBack',
'handleSendToFront', 'handleSendToFront',
'handleSetSelectedItems',
'handleGroup', 'handleGroup',
'handleUngroup', 'handleUngroup',
'handleZoomIn', 'handleZoomIn',
@ -218,16 +219,16 @@ class PaintEditor extends React.Component {
} }
} }
handleUndo () { 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 () { 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 () { handleGroup () {
groupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateImage); groupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage);
} }
handleUngroup () { handleUngroup () {
ungroupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateImage); ungroupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage);
} }
handleSendBackward () { handleSendBackward () {
sendBackward(this.handleUpdateImage); sendBackward(this.handleUpdateImage);
@ -241,6 +242,9 @@ class PaintEditor extends React.Component {
handleSendToFront () { handleSendToFront () {
bringToFront(this.handleUpdateImage); bringToFront(this.handleUpdateImage);
} }
handleSetSelectedItems () {
this.props.setSelectedItems(this.props.format);
}
canUndo () { canUndo () {
return shouldShowUndo(this.props.undoState); return shouldShowUndo(this.props.undoState);
} }
@ -250,17 +254,17 @@ class PaintEditor extends React.Component {
handleZoomIn () { handleZoomIn () {
zoomOnSelection(PaintEditor.ZOOM_INCREMENT); zoomOnSelection(PaintEditor.ZOOM_INCREMENT);
this.props.updateViewBounds(paper.view.matrix); this.props.updateViewBounds(paper.view.matrix);
this.props.setSelectedItems(); this.handleSetSelectedItems();
} }
handleZoomOut () { handleZoomOut () {
zoomOnSelection(-PaintEditor.ZOOM_INCREMENT); zoomOnSelection(-PaintEditor.ZOOM_INCREMENT);
this.props.updateViewBounds(paper.view.matrix); this.props.updateViewBounds(paper.view.matrix);
this.props.setSelectedItems(); this.handleSetSelectedItems();
} }
handleZoomReset () { handleZoomReset () {
resetZoom(); resetZoom();
this.props.updateViewBounds(paper.view.matrix); this.props.updateViewBounds(paper.view.matrix);
this.props.setSelectedItems(); this.handleSetSelectedItems();
} }
setCanvas (canvas) { setCanvas (canvas) {
this.setState({canvas: canvas}); this.setState({canvas: canvas});
@ -457,8 +461,8 @@ const mapDispatchToProps = dispatch => ({
removeTextEditTarget: () => { removeTextEditTarget: () => {
dispatch(setTextEditTarget()); dispatch(setTextEditTarget());
}, },
setSelectedItems: () => { setSelectedItems: format => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
}, },
onDeactivateEyeDropper: () => { onDeactivateEyeDropper: () => {
// set redux values to default for eye dropper reducer // 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 {connect} from 'react-redux';
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import Formats from '../lib/format'; import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
import log from '../log/log'; import log from '../log/log';
@ -68,7 +69,7 @@ class PaperCanvas extends React.Component {
// Backspace, delete // Backspace, delete
if (event.key === 'Delete' || event.key === 'Backspace') { if (event.key === 'Delete' || event.key === 'Backspace') {
if (deleteSelection(this.props.mode, this.props.onUpdateImage)) { 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); zoomOnFixedPoint(-deltaY / 100, fixedPoint);
this.props.updateViewBounds(paper.view.matrix); this.props.updateViewBounds(paper.view.matrix);
this.props.setSelectedItems(); this.props.setSelectedItems(this.props.format);
} else if (event.shiftKey && event.deltaX === 0) { } else if (event.shiftKey && event.deltaX === 0) {
// Scroll horizontally (based on vertical scroll delta) // Scroll horizontally (based on vertical scroll delta)
// This is needed as for some browser/system combinations which do not set deltaX. // This is needed as for some browser/system combinations which do not set deltaX.
@ -265,6 +266,7 @@ PaperCanvas.propTypes = {
clearPasteOffset: PropTypes.func.isRequired, clearPasteOffset: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired,
clearUndo: PropTypes.func.isRequired, clearUndo: PropTypes.func.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)), // Internal, up-to-date data format
image: PropTypes.oneOfType([ image: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.instanceOf(HTMLImageElement) PropTypes.instanceOf(HTMLImageElement)
@ -290,8 +292,8 @@ const mapDispatchToProps = dispatch => ({
clearUndo: () => { clearUndo: () => {
dispatch(clearUndoState()); dispatch(clearUndoState());
}, },
setSelectedItems: () => { setSelectedItems: format => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
}, },
clearSelectedItems: () => { clearSelectedItems: () => {
dispatch(clearSelectedItems()); dispatch(clearSelectedItems());

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,8 @@ import bindAll from 'lodash.bindall';
import {changeStrokeColor} from '../reducers/stroke-color'; import {changeStrokeColor} from '../reducers/stroke-color';
import {openStrokeColor, closeStrokeColor} from '../reducers/modals'; import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
import Modes from '../lib/modes'; import Modes from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
import {applyStrokeColorToSelection} from '../helper/style-path'; import {applyStrokeColorToSelection} from '../helper/style-path';
@ -30,7 +32,8 @@ class StrokeColorIndicator extends React.Component {
} }
handleChangeStrokeColor (newColor) { handleChangeStrokeColor (newColor) {
// Apply color and update redux, but do not update svg until picker closes. // 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._hasChanged = this._hasChanged || isDifferent;
this.props.onChangeStrokeColor(newColor); this.props.onChangeStrokeColor(newColor);
} }
@ -53,6 +56,7 @@ class StrokeColorIndicator extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
disabled: state.scratchPaint.mode === Modes.BRUSH || disabled: state.scratchPaint.mode === Modes.BRUSH ||
state.scratchPaint.mode === Modes.TEXT, state.scratchPaint.mode === Modes.TEXT,
format: state.scratchPaint.format,
isEyeDropping: state.scratchPaint.color.eyeDropper.active, isEyeDropping: state.scratchPaint.color.eyeDropper.active,
strokeColor: state.scratchPaint.color.strokeColor, strokeColor: state.scratchPaint.color.strokeColor,
strokeColorModalVisible: state.scratchPaint.modals.strokeColor, strokeColorModalVisible: state.scratchPaint.modals.strokeColor,
@ -73,6 +77,7 @@ const mapDispatchToProps = dispatch => ({
StrokeColorIndicator.propTypes = { StrokeColorIndicator.propTypes = {
disabled: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)),
isEyeDropping: PropTypes.bool.isRequired, isEyeDropping: PropTypes.bool.isRequired,
onChangeStrokeColor: PropTypes.func.isRequired, onChangeStrokeColor: PropTypes.func.isRequired,
onCloseStrokeColor: PropTypes.func.isRequired, onCloseStrokeColor: PropTypes.func.isRequired,

View file

@ -148,7 +148,7 @@ const mapStateToProps = (state, ownProps) => ({
textEditTarget: state.scratchPaint.textEditTarget, textEditTarget: state.scratchPaint.textEditTarget,
viewBounds: state.scratchPaint.viewBounds viewBounds: state.scratchPaint.viewBounds
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch, ownProps) => ({
changeFont: font => { changeFont: font => {
dispatch(changeFont(font)); dispatch(changeFont(font));
}, },
@ -162,7 +162,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeMode(Modes.TEXT)); dispatch(changeMode(Modes.TEXT));
}, },
setSelectedItems: () => { setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems())); dispatch(setSelectedItems(getSelectedLeafItems(), ownProps.isBitmap));
}, },
setTextEditTarget: targetId => { setTextEditTarget: targetId => {
dispatch(setTextEditTarget(targetId)); dispatch(setTextEditTarget(targetId));

View file

@ -57,17 +57,49 @@ class OvalTool extends paper.Tool {
*/ */
onSelectionChanged (selectedItems) { onSelectionChanged (selectedItems) {
this.boundingBoxTool.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') { selectedItems && selectedItems.length === 1 && selectedItems[0].shape === 'ellipse') {
// Infer that an undo occurred and get back the active oval // Infer that an undo occurred and get back the active oval
this.oval = selectedItems[0]; 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 // Oval got deselected
this.commitOval(); this.commitOval();
} }
} }
setColor (color) { setColor (color) {
this.color = 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) { handleMouseDown (event) {
if (event.event.button > 0) return; // only first mouse button if (event.event.button > 0) return; // only first mouse button
@ -79,11 +111,24 @@ class OvalTool extends paper.Tool {
this.isBoundingBoxMode = false; this.isBoundingBoxMode = false;
clearSelection(this.clearSelectedItems); clearSelection(this.clearSelectedItems);
this.commitOval(); this.commitOval();
if (this.filled) {
this.oval = new paper.Shape.Ellipse({ this.oval = new paper.Shape.Ellipse({
fillColor: this.color, fillColor: this.color,
point: event.downPoint, point: event.downPoint,
strokeWidth: 0,
strokeScaling: false,
size: 0 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) { handleMouseDrag (event) {
@ -132,19 +177,21 @@ class OvalTool extends paper.Tool {
this.active = false; this.active = false;
} }
commitOval () { 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 radiusX = Math.abs(this.oval.size.width / 2);
const radiusY = Math.abs(this.oval.size.height / 2); const radiusY = Math.abs(this.oval.size.height / 2);
const context = getRaster().getContext('2d'); const context = getRaster().getContext('2d');
context.fillStyle = this.color; context.fillStyle = this.color;
const drew = drawEllipse( const drew = drawEllipse({
this.oval.position.x, this.oval.position.y, position: this.oval.position,
radiusX, radiusY, radiusX,
this.oval.matrix, radiusY,
true, /* isFilled */ matrix: this.oval.matrix,
context); isFilled: this.filled,
thickness: this.thickness / paper.view.zoom
}, context);
this.oval.remove(); this.oval.remove();
this.oval = null; this.oval = null;

View file

@ -1,6 +1,6 @@
import paper from '@scratch/paper'; import paper from '@scratch/paper';
import Modes from '../../lib/modes'; import Modes from '../../lib/modes';
import {fillRect} from '../bitmap'; import {fillRect, outlineRect} from '../bitmap';
import {createCanvas, getRaster} from '../layer'; import {createCanvas, getRaster} from '../layer';
import {clearSelection} from '../selection'; import {clearSelection} from '../selection';
import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import BoundingBoxTool from '../selection-tools/bounding-box-tool';
@ -57,17 +57,49 @@ class RectTool extends paper.Tool {
*/ */
onSelectionChanged (selectedItems) { onSelectionChanged (selectedItems) {
this.boundingBoxTool.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') { selectedItems && selectedItems.length === 1 && selectedItems[0].shape === 'rectangle') {
// Infer that an undo occurred and get back the active rect // Infer that an undo occurred and get back the active rect
this.rect = selectedItems[0]; 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 // Rectangle got deselected
this.commitRect(); this.commitRect();
} }
} }
setColor (color) { setColor (color) {
this.color = 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) { handleMouseDown (event) {
if (event.event.button > 0) return; // only first mouse button 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(); if (this.rect) this.rect.remove();
this.rect = new paper.Shape.Rectangle(baseRect); this.rect = new paper.Shape.Rectangle(baseRect);
if (this.filled) {
this.rect.fillColor = this.color; 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) { if (event.modifiers.alt) {
this.rect.position = event.downPoint; this.rect.position = event.downPoint;
@ -129,12 +170,16 @@ class RectTool extends paper.Tool {
this.active = false; this.active = false;
} }
commitRect () { commitRect () {
if (!this.rect || !this.rect.parent) return; if (!this.rect || !this.rect.isInserted()) return;
const tmpCanvas = createCanvas(); const tmpCanvas = createCanvas();
const context = tmpCanvas.getContext('2d'); const context = tmpCanvas.getContext('2d');
context.fillStyle = this.color; context.fillStyle = this.color;
if (this.filled) {
fillRect(this.rect, context); fillRect(this.rect, context);
} else {
outlineRect(this.rect, this.thickness / paper.view.zoom, context);
}
getRaster().drawImage(tmpCanvas, new paper.Point()); getRaster().drawImage(tmpCanvas, new paper.Point());
this.rect.remove(); 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.radiusY minor radius of ellipse
* @param {!number} options.shearSlope slope of the sheared x axis * @param {!number} options.shearSlope slope of the sheared x axis
* @param {?boolean} options.isFilled true if isFilled * @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 * @param {!CanvasRenderingContext2D} context for drawing
* @return {boolean} true if anything was drawn, false if not * @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 radiusY = ~~Math.abs(options.radiusY) - .5;
const shearSlope = options.shearSlope; const shearSlope = options.shearSlope;
const isFilled = options.isFilled; const isFilled = options.isFilled;
const drawFn = options.drawFn;
if (shearSlope === Infinity || radiusX < 1 || radiusY < 1) { if (shearSlope === Infinity || radiusX < 1 || radiusY < 1) {
return false; 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 - pX1 - 1, centerY + pY, pX1 - pX2 + 1, 1);
context.fillRect(centerX + pX2, centerY - pY - 1, pX1 - pX2 + 1, 1); context.fillRect(centerX + pX2, centerY - pY - 1, pX1 - pX2 + 1, 1);
} else { } else {
context.fillRect(centerX - pX1 - 1, centerY + pY, 1, 1); drawFn(centerX - pX1 - 1, centerY + pY);
context.fillRect(centerX + pX1, centerY - pY - 1, 1, 1); drawFn(centerX + pX1, centerY - pY - 1);
} }
y--; y--;
x = solveQuadratic_(A, B * y, (C * y * y) - 1); 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 - 1, centerY + pY2, 1, pY1 - pY2 + 1);
context.fillRect(centerX + pX, centerY - pY1 - 1, 1, pY1 - pY2 + 1); context.fillRect(centerX + pX, centerY - pY1 - 1, 1, pY1 - pY2 + 1);
} else { } else {
context.fillRect(centerX - pX - 1, centerY + pY1, 1, 1); drawFn(centerX - pX - 1, centerY + pY1);
context.fillRect(centerX + pX, centerY - pY1 - 1, 1, 1); drawFn(centerX + pX, centerY - pY1 - 1);
} }
x++; x++;
y = solveQuadratic_(C, B * x, (A * x * x) - 1); y = solveQuadratic_(C, B * x, (A * x * x) - 1);
@ -188,46 +191,6 @@ const drawShearedEllipse_ = function (options, context) {
return true; 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 {!number} size The diameter of the brush
* @param {!string} color The css color 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, radiusX: size / 2,
radiusY: size / 2, radiusY: size / 2,
shearSlope: 0, shearSlope: 0,
isFilled: false isFilled: false,
drawFn: (x, y) => context.fillRect(x, y, 1, 1)
}, context); }, context);
} }
} }
return canvas; 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) { const rowBlank_ = function (imageData, width, y) {
for (let x = 0; x < width; ++x) { for (let x = 0; x < width; ++x) {
if (imageData.data[(y * width << 2) + (x << 2) + 3] !== 0) return false; 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 flipBitmapHorizontal = function (canvas) {
const tmpCanvas = createCanvas(canvas.width, canvas.height); const tmpCanvas = createCanvas(canvas.width, canvas.height);
const context = tmpCanvas.getContext('2d'); const context = tmpCanvas.getContext('2d');
@ -597,6 +643,7 @@ export {
convertToBitmap, convertToBitmap,
convertToVector, convertToVector,
fillRect, fillRect,
outlineRect,
floodFill, floodFill,
floodFillAll, floodFillAll,
getBrushMark, getBrushMark,

View file

@ -33,10 +33,11 @@ const _getColorStateListeners = function (textEditTargetId) {
/** /**
* Called when setting fill color * Called when setting fill color
* @param {string} colorString New color, css format * @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 * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly. * @return {boolean} Whether the color application actually changed visibly.
*/ */
const applyFillColorToSelection = function (colorString, textEditTargetId) { const applyFillColorToSelection = function (colorString, bitmapMode, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId); const items = _getColorStateListeners(textEditTargetId);
let changed = false; let changed = false;
for (let item of items) { for (let item of items) {
@ -45,7 +46,13 @@ const applyFillColorToSelection = function (colorString, textEditTargetId) {
} else if (item.parent instanceof paper.CompoundPath) { } else if (item.parent instanceof paper.CompoundPath) {
item = item.parent; 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; changed = true;
item.fillColor = colorString; item.fillColor = colorString;
} }
@ -56,10 +63,14 @@ const applyFillColorToSelection = function (colorString, textEditTargetId) {
/** /**
* Called when setting stroke color * Called when setting stroke color
* @param {string} colorString New color, css format * @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 * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly. * @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); const items = _getColorStateListeners(textEditTargetId);
let changed = false; let changed = false;
for (let item of items) { for (let item of items) {
@ -125,14 +136,17 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
/** /**
* Get state of colors and stroke width for selection * Get state of colors and stroke width for selection
* @param {!Array<paper.Item>} selectedItems Selected paper items * @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 MIXED when there are mixed values for a color, and null for transparent.
* Gives null when there are mixed values for stroke width. * 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 selectionFillColorString;
let selectionStrokeColorString; let selectionStrokeColorString;
let selectionStrokeWidth; let selectionStrokeWidth;
let selectionThickness;
let firstChild = true; let firstChild = true;
for (let item of selectedItems) { for (let item of selectedItems) {
@ -185,7 +199,10 @@ const getColorsFromSelection = function (selectedItems) {
} }
} }
if (item.strokeColor) { 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; itemStrokeColorString = MIXED;
} else { } else {
itemStrokeColorString = item.strokeColor.toCSS(); itemStrokeColorString = item.strokeColor.toCSS();
@ -197,6 +214,9 @@ const getColorsFromSelection = function (selectedItems) {
selectionFillColorString = itemFillColorString; selectionFillColorString = itemFillColorString;
selectionStrokeColorString = itemStrokeColorString; selectionStrokeColorString = itemStrokeColorString;
selectionStrokeWidth = item.strokeWidth; selectionStrokeWidth = item.strokeWidth;
if (item.strokeWidth && item.data && item.data.zoomLevel) {
selectionThickness = item.strokeWidth / item.data.zoomLevel;
}
} }
if (itemFillColorString !== selectionFillColorString) { if (itemFillColorString !== selectionFillColorString) {
selectionFillColorString = MIXED; selectionFillColorString = MIXED;
@ -209,6 +229,12 @@ const getColorsFromSelection = function (selectedItems) {
} }
} }
} }
if (bitmapMode) {
return {
fillColor: selectionFillColorString ? selectionFillColorString : null,
thickness: selectionThickness
};
}
return { return {
fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor: selectionFillColorString ? selectionFillColorString : null,
strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null,

View file

@ -1,4 +1,6 @@
import log from '../log/log'; 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 // Bit brush size affects bit brush width, circle/rectangle outline drawing width, and line width
// in the bitmap paint editor. // in the bitmap paint editor.
@ -14,6 +16,20 @@ const reducer = function (state, action) {
return state; return state;
} }
return Math.max(1, action.brushSize); 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: default:
return state; 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) { if (!action.selectedItems || !action.selectedItems.length) {
return state; return state;
} }
return getColorsFromSelection(action.selectedItems).fillColor; return getColorsFromSelection(action.selectedItems, action.bitmapMode).fillColor;
default: default:
return state; return state;
} }

View file

@ -6,6 +6,7 @@ import brushModeReducer from './brush-mode';
import eraserModeReducer from './eraser-mode'; import eraserModeReducer from './eraser-mode';
import colorReducer from './color'; import colorReducer from './color';
import clipboardReducer from './clipboard'; import clipboardReducer from './clipboard';
import fillBitmapShapesReducer from './fill-bitmap-shapes';
import fontReducer from './font'; import fontReducer from './font';
import formatReducer from './format'; import formatReducer from './format';
import hoverReducer from './hover'; import hoverReducer from './hover';
@ -23,6 +24,7 @@ export default combineReducers({
color: colorReducer, color: colorReducer,
clipboard: clipboardReducer, clipboard: clipboardReducer,
eraserMode: eraserModeReducer, eraserMode: eraserModeReducer,
fillBitmapShapes: fillBitmapShapesReducer,
font: fontReducer, font: fontReducer,
format: formatReducer, format: formatReducer,
hoveredItemId: hoverReducer, hoveredItemId: hoverReducer,

View file

@ -10,6 +10,10 @@ const reducer = function (state, action) {
log.warn(`No selected items or wrong format provided: ${action.selectedItems}`); log.warn(`No selected items or wrong format provided: ${action.selectedItems}`);
return state; 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 they are both empty, no change
if (action.selectedItems.length === 0 && state.length === 0) { if (action.selectedItems.length === 0 && state.length === 0) {
return state; return state;
@ -24,12 +28,14 @@ const reducer = function (state, action) {
/** /**
* Set the selected item state to the given array of items * Set the selected item state to the given array of items
* @param {Array<paper.Item>} selectedItems from paper.project.selectedItems * @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. * @return {object} Redux action to change the selected items.
*/ */
const setSelectedItems = function (selectedItems) { const setSelectedItems = function (selectedItems, bitmapMode) {
return { return {
type: CHANGE_SELECTED_ITEMS, type: CHANGE_SELECTED_ITEMS,
selectedItems: selectedItems selectedItems: selectedItems,
bitmapMode: bitmapMode
}; };
}; };
const clearSelectedItems = function () { const clearSelectedItems = function () {

View file

@ -21,7 +21,11 @@ const reducer = function (state, action) {
if (!action.selectedItems || !action.selectedItems.length) { if (!action.selectedItems || !action.selectedItems.length) {
return state; 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: default:
return state; return state;
} }

View file

@ -20,7 +20,11 @@ const reducer = function (state, action) {
if (!action.selectedItems || !action.selectedItems.length) { if (!action.selectedItems || !action.selectedItems.length) {
return state; 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: default:
return state; return state;
} }