mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Merge pull request #1004 from adroitwhiz/stroke-gradient
Implement gradient outlines
This commit is contained in:
commit
4f86762737
29 changed files with 816 additions and 726 deletions
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Popover from 'react-popover';
|
import Popover from 'react-popover';
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
|
||||||
|
|
||||||
import ColorButton from './color-button/color-button.jsx';
|
import ColorButton from './color-button/color-button.jsx';
|
||||||
import ColorPicker from '../containers/color-picker.jsx';
|
import ColorPicker from '../containers/color-picker.jsx';
|
||||||
|
@ -10,15 +9,7 @@ import Label from './forms/label.jsx';
|
||||||
|
|
||||||
import GradientTypes from '../lib/gradient-types';
|
import GradientTypes from '../lib/gradient-types';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const ColorIndicatorComponent = props => (
|
||||||
fill: {
|
|
||||||
id: 'paint.paintEditor.fill',
|
|
||||||
description: 'Label for the color picker for the fill color',
|
|
||||||
defaultMessage: 'Fill'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const FillColorIndicatorComponent = props => (
|
|
||||||
<InputGroup
|
<InputGroup
|
||||||
className={props.className}
|
className={props.className}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
@ -26,45 +17,47 @@ const FillColorIndicatorComponent = props => (
|
||||||
<Popover
|
<Popover
|
||||||
body={
|
body={
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={props.fillColor}
|
color={props.color}
|
||||||
color2={props.fillColor2}
|
color2={props.color2}
|
||||||
gradientType={props.gradientType}
|
gradientType={props.gradientType}
|
||||||
shouldShowGradientTools={props.shouldShowGradientTools}
|
shouldShowGradientTools={props.shouldShowGradientTools}
|
||||||
onChangeColor={props.onChangeFillColor}
|
onChangeColor={props.onChangeColor}
|
||||||
onChangeGradientType={props.onChangeGradientType}
|
onChangeGradientType={props.onChangeGradientType}
|
||||||
onSwap={props.onSwap}
|
onSwap={props.onSwap}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
isOpen={props.fillColorModalVisible}
|
isOpen={props.colorModalVisible}
|
||||||
preferPlace="below"
|
preferPlace="below"
|
||||||
onOuterAction={props.onCloseFillColor}
|
onOuterAction={props.onCloseColor}
|
||||||
>
|
>
|
||||||
<Label text={props.intl.formatMessage(messages.fill)}>
|
<Label text={props.label}>
|
||||||
<ColorButton
|
<ColorButton
|
||||||
color={props.fillColor}
|
color={props.color}
|
||||||
color2={props.fillColor2}
|
color2={props.color2}
|
||||||
gradientType={props.gradientType}
|
gradientType={props.gradientType}
|
||||||
onClick={props.onOpenFillColor}
|
onClick={props.onOpenColor}
|
||||||
|
outline={props.outline}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
</Popover>
|
</Popover>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
|
|
||||||
FillColorIndicatorComponent.propTypes = {
|
ColorIndicatorComponent.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabled: PropTypes.bool.isRequired,
|
disabled: PropTypes.bool.isRequired,
|
||||||
fillColor: PropTypes.string,
|
color: PropTypes.string,
|
||||||
fillColor2: PropTypes.string,
|
color2: PropTypes.string,
|
||||||
fillColorModalVisible: PropTypes.bool.isRequired,
|
colorModalVisible: PropTypes.bool.isRequired,
|
||||||
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
||||||
intl: intlShape,
|
label: PropTypes.string.isRequired,
|
||||||
onChangeFillColor: PropTypes.func.isRequired,
|
onChangeColor: PropTypes.func.isRequired,
|
||||||
onChangeGradientType: PropTypes.func.isRequired,
|
onChangeGradientType: PropTypes.func.isRequired,
|
||||||
onCloseFillColor: PropTypes.func.isRequired,
|
onCloseColor: PropTypes.func.isRequired,
|
||||||
onOpenFillColor: PropTypes.func.isRequired,
|
onOpenColor: PropTypes.func.isRequired,
|
||||||
onSwap: PropTypes.func.isRequired,
|
onSwap: PropTypes.func.isRequired,
|
||||||
|
outline: PropTypes.bool.isRequired,
|
||||||
shouldShowGradientTools: PropTypes.bool.isRequired
|
shouldShowGradientTools: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(FillColorIndicatorComponent);
|
export default ColorIndicatorComponent;
|
|
@ -1,65 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Popover from 'react-popover';
|
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
|
||||||
|
|
||||||
import ColorButton from './color-button/color-button.jsx';
|
|
||||||
import ColorPicker from '../containers/color-picker.jsx';
|
|
||||||
import InputGroup from './input-group/input-group.jsx';
|
|
||||||
import Label from './forms/label.jsx';
|
|
||||||
import GradientTypes from '../lib/gradient-types';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
stroke: {
|
|
||||||
id: 'paint.paintEditor.stroke',
|
|
||||||
description: 'Label for the color picker for the outline color',
|
|
||||||
defaultMessage: 'Outline'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const StrokeColorIndicatorComponent = props => (
|
|
||||||
<InputGroup
|
|
||||||
className={props.className}
|
|
||||||
disabled={props.disabled}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
body={
|
|
||||||
<ColorPicker
|
|
||||||
color={props.strokeColor}
|
|
||||||
color2={null}
|
|
||||||
gradientType={GradientTypes.SOLID}
|
|
||||||
shouldShowGradientTools={false}
|
|
||||||
// @todo handle stroke gradient
|
|
||||||
onChangeColor={props.onChangeStrokeColor}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isOpen={props.strokeColorModalVisible}
|
|
||||||
preferPlace="below"
|
|
||||||
onOuterAction={props.onCloseStrokeColor}
|
|
||||||
>
|
|
||||||
<Label text={props.intl.formatMessage(messages.stroke)}>
|
|
||||||
<ColorButton
|
|
||||||
outline
|
|
||||||
color={props.strokeColor}
|
|
||||||
color2={null}
|
|
||||||
gradientType={GradientTypes.SOLID}
|
|
||||||
// @todo handle stroke gradient
|
|
||||||
onClick={props.onOpenStrokeColor}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</Popover>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
|
||||||
|
|
||||||
StrokeColorIndicatorComponent.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool.isRequired,
|
|
||||||
intl: intlShape,
|
|
||||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
|
||||||
onCloseStrokeColor: PropTypes.func.isRequired,
|
|
||||||
onOpenStrokeColor: PropTypes.func.isRequired,
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeColorModalVisible: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(StrokeColorIndicatorComponent);
|
|
|
@ -4,9 +4,10 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -61,9 +62,8 @@ class BitOvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// Force the default brush color if fill is MIXED or transparent
|
// Force the default brush color if fill is MIXED or transparent
|
||||||
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
|
const fillColorPresent = this.props.color.primary !== MIXED && this.props.color.primary !== null;
|
||||||
if (!fillColorPresent) {
|
if (!fillColorPresent) {
|
||||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
}
|
}
|
||||||
|
@ -94,9 +94,8 @@ class BitOvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
BitOvalMode.propTypes = {
|
BitOvalMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
color: PropTypes.string,
|
color: ColorStyleProptype,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isOvalModeActive: PropTypes.bool.isRequired,
|
isOvalModeActive: PropTypes.bool.isRequired,
|
||||||
|
@ -110,7 +109,7 @@ BitOvalMode.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
color: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor,
|
||||||
filled: state.scratchPaint.fillBitmapShapes,
|
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,
|
||||||
|
@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setCursor: cursorString => {
|
setCursor: cursorString => {
|
||||||
dispatch(setCursor(cursorString));
|
dispatch(setCursor(cursorString));
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,9 +4,10 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -61,9 +62,8 @@ class BitRectMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// Force the default brush color if fill is MIXED or transparent
|
// Force the default brush color if fill is MIXED or transparent
|
||||||
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
|
const fillColorPresent = this.props.color.primary !== MIXED && this.props.color.primary !== null;
|
||||||
if (!fillColorPresent) {
|
if (!fillColorPresent) {
|
||||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||||
}
|
}
|
||||||
|
@ -94,9 +94,8 @@ class BitRectMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
BitRectMode.propTypes = {
|
BitRectMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
color: PropTypes.string,
|
color: ColorStyleProptype,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
isRectModeActive: PropTypes.bool.isRequired,
|
isRectModeActive: PropTypes.bool.isRequired,
|
||||||
|
@ -110,7 +109,7 @@ BitRectMode.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
color: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor,
|
||||||
filled: state.scratchPaint.fillBitmapShapes,
|
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,
|
||||||
|
@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setCursor: cursorString => {
|
setCursor: cursorString => {
|
||||||
dispatch(setCursor(cursorString));
|
dispatch(setCursor(cursorString));
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import Blobbiness from '../helper/blob-tools/blob';
|
import Blobbiness from '../helper/blob-tools/blob';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ class BrushMode extends React.Component {
|
||||||
this.blob.setOptions({
|
this.blob.setOptions({
|
||||||
isEraser: false,
|
isEraser: false,
|
||||||
fillColor: fillColor.primary,
|
fillColor: fillColor.primary,
|
||||||
strokeColor,
|
strokeColor: strokeColor.primary,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
...nextProps.brushModeState
|
...nextProps.brushModeState
|
||||||
});
|
});
|
||||||
|
@ -88,11 +89,8 @@ BrushMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
clearGradient: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
colorState: PropTypes.shape({
|
colorState: PropTypes.shape({
|
||||||
fillColor: PropTypes.shape({
|
fillColor: ColorStyleProptype,
|
||||||
primary: PropTypes.string,
|
strokeColor: ColorStyleProptype,
|
||||||
secondary: PropTypes.string
|
|
||||||
}),
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number
|
strokeWidth: PropTypes.number
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
|
173
src/containers/color-indicator.jsx
Normal file
173
src/containers/color-indicator.jsx
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
import parseColor from 'parse-color';
|
||||||
|
import {injectIntl, intlShape} from 'react-intl';
|
||||||
|
|
||||||
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
|
import Formats from '../lib/format';
|
||||||
|
import {isBitmap} from '../lib/format';
|
||||||
|
import GradientTypes from '../lib/gradient-types';
|
||||||
|
|
||||||
|
import ColorIndicatorComponent from '../components/color-indicator.jsx';
|
||||||
|
import {applyColorToSelection,
|
||||||
|
applyGradientTypeToSelection,
|
||||||
|
applyStrokeWidthToSelection,
|
||||||
|
getRotatedColor,
|
||||||
|
swapColorsInSelection,
|
||||||
|
MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
|
const makeColorIndicator = (label, isStroke) => {
|
||||||
|
class ColorIndicator extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleChangeColor',
|
||||||
|
'handleChangeGradientType',
|
||||||
|
'handleCloseColor',
|
||||||
|
'handleSwap'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Flag to track whether an svg-update-worthy change has been made
|
||||||
|
this._hasChanged = false;
|
||||||
|
}
|
||||||
|
componentWillReceiveProps (newProps) {
|
||||||
|
const {colorModalVisible, onUpdateImage} = this.props;
|
||||||
|
if (colorModalVisible && !newProps.colorModalVisible) {
|
||||||
|
// Submit the new SVG, which also stores a single undo/redo action.
|
||||||
|
if (this._hasChanged) onUpdateImage();
|
||||||
|
this._hasChanged = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleChangeColor (newColor) {
|
||||||
|
// Stroke-selector-specific logic: if we change the stroke color from "none" to something visible, ensure
|
||||||
|
// there's a nonzero stroke width. If we change the stroke color to "none", set the stroke width to zero.
|
||||||
|
if (isStroke) {
|
||||||
|
|
||||||
|
// Whether the old color style in this color indicator was null (completely transparent).
|
||||||
|
// If it's a solid color, this means that the first color is null.
|
||||||
|
// If it's a gradient, this means both colors are null.
|
||||||
|
const oldStyleWasNull = this.props.gradientType === GradientTypes.SOLID ?
|
||||||
|
this.props.color === null :
|
||||||
|
this.props.color === null && this.props.color2 === null;
|
||||||
|
|
||||||
|
const otherColor = this.props.colorIndex === 1 ? this.props.color : this.props.color2;
|
||||||
|
// Whether the new color style in this color indicator is null.
|
||||||
|
const newStyleIsNull = this.props.gradientType === GradientTypes.SOLID ?
|
||||||
|
newColor === null :
|
||||||
|
newColor === null && otherColor === null;
|
||||||
|
|
||||||
|
if (oldStyleWasNull && !newStyleIsNull) {
|
||||||
|
this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged;
|
||||||
|
this.props.onChangeStrokeWidth(1);
|
||||||
|
} else if (!oldStyleWasNull && newStyleIsNull) {
|
||||||
|
this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged;
|
||||||
|
this.props.onChangeStrokeWidth(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
|
// Apply color and update redux, but do not update svg until picker closes.
|
||||||
|
const isDifferent = applyColorToSelection(
|
||||||
|
newColor,
|
||||||
|
this.props.colorIndex,
|
||||||
|
this.props.gradientType === GradientTypes.SOLID,
|
||||||
|
// In bitmap mode, only the fill color selector is used, but it applies to stroke if fillBitmapShapes
|
||||||
|
// is set to true via the "Fill"/"Outline" selector button
|
||||||
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
|
this.props.textEditTarget);
|
||||||
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
|
this.props.onChangeColor(newColor, this.props.colorIndex);
|
||||||
|
}
|
||||||
|
handleChangeGradientType (gradientType) {
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
|
// Apply color and update redux, but do not update svg until picker closes.
|
||||||
|
const isDifferent = applyGradientTypeToSelection(
|
||||||
|
gradientType,
|
||||||
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
|
this.props.textEditTarget);
|
||||||
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
|
const hasSelectedItems = getSelectedLeafItems().length > 0;
|
||||||
|
if (hasSelectedItems) {
|
||||||
|
if (isDifferent) {
|
||||||
|
// Recalculates the swatch colors
|
||||||
|
this.props.setSelectedItems(this.props.format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) {
|
||||||
|
// Generate color 2 and change to the 2nd swatch when switching from solid to gradient
|
||||||
|
if (!hasSelectedItems) {
|
||||||
|
this.props.onChangeColor(getRotatedColor(this.props.color), 1);
|
||||||
|
}
|
||||||
|
this.props.onChangeColorIndex(1);
|
||||||
|
}
|
||||||
|
if (this.props.onChangeGradientType) this.props.onChangeGradientType(gradientType);
|
||||||
|
}
|
||||||
|
handleCloseColor () {
|
||||||
|
// If the eyedropper is currently being used, don't
|
||||||
|
// close the color menu.
|
||||||
|
if (this.props.isEyeDropping) return;
|
||||||
|
|
||||||
|
// Otherwise, close the color menu and
|
||||||
|
// also reset the color index to indicate
|
||||||
|
// that `color1` is selected.
|
||||||
|
this.props.onCloseColor();
|
||||||
|
this.props.onChangeColorIndex(0);
|
||||||
|
}
|
||||||
|
handleSwap () {
|
||||||
|
if (getSelectedLeafItems().length) {
|
||||||
|
const formatIsBitmap = isBitmap(this.props.format);
|
||||||
|
const isDifferent = swapColorsInSelection(
|
||||||
|
isStroke || (formatIsBitmap && !this.props.fillBitmapShapes),
|
||||||
|
this.props.textEditTarget);
|
||||||
|
this.props.setSelectedItems(this.props.format);
|
||||||
|
this._hasChanged = this._hasChanged || isDifferent;
|
||||||
|
} else {
|
||||||
|
let color1 = this.props.color;
|
||||||
|
let color2 = this.props.color2;
|
||||||
|
color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex;
|
||||||
|
color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex;
|
||||||
|
this.props.onChangeColor(color1, 1);
|
||||||
|
this.props.onChangeColor(color2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<ColorIndicatorComponent
|
||||||
|
{...this.props}
|
||||||
|
label={this.props.intl.formatMessage(label)}
|
||||||
|
outline={isStroke}
|
||||||
|
onChangeColor={this.handleChangeColor}
|
||||||
|
onChangeGradientType={this.handleChangeGradientType}
|
||||||
|
onCloseColor={this.handleCloseColor}
|
||||||
|
onSwap={this.handleSwap}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorIndicator.propTypes = {
|
||||||
|
colorIndex: PropTypes.number.isRequired,
|
||||||
|
disabled: PropTypes.bool.isRequired,
|
||||||
|
color: PropTypes.string,
|
||||||
|
color2: PropTypes.string,
|
||||||
|
colorModalVisible: PropTypes.bool.isRequired,
|
||||||
|
fillBitmapShapes: PropTypes.bool.isRequired,
|
||||||
|
format: PropTypes.oneOf(Object.keys(Formats)),
|
||||||
|
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
||||||
|
intl: intlShape,
|
||||||
|
isEyeDropping: PropTypes.bool.isRequired,
|
||||||
|
onChangeColorIndex: PropTypes.func.isRequired,
|
||||||
|
onChangeColor: PropTypes.func.isRequired,
|
||||||
|
onChangeGradientType: PropTypes.func,
|
||||||
|
onChangeStrokeWidth: PropTypes.func,
|
||||||
|
onCloseColor: PropTypes.func.isRequired,
|
||||||
|
onUpdateImage: PropTypes.func.isRequired,
|
||||||
|
setSelectedItems: PropTypes.func.isRequired,
|
||||||
|
textEditTarget: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
return injectIntl(ColorIndicator);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default makeColorIndicator;
|
|
@ -1,8 +1,5 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import {defineMessages} from 'react-intl';
|
||||||
import React from 'react';
|
|
||||||
import bindAll from 'lodash.bindall';
|
|
||||||
import parseColor from 'parse-color';
|
|
||||||
|
|
||||||
import {changeColorIndex} from '../reducers/color-index';
|
import {changeColorIndex} from '../reducers/color-index';
|
||||||
import {changeFillColor, changeFillColor2} from '../reducers/fill-style';
|
import {changeFillColor, changeFillColor2} from '../reducers/fill-style';
|
||||||
|
@ -10,131 +7,33 @@ import {changeGradientType} from '../reducers/fill-mode-gradient-type';
|
||||||
import {openFillColor, closeFillColor} from '../reducers/modals';
|
import {openFillColor, closeFillColor} from '../reducers/modals';
|
||||||
import {getSelectedLeafItems} from '../helper/selection';
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
import {setSelectedItems} from '../reducers/selected-items';
|
import {setSelectedItems} from '../reducers/selected-items';
|
||||||
import Modes from '../lib/modes';
|
import Modes, {GradientToolsModes} from '../lib/modes';
|
||||||
import Formats from '../lib/format';
|
|
||||||
import {isBitmap} from '../lib/format';
|
import {isBitmap} from '../lib/format';
|
||||||
import GradientTypes from '../lib/gradient-types';
|
|
||||||
|
|
||||||
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
import makeColorIndicator from './color-indicator.jsx';
|
||||||
import {applyColorToSelection,
|
|
||||||
applyGradientTypeToSelection,
|
|
||||||
getRotatedColor,
|
|
||||||
swapColorsInSelection,
|
|
||||||
MIXED} from '../helper/style-path';
|
|
||||||
|
|
||||||
class FillColorIndicator extends React.Component {
|
const messages = defineMessages({
|
||||||
constructor (props) {
|
label: {
|
||||||
super(props);
|
id: 'paint.paintEditor.fill',
|
||||||
bindAll(this, [
|
description: 'Label for the color picker for the fill color',
|
||||||
'handleChangeFillColor',
|
defaultMessage: 'Fill'
|
||||||
'handleChangeGradientType',
|
}
|
||||||
'handleCloseFillColor',
|
});
|
||||||
'handleSwap'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Flag to track whether an svg-update-worthy change has been made
|
const FillColorIndicator = makeColorIndicator(messages.label, false);
|
||||||
this._hasChanged = false;
|
|
||||||
}
|
|
||||||
componentWillReceiveProps (newProps) {
|
|
||||||
const {fillColorModalVisible, onUpdateImage} = this.props;
|
|
||||||
if (fillColorModalVisible && !newProps.fillColorModalVisible) {
|
|
||||||
// Submit the new SVG, which also stores a single undo/redo action.
|
|
||||||
if (this._hasChanged) onUpdateImage();
|
|
||||||
this._hasChanged = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleChangeFillColor (newColor) {
|
|
||||||
// Apply color and update redux, but do not update svg until picker closes.
|
|
||||||
const isDifferent = applyColorToSelection(
|
|
||||||
newColor,
|
|
||||||
this.props.colorIndex,
|
|
||||||
this.props.gradientType === GradientTypes.SOLID,
|
|
||||||
isBitmap(this.props.format),
|
|
||||||
false, // applyToStroke
|
|
||||||
this.props.textEditTarget);
|
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
|
||||||
this.props.onChangeFillColor(newColor, this.props.colorIndex);
|
|
||||||
}
|
|
||||||
handleChangeGradientType (gradientType) {
|
|
||||||
// Apply color and update redux, but do not update svg until picker closes.
|
|
||||||
const isDifferent = applyGradientTypeToSelection(
|
|
||||||
gradientType,
|
|
||||||
isBitmap(this.props.format),
|
|
||||||
false, // applyToStroke
|
|
||||||
this.props.textEditTarget);
|
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
|
||||||
const hasSelectedItems = getSelectedLeafItems().length > 0;
|
|
||||||
if (hasSelectedItems) {
|
|
||||||
if (isDifferent) {
|
|
||||||
// Recalculates the swatch colors
|
|
||||||
this.props.setSelectedItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) {
|
|
||||||
// Generate color 2 and change to the 2nd swatch when switching from solid to gradient
|
|
||||||
if (!hasSelectedItems) {
|
|
||||||
this.props.onChangeFillColor(getRotatedColor(this.props.fillColor), 1);
|
|
||||||
}
|
|
||||||
this.props.onChangeColorIndex(1);
|
|
||||||
}
|
|
||||||
this.props.onChangeGradientType(gradientType);
|
|
||||||
}
|
|
||||||
handleCloseFillColor () {
|
|
||||||
// If the eyedropper is currently being used, don't
|
|
||||||
// close the fill color menu.
|
|
||||||
if (this.props.isEyeDropping) return;
|
|
||||||
|
|
||||||
// Otherwise, close the fill color menu and
|
|
||||||
// also reset the color index to indicate
|
|
||||||
// that `color1` is selected.
|
|
||||||
this.props.onCloseFillColor();
|
|
||||||
this.props.onChangeColorIndex(0);
|
|
||||||
}
|
|
||||||
handleSwap () {
|
|
||||||
if (getSelectedLeafItems().length) {
|
|
||||||
const isDifferent = swapColorsInSelection(
|
|
||||||
isBitmap(this.props.format),
|
|
||||||
false, // applyToStroke
|
|
||||||
this.props.textEditTarget);
|
|
||||||
this.props.setSelectedItems();
|
|
||||||
this._hasChanged = this._hasChanged || isDifferent;
|
|
||||||
} else {
|
|
||||||
let color1 = this.props.fillColor;
|
|
||||||
let color2 = this.props.fillColor2;
|
|
||||||
color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex;
|
|
||||||
color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex;
|
|
||||||
this.props.onChangeFillColor(color1, 1);
|
|
||||||
this.props.onChangeFillColor(color2, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<FillColorIndicatorComponent
|
|
||||||
{...this.props}
|
|
||||||
onChangeFillColor={this.handleChangeFillColor}
|
|
||||||
onChangeGradientType={this.handleChangeGradientType}
|
|
||||||
onCloseFillColor={this.handleCloseFillColor}
|
|
||||||
onSwap={this.handleSwap}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
colorIndex: state.scratchPaint.fillMode.colorIndex,
|
colorIndex: state.scratchPaint.fillMode.colorIndex,
|
||||||
disabled: state.scratchPaint.mode === Modes.LINE,
|
disabled: state.scratchPaint.mode === Modes.LINE,
|
||||||
fillColor: state.scratchPaint.color.fillColor.primary,
|
color: state.scratchPaint.color.fillColor.primary,
|
||||||
fillColor2: state.scratchPaint.color.fillColor.secondary,
|
color2: state.scratchPaint.color.fillColor.secondary,
|
||||||
fillColorModalVisible: state.scratchPaint.modals.fillColor,
|
colorModalVisible: state.scratchPaint.modals.fillColor,
|
||||||
|
fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
|
||||||
format: state.scratchPaint.format,
|
format: state.scratchPaint.format,
|
||||||
gradientType: state.scratchPaint.color.fillColor.gradientType,
|
gradientType: state.scratchPaint.color.fillColor.gradientType,
|
||||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||||
mode: state.scratchPaint.mode,
|
mode: state.scratchPaint.mode,
|
||||||
shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT ||
|
shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes,
|
||||||
state.scratchPaint.mode === Modes.RESHAPE ||
|
|
||||||
state.scratchPaint.mode === Modes.FILL ||
|
|
||||||
state.scratchPaint.mode === Modes.BIT_SELECT ||
|
|
||||||
state.scratchPaint.mode === Modes.BIT_FILL,
|
|
||||||
textEditTarget: state.scratchPaint.textEditTarget
|
textEditTarget: state.scratchPaint.textEditTarget
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -142,17 +41,17 @@ const mapDispatchToProps = dispatch => ({
|
||||||
onChangeColorIndex: index => {
|
onChangeColorIndex: index => {
|
||||||
dispatch(changeColorIndex(index));
|
dispatch(changeColorIndex(index));
|
||||||
},
|
},
|
||||||
onChangeFillColor: (fillColor, index) => {
|
onChangeColor: (fillColor, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
dispatch(changeFillColor(fillColor));
|
dispatch(changeFillColor(fillColor));
|
||||||
} else if (index === 1) {
|
} else if (index === 1) {
|
||||||
dispatch(changeFillColor2(fillColor));
|
dispatch(changeFillColor2(fillColor));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenFillColor: () => {
|
onOpenColor: () => {
|
||||||
dispatch(openFillColor());
|
dispatch(openFillColor());
|
||||||
},
|
},
|
||||||
onCloseFillColor: () => {
|
onCloseColor: () => {
|
||||||
dispatch(closeFillColor());
|
dispatch(closeFillColor());
|
||||||
},
|
},
|
||||||
onChangeGradientType: gradientType => {
|
onChangeGradientType: gradientType => {
|
||||||
|
@ -163,24 +62,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
FillColorIndicator.propTypes = {
|
|
||||||
colorIndex: PropTypes.number.isRequired,
|
|
||||||
disabled: PropTypes.bool.isRequired,
|
|
||||||
fillColor: PropTypes.string,
|
|
||||||
fillColor2: PropTypes.string,
|
|
||||||
fillColorModalVisible: PropTypes.bool.isRequired,
|
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)),
|
|
||||||
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
|
|
||||||
isEyeDropping: PropTypes.bool.isRequired,
|
|
||||||
onChangeColorIndex: PropTypes.func.isRequired,
|
|
||||||
onChangeFillColor: PropTypes.func.isRequired,
|
|
||||||
onChangeGradientType: PropTypes.func.isRequired,
|
|
||||||
onCloseFillColor: PropTypes.func.isRequired,
|
|
||||||
onUpdateImage: PropTypes.func.isRequired,
|
|
||||||
setSelectedItems: PropTypes.func.isRequired,
|
|
||||||
textEditTarget: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
|
|
|
@ -4,11 +4,12 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {clearSelection} from '../helper/selection';
|
import {clearSelection} from '../helper/selection';
|
||||||
import {endPointHit, touching} from '../helper/snapping';
|
import {endPointHit, touching} from '../helper/snapping';
|
||||||
import {drawHitPoint, removeHitPoint} from '../helper/guides';
|
import {drawHitPoint, removeHitPoint} from '../helper/guides';
|
||||||
import {stylePath} from '../helper/style-path';
|
import {styleShape} from '../helper/style-path';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor, clearStrokeGradient} from '../reducers/stroke-style';
|
||||||
import {changeStrokeWidth} from '../reducers/stroke-width';
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems} from '../reducers/selected-items';
|
||||||
|
@ -58,9 +59,10 @@ class LineMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
|
this.props.clearGradient();
|
||||||
|
|
||||||
// Force the default line color if stroke is MIXED or transparent
|
// Force the default line color if stroke is MIXED or transparent
|
||||||
const {strokeColor} = this.props.colorState;
|
const strokeColor = this.props.colorState.strokeColor.primary;
|
||||||
if (strokeColor === MIXED || strokeColor === null) {
|
if (strokeColor === MIXED || strokeColor === null) {
|
||||||
this.props.onChangeStrokeColor(LineMode.DEFAULT_COLOR);
|
this.props.onChangeStrokeColor(LineMode.DEFAULT_COLOR);
|
||||||
}
|
}
|
||||||
|
@ -101,7 +103,11 @@ class LineMode extends React.Component {
|
||||||
this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE);
|
this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE);
|
||||||
if (this.hitResult) {
|
if (this.hitResult) {
|
||||||
this.path = this.hitResult.path;
|
this.path = this.hitResult.path;
|
||||||
stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth);
|
styleShape(this.path, {
|
||||||
|
fillColor: null,
|
||||||
|
strokeColor: this.props.colorState.strokeColor,
|
||||||
|
strokeWidth: this.props.colorState.strokeWidth
|
||||||
|
});
|
||||||
if (this.hitResult.isFirst) {
|
if (this.hitResult.isFirst) {
|
||||||
this.path.reverse();
|
this.path.reverse();
|
||||||
}
|
}
|
||||||
|
@ -114,7 +120,11 @@ class LineMode extends React.Component {
|
||||||
if (!this.path) {
|
if (!this.path) {
|
||||||
this.path = new paper.Path();
|
this.path = new paper.Path();
|
||||||
this.path.strokeCap = 'round';
|
this.path.strokeCap = 'round';
|
||||||
stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth);
|
styleShape(this.path, {
|
||||||
|
fillColor: null,
|
||||||
|
strokeColor: this.props.colorState.strokeColor,
|
||||||
|
strokeWidth: this.props.colorState.strokeWidth
|
||||||
|
});
|
||||||
|
|
||||||
this.path.add(event.point);
|
this.path.add(event.point);
|
||||||
this.path.add(event.point); // Add second point, which is what will move when dragged
|
this.path.add(event.point); // Add second point, which is what will move when dragged
|
||||||
|
@ -186,6 +196,12 @@ class LineMode extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.path.lastSegment.point = endPoint;
|
this.path.lastSegment.point = endPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
styleShape(this.path, {
|
||||||
|
fillColor: null,
|
||||||
|
strokeColor: this.props.colorState.strokeColor,
|
||||||
|
strokeWidth: this.props.colorState.strokeWidth
|
||||||
|
});
|
||||||
}
|
}
|
||||||
onMouseUp (event) {
|
onMouseUp (event) {
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
@ -225,6 +241,12 @@ class LineMode extends React.Component {
|
||||||
this.hitResult = null;
|
this.hitResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
styleShape(this.path, {
|
||||||
|
fillColor: null,
|
||||||
|
strokeColor: this.props.colorState.strokeColor,
|
||||||
|
strokeWidth: this.props.colorState.strokeWidth
|
||||||
|
});
|
||||||
|
|
||||||
if (this.path) {
|
if (this.path) {
|
||||||
this.props.onUpdateImage();
|
this.props.onUpdateImage();
|
||||||
this.path = null;
|
this.path = null;
|
||||||
|
@ -253,13 +275,11 @@ class LineMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
LineMode.propTypes = {
|
LineMode.propTypes = {
|
||||||
|
clearGradient: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
colorState: PropTypes.shape({
|
colorState: PropTypes.shape({
|
||||||
fillColor: PropTypes.shape({
|
fillColor: ColorStyleProptype,
|
||||||
primary: PropTypes.string,
|
strokeColor: ColorStyleProptype,
|
||||||
secondary: PropTypes.string
|
|
||||||
}),
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number
|
strokeWidth: PropTypes.number
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
@ -274,6 +294,9 @@ const mapStateToProps = state => ({
|
||||||
isLineModeActive: state.scratchPaint.mode === Modes.LINE
|
isLineModeActive: state.scratchPaint.mode === Modes.LINE
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
clearGradient: () => {
|
||||||
|
dispatch(clearStrokeGradient());
|
||||||
|
},
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,11 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor} from '../reducers/stroke-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -53,12 +54,12 @@ class OvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||||
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||||
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||||
const {strokeColor, strokeWidth} = this.props.colorState;
|
const {strokeWidth} = this.props.colorState;
|
||||||
const fillColor = this.props.colorState.fillColor.primary;
|
const fillColor = this.props.colorState.fillColor.primary;
|
||||||
|
const strokeColor = this.props.colorState.strokeColor.primary;
|
||||||
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||||
const strokeColorPresent =
|
const strokeColorPresent =
|
||||||
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||||
|
@ -95,14 +96,10 @@ class OvalMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
OvalMode.propTypes = {
|
OvalMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
colorState: PropTypes.shape({
|
colorState: PropTypes.shape({
|
||||||
fillColor: PropTypes.shape({
|
fillColor: ColorStyleProptype,
|
||||||
primary: PropTypes.string,
|
strokeColor: ColorStyleProptype,
|
||||||
secondary: PropTypes.string
|
|
||||||
}),
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number
|
strokeWidth: PropTypes.number
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
@ -124,9 +121,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setCursor: cursorString => {
|
setCursor: cursorString => {
|
||||||
dispatch(setCursor(cursorString));
|
dispatch(setCursor(cursorString));
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,11 @@ import React from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor} from '../reducers/stroke-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
import {setCursor} from '../reducers/cursor';
|
import {setCursor} from '../reducers/cursor';
|
||||||
|
@ -53,12 +54,12 @@ class RectMode extends React.Component {
|
||||||
}
|
}
|
||||||
activateTool () {
|
activateTool () {
|
||||||
clearSelection(this.props.clearSelectedItems);
|
clearSelection(this.props.clearSelectedItems);
|
||||||
this.props.clearGradient();
|
|
||||||
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||||
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||||
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||||
const {strokeColor, strokeWidth} = this.props.colorState;
|
const {strokeWidth} = this.props.colorState;
|
||||||
const fillColor = this.props.colorState.fillColor.primary;
|
const fillColor = this.props.colorState.fillColor.primary;
|
||||||
|
const strokeColor = this.props.colorState.strokeColor.primary;
|
||||||
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||||
const strokeColorPresent =
|
const strokeColorPresent =
|
||||||
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||||
|
@ -95,14 +96,10 @@ class RectMode extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
RectMode.propTypes = {
|
RectMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
colorState: PropTypes.shape({
|
colorState: PropTypes.shape({
|
||||||
fillColor: PropTypes.shape({
|
fillColor: ColorStyleProptype,
|
||||||
primary: PropTypes.string,
|
strokeColor: ColorStyleProptype,
|
||||||
secondary: PropTypes.string
|
|
||||||
}),
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number
|
strokeWidth: PropTypes.number
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleMouseDown: PropTypes.func.isRequired,
|
handleMouseDown: PropTypes.func.isRequired,
|
||||||
|
@ -124,9 +121,6 @@ const mapDispatchToProps = dispatch => ({
|
||||||
clearSelectedItems: () => {
|
clearSelectedItems: () => {
|
||||||
dispatch(clearSelectedItems());
|
dispatch(clearSelectedItems());
|
||||||
},
|
},
|
||||||
clearGradient: () => {
|
|
||||||
dispatch(clearFillGradient());
|
|
||||||
},
|
|
||||||
setSelectedItems: () => {
|
setSelectedItems: () => {
|
||||||
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
|
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,109 +1,73 @@
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import {defineMessages} from 'react-intl';
|
||||||
import React from 'react';
|
|
||||||
import bindAll from 'lodash.bindall';
|
import {changeColorIndex} from '../reducers/color-index';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor, changeStrokeColor2} from '../reducers/stroke-style';
|
||||||
import {changeStrokeWidth} from '../reducers/stroke-width';
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
||||||
|
import {changeStrokeGradientType} from '../reducers/stroke-style';
|
||||||
import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
|
import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
|
||||||
import Modes from '../lib/modes';
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
import Formats from '../lib/format';
|
import {setSelectedItems} from '../reducers/selected-items';
|
||||||
|
import Modes, {GradientToolsModes} from '../lib/modes';
|
||||||
import {isBitmap} from '../lib/format';
|
import {isBitmap} from '../lib/format';
|
||||||
|
|
||||||
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
import makeColorIndicator from './color-indicator.jsx';
|
||||||
import {applyColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path';
|
|
||||||
|
|
||||||
class StrokeColorIndicator extends React.Component {
|
const messages = defineMessages({
|
||||||
constructor (props) {
|
label: {
|
||||||
super(props);
|
id: 'paint.paintEditor.stroke',
|
||||||
bindAll(this, [
|
description: 'Label for the color picker for the outline color',
|
||||||
'handleChangeStrokeColor',
|
defaultMessage: 'Outline'
|
||||||
'handleCloseStrokeColor'
|
}
|
||||||
]);
|
});
|
||||||
|
|
||||||
// Flag to track whether an svg-update-worthy change has been made
|
const StrokeColorIndicator = makeColorIndicator(messages.label, true);
|
||||||
this._hasChanged = false;
|
|
||||||
}
|
|
||||||
componentWillReceiveProps (newProps) {
|
|
||||||
const {strokeColorModalVisible, onUpdateImage} = this.props;
|
|
||||||
if (strokeColorModalVisible && !newProps.strokeColorModalVisible) {
|
|
||||||
// Submit the new SVG, which also stores a single undo/redo action.
|
|
||||||
if (this._hasChanged) onUpdateImage();
|
|
||||||
this._hasChanged = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleChangeStrokeColor (newColor) {
|
|
||||||
if (this.props.strokeColor === null && newColor !== null) {
|
|
||||||
this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged;
|
|
||||||
this.props.onChangeStrokeWidth(1);
|
|
||||||
} else if (this.props.strokeColor !== null && newColor === null) {
|
|
||||||
this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged;
|
|
||||||
this.props.onChangeStrokeWidth(0);
|
|
||||||
}
|
|
||||||
// Apply color and update redux, but do not update svg until picker closes.
|
|
||||||
this._hasChanged = applyColorToSelection(
|
|
||||||
newColor,
|
|
||||||
0, // colorIndex,
|
|
||||||
true, // isSolidGradient
|
|
||||||
isBitmap(this.props.format),
|
|
||||||
true, // applyToStroke
|
|
||||||
this.props.textEditTarget) ||
|
|
||||||
this._hasChanged;
|
|
||||||
this.props.onChangeStrokeColor(newColor);
|
|
||||||
}
|
|
||||||
handleCloseStrokeColor () {
|
|
||||||
if (!this.props.isEyeDropping) {
|
|
||||||
this.props.onCloseStrokeColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<StrokeColorIndicatorComponent
|
|
||||||
{...this.props}
|
|
||||||
onChangeStrokeColor={this.handleChangeStrokeColor}
|
|
||||||
onCloseStrokeColor={this.handleCloseStrokeColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
colorIndex: state.scratchPaint.fillMode.colorIndex,
|
||||||
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
||||||
state.scratchPaint.mode === Modes.TEXT ||
|
state.scratchPaint.mode === Modes.TEXT ||
|
||||||
state.scratchPaint.mode === Modes.FILL,
|
state.scratchPaint.mode === Modes.FILL,
|
||||||
|
color: state.scratchPaint.color.strokeColor.primary,
|
||||||
|
color2: state.scratchPaint.color.strokeColor.secondary,
|
||||||
|
fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
|
||||||
|
colorModalVisible: state.scratchPaint.modals.strokeColor,
|
||||||
format: state.scratchPaint.format,
|
format: state.scratchPaint.format,
|
||||||
|
gradientType: state.scratchPaint.color.strokeColor.gradientType,
|
||||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||||
strokeColor: state.scratchPaint.color.strokeColor,
|
mode: state.scratchPaint.mode,
|
||||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor,
|
shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes,
|
||||||
textEditTarget: state.scratchPaint.textEditTarget
|
textEditTarget: state.scratchPaint.textEditTarget
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeStrokeColor: strokeColor => {
|
onChangeColorIndex: index => {
|
||||||
dispatch(changeStrokeColor(strokeColor));
|
dispatch(changeColorIndex(index));
|
||||||
|
},
|
||||||
|
onChangeColor: (strokeColor, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
dispatch(changeStrokeColor(strokeColor));
|
||||||
|
} else if (index === 1) {
|
||||||
|
dispatch(changeStrokeColor2(strokeColor));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onChangeStrokeWidth: strokeWidth => {
|
onChangeStrokeWidth: strokeWidth => {
|
||||||
dispatch(changeStrokeWidth(strokeWidth));
|
dispatch(changeStrokeWidth(strokeWidth));
|
||||||
},
|
},
|
||||||
onOpenStrokeColor: () => {
|
onOpenColor: () => {
|
||||||
dispatch(openStrokeColor());
|
dispatch(openStrokeColor());
|
||||||
},
|
},
|
||||||
onCloseStrokeColor: () => {
|
onCloseColor: () => {
|
||||||
dispatch(closeStrokeColor());
|
dispatch(closeStrokeColor());
|
||||||
|
},
|
||||||
|
onChangeGradientType: gradientType => {
|
||||||
|
dispatch(changeStrokeGradientType(gradientType));
|
||||||
|
},
|
||||||
|
setSelectedItems: format => {
|
||||||
|
dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
StrokeColorIndicator.propTypes = {
|
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)),
|
|
||||||
isEyeDropping: PropTypes.bool.isRequired,
|
|
||||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
|
||||||
onChangeStrokeWidth: PropTypes.func.isRequired,
|
|
||||||
onCloseStrokeColor: PropTypes.func.isRequired,
|
|
||||||
onUpdateImage: PropTypes.func.isRequired,
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeColorModalVisible: PropTypes.bool.isRequired,
|
|
||||||
textEditTarget: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
|
|
|
@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import parseColor from 'parse-color';
|
import parseColor from 'parse-color';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor, changeStrokeColor2, changeStrokeGradientType} from '../reducers/stroke-style';
|
||||||
import {changeStrokeWidth} from '../reducers/stroke-width';
|
import {changeStrokeWidth} from '../reducers/stroke-width';
|
||||||
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
|
import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
|
||||||
import {getSelectedLeafItems} from '../helper/selection';
|
import {getSelectedLeafItems} from '../helper/selection';
|
||||||
import {applyColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED}
|
import {applyColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED}
|
||||||
from '../helper/style-path';
|
from '../helper/style-path';
|
||||||
|
import GradientTypes from '../lib/gradient-types';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
import Formats from '../lib/format';
|
import Formats from '../lib/format';
|
||||||
import {isBitmap} from '../lib/format';
|
import {isBitmap} from '../lib/format';
|
||||||
|
@ -23,21 +24,31 @@ class StrokeWidthIndicator extends React.Component {
|
||||||
handleChangeStrokeWidth (newWidth) {
|
handleChangeStrokeWidth (newWidth) {
|
||||||
let changed = applyStrokeWidthToSelection(newWidth, this.props.textEditTarget);
|
let changed = applyStrokeWidthToSelection(newWidth, this.props.textEditTarget);
|
||||||
if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) {
|
if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) {
|
||||||
let currentColor = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format)).strokeColor;
|
const currentColorState = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format));
|
||||||
if (currentColor === null) {
|
|
||||||
|
// Color counts as null if either both colors are null or the primary color is null and it's solid
|
||||||
|
// TODO: consolidate this check in one place
|
||||||
|
const wasNull = currentColorState.strokeColor === null &&
|
||||||
|
(currentColorState.strokeColor2 === null ||
|
||||||
|
currentColorState.strokeGradientType === GradientTypes.SOLID);
|
||||||
|
|
||||||
|
if (wasNull) {
|
||||||
changed = applyColorToSelection(
|
changed = applyColorToSelection(
|
||||||
'#000',
|
'#000',
|
||||||
0, // colorIndex,
|
0, // colorIndex,
|
||||||
true, // isSolidGradient
|
true, // isSolidGradient
|
||||||
isBitmap(this.props.format),
|
|
||||||
true, // applyToStroke
|
true, // applyToStroke
|
||||||
this.props.textEditTarget) ||
|
this.props.textEditTarget) ||
|
||||||
changed;
|
changed;
|
||||||
currentColor = '#000';
|
// If there's no previous stroke color, default to solid black
|
||||||
} else if (currentColor !== MIXED) {
|
this.props.onChangeStrokeGradientType(GradientTypes.SOLID);
|
||||||
currentColor = parseColor(currentColor).hex;
|
this.props.onChangeStrokeColor('#000');
|
||||||
|
} else if (currentColorState.strokeColor !== MIXED) {
|
||||||
|
// Set color state from the selected item's stroke color
|
||||||
|
this.props.onChangeStrokeGradientType(currentColorState.strokeGradientType);
|
||||||
|
this.props.onChangeStrokeColor(parseColor(currentColorState.strokeColor).hex);
|
||||||
|
this.props.onChangeStrokeColor2(parseColor(currentColorState.strokeColor2).hex);
|
||||||
}
|
}
|
||||||
this.props.onChangeStrokeColor(currentColor);
|
|
||||||
}
|
}
|
||||||
this.props.onChangeStrokeWidth(newWidth);
|
this.props.onChangeStrokeWidth(newWidth);
|
||||||
if (changed) this.props.onUpdateImage();
|
if (changed) this.props.onUpdateImage();
|
||||||
|
@ -65,6 +76,12 @@ const mapDispatchToProps = dispatch => ({
|
||||||
onChangeStrokeColor: strokeColor => {
|
onChangeStrokeColor: strokeColor => {
|
||||||
dispatch(changeStrokeColor(strokeColor));
|
dispatch(changeStrokeColor(strokeColor));
|
||||||
},
|
},
|
||||||
|
onChangeStrokeColor2: strokeColor => {
|
||||||
|
dispatch(changeStrokeColor2(strokeColor));
|
||||||
|
},
|
||||||
|
onChangeStrokeGradientType: strokeColor => {
|
||||||
|
dispatch(changeStrokeGradientType(strokeColor));
|
||||||
|
},
|
||||||
onChangeStrokeWidth: strokeWidth => {
|
onChangeStrokeWidth: strokeWidth => {
|
||||||
dispatch(changeStrokeWidth(strokeWidth));
|
dispatch(changeStrokeWidth(strokeWidth));
|
||||||
}
|
}
|
||||||
|
@ -74,6 +91,8 @@ StrokeWidthIndicator.propTypes = {
|
||||||
disabled: PropTypes.bool.isRequired,
|
disabled: PropTypes.bool.isRequired,
|
||||||
format: PropTypes.oneOf(Object.keys(Formats)),
|
format: PropTypes.oneOf(Object.keys(Formats)),
|
||||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||||
|
onChangeStrokeColor2: PropTypes.func.isRequired,
|
||||||
|
onChangeStrokeGradientType: PropTypes.func.isRequired,
|
||||||
onChangeStrokeWidth: PropTypes.func.isRequired,
|
onChangeStrokeWidth: PropTypes.func.isRequired,
|
||||||
onUpdateImage: PropTypes.func.isRequired,
|
onUpdateImage: PropTypes.func.isRequired,
|
||||||
strokeWidth: PropTypes.number,
|
strokeWidth: PropTypes.number,
|
||||||
|
|
|
@ -5,11 +5,12 @@ import {connect} from 'react-redux';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import Fonts from '../lib/fonts';
|
import Fonts from '../lib/fonts';
|
||||||
import Modes from '../lib/modes';
|
import Modes from '../lib/modes';
|
||||||
|
import ColorStyleProptype from '../lib/color-style-proptype';
|
||||||
import {MIXED} from '../helper/style-path';
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
import {changeFont} from '../reducers/font';
|
import {changeFont} from '../reducers/font';
|
||||||
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor} from '../reducers/stroke-style';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
|
@ -81,8 +82,9 @@ class TextMode extends React.Component {
|
||||||
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||||
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||||
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||||
const {strokeColor, strokeWidth} = nextProps.colorState;
|
const {strokeWidth} = nextProps.colorState;
|
||||||
const fillColor = this.props.colorState.fillColor.primary;
|
const fillColor = nextProps.colorState.fillColor.primary;
|
||||||
|
const strokeColor = nextProps.colorState.strokeColor.primary;
|
||||||
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||||
const strokeColorPresent = nextProps.isBitmap ? false :
|
const strokeColorPresent = nextProps.isBitmap ? false :
|
||||||
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||||
|
@ -143,11 +145,8 @@ TextMode.propTypes = {
|
||||||
clearGradient: PropTypes.func.isRequired,
|
clearGradient: PropTypes.func.isRequired,
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
colorState: PropTypes.shape({
|
colorState: PropTypes.shape({
|
||||||
fillColor: PropTypes.shape({
|
fillColor: ColorStyleProptype,
|
||||||
primary: PropTypes.string,
|
strokeColor: ColorStyleProptype,
|
||||||
secondary: PropTypes.string
|
|
||||||
}),
|
|
||||||
strokeColor: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number
|
strokeWidth: PropTypes.number
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
font: PropTypes.string,
|
font: PropTypes.string,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
import {styleShape} from '../style-path';
|
||||||
import {commitOvalToBitmap} from '../bitmap';
|
import {commitOvalToBitmap} from '../bitmap';
|
||||||
import {getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {clearSelection} from '../selection';
|
import {clearSelection} from '../selection';
|
||||||
|
@ -76,36 +77,29 @@ class OvalTool extends paper.Tool {
|
||||||
this.thickness = this.oval.strokeWidth;
|
this.thickness = this.oval.strokeWidth;
|
||||||
}
|
}
|
||||||
this.filled = this.oval.strokeWidth === 0;
|
this.filled = this.oval.strokeWidth === 0;
|
||||||
const color = this.filled ? this.oval.fillColor : this.oval.strokeColor;
|
// We don't need to set our color from the selected oval's color because the color state reducers will
|
||||||
this.color = color ? color.toCSS() : null;
|
// do that for us every time the selection changes.
|
||||||
} else if (this.oval && this.oval.isInserted() && !this.oval.selected) {
|
} else if (this.oval && this.oval.isInserted() && !this.oval.selected) {
|
||||||
// Oval got deselected
|
// Oval got deselected
|
||||||
this.commitOval();
|
this.commitOval();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
styleOval () {
|
||||||
|
styleShape(this.oval, {
|
||||||
|
fillColor: this.filled ? this.color : null,
|
||||||
|
strokeColor: this.filled ? null : this.color,
|
||||||
|
strokeWidth: this.filled ? 0 : this.thickness
|
||||||
|
});
|
||||||
|
}
|
||||||
setColor (color) {
|
setColor (color) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
if (this.oval) {
|
if (this.oval) this.styleOval();
|
||||||
if (this.filled) {
|
|
||||||
this.oval.fillColor = this.color;
|
|
||||||
} else {
|
|
||||||
this.oval.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setFilled (filled) {
|
setFilled (filled) {
|
||||||
if (this.filled === filled) return;
|
if (this.filled === filled) return;
|
||||||
this.filled = filled;
|
this.filled = filled;
|
||||||
if (this.oval && this.oval.isInserted()) {
|
if (this.oval && this.oval.isInserted()) {
|
||||||
if (this.filled) {
|
this.styleOval();
|
||||||
this.oval.fillColor = this.color;
|
|
||||||
this.oval.strokeWidth = 0;
|
|
||||||
this.oval.strokeColor = null;
|
|
||||||
} else {
|
|
||||||
this.oval.fillColor = null;
|
|
||||||
this.oval.strokeWidth = this.thickness;
|
|
||||||
this.oval.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
this.onUpdateImage();
|
this.onUpdateImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,23 +125,12 @@ 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({
|
point: event.downPoint,
|
||||||
fillColor: this.color,
|
size: 0,
|
||||||
point: event.downPoint,
|
strokeScaling: false
|
||||||
strokeWidth: 0,
|
});
|
||||||
strokeScaling: false,
|
this.styleOval();
|
||||||
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};
|
this.oval.data = {zoomLevel: paper.view.zoom};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,6 +158,7 @@ class OvalTool extends paper.Tool {
|
||||||
} else {
|
} else {
|
||||||
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
||||||
}
|
}
|
||||||
|
this.styleOval();
|
||||||
}
|
}
|
||||||
handleMouseMove (event) {
|
handleMouseMove (event) {
|
||||||
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
||||||
|
@ -197,6 +181,7 @@ class OvalTool extends paper.Tool {
|
||||||
// Hit testing does not work correctly unless the width and height are positive
|
// Hit testing does not work correctly unless the width and height are positive
|
||||||
this.oval.size = new paper.Point(Math.abs(this.oval.size.width), Math.abs(this.oval.size.height));
|
this.oval.size = new paper.Point(Math.abs(this.oval.size.width), Math.abs(this.oval.size.height));
|
||||||
this.oval.selected = true;
|
this.oval.selected = true;
|
||||||
|
this.styleOval();
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import Modes from '../../lib/modes';
|
import Modes from '../../lib/modes';
|
||||||
|
import {styleShape} from '../../helper/style-path';
|
||||||
import {commitRectToBitmap} from '../bitmap';
|
import {commitRectToBitmap} from '../bitmap';
|
||||||
import {getRaster} from '../layer';
|
import {getRaster} from '../layer';
|
||||||
import {clearSelection} from '../selection';
|
import {clearSelection} from '../selection';
|
||||||
|
@ -76,36 +77,27 @@ class RectTool extends paper.Tool {
|
||||||
this.thickness = this.rect.strokeWidth;
|
this.thickness = this.rect.strokeWidth;
|
||||||
}
|
}
|
||||||
this.filled = this.rect.strokeWidth === 0;
|
this.filled = this.rect.strokeWidth === 0;
|
||||||
const color = this.filled ? this.rect.fillColor : this.rect.strokeColor;
|
|
||||||
this.color = color ? color.toCSS() : null;
|
|
||||||
} else if (this.rect && this.rect.isInserted() && !this.rect.selected) {
|
} else if (this.rect && this.rect.isInserted() && !this.rect.selected) {
|
||||||
// Rectangle got deselected
|
// Rectangle got deselected
|
||||||
this.commitRect();
|
this.commitRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
styleRect () {
|
||||||
|
styleShape(this.rect, {
|
||||||
|
fillColor: this.filled ? this.color : null,
|
||||||
|
strokeColor: this.filled ? null : this.color,
|
||||||
|
strokeWidth: this.filled ? 0 : this.thickness
|
||||||
|
});
|
||||||
|
}
|
||||||
setColor (color) {
|
setColor (color) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
if (this.rect) {
|
if (this.rect) this.styleRect();
|
||||||
if (this.filled) {
|
|
||||||
this.rect.fillColor = this.color;
|
|
||||||
} else {
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setFilled (filled) {
|
setFilled (filled) {
|
||||||
if (this.filled === filled) return;
|
if (this.filled === filled) return;
|
||||||
this.filled = filled;
|
this.filled = filled;
|
||||||
if (this.rect && this.rect.isInserted()) {
|
if (this.rect && this.rect.isInserted()) {
|
||||||
if (this.filled) {
|
this.styleRect();
|
||||||
this.rect.fillColor = this.color;
|
|
||||||
this.rect.strokeWidth = 0;
|
|
||||||
this.rect.strokeColor = null;
|
|
||||||
} else {
|
|
||||||
this.rect.fillColor = null;
|
|
||||||
this.rect.strokeWidth = this.thickness;
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
}
|
|
||||||
this.onUpdateImage();
|
this.onUpdateImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,16 +142,10 @@ 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.strokeWidth = 0;
|
|
||||||
} else {
|
|
||||||
this.rect.strokeColor = this.color;
|
|
||||||
this.rect.strokeWidth = this.thickness;
|
|
||||||
}
|
|
||||||
this.rect.strokeJoin = 'round';
|
this.rect.strokeJoin = 'round';
|
||||||
this.rect.strokeScaling = false;
|
this.rect.strokeScaling = false;
|
||||||
this.rect.data = {zoomLevel: paper.view.zoom};
|
this.rect.data = {zoomLevel: paper.view.zoom};
|
||||||
|
this.styleRect();
|
||||||
|
|
||||||
if (event.modifiers.alt) {
|
if (event.modifiers.alt) {
|
||||||
this.rect.position = event.downPoint;
|
this.rect.position = event.downPoint;
|
||||||
|
@ -190,6 +176,7 @@ class RectTool extends paper.Tool {
|
||||||
// Hit testing does not work correctly unless the width and height are positive
|
// Hit testing does not work correctly unless the width and height are positive
|
||||||
this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height));
|
this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height));
|
||||||
this.rect.selected = true;
|
this.rect.selected = true;
|
||||||
|
this.styleRect();
|
||||||
this.setSelectedItems();
|
this.setSelectedItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,8 +275,24 @@ const drawEllipse = function (options, context) {
|
||||||
if (!matrix.isInvertible()) return false;
|
if (!matrix.isInvertible()) return false;
|
||||||
const inverse = matrix.clone().invert();
|
const inverse = matrix.clone().invert();
|
||||||
|
|
||||||
|
const isGradient = context.fillStyle instanceof CanvasGradient;
|
||||||
|
|
||||||
|
// If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas
|
||||||
|
// only where the shape appears. drawShearedEllipse draws some pixels twice, which would be a problem if the
|
||||||
|
// gradient fades to transparent as those pixels would end up looking more opaque. Instead, mask in the gradient.
|
||||||
|
// https://github.com/LLK/scratch-paint/issues/1152
|
||||||
|
// Outlines are drawn as a series of brush mark images and as such can't be drawn as gradients in the first place.
|
||||||
|
let origContext;
|
||||||
|
let tmpCanvas;
|
||||||
|
const {width: canvasWidth, height: canvasHeight} = context.canvas;
|
||||||
|
if (isGradient) {
|
||||||
|
tmpCanvas = createCanvas(canvasWidth, canvasHeight);
|
||||||
|
origContext = context;
|
||||||
|
context = tmpCanvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFilled) {
|
if (!isFilled) {
|
||||||
const brushMark = getBrushMark(thickness, context.fillStyle);
|
const brushMark = getBrushMark(thickness, isGradient ? 'black' : context.fillStyle);
|
||||||
const roundedUpRadius = Math.ceil(thickness / 2);
|
const roundedUpRadius = Math.ceil(thickness / 2);
|
||||||
drawFn = (x, y) => {
|
drawFn = (x, y) => {
|
||||||
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
||||||
|
@ -295,7 +311,7 @@ const drawEllipse = function (options, context) {
|
||||||
const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C)));
|
const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C)));
|
||||||
const slope = B / 2 / C;
|
const slope = B / 2 / C;
|
||||||
|
|
||||||
return drawShearedEllipse_({
|
const wasDrawn = drawShearedEllipse_({
|
||||||
centerX: positionX,
|
centerX: positionX,
|
||||||
centerY: positionY,
|
centerY: positionY,
|
||||||
radiusX: radiusA,
|
radiusX: radiusA,
|
||||||
|
@ -304,6 +320,17 @@ const drawEllipse = function (options, context) {
|
||||||
isFilled: isFilled,
|
isFilled: isFilled,
|
||||||
drawFn: drawFn
|
drawFn: drawFn
|
||||||
}, context);
|
}, context);
|
||||||
|
|
||||||
|
// Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the
|
||||||
|
// original canvas normally.
|
||||||
|
if (isGradient && wasDrawn) {
|
||||||
|
context.globalCompositeOperation = 'source-in';
|
||||||
|
context.fillStyle = origContext.fillStyle;
|
||||||
|
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
origContext.drawImage(tmpCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasDrawn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowBlank_ = function (imageData, width, y) {
|
const rowBlank_ = function (imageData, width, y) {
|
||||||
|
@ -658,6 +685,20 @@ const outlineRect = function (rect, thickness, context) {
|
||||||
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isGradient = context.fillStyle instanceof CanvasGradient;
|
||||||
|
|
||||||
|
// If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas
|
||||||
|
// only where the shape appears. Outlines are drawn as a series of brush mark images and as such can't be drawn as
|
||||||
|
// gradients.
|
||||||
|
let origContext;
|
||||||
|
let tmpCanvas;
|
||||||
|
const {width: canvasWidth, height: canvasHeight} = context.canvas;
|
||||||
|
if (isGradient) {
|
||||||
|
tmpCanvas = createCanvas(canvasWidth, canvasHeight);
|
||||||
|
origContext = context;
|
||||||
|
context = tmpCanvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2));
|
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 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 heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2));
|
||||||
|
@ -667,6 +708,16 @@ const outlineRect = function (rect, thickness, context) {
|
||||||
forEachLinePoint(startPoint, heightPoint, drawFn);
|
forEachLinePoint(startPoint, heightPoint, drawFn);
|
||||||
forEachLinePoint(endPoint, widthPoint, drawFn);
|
forEachLinePoint(endPoint, widthPoint, drawFn);
|
||||||
forEachLinePoint(endPoint, heightPoint, drawFn);
|
forEachLinePoint(endPoint, heightPoint, drawFn);
|
||||||
|
|
||||||
|
// Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the
|
||||||
|
// original canvas normally.
|
||||||
|
if (isGradient) {
|
||||||
|
context.globalCompositeOperation = 'source-in';
|
||||||
|
context.fillStyle = origContext.fillStyle;
|
||||||
|
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
origContext.drawImage(tmpCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const flipBitmapHorizontal = function (canvas) {
|
const flipBitmapHorizontal = function (canvas) {
|
||||||
|
@ -773,6 +824,62 @@ const commitSelectionToBitmap = function (selection, bitmap) {
|
||||||
commitArbitraryTransformation_(selection, bitmap);
|
commitArbitraryTransformation_(selection, bitmap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Paper.js color style (an item's fillColor or strokeColor) into a canvas-applicable color style.
|
||||||
|
* Note that a "color style" as applied to an item is different from a plain paper.Color or paper.Gradient.
|
||||||
|
* For instance, a gradient "color style" has origin and destination points whereas an unattached paper.Gradient
|
||||||
|
* does not.
|
||||||
|
* @param {paper.Color} color The color to convert to a canvas color/gradient
|
||||||
|
* @param {CanvasRenderingContext2D} context The rendering context on which the style will be used
|
||||||
|
* @returns {string|CanvasGradient} The canvas fill/stroke style.
|
||||||
|
*/
|
||||||
|
const _paperColorToCanvasStyle = function (color, context) {
|
||||||
|
if (!color) return null;
|
||||||
|
if (color.type === 'gradient') {
|
||||||
|
let canvasGradient;
|
||||||
|
const {origin, destination} = color;
|
||||||
|
if (color.gradient.radial) {
|
||||||
|
// Adapted from:
|
||||||
|
// https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L926-L935
|
||||||
|
let {highlight} = color;
|
||||||
|
const start = highlight || origin;
|
||||||
|
const radius = destination.getDistance(origin);
|
||||||
|
if (highlight) {
|
||||||
|
const vector = highlight.subtract(origin);
|
||||||
|
if (vector.getLength() > radius) {
|
||||||
|
// Paper ¯\_(ツ)_/¯
|
||||||
|
highlight = origin.add(vector.normalize(radius - 0.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvasGradient = context.createRadialGradient(
|
||||||
|
start.x, start.y,
|
||||||
|
0,
|
||||||
|
origin.x, origin.y,
|
||||||
|
radius
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
canvasGradient = context.createLinearGradient(
|
||||||
|
origin.x, origin.y,
|
||||||
|
destination.x, destination.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {stops} = color.gradient;
|
||||||
|
// Adapted from:
|
||||||
|
// https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L940-L950
|
||||||
|
for (let i = 0, len = stops.length; i < len; i++) {
|
||||||
|
const stop = stops[i];
|
||||||
|
const offset = stop.offset;
|
||||||
|
canvasGradient.addColorStop(
|
||||||
|
offset || i / (len - 1),
|
||||||
|
stop.color.toCSS()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return canvasGradient;
|
||||||
|
}
|
||||||
|
return color.toCSS();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {paper.Shape.Ellipse} oval Vector oval to convert
|
* @param {paper.Shape.Ellipse} oval Vector oval to convert
|
||||||
* @param {paper.Raster} bitmap raster to draw selection
|
* @param {paper.Raster} bitmap raster to draw selection
|
||||||
|
@ -784,12 +891,12 @@ const commitOvalToBitmap = function (oval, bitmap) {
|
||||||
const context = bitmap.getContext('2d');
|
const context = bitmap.getContext('2d');
|
||||||
const filled = oval.strokeWidth === 0;
|
const filled = oval.strokeWidth === 0;
|
||||||
|
|
||||||
const canvasColor = filled ? oval.fillColor : oval.strokeColor;
|
const canvasColor = _paperColorToCanvasStyle(filled ? oval.fillColor : oval.strokeColor, context);
|
||||||
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything,
|
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything
|
||||||
// and especially don't try calling `toCSS` on it
|
|
||||||
if (!canvasColor) return;
|
if (!canvasColor) return;
|
||||||
|
|
||||||
context.fillStyle = canvasColor.toCSS();
|
context.fillStyle = canvasColor;
|
||||||
|
|
||||||
const drew = drawEllipse({
|
const drew = drawEllipse({
|
||||||
position: oval.position,
|
position: oval.position,
|
||||||
radiusX,
|
radiusX,
|
||||||
|
@ -811,12 +918,12 @@ const commitRectToBitmap = function (rect, bitmap) {
|
||||||
const context = tmpCanvas.getContext('2d');
|
const context = tmpCanvas.getContext('2d');
|
||||||
const filled = rect.strokeWidth === 0;
|
const filled = rect.strokeWidth === 0;
|
||||||
|
|
||||||
const canvasColor = filled ? rect.fillColor : rect.strokeColor;
|
const canvasColor = _paperColorToCanvasStyle(filled ? rect.fillColor : rect.strokeColor, context);
|
||||||
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything,
|
// If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything
|
||||||
// and especially don't try calling `toCSS` on it
|
|
||||||
if (!canvasColor) return;
|
if (!canvasColor) return;
|
||||||
|
|
||||||
context.fillStyle = canvasColor.toCSS();
|
context.fillStyle = canvasColor;
|
||||||
|
|
||||||
if (filled) {
|
if (filled) {
|
||||||
fillRect(rect, context);
|
fillRect(rect, context);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,12 +35,17 @@ const getHoveredItem = function (event, hitOptions, subselect) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hoverGuide;
|
||||||
if (isBoundsItem(item)) {
|
if (isBoundsItem(item)) {
|
||||||
return hoverBounds(item);
|
hoverGuide = hoverBounds(item);
|
||||||
} else if (!subselect && isGroupChild(item)) {
|
} else if (!subselect && isGroupChild(item)) {
|
||||||
return hoverBounds(getRootItem(item));
|
hoverGuide = hoverBounds(getRootItem(item));
|
||||||
|
} else {
|
||||||
|
hoverGuide = hoverItem(item);
|
||||||
}
|
}
|
||||||
return hoverItem(item);
|
hoverGuide.data.hitResult = hitResult;
|
||||||
|
|
||||||
|
return hoverGuide;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -64,9 +64,10 @@ const getRotatedColor = function (firstColor) {
|
||||||
* @param {paper.Rectangle} bounds Bounds of the object
|
* @param {paper.Rectangle} bounds Bounds of the object
|
||||||
* @param {?paper.Point} [radialCenter] Where the center of a radial gradient should be, if the gradient is radial.
|
* @param {?paper.Point} [radialCenter] Where the center of a radial gradient should be, if the gradient is radial.
|
||||||
* Defaults to center of bounds.
|
* Defaults to center of bounds.
|
||||||
|
* @param {number} [minSize] The minimum width/height of the gradient object.
|
||||||
* @return {paper.Color} Color object with gradient, may be null or color string if the gradient type is solid
|
* @return {paper.Color} Color object with gradient, may be null or color string if the gradient type is solid
|
||||||
*/
|
*/
|
||||||
const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter) {
|
const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter, minSize) {
|
||||||
if (gradientType === GradientTypes.SOLID) return color1;
|
if (gradientType === GradientTypes.SOLID) return color1;
|
||||||
if (color1 === null) {
|
if (color1 === null) {
|
||||||
color1 = getColorStringForTransparent(color2);
|
color1 = getColorStringForTransparent(color2);
|
||||||
|
@ -74,15 +75,50 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
|
||||||
if (color2 === null) {
|
if (color2 === null) {
|
||||||
color2 = getColorStringForTransparent(color1);
|
color2 = getColorStringForTransparent(color1);
|
||||||
}
|
}
|
||||||
const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2;
|
|
||||||
const start = gradientType === GradientTypes.RADIAL ? (radialCenter || bounds.center) :
|
// Force gradients to have a minimum length. If the gradient start and end points are the same or very close
|
||||||
gradientType === GradientTypes.VERTICAL ? bounds.topCenter :
|
// (e.g. applying a vertical gradient to a perfectly horizontal line or vice versa), the gradient will not appear.
|
||||||
gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter :
|
if (!minSize) minSize = 1e-2;
|
||||||
null;
|
|
||||||
const end = gradientType === GradientTypes.RADIAL ? start.add(new paper.Point(halfLongestDimension, 0)) :
|
let start;
|
||||||
gradientType === GradientTypes.VERTICAL ? bounds.bottomCenter :
|
let end;
|
||||||
gradientType === GradientTypes.HORIZONTAL ? bounds.rightCenter :
|
switch (gradientType) {
|
||||||
null;
|
case GradientTypes.HORIZONTAL: {
|
||||||
|
// clone these points so that adding/subtracting doesn't affect actual bounds
|
||||||
|
start = bounds.leftCenter.clone();
|
||||||
|
end = bounds.rightCenter.clone();
|
||||||
|
|
||||||
|
const gradientSize = Math.abs(end.x - start.x);
|
||||||
|
if (gradientSize < minSize) {
|
||||||
|
const sizeDiff = (minSize - gradientSize) / 2;
|
||||||
|
end.x += sizeDiff;
|
||||||
|
start.x -= sizeDiff;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GradientTypes.VERTICAL: {
|
||||||
|
// clone these points so that adding/subtracting doesn't affect actual bounds
|
||||||
|
start = bounds.topCenter.clone();
|
||||||
|
end = bounds.bottomCenter.clone();
|
||||||
|
|
||||||
|
const gradientSize = Math.abs(end.y - start.y);
|
||||||
|
if (gradientSize < minSize) {
|
||||||
|
const sizeDiff = (minSize - gradientSize) / 2;
|
||||||
|
end.y += sizeDiff;
|
||||||
|
start.y -= sizeDiff;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case GradientTypes.RADIAL: {
|
||||||
|
const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2;
|
||||||
|
start = radialCenter || bounds.center;
|
||||||
|
end = start.add(new paper.Point(
|
||||||
|
Math.max(halfLongestDimension, minSize / 2),
|
||||||
|
0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
gradient: {
|
gradient: {
|
||||||
stops: [color1, color2],
|
stops: [color1, color2],
|
||||||
|
@ -100,7 +136,6 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
|
||||||
* @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color
|
* @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color
|
||||||
* picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected.
|
* picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected.
|
||||||
* When changing the color in this case, the solid gradient should override the existing gradient on the item.
|
* When changing the color in this case, the solid gradient should override the existing gradient on the item.
|
||||||
* @param {?boolean} bitmapMode True if the color is being set in bitmap mode
|
|
||||||
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
||||||
* @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.
|
||||||
|
@ -109,7 +144,6 @@ const applyColorToSelection = function (
|
||||||
colorString,
|
colorString,
|
||||||
colorIndex,
|
colorIndex,
|
||||||
isSolidGradient,
|
isSolidGradient,
|
||||||
bitmapMode,
|
|
||||||
applyToStroke,
|
applyToStroke,
|
||||||
textEditTargetId
|
textEditTargetId
|
||||||
) {
|
) {
|
||||||
|
@ -120,20 +154,6 @@ const applyColorToSelection = function (
|
||||||
item = item.parent;
|
item = item.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In bitmap mode, fill color applies to the stroke if there is a stroke
|
|
||||||
if (
|
|
||||||
bitmapMode &&
|
|
||||||
!applyToStroke &&
|
|
||||||
item.strokeColor !== null &&
|
|
||||||
item.strokeWidth
|
|
||||||
) {
|
|
||||||
if (!_colorMatch(item.strokeColor, colorString)) {
|
|
||||||
changed = true;
|
|
||||||
item.strokeColor = colorString;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
|
const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
|
||||||
const itemColor = item[itemColorProp];
|
const itemColor = item[itemColorProp];
|
||||||
|
|
||||||
|
@ -173,14 +193,11 @@ const applyColorToSelection = function (
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to swap gradient colors
|
* Called to swap gradient colors
|
||||||
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
|
|
||||||
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
||||||
* @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 swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) {
|
const swapColorsInSelection = function (applyToStroke, textEditTargetId) {
|
||||||
if (bitmapMode) return; // @todo
|
|
||||||
|
|
||||||
const items = _getColorStateListeners(textEditTargetId);
|
const items = _getColorStateListeners(textEditTargetId);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
@ -210,12 +227,11 @@ const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTarge
|
||||||
/**
|
/**
|
||||||
* Called when setting gradient type
|
* Called when setting gradient type
|
||||||
* @param {GradientType} gradientType gradient type
|
* @param {GradientType} gradientType gradient type
|
||||||
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
|
* @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
||||||
* @param {boolean} applyToStroke True if changing the selection's stroke, false if changing its fill.
|
|
||||||
* @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 applyGradientTypeToSelection = function (gradientType, bitmapMode, applyToStroke, textEditTargetId) {
|
const applyGradientTypeToSelection = function (gradientType, applyToStroke, 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) {
|
||||||
|
@ -255,10 +271,7 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyTo
|
||||||
itemColor2 = itemColor.gradient.stops[1].color.toCSS();
|
itemColor2 = itemColor.gradient.stops[1].color.toCSS();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bitmapMode) {
|
if (gradientType === GradientTypes.SOLID) {
|
||||||
// @todo Add when we apply gradients to selections in bitmap mode
|
|
||||||
continue;
|
|
||||||
} else if (gradientType === GradientTypes.SOLID) {
|
|
||||||
if (itemColor && itemColor.gradient) {
|
if (itemColor && itemColor.gradient) {
|
||||||
changed = true;
|
changed = true;
|
||||||
item[itemColorProp] = itemColor1;
|
item[itemColorProp] = itemColor1;
|
||||||
|
@ -301,7 +314,9 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyTo
|
||||||
itemColor1,
|
itemColor1,
|
||||||
itemColor2,
|
itemColor2,
|
||||||
gradientType,
|
gradientType,
|
||||||
item.bounds
|
item.bounds,
|
||||||
|
null, // radialCenter
|
||||||
|
item.strokeWidth
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,12 +423,18 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.strokeColor) {
|
if (item.strokeColor) {
|
||||||
|
|
||||||
if (item.strokeColor.type === 'gradient') {
|
if (item.strokeColor.type === 'gradient') {
|
||||||
const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient);
|
const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient);
|
||||||
const strokeColorString = primary;
|
|
||||||
|
let strokeColorString = primary;
|
||||||
const strokeColor2String = secondary;
|
const strokeColor2String = secondary;
|
||||||
const strokeGradientType = gradientType;
|
let strokeGradientType = gradientType;
|
||||||
|
|
||||||
|
// If the item's stroke width is 0, pretend the stroke color is null
|
||||||
|
if (!item.strokeWidth) {
|
||||||
|
strokeColorString = null;
|
||||||
|
strokeGradientType = GradientTypes.SOLID;
|
||||||
|
}
|
||||||
|
|
||||||
// Stroke color is fill color in bitmap
|
// Stroke color is fill color in bitmap
|
||||||
if (bitmapMode) {
|
if (bitmapMode) {
|
||||||
|
@ -516,10 +537,6 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Treat stroke gradients as MIXED
|
|
||||||
// TODO: remove this once stroke gradients are supported
|
|
||||||
if (selectionStrokeGradientType !== GradientTypes.SOLID) selectionStrokeColorString = MIXED;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fillColor: selectionFillColorString ? selectionFillColorString : null,
|
fillColor: selectionFillColorString ? selectionFillColorString : null,
|
||||||
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
|
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
|
||||||
|
@ -542,14 +559,6 @@ const styleBlob = function (path, options) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stylePath = function (path, strokeColor, strokeWidth) {
|
|
||||||
// Make sure a visible line is drawn
|
|
||||||
path.setStrokeColor(
|
|
||||||
(strokeColor === MIXED || strokeColor === null) ? 'black' : strokeColor);
|
|
||||||
path.setStrokeWidth(
|
|
||||||
strokeWidth === null || strokeWidth === 0 ? 1 : strokeWidth);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styleCursorPreview = function (path, options) {
|
const styleCursorPreview = function (path, options) {
|
||||||
if (options.isEraser) {
|
if (options.isEraser) {
|
||||||
path.fillColor = 'white';
|
path.fillColor = 'white';
|
||||||
|
@ -563,11 +572,26 @@ const styleCursorPreview = function (path, options) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: style using gradient?
|
|
||||||
const styleShape = function (path, options) {
|
const styleShape = function (path, options) {
|
||||||
path.fillColor = options.fillColor.primary;
|
for (const colorKey of ['fillColor', 'strokeColor']) {
|
||||||
path.strokeColor = options.strokeColor;
|
if (options[colorKey] === null) {
|
||||||
path.strokeWidth = options.strokeWidth;
|
path[colorKey] = null;
|
||||||
|
} else if (options[colorKey].gradientType === GradientTypes.SOLID) {
|
||||||
|
path[colorKey] = options[colorKey].primary;
|
||||||
|
} else {
|
||||||
|
const {primary, secondary, gradientType} = options[colorKey];
|
||||||
|
path[colorKey] = createGradientObject(
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
gradientType,
|
||||||
|
path.bounds,
|
||||||
|
null, // radialCenter
|
||||||
|
options.strokeWidth // minimum gradient size is stroke width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.hasOwnProperty('strokeWidth')) path.strokeWidth = options.strokeWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -580,7 +604,6 @@ export {
|
||||||
MIXED,
|
MIXED,
|
||||||
styleBlob,
|
styleBlob,
|
||||||
styleShape,
|
styleShape,
|
||||||
stylePath,
|
|
||||||
styleCursorPreview,
|
styleCursorPreview,
|
||||||
swapColorsInSelection
|
swapColorsInSelection
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,8 @@ class FillTool extends paper.Tool {
|
||||||
|
|
||||||
// The path that's being hovered over.
|
// The path that's being hovered over.
|
||||||
this.fillItem = null;
|
this.fillItem = null;
|
||||||
|
// The style property that we're applying the color to (either fill or stroke).
|
||||||
|
this.fillProperty = null;
|
||||||
// If we're hovering over a hole in a compound path, we can't just recolor it. This is the
|
// If we're hovering over a hole in a compound path, we can't just recolor it. This is the
|
||||||
// added item that's the same shape as the hole that's drawn over the hole when we fill a hole.
|
// added item that's the same shape as the hole that's drawn over the hole when we fill a hole.
|
||||||
this.addedFillItem = null;
|
this.addedFillItem = null;
|
||||||
|
@ -43,14 +45,17 @@ class FillTool extends paper.Tool {
|
||||||
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
|
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
segments: true,
|
segments: false,
|
||||||
stroke: true,
|
stroke: true,
|
||||||
curves: true,
|
curves: false,
|
||||||
fill: true,
|
fill: true,
|
||||||
guide: false,
|
guide: false,
|
||||||
match: function (hitResult) {
|
match: function (hitResult) {
|
||||||
|
// Allow fills to be hit only if the item has a fill already or the path is closed/nearly closed
|
||||||
|
const hitFill = hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item);
|
||||||
if (hitResult.item instanceof paper.Path &&
|
if (hitResult.item instanceof paper.Path &&
|
||||||
(hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item))) {
|
// Disallow hits that don't qualify for the fill criteria, but only if they're fills
|
||||||
|
(hitFill || hitResult.type !== 'fill')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (hitResult.item instanceof paper.PointText) {
|
if (hitResult.item instanceof paper.PointText) {
|
||||||
|
@ -58,6 +63,12 @@ class FillTool extends paper.Tool {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hitUnfilledPaths: true,
|
hitUnfilledPaths: true,
|
||||||
|
// If the color is transparent/none, then we need to be able to hit "invisible" outlines so that we don't
|
||||||
|
// prevent ourselves from hitting an outline when we make it transparent via the fill preview, causing it to
|
||||||
|
// flicker back and forth between transparent/its previous color as we hit it, then stop hitting it, etc.
|
||||||
|
// If the color *is* visible, then don't hit "invisible" outlines, since this would add visible outlines to
|
||||||
|
// non-outlined shapes when you hovered over where their outlines would be.
|
||||||
|
hitUnstrokedPaths: this.gradientType === GradientTypes.SOLID && this.fillColor === null,
|
||||||
tolerance: FillTool.TOLERANCE / paper.view.zoom
|
tolerance: FillTool.TOLERANCE / paper.view.zoom
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -89,8 +100,13 @@ class FillTool extends paper.Tool {
|
||||||
this.setHoveredItem(hoveredItem ? hoveredItem.id : null);
|
this.setHoveredItem(hoveredItem ? hoveredItem.id : null);
|
||||||
}
|
}
|
||||||
const hitItem = hoveredItem ? hoveredItem.data.origItem : null;
|
const hitItem = hoveredItem ? hoveredItem.data.origItem : null;
|
||||||
|
const hitType = hoveredItem ? hoveredItem.data.hitResult.type : null;
|
||||||
|
|
||||||
|
// The hit "target" changes if we switch items or switch between fill/outline on the same item
|
||||||
|
const hitTargetChanged = hitItem !== this.fillItem || hitType !== this.fillProperty;
|
||||||
|
|
||||||
// Still hitting the same thing
|
// Still hitting the same thing
|
||||||
if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) {
|
if (!hitTargetChanged) {
|
||||||
// Only radial gradient needs to be updated
|
// Only radial gradient needs to be updated
|
||||||
if (this.gradientType === GradientTypes.RADIAL) {
|
if (this.gradientType === GradientTypes.RADIAL) {
|
||||||
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
|
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
|
||||||
|
@ -106,14 +122,18 @@ class FillTool extends paper.Tool {
|
||||||
}
|
}
|
||||||
this.fillItemOrigColor = null;
|
this.fillItemOrigColor = null;
|
||||||
this.fillItem = null;
|
this.fillItem = null;
|
||||||
|
this.fillProperty = null;
|
||||||
}
|
}
|
||||||
if (hitItem) {
|
if (hitItem) {
|
||||||
this.fillItem = hitItem;
|
this.fillItem = hitItem;
|
||||||
this.fillItemOrigColor = hitItem.fillColor;
|
this.fillProperty = hitType;
|
||||||
if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0) { // hole
|
const colorProp = hitType === 'fill' ? 'fillColor' : 'strokeColor';
|
||||||
|
this.fillItemOrigColor = hitItem[colorProp];
|
||||||
|
if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0 && hitType === 'fill') { // hole
|
||||||
if (!this.fillColor) {
|
if (!this.fillColor) {
|
||||||
// Hole filled with transparent is no-op
|
// Hole filled with transparent is no-op
|
||||||
this.fillItem = null;
|
this.fillItem = null;
|
||||||
|
this.fillProperty = null;
|
||||||
this.fillItemOrigColor = null;
|
this.fillItemOrigColor = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -127,7 +147,7 @@ class FillTool extends paper.Tool {
|
||||||
expandBy(this.addedFillItem, .1);
|
expandBy(this.addedFillItem, .1);
|
||||||
this.addedFillItem.insertAbove(hitItem.parent);
|
this.addedFillItem.insertAbove(hitItem.parent);
|
||||||
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
|
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
|
||||||
this.fillItemOrigColor = hitItem.parent.fillColor;
|
this.fillItemOrigColor = hitItem.parent[colorProp];
|
||||||
}
|
}
|
||||||
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
|
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
|
||||||
}
|
}
|
||||||
|
@ -163,6 +183,7 @@ class FillTool extends paper.Tool {
|
||||||
|
|
||||||
this.clearHoveredItem();
|
this.clearHoveredItem();
|
||||||
this.fillItem = null;
|
this.fillItem = null;
|
||||||
|
this.fillProperty = null;
|
||||||
this.addedFillItem = null;
|
this.addedFillItem = null;
|
||||||
this.fillItemOrigColor = null;
|
this.fillItemOrigColor = null;
|
||||||
this.onUpdateImage();
|
this.onUpdateImage();
|
||||||
|
@ -178,12 +199,20 @@ class FillTool extends paper.Tool {
|
||||||
_setFillItemColor (color1, color2, gradientType, pointerLocation) {
|
_setFillItemColor (color1, color2, gradientType, pointerLocation) {
|
||||||
const item = this._getFillItem();
|
const item = this._getFillItem();
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
const colorProp = this.fillProperty === 'fill' ? 'fillColor' : 'strokeColor';
|
||||||
// Only create a gradient if specifically requested, else use color1 directly
|
// Only create a gradient if specifically requested, else use color1 directly
|
||||||
// This ensures we do not set a gradient by accident (see scratch-paint#830).
|
// This ensures we do not set a gradient by accident (see scratch-paint#830).
|
||||||
if (gradientType && gradientType !== GradientTypes.SOLID) {
|
if (gradientType && gradientType !== GradientTypes.SOLID) {
|
||||||
item.fillColor = createGradientObject(color1, color2, gradientType, item.bounds, pointerLocation);
|
item[colorProp] = createGradientObject(
|
||||||
|
color1,
|
||||||
|
color2,
|
||||||
|
gradientType,
|
||||||
|
item.bounds,
|
||||||
|
pointerLocation,
|
||||||
|
item.strokeWidth
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
item.fillColor = color1;
|
item[colorProp] = color1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_getFillItem () {
|
_getFillItem () {
|
||||||
|
@ -199,6 +228,7 @@ class FillTool extends paper.Tool {
|
||||||
this._setFillItemColor(this.fillItemOrigColor);
|
this._setFillItemColor(this.fillItemOrigColor);
|
||||||
this.fillItemOrigColor = null;
|
this.fillItemOrigColor = null;
|
||||||
this.fillItem = null;
|
this.fillItem = null;
|
||||||
|
this.fillProperty = null;
|
||||||
}
|
}
|
||||||
this.clearHoveredItem();
|
this.clearHoveredItem();
|
||||||
this.setHoveredItem = null;
|
this.setHoveredItem = null;
|
||||||
|
|
|
@ -111,6 +111,8 @@ class OvalTool extends paper.Tool {
|
||||||
} else {
|
} else {
|
||||||
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
styleShape(this.oval, this.colorState);
|
||||||
}
|
}
|
||||||
handleMouseMove (event) {
|
handleMouseMove (event) {
|
||||||
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
|
||||||
|
|
9
src/lib/color-style-proptype.js
Normal file
9
src/lib/color-style-proptype.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import {PropTypes} from 'prop-types';
|
||||||
|
|
||||||
|
import GradientTypes from './gradient-types';
|
||||||
|
|
||||||
|
export default PropTypes.shape({
|
||||||
|
primary: PropTypes.string,
|
||||||
|
secondary: PropTypes.string,
|
||||||
|
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired
|
||||||
|
});
|
|
@ -26,8 +26,23 @@ const VectorModes = keyMirror(vectorModesObj);
|
||||||
const BitmapModes = keyMirror(bitmapModesObj);
|
const BitmapModes = keyMirror(bitmapModesObj);
|
||||||
const Modes = keyMirror({...vectorModesObj, ...bitmapModesObj});
|
const Modes = keyMirror({...vectorModesObj, ...bitmapModesObj});
|
||||||
|
|
||||||
|
const GradientToolsModes = keyMirror({
|
||||||
|
FILL: null,
|
||||||
|
SELECT: null,
|
||||||
|
RESHAPE: null,
|
||||||
|
OVAL: null,
|
||||||
|
RECT: null,
|
||||||
|
LINE: null,
|
||||||
|
|
||||||
|
BIT_OVAL: null,
|
||||||
|
BIT_RECT: null,
|
||||||
|
BIT_SELECT: null,
|
||||||
|
BIT_FILL: null
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Modes as default,
|
Modes as default,
|
||||||
VectorModes,
|
VectorModes,
|
||||||
BitmapModes
|
BitmapModes,
|
||||||
|
GradientToolsModes
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
import eyeDropperReducer from './eye-dropper';
|
import eyeDropperReducer from './eye-dropper';
|
||||||
import fillColorReducer from './fill-style';
|
import fillColorReducer from './fill-style';
|
||||||
import strokeColorReducer from './stroke-color';
|
import strokeColorReducer from './stroke-style';
|
||||||
import strokeWidthReducer from './stroke-width';
|
import strokeWidthReducer from './stroke-width';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
|
|
@ -35,7 +35,7 @@ const changeFillColor2 = function (fillColor) {
|
||||||
const changeFillGradientType = function (gradientType) {
|
const changeFillGradientType = function (gradientType) {
|
||||||
return {
|
return {
|
||||||
type: CHANGE_FILL_GRADIENT_TYPE,
|
type: CHANGE_FILL_GRADIENT_TYPE,
|
||||||
gradientType: gradientType
|
gradientType
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import log from '../log/log';
|
|
||||||
import {CHANGE_SELECTED_ITEMS} from './selected-items';
|
|
||||||
import {CHANGE_STROKE_WIDTH} from './stroke-width';
|
|
||||||
import {getColorsFromSelection, MIXED} from '../helper/style-path';
|
|
||||||
|
|
||||||
const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-color/CHANGE_STROKE_COLOR';
|
|
||||||
const initialState = '#000';
|
|
||||||
// Matches hex colors
|
|
||||||
const regExp = /^#([0-9a-f]{3}){1,2}$/i;
|
|
||||||
|
|
||||||
const reducer = function (state, action) {
|
|
||||||
if (typeof state === 'undefined') state = initialState;
|
|
||||||
switch (action.type) {
|
|
||||||
case CHANGE_STROKE_WIDTH:
|
|
||||||
if (Math.max(0, action.strokeWidth) === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
case CHANGE_STROKE_COLOR:
|
|
||||||
if (!regExp.test(action.strokeColor) && action.strokeColor !== null && action.strokeColor !== MIXED) {
|
|
||||||
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return action.strokeColor;
|
|
||||||
case CHANGE_SELECTED_ITEMS:
|
|
||||||
// Don't change state if no selection
|
|
||||||
if (!action.selectedItems || !action.selectedItems.length) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
// Bitmap mode doesn't have stroke color
|
|
||||||
if (action.bitmapMode) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return getColorsFromSelection(action.selectedItems, action.bitmapMode).strokeColor;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Action creators ==================================
|
|
||||||
const changeStrokeColor = function (strokeColor) {
|
|
||||||
return {
|
|
||||||
type: CHANGE_STROKE_COLOR,
|
|
||||||
strokeColor: strokeColor
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
reducer as default,
|
|
||||||
changeStrokeColor
|
|
||||||
};
|
|
74
src/reducers/stroke-style.js
Normal file
74
src/reducers/stroke-style.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import makeColorStyleReducer from '../lib/make-color-style-reducer';
|
||||||
|
|
||||||
|
const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR';
|
||||||
|
const CHANGE_STROKE_COLOR_2 = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR_2';
|
||||||
|
const CHANGE_STROKE_GRADIENT_TYPE = 'scratch-paint/stroke-style/CHANGE_STROKE_GRADIENT_TYPE';
|
||||||
|
const CLEAR_STROKE_GRADIENT = 'scratch-paint/stroke-style/CLEAR_STROKE_GRADIENT';
|
||||||
|
const DEFAULT_COLOR = '#000000';
|
||||||
|
|
||||||
|
import {CHANGE_STROKE_WIDTH} from './stroke-width';
|
||||||
|
|
||||||
|
const reducer = makeColorStyleReducer({
|
||||||
|
changePrimaryColorAction: CHANGE_STROKE_COLOR,
|
||||||
|
changeSecondaryColorAction: CHANGE_STROKE_COLOR_2,
|
||||||
|
changeGradientTypeAction: CHANGE_STROKE_GRADIENT_TYPE,
|
||||||
|
clearGradientAction: CLEAR_STROKE_GRADIENT,
|
||||||
|
defaultColor: DEFAULT_COLOR,
|
||||||
|
selectionPrimaryColorKey: 'strokeColor',
|
||||||
|
selectionSecondaryColorKey: 'strokeColor2',
|
||||||
|
selectionGradientTypeKey: 'strokeGradientType'
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is mostly the same as the generated reducer, but with one piece of extra logic to set the color to null when the
|
||||||
|
// stroke width is set to 0.
|
||||||
|
// https://redux.js.org/recipes/structuring-reducers/reusing-reducer-logic
|
||||||
|
const strokeReducer = function (state, action) {
|
||||||
|
if (action.type === CHANGE_STROKE_WIDTH && Math.max(action.strokeWidth, 0) === 0) {
|
||||||
|
// TODO: this preserves the gradient type when you change the stroke width to 0.
|
||||||
|
// Alternatively, we could set gradientType to SOLID instead of setting secondary to null, but since
|
||||||
|
// the stroke width is automatically set to 0 as soon as a "null" color is detected (including a gradient for
|
||||||
|
// which both colors are null), that would change the gradient type back to solid if you selected null for both
|
||||||
|
// gradient colors.
|
||||||
|
return {...state, primary: null, secondary: null};
|
||||||
|
}
|
||||||
|
|
||||||
|
return reducer(state, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
const changeStrokeColor = function (strokeColor) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_STROKE_COLOR,
|
||||||
|
color: strokeColor
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeStrokeColor2 = function (strokeColor) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_STROKE_COLOR_2,
|
||||||
|
color: strokeColor
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeStrokeGradientType = function (gradientType) {
|
||||||
|
return {
|
||||||
|
type: CHANGE_STROKE_GRADIENT_TYPE,
|
||||||
|
gradientType
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearStrokeGradient = function () {
|
||||||
|
return {
|
||||||
|
type: CLEAR_STROKE_GRADIENT
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
strokeReducer as default,
|
||||||
|
changeStrokeColor,
|
||||||
|
changeStrokeColor2,
|
||||||
|
changeStrokeGradientType,
|
||||||
|
clearStrokeGradient,
|
||||||
|
DEFAULT_COLOR,
|
||||||
|
CHANGE_STROKE_GRADIENT_TYPE
|
||||||
|
};
|
86
test/unit/color-reducer.test.js
Normal file
86
test/unit/color-reducer.test.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/* eslint-env jest */
|
||||||
|
import fillColorReducer, {changeFillColor} from '../../src/reducers/fill-style';
|
||||||
|
import strokeColorReducer, {changeStrokeColor} from '../../src/reducers/stroke-style';
|
||||||
|
import {setSelectedItems} from '../../src/reducers/selected-items';
|
||||||
|
import {MIXED} from '../../src/helper/style-path';
|
||||||
|
import GradientTypes from '../../src/lib/gradient-types';
|
||||||
|
import {mockPaperRootItem} from '../__mocks__/paperMocks';
|
||||||
|
|
||||||
|
for (const [colorReducer, changeColor, colorProp] of [
|
||||||
|
[fillColorReducer, changeFillColor, 'fillColor'],
|
||||||
|
[strokeColorReducer, changeStrokeColor, 'strokeColor']
|
||||||
|
]) {
|
||||||
|
test('initialState', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
expect(colorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeColor', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
// 3 value hex code
|
||||||
|
let newColor = '#fff';
|
||||||
|
expect(colorReducer(defaultState /* state */, changeColor(newColor) /* action */).primary)
|
||||||
|
.toEqual(newColor);
|
||||||
|
expect(colorReducer({
|
||||||
|
primary: '#010',
|
||||||
|
secondary: null,
|
||||||
|
gradientType: GradientTypes.SOLID
|
||||||
|
} /* state */, changeColor(newColor) /* action */).primary)
|
||||||
|
.toEqual(newColor);
|
||||||
|
|
||||||
|
// 6 value hex code
|
||||||
|
newColor = '#facade';
|
||||||
|
expect(colorReducer(defaultState /* state */, changeColor(newColor) /* action */).primary)
|
||||||
|
.toEqual(newColor);
|
||||||
|
expect(colorReducer({
|
||||||
|
primary: '#010',
|
||||||
|
secondary: null,
|
||||||
|
gradientType: GradientTypes.SOLID
|
||||||
|
} /* state */, changeColor(newColor) /* action */).primary)
|
||||||
|
.toEqual(newColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeColorViaSelectedItems', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
const color1 = 6;
|
||||||
|
const color2 = null; // transparent
|
||||||
|
let selectedItems = [mockPaperRootItem({[colorProp]: color1, strokeWidth: 1})];
|
||||||
|
|
||||||
|
expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
||||||
|
.toEqual(color1);
|
||||||
|
selectedItems = [mockPaperRootItem({[colorProp]: color2, strokeWidth: 1})];
|
||||||
|
expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
||||||
|
.toEqual(color2);
|
||||||
|
selectedItems = [
|
||||||
|
mockPaperRootItem({[colorProp]: color1, strokeWidth: 1}),
|
||||||
|
mockPaperRootItem({[colorProp]: color2, strokeWidth: 1})
|
||||||
|
];
|
||||||
|
expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
||||||
|
.toEqual(MIXED);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalidChangeColor', () => {
|
||||||
|
const origState = {primary: '#fff', secondary: null, gradientType: GradientTypes.SOLID};
|
||||||
|
|
||||||
|
expect(colorReducer(origState /* state */, changeColor() /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#1') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#12') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#1234') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#12345') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('#1234567') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(colorReducer(origState /* state */, changeColor('invalid argument') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
/* eslint-env jest */
|
|
||||||
import fillColorReducer from '../../src/reducers/fill-style';
|
|
||||||
import {changeFillColor} from '../../src/reducers/fill-style';
|
|
||||||
import {setSelectedItems} from '../../src/reducers/selected-items';
|
|
||||||
import {MIXED} from '../../src/helper/style-path';
|
|
||||||
import GradientTypes from '../../src/lib/gradient-types';
|
|
||||||
import {mockPaperRootItem} from '../__mocks__/paperMocks';
|
|
||||||
|
|
||||||
test('initialState', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
expect(fillColorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('changeFillColor', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
// 3 value hex code
|
|
||||||
let newFillColor = '#fff';
|
|
||||||
expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary)
|
|
||||||
.toEqual(newFillColor);
|
|
||||||
expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary)
|
|
||||||
.toEqual(newFillColor);
|
|
||||||
|
|
||||||
// 6 value hex code
|
|
||||||
newFillColor = '#facade';
|
|
||||||
expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary)
|
|
||||||
.toEqual(newFillColor);
|
|
||||||
expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary)
|
|
||||||
.toEqual(newFillColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('changefillColorViaSelectedItems', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
const fillColor1 = 6;
|
|
||||||
const fillColor2 = null; // transparent
|
|
||||||
let selectedItems = [mockPaperRootItem({fillColor: fillColor1})];
|
|
||||||
expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
|
||||||
.toEqual(fillColor1);
|
|
||||||
selectedItems = [mockPaperRootItem({fillColor: fillColor2})];
|
|
||||||
expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
|
||||||
.toEqual(fillColor2);
|
|
||||||
selectedItems = [mockPaperRootItem({fillColor: fillColor1}), mockPaperRootItem({fillColor: fillColor2})];
|
|
||||||
expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary)
|
|
||||||
.toEqual(MIXED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('invalidChangeFillColor', () => {
|
|
||||||
const origState = {primary: '#fff', secondary: null, gradientType: GradientTypes.SOLID};
|
|
||||||
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor() /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#1') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#12') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#1234') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#12345') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('#1234567') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(fillColorReducer(origState /* state */, changeFillColor('invalid argument') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
});
|
|
|
@ -1,79 +0,0 @@
|
||||||
/* eslint-env jest */
|
|
||||||
import strokeColorReducer from '../../src/reducers/stroke-color';
|
|
||||||
import {changeStrokeColor} from '../../src/reducers/stroke-color';
|
|
||||||
import {setSelectedItems} from '../../src/reducers/selected-items';
|
|
||||||
import {MIXED} from '../../src/helper/style-path';
|
|
||||||
import {mockPaperRootItem} from '../__mocks__/paperMocks';
|
|
||||||
|
|
||||||
test('initialState', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('changeStrokeColor', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
// 3 value hex code
|
|
||||||
let newStrokeColor = '#fff';
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, changeStrokeColor(newStrokeColor) /* action */))
|
|
||||||
.toEqual(newStrokeColor);
|
|
||||||
expect(strokeColorReducer('#010' /* state */, changeStrokeColor(newStrokeColor) /* action */))
|
|
||||||
.toEqual(newStrokeColor);
|
|
||||||
|
|
||||||
// 6 value hex code
|
|
||||||
newStrokeColor = '#facade';
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, changeStrokeColor(newStrokeColor) /* action */))
|
|
||||||
.toEqual(newStrokeColor);
|
|
||||||
expect(strokeColorReducer('#010' /* state */, changeStrokeColor(newStrokeColor) /* action */))
|
|
||||||
.toEqual(newStrokeColor);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('changeStrokeColorViaSelectedItems', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
const strokeColor1 = 6;
|
|
||||||
const strokeColor2 = null; // transparent
|
|
||||||
let selectedItems = [mockPaperRootItem({strokeColor: strokeColor1, strokeWidth: 1})];
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
|
|
||||||
.toEqual(strokeColor1);
|
|
||||||
selectedItems = [mockPaperRootItem({strokeColor: strokeColor2, strokeWidth: 1})];
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
|
|
||||||
.toEqual(strokeColor2);
|
|
||||||
selectedItems = [mockPaperRootItem({strokeColor: strokeColor1, strokeWidth: 1}),
|
|
||||||
mockPaperRootItem({strokeColor: strokeColor2, strokeWidth: 1})];
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
|
|
||||||
.toEqual(MIXED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('showNoStrokeColorIfNoStrokeWidth', () => {
|
|
||||||
let defaultState;
|
|
||||||
|
|
||||||
let selectedItems = [mockPaperRootItem({strokeColor: '#fff', strokeWidth: null})];
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
|
|
||||||
.toEqual(null);
|
|
||||||
selectedItems = [mockPaperRootItem({strokeColor: '#fff', strokeWidth: 0})];
|
|
||||||
expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
|
|
||||||
.toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('invalidChangeStrokeColor', () => {
|
|
||||||
const origState = '#fff';
|
|
||||||
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor() /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#12') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1234') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#12345') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1234567') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
expect(strokeColorReducer(origState /* state */, changeStrokeColor('invalid argument') /* action */))
|
|
||||||
.toBe(origState);
|
|
||||||
});
|
|
Loading…
Reference in a new issue