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