mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-23 05:52:42 -05:00
Draw oval and rectangle outlines in bitmap (#550)
This commit is contained in:
parent
11bab6ebe2
commit
4e4bb396a6
30 changed files with 507 additions and 133 deletions
19
src/components/bit-oval-mode/oval-outlined.svg
Normal file
19
src/components/bit-oval-mode/oval-outlined.svg
Normal 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 |
10
src/components/bit-rect-mode/rectangle-outlined.svg
Normal file
10
src/components/bit-rect-mode/rectangle-outlined.svg
Normal 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 |
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
25
src/reducers/fill-bitmap-shapes.js
Normal file
25
src/reducers/fill-bitmap-shapes.js
Normal 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
|
||||||
|
};
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue