diff --git a/src/components/fill-color-indicator.jsx b/src/components/color-indicator.jsx
similarity index 53%
rename from src/components/fill-color-indicator.jsx
rename to src/components/color-indicator.jsx
index 0a0fa0cf05..d04ea2bbf2 100644
--- a/src/components/fill-color-indicator.jsx
+++ b/src/components/color-indicator.jsx
@@ -1,7 +1,6 @@
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';
@@ -10,15 +9,7 @@ import Label from './forms/label.jsx';
import GradientTypes from '../lib/gradient-types';
-const messages = defineMessages({
- fill: {
- id: 'paint.paintEditor.fill',
- description: 'Label for the color picker for the fill color',
- defaultMessage: 'Fill'
- }
-});
-
-const FillColorIndicatorComponent = props => (
+const ColorIndicatorComponent = props => (
(
}
- isOpen={props.fillColorModalVisible}
+ isOpen={props.colorModalVisible}
preferPlace="below"
- onOuterAction={props.onCloseFillColor}
+ onOuterAction={props.onCloseColor}
>
-
);
-FillColorIndicatorComponent.propTypes = {
+ColorIndicatorComponent.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool.isRequired,
- fillColor: PropTypes.string,
- fillColor2: PropTypes.string,
- fillColorModalVisible: PropTypes.bool.isRequired,
+ color: PropTypes.string,
+ color2: PropTypes.string,
+ colorModalVisible: PropTypes.bool.isRequired,
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
- intl: intlShape,
- onChangeFillColor: PropTypes.func.isRequired,
+ label: PropTypes.string.isRequired,
+ onChangeColor: PropTypes.func.isRequired,
onChangeGradientType: PropTypes.func.isRequired,
- onCloseFillColor: PropTypes.func.isRequired,
- onOpenFillColor: PropTypes.func.isRequired,
+ onCloseColor: PropTypes.func.isRequired,
+ onOpenColor: PropTypes.func.isRequired,
onSwap: PropTypes.func.isRequired,
+ outline: PropTypes.bool.isRequired,
shouldShowGradientTools: PropTypes.bool.isRequired
};
-export default injectIntl(FillColorIndicatorComponent);
+export default ColorIndicatorComponent;
diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx
deleted file mode 100644
index 15c40d9bd7..0000000000
--- a/src/components/stroke-color-indicator.jsx
+++ /dev/null
@@ -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 => (
-
-
- }
- isOpen={props.strokeColorModalVisible}
- preferPlace="below"
- onOuterAction={props.onCloseStrokeColor}
- >
-
-
-
-
-
-);
-
-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);
diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx
index 0697821625..652395d8eb 100644
--- a/src/containers/bit-brush-mode.jsx
+++ b/src/containers/bit-brush-mode.jsx
@@ -5,11 +5,10 @@ import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
+import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
-import {clearGradient} from '../reducers/selection-gradient-type';
import BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx';
import BitBrushTool from '../helper/bit-tools/brush-tool';
@@ -94,7 +93,7 @@ BitBrushMode.propTypes = {
const mapStateToProps = state => ({
bitBrushSize: state.scratchPaint.bitBrushSize,
- color: state.scratchPaint.color.fillColor,
+ color: state.scratchPaint.color.fillColor.primary,
isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH
});
const mapDispatchToProps = dispatch => ({
@@ -102,7 +101,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
clearGradient: () => {
- dispatch(clearGradient());
+ dispatch(clearFillGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_BRUSH));
diff --git a/src/containers/bit-fill-mode.jsx b/src/containers/bit-fill-mode.jsx
index 4987bae8aa..85dbd11881 100644
--- a/src/containers/bit-fill-mode.jsx
+++ b/src/containers/bit-fill-mode.jsx
@@ -7,8 +7,7 @@ import GradientTypes from '../lib/gradient-types';
import FillModeComponent from '../components/bit-fill-mode/bit-fill-mode.jsx';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
-import {changeFillColor2} from '../reducers/fill-color-2';
+import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {changeGradientType} from '../reducers/fill-mode-gradient-type';
@@ -66,10 +65,10 @@ class BitFillMode extends React.Component {
this.props.onChangeFillColor(DEFAULT_COLOR, 0);
}
const gradientType = this.props.fillModeGradientType ?
- this.props.fillModeGradientType : this.props.selectModeGradientType;
+ this.props.fillModeGradientType : this.props.styleGradientType;
let color2 = this.props.color2;
- if (gradientType !== this.props.selectModeGradientType) {
- if (this.props.selectModeGradientType === GradientTypes.SOLID) {
+ if (gradientType !== this.props.styleGradientType) {
+ if (this.props.styleGradientType === GradientTypes.SOLID) {
color2 = getRotatedColor(color);
this.props.onChangeFillColor(color2, 1);
}
@@ -105,20 +104,20 @@ BitFillMode.propTypes = {
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
color2: PropTypes.string,
+ styleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)),
handleMouseDown: PropTypes.func.isRequired,
isFillModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
- onUpdateImage: PropTypes.func.isRequired,
- selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired
+ onUpdateImage: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type
- color: state.scratchPaint.color.fillColor,
- color2: state.scratchPaint.color.fillColor2,
- isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL,
- selectModeGradientType: state.scratchPaint.color.gradientType
+ color: state.scratchPaint.color.fillColor.primary,
+ color2: state.scratchPaint.color.fillColor.secondary,
+ styleGradientType: state.scratchPaint.color.fillColor.gradientType,
+ isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL
});
const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
diff --git a/src/containers/bit-line-mode.jsx b/src/containers/bit-line-mode.jsx
index c2a9e9bf59..5b57cdafb1 100644
--- a/src/containers/bit-line-mode.jsx
+++ b/src/containers/bit-line-mode.jsx
@@ -5,11 +5,10 @@ import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
+import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
-import {clearGradient} from '../reducers/selection-gradient-type';
import BitLineModeComponent from '../components/bit-line-mode/bit-line-mode.jsx';
import BitLineTool from '../helper/bit-tools/line-tool';
@@ -94,7 +93,7 @@ BitLineMode.propTypes = {
const mapStateToProps = state => ({
bitBrushSize: state.scratchPaint.bitBrushSize,
- color: state.scratchPaint.color.fillColor,
+ color: state.scratchPaint.color.fillColor.primary,
isBitLineModeActive: state.scratchPaint.mode === Modes.BIT_LINE
});
const mapDispatchToProps = dispatch => ({
@@ -102,7 +101,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
clearGradient: () => {
- dispatch(clearGradient());
+ dispatch(clearFillGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_LINE));
diff --git a/src/containers/bit-oval-mode.jsx b/src/containers/bit-oval-mode.jsx
index 0a7f356bf9..bf3ae380c1 100644
--- a/src/containers/bit-oval-mode.jsx
+++ b/src/containers/bit-oval-mode.jsx
@@ -4,12 +4,12 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
+import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
@@ -62,9 +62,8 @@ class BitOvalMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
- this.props.clearGradient();
// 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) {
this.props.onChangeFillColor(DEFAULT_COLOR);
}
@@ -95,9 +94,8 @@ class BitOvalMode extends React.Component {
}
BitOvalMode.propTypes = {
- clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
- color: PropTypes.string,
+ color: ColorStyleProptype,
filled: PropTypes.bool,
handleMouseDown: PropTypes.func.isRequired,
isOvalModeActive: PropTypes.bool.isRequired,
@@ -122,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
- clearGradient: () => {
- dispatch(clearGradient());
- },
setCursor: cursorString => {
dispatch(setCursor(cursorString));
},
diff --git a/src/containers/bit-rect-mode.jsx b/src/containers/bit-rect-mode.jsx
index 97c1f5cb98..6263953a6b 100644
--- a/src/containers/bit-rect-mode.jsx
+++ b/src/containers/bit-rect-mode.jsx
@@ -4,12 +4,12 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
+import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
@@ -62,9 +62,8 @@ class BitRectMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
- this.props.clearGradient();
// 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) {
this.props.onChangeFillColor(DEFAULT_COLOR);
}
@@ -95,9 +94,8 @@ class BitRectMode extends React.Component {
}
BitRectMode.propTypes = {
- clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
- color: PropTypes.string,
+ color: ColorStyleProptype,
filled: PropTypes.bool,
handleMouseDown: PropTypes.func.isRequired,
isRectModeActive: PropTypes.bool.isRequired,
@@ -122,9 +120,6 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
- clearGradient: () => {
- dispatch(clearGradient());
- },
setCursor: cursorString => {
dispatch(setCursor(cursorString));
},
diff --git a/src/containers/bit-select-mode.jsx b/src/containers/bit-select-mode.jsx
index 189799f5fa..7f049b1416 100644
--- a/src/containers/bit-select-mode.jsx
+++ b/src/containers/bit-select-mode.jsx
@@ -5,9 +5,9 @@ import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import {clearFillGradient} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {getSelectedLeafItems} from '../helper/selection';
@@ -88,7 +88,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
clearGradient: () => {
- dispatch(clearGradient());
+ dispatch(clearFillGradient());
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx
index f47a71a96b..f1936ad617 100644
--- a/src/containers/brush-mode.jsx
+++ b/src/containers/brush-mode.jsx
@@ -3,13 +3,13 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import Blobbiness from '../helper/blob-tools/blob';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
+import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection} from '../helper/selection';
import BrushModeComponent from '../components/brush-mode/brush-mode.jsx';
@@ -35,9 +35,12 @@ class BrushMode extends React.Component {
} else if (!nextProps.isBrushModeActive && this.props.isBrushModeActive) {
this.deactivateTool();
} else if (nextProps.isBrushModeActive && this.props.isBrushModeActive) {
+ const {fillColor, strokeColor, strokeWidth} = nextProps.colorState;
this.blob.setOptions({
isEraser: false,
- ...nextProps.colorState,
+ fillColor: fillColor.primary,
+ strokeColor: strokeColor.primary,
+ strokeWidth,
...nextProps.brushModeState
});
}
@@ -56,7 +59,7 @@ class BrushMode extends React.Component {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
- const {fillColor} = this.props.colorState;
+ const fillColor = this.props.colorState.fillColor.primary;
if (fillColor === MIXED || fillColor === null) {
this.props.onChangeFillColor(DEFAULT_COLOR);
}
@@ -86,8 +89,8 @@ BrushMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
- fillColor: PropTypes.string,
- strokeColor: PropTypes.string,
+ fillColor: ColorStyleProptype,
+ strokeColor: ColorStyleProptype,
strokeWidth: PropTypes.number
}).isRequired,
handleMouseDown: PropTypes.func.isRequired,
@@ -106,7 +109,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSelectedItems());
},
clearGradient: () => {
- dispatch(clearGradient());
+ dispatch(clearFillGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BRUSH));
diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx
new file mode 100644
index 0000000000..a6cabe50e7
--- /dev/null
+++ b/src/containers/color-indicator.jsx
@@ -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 (
+
+ );
+ }
+ }
+
+ 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;
diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx
index 757e84737f..fc86f31f72 100644
--- a/src/containers/fill-color-indicator.jsx
+++ b/src/containers/fill-color-indicator.jsx
@@ -1,138 +1,39 @@
import {connect} from 'react-redux';
-import PropTypes from 'prop-types';
-import React from 'react';
-import bindAll from 'lodash.bindall';
-import parseColor from 'parse-color';
+import {defineMessages} from 'react-intl';
import {changeColorIndex} from '../reducers/color-index';
-import {changeFillColor} from '../reducers/fill-color';
-import {changeFillColor2} from '../reducers/fill-color-2';
+import {changeFillColor, changeFillColor2} from '../reducers/fill-style';
import {changeGradientType} from '../reducers/fill-mode-gradient-type';
import {openFillColor, closeFillColor} from '../reducers/modals';
import {getSelectedLeafItems} from '../helper/selection';
import {setSelectedItems} from '../reducers/selected-items';
-import Modes from '../lib/modes';
-import Formats from '../lib/format';
+import Modes, {GradientToolsModes} from '../lib/modes';
import {isBitmap} from '../lib/format';
-import GradientTypes from '../lib/gradient-types';
-import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
-import {applyFillColorToSelection,
- applyGradientTypeToSelection,
- getRotatedColor,
- swapColorsInSelection,
- MIXED} from '../helper/style-path';
+import makeColorIndicator from './color-indicator.jsx';
-class FillColorIndicator extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleChangeFillColor',
- 'handleChangeGradientType',
- 'handleCloseFillColor',
- 'handleSwap'
- ]);
-
- // Flag to track whether an svg-update-worthy change has been made
- 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 = applyFillColorToSelection(
- newColor,
- this.props.colorIndex,
- this.props.gradientType === GradientTypes.SOLID,
- isBitmap(this.props.format),
- 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),
- 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);
+const messages = defineMessages({
+ label: {
+ id: 'paint.paintEditor.fill',
+ description: 'Label for the color picker for the fill color',
+ defaultMessage: 'Fill'
}
- 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),
- 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 (
-
- );
- }
-}
+const FillColorIndicator = makeColorIndicator(messages.label, false);
const mapStateToProps = state => ({
colorIndex: state.scratchPaint.fillMode.colorIndex,
disabled: state.scratchPaint.mode === Modes.LINE,
- fillColor: state.scratchPaint.color.fillColor,
- fillColor2: state.scratchPaint.color.fillColor2,
- fillColorModalVisible: state.scratchPaint.modals.fillColor,
+ color: state.scratchPaint.color.fillColor.primary,
+ color2: state.scratchPaint.color.fillColor.secondary,
+ colorModalVisible: state.scratchPaint.modals.fillColor,
+ fillBitmapShapes: state.scratchPaint.fillBitmapShapes,
format: state.scratchPaint.format,
- gradientType: state.scratchPaint.color.gradientType,
+ gradientType: state.scratchPaint.color.fillColor.gradientType,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
mode: state.scratchPaint.mode,
- shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT ||
- state.scratchPaint.mode === Modes.RESHAPE ||
- state.scratchPaint.mode === Modes.FILL ||
- state.scratchPaint.mode === Modes.BIT_SELECT ||
- state.scratchPaint.mode === Modes.BIT_FILL,
+ shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes,
textEditTarget: state.scratchPaint.textEditTarget
});
@@ -140,17 +41,17 @@ const mapDispatchToProps = dispatch => ({
onChangeColorIndex: index => {
dispatch(changeColorIndex(index));
},
- onChangeFillColor: (fillColor, index) => {
+ onChangeColor: (fillColor, index) => {
if (index === 0) {
dispatch(changeFillColor(fillColor));
} else if (index === 1) {
dispatch(changeFillColor2(fillColor));
}
},
- onOpenFillColor: () => {
+ onOpenColor: () => {
dispatch(openFillColor());
},
- onCloseFillColor: () => {
+ onCloseColor: () => {
dispatch(closeFillColor());
},
onChangeGradientType: gradientType => {
@@ -161,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(
mapStateToProps,
mapDispatchToProps
diff --git a/src/containers/fill-mode.jsx b/src/containers/fill-mode.jsx
index 1abc9c3709..63d10815b2 100644
--- a/src/containers/fill-mode.jsx
+++ b/src/containers/fill-mode.jsx
@@ -7,8 +7,7 @@ import GradientTypes from '../lib/gradient-types';
import FillTool from '../helper/tools/fill-tool';
import {getRotatedColor, MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
-import {changeFillColor2} from '../reducers/fill-color-2';
+import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
@@ -70,10 +69,10 @@ class FillMode extends React.Component {
this.props.onChangeFillColor(DEFAULT_COLOR, 0);
}
const gradientType = this.props.fillModeGradientType ?
- this.props.fillModeGradientType : this.props.selectModeGradientType;
+ this.props.fillModeGradientType : this.props.fillStyleGradientType;
let fillColor2 = this.props.fillColor2;
- if (gradientType !== this.props.selectModeGradientType) {
- if (this.props.selectModeGradientType === GradientTypes.SOLID) {
+ if (gradientType !== this.props.fillStyleGradientType) {
+ if (this.props.fillStyleGradientType === GradientTypes.SOLID) {
fillColor2 = getRotatedColor(fillColor);
this.props.onChangeFillColor(fillColor2, 1);
}
@@ -115,23 +114,23 @@ FillMode.propTypes = {
clearSelectedItems: PropTypes.func.isRequired,
fillColor: PropTypes.string,
fillColor2: PropTypes.string,
+ fillStyleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)),
handleMouseDown: PropTypes.func.isRequired,
hoveredItemId: PropTypes.number,
isFillModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
- selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
setHoveredItem: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type
- fillColor: state.scratchPaint.color.fillColor,
- fillColor2: state.scratchPaint.color.fillColor2,
+ fillColor: state.scratchPaint.color.fillColor.primary,
+ fillColor2: state.scratchPaint.color.fillColor.secondary,
+ fillStyleGradientType: state.scratchPaint.color.fillColor.gradientType, // Selected item(s)' gradient type
hoveredItemId: state.scratchPaint.hoveredItemId,
- isFillModeActive: state.scratchPaint.mode === Modes.FILL,
- selectModeGradientType: state.scratchPaint.color.gradientType
+ isFillModeActive: state.scratchPaint.mode === Modes.FILL
});
const mapDispatchToProps = dispatch => ({
setHoveredItem: hoveredItemId => {
diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx
index ebe011e51f..4a67111073 100644
--- a/src/containers/line-mode.jsx
+++ b/src/containers/line-mode.jsx
@@ -4,11 +4,12 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {clearSelection} from '../helper/selection';
import {endPointHit, touching} from '../helper/snapping';
import {drawHitPoint, removeHitPoint} from '../helper/guides';
-import {stylePath} from '../helper/style-path';
-import {changeStrokeColor} from '../reducers/stroke-color';
+import {styleShape} from '../helper/style-path';
+import {changeStrokeColor, clearStrokeGradient} from '../reducers/stroke-style';
import {changeStrokeWidth} from '../reducers/stroke-width';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
@@ -58,9 +59,10 @@ class LineMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
+ this.props.clearGradient();
// 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) {
this.props.onChangeStrokeColor(LineMode.DEFAULT_COLOR);
}
@@ -101,7 +103,11 @@ class LineMode extends React.Component {
this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE);
if (this.hitResult) {
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) {
this.path.reverse();
}
@@ -114,7 +120,11 @@ class LineMode extends React.Component {
if (!this.path) {
this.path = new paper.Path();
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); // Add second point, which is what will move when dragged
@@ -186,6 +196,12 @@ class LineMode extends React.Component {
} else {
this.path.lastSegment.point = endPoint;
}
+
+ styleShape(this.path, {
+ fillColor: null,
+ strokeColor: this.props.colorState.strokeColor,
+ strokeWidth: this.props.colorState.strokeWidth
+ });
}
onMouseUp (event) {
if (event.event.button > 0 || !this.active) return; // only first mouse button
@@ -225,6 +241,12 @@ class LineMode extends React.Component {
this.hitResult = null;
}
+ styleShape(this.path, {
+ fillColor: null,
+ strokeColor: this.props.colorState.strokeColor,
+ strokeWidth: this.props.colorState.strokeWidth
+ });
+
if (this.path) {
this.props.onUpdateImage();
this.path = null;
@@ -253,10 +275,11 @@ class LineMode extends React.Component {
}
LineMode.propTypes = {
+ clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
- fillColor: PropTypes.string,
- strokeColor: PropTypes.string,
+ fillColor: ColorStyleProptype,
+ strokeColor: ColorStyleProptype,
strokeWidth: PropTypes.number
}).isRequired,
handleMouseDown: PropTypes.func.isRequired,
@@ -271,6 +294,9 @@ const mapStateToProps = state => ({
isLineModeActive: state.scratchPaint.mode === Modes.LINE
});
const mapDispatchToProps = dispatch => ({
+ clearGradient: () => {
+ dispatch(clearStrokeGradient());
+ },
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx
index b525ec9a1c..03678acd7c 100644
--- a/src/containers/oval-mode.jsx
+++ b/src/containers/oval-mode.jsx
@@ -4,13 +4,13 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
-import {changeStrokeColor} from '../reducers/stroke-color';
+import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
+import {changeStrokeColor} from '../reducers/stroke-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
@@ -54,11 +54,12 @@ class OvalMode extends React.Component {
}
activateTool () {
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 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.
- const {fillColor, strokeColor, strokeWidth} = this.props.colorState;
+ const {strokeWidth} = this.props.colorState;
+ const fillColor = this.props.colorState.fillColor.primary;
+ const strokeColor = this.props.colorState.strokeColor.primary;
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
const strokeColorPresent =
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
@@ -95,11 +96,10 @@ class OvalMode extends React.Component {
}
OvalMode.propTypes = {
- clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
- fillColor: PropTypes.string,
- strokeColor: PropTypes.string,
+ fillColor: ColorStyleProptype,
+ strokeColor: ColorStyleProptype,
strokeWidth: PropTypes.number
}).isRequired,
handleMouseDown: PropTypes.func.isRequired,
@@ -121,9 +121,6 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
- clearGradient: () => {
- dispatch(clearGradient());
- },
setCursor: cursorString => {
dispatch(setCursor(cursorString));
},
diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx
index 9675b7f910..df8c8a3efc 100644
--- a/src/containers/rect-mode.jsx
+++ b/src/containers/rect-mode.jsx
@@ -4,13 +4,13 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {MIXED} from '../helper/style-path';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
-import {changeStrokeColor} from '../reducers/stroke-color';
+import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style';
+import {changeStrokeColor} from '../reducers/stroke-style';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
@@ -54,11 +54,12 @@ class RectMode extends React.Component {
}
activateTool () {
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 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.
- const {fillColor, strokeColor, strokeWidth} = this.props.colorState;
+ const {strokeWidth} = this.props.colorState;
+ const fillColor = this.props.colorState.fillColor.primary;
+ const strokeColor = this.props.colorState.strokeColor.primary;
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
const strokeColorPresent =
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
@@ -95,11 +96,10 @@ class RectMode extends React.Component {
}
RectMode.propTypes = {
- clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
- fillColor: PropTypes.string,
- strokeColor: PropTypes.string,
+ fillColor: ColorStyleProptype,
+ strokeColor: ColorStyleProptype,
strokeWidth: PropTypes.number
}).isRequired,
handleMouseDown: PropTypes.func.isRequired,
@@ -121,9 +121,6 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
- clearGradient: () => {
- dispatch(clearGradient());
- },
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},
diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx
index ea78e0cb83..b912bcc3dd 100644
--- a/src/containers/stroke-color-indicator.jsx
+++ b/src/containers/stroke-color-indicator.jsx
@@ -1,104 +1,73 @@
import {connect} from 'react-redux';
-import PropTypes from 'prop-types';
-import React from 'react';
-import bindAll from 'lodash.bindall';
-import {changeStrokeColor} from '../reducers/stroke-color';
+import {defineMessages} from 'react-intl';
+
+import {changeColorIndex} from '../reducers/color-index';
+import {changeStrokeColor, changeStrokeColor2} from '../reducers/stroke-style';
import {changeStrokeWidth} from '../reducers/stroke-width';
+import {changeStrokeGradientType} from '../reducers/stroke-style';
import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
-import Modes from '../lib/modes';
-import Formats from '../lib/format';
+import {getSelectedLeafItems} from '../helper/selection';
+import {setSelectedItems} from '../reducers/selected-items';
+import Modes, {GradientToolsModes} from '../lib/modes';
import {isBitmap} from '../lib/format';
-import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
-import {applyStrokeColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path';
-
-class StrokeColorIndicator extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleChangeStrokeColor',
- 'handleCloseStrokeColor'
- ]);
+import makeColorIndicator from './color-indicator.jsx';
- // Flag to track whether an svg-update-worthy change has been made
- 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 =
- applyStrokeColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget) ||
- this._hasChanged;
- this.props.onChangeStrokeColor(newColor);
- }
- handleCloseStrokeColor () {
- if (!this.props.isEyeDropping) {
- this.props.onCloseStrokeColor();
- }
+const messages = defineMessages({
+ label: {
+ id: 'paint.paintEditor.stroke',
+ description: 'Label for the color picker for the outline color',
+ defaultMessage: 'Outline'
}
- render () {
- return (
-
- );
- }
-}
+});
+
+const StrokeColorIndicator = makeColorIndicator(messages.label, true);
const mapStateToProps = state => ({
+ colorIndex: state.scratchPaint.fillMode.colorIndex,
disabled: state.scratchPaint.mode === Modes.BRUSH ||
state.scratchPaint.mode === Modes.TEXT ||
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,
+ gradientType: state.scratchPaint.color.strokeColor.gradientType,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
- strokeColor: state.scratchPaint.color.strokeColor,
- strokeColorModalVisible: state.scratchPaint.modals.strokeColor,
+ mode: state.scratchPaint.mode,
+ shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes,
textEditTarget: state.scratchPaint.textEditTarget
});
const mapDispatchToProps = dispatch => ({
- onChangeStrokeColor: strokeColor => {
- dispatch(changeStrokeColor(strokeColor));
+ onChangeColorIndex: index => {
+ dispatch(changeColorIndex(index));
+ },
+ onChangeColor: (strokeColor, index) => {
+ if (index === 0) {
+ dispatch(changeStrokeColor(strokeColor));
+ } else if (index === 1) {
+ dispatch(changeStrokeColor2(strokeColor));
+ }
},
onChangeStrokeWidth: strokeWidth => {
dispatch(changeStrokeWidth(strokeWidth));
},
- onOpenStrokeColor: () => {
+ onOpenColor: () => {
dispatch(openStrokeColor());
},
- onCloseStrokeColor: () => {
+ onCloseColor: () => {
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(
mapStateToProps,
mapDispatchToProps
diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx
index d224f44d59..ed27ff967e 100644
--- a/src/containers/stroke-width-indicator.jsx
+++ b/src/containers/stroke-width-indicator.jsx
@@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
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 StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx';
import {getSelectedLeafItems} from '../helper/selection';
-import {applyStrokeColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED}
+import {applyColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED}
from '../helper/style-path';
+import GradientTypes from '../lib/gradient-types';
import Modes from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
@@ -23,15 +24,31 @@ class StrokeWidthIndicator extends React.Component {
handleChangeStrokeWidth (newWidth) {
let changed = applyStrokeWidthToSelection(newWidth, this.props.textEditTarget);
if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) {
- let currentColor = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format)).strokeColor;
- if (currentColor === null) {
- changed = applyStrokeColorToSelection('#000', isBitmap(this.props.format), this.props.textEditTarget) ||
+ const currentColorState = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format));
+
+ // 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(
+ '#000',
+ 0, // colorIndex,
+ true, // isSolidGradient
+ true, // applyToStroke
+ this.props.textEditTarget) ||
changed;
- currentColor = '#000';
- } else if (currentColor !== MIXED) {
- currentColor = parseColor(currentColor).hex;
+ // If there's no previous stroke color, default to solid black
+ this.props.onChangeStrokeGradientType(GradientTypes.SOLID);
+ 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);
if (changed) this.props.onUpdateImage();
@@ -59,6 +76,12 @@ const mapDispatchToProps = dispatch => ({
onChangeStrokeColor: strokeColor => {
dispatch(changeStrokeColor(strokeColor));
},
+ onChangeStrokeColor2: strokeColor => {
+ dispatch(changeStrokeColor2(strokeColor));
+ },
+ onChangeStrokeGradientType: strokeColor => {
+ dispatch(changeStrokeGradientType(strokeColor));
+ },
onChangeStrokeWidth: strokeWidth => {
dispatch(changeStrokeWidth(strokeWidth));
}
@@ -68,6 +91,8 @@ StrokeWidthIndicator.propTypes = {
disabled: PropTypes.bool.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)),
onChangeStrokeColor: PropTypes.func.isRequired,
+ onChangeStrokeColor2: PropTypes.func.isRequired,
+ onChangeStrokeGradientType: PropTypes.func.isRequired,
onChangeStrokeWidth: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
strokeWidth: PropTypes.number,
diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx
index 15a10bddf3..ce270cd5aa 100644
--- a/src/containers/text-mode.jsx
+++ b/src/containers/text-mode.jsx
@@ -5,15 +5,15 @@ import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Fonts from '../lib/fonts';
import Modes from '../lib/modes';
+import ColorStyleProptype from '../lib/color-style-proptype';
import {MIXED} from '../helper/style-path';
import {changeFont} from '../reducers/font';
-import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
-import {changeStrokeColor} from '../reducers/stroke-color';
+import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style';
+import {changeStrokeColor} from '../reducers/stroke-style';
import {changeMode} from '../reducers/modes';
import {setTextEditTarget} from '../reducers/text-edit-target';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
-import {clearGradient} from '../reducers/selection-gradient-type';
import {setCursor} from '../reducers/cursor';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
@@ -82,7 +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 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.
- const {fillColor, strokeColor, strokeWidth} = nextProps.colorState;
+ const {strokeWidth} = nextProps.colorState;
+ const fillColor = nextProps.colorState.fillColor.primary;
+ const strokeColor = nextProps.colorState.strokeColor.primary;
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
const strokeColorPresent = nextProps.isBitmap ? false :
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
@@ -143,8 +145,8 @@ TextMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
- fillColor: PropTypes.string,
- strokeColor: PropTypes.string,
+ fillColor: ColorStyleProptype,
+ strokeColor: ColorStyleProptype,
strokeWidth: PropTypes.number
}).isRequired,
font: PropTypes.string,
@@ -184,7 +186,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
dispatch(clearSelectedItems());
},
clearGradient: () => {
- dispatch(clearGradient());
+ dispatch(clearFillGradient());
},
handleChangeModeBitText: () => {
dispatch(changeMode(Modes.BIT_TEXT));
diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js
index 2afdba68bd..f13f595b69 100644
--- a/src/helper/bit-tools/oval-tool.js
+++ b/src/helper/bit-tools/oval-tool.js
@@ -1,5 +1,6 @@
import paper from '@scratch/paper';
import Modes from '../../lib/modes';
+import {styleShape} from '../style-path';
import {commitOvalToBitmap} from '../bitmap';
import {getRaster} from '../layer';
import {clearSelection} from '../selection';
@@ -76,36 +77,29 @@ class OvalTool extends paper.Tool {
this.thickness = this.oval.strokeWidth;
}
this.filled = this.oval.strokeWidth === 0;
- const color = this.filled ? this.oval.fillColor : this.oval.strokeColor;
- this.color = color ? color.toCSS() : null;
+ // We don't need to set our color from the selected oval's color because the color state reducers will
+ // do that for us every time the selection changes.
} else if (this.oval && this.oval.isInserted() && !this.oval.selected) {
// Oval got deselected
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) {
this.color = color;
- if (this.oval) {
- if (this.filled) {
- this.oval.fillColor = this.color;
- } else {
- this.oval.strokeColor = this.color;
- }
- }
+ if (this.oval) this.styleOval();
}
setFilled (filled) {
if (this.filled === filled) return;
this.filled = filled;
if (this.oval && this.oval.isInserted()) {
- if (this.filled) {
- 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.styleOval();
this.onUpdateImage();
}
}
@@ -131,23 +125,12 @@ class OvalTool extends paper.Tool {
this.isBoundingBoxMode = false;
clearSelection(this.clearSelectedItems);
this.commitOval();
- if (this.filled) {
- this.oval = new paper.Shape.Ellipse({
- fillColor: this.color,
- point: event.downPoint,
- strokeWidth: 0,
- strokeScaling: false,
- size: 0
- });
- } else {
- this.oval = new paper.Shape.Ellipse({
- strokeColor: this.color,
- strokeWidth: this.thickness,
- point: event.downPoint,
- strokeScaling: false,
- size: 0
- });
- }
+ this.oval = new paper.Shape.Ellipse({
+ point: event.downPoint,
+ size: 0,
+ strokeScaling: false
+ });
+ this.styleOval();
this.oval.data = {zoomLevel: paper.view.zoom};
}
}
@@ -175,6 +158,7 @@ class OvalTool extends paper.Tool {
} else {
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
}
+ this.styleOval();
}
handleMouseMove (event) {
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
this.oval.size = new paper.Point(Math.abs(this.oval.size.width), Math.abs(this.oval.size.height));
this.oval.selected = true;
+ this.styleOval();
this.setSelectedItems();
}
}
diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js
index f65af52568..e31aeedbaa 100644
--- a/src/helper/bit-tools/rect-tool.js
+++ b/src/helper/bit-tools/rect-tool.js
@@ -1,5 +1,6 @@
import paper from '@scratch/paper';
import Modes from '../../lib/modes';
+import {styleShape} from '../../helper/style-path';
import {commitRectToBitmap} from '../bitmap';
import {getRaster} from '../layer';
import {clearSelection} from '../selection';
@@ -76,36 +77,27 @@ class RectTool extends paper.Tool {
this.thickness = this.rect.strokeWidth;
}
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) {
// Rectangle got deselected
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) {
this.color = color;
- if (this.rect) {
- if (this.filled) {
- this.rect.fillColor = this.color;
- } else {
- this.rect.strokeColor = this.color;
- }
- }
+ if (this.rect) this.styleRect();
}
setFilled (filled) {
if (this.filled === filled) return;
this.filled = filled;
if (this.rect && this.rect.isInserted()) {
- if (this.filled) {
- 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.styleRect();
this.onUpdateImage();
}
}
@@ -150,16 +142,10 @@ class RectTool extends paper.Tool {
if (this.rect) this.rect.remove();
this.rect = new paper.Shape.Rectangle(baseRect);
- if (this.filled) {
- this.rect.fillColor = this.color;
- this.rect.strokeWidth = 0;
- } else {
- this.rect.strokeColor = this.color;
- this.rect.strokeWidth = this.thickness;
- }
this.rect.strokeJoin = 'round';
this.rect.strokeScaling = false;
this.rect.data = {zoomLevel: paper.view.zoom};
+ this.styleRect();
if (event.modifiers.alt) {
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
this.rect.size = new paper.Point(Math.abs(this.rect.size.width), Math.abs(this.rect.size.height));
this.rect.selected = true;
+ this.styleRect();
this.setSelectedItems();
}
}
diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js
index b4b7303c3d..b070e2a215 100644
--- a/src/helper/bitmap.js
+++ b/src/helper/bitmap.js
@@ -275,8 +275,24 @@ const drawEllipse = function (options, context) {
if (!matrix.isInvertible()) return false;
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) {
- const brushMark = getBrushMark(thickness, context.fillStyle);
+ const brushMark = getBrushMark(thickness, isGradient ? 'black' : context.fillStyle);
const roundedUpRadius = Math.ceil(thickness / 2);
drawFn = (x, y) => {
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 slope = B / 2 / C;
- return drawShearedEllipse_({
+ const wasDrawn = drawShearedEllipse_({
centerX: positionX,
centerY: positionY,
radiusX: radiusA,
@@ -304,6 +320,17 @@ const drawEllipse = function (options, context) {
isFilled: isFilled,
drawFn: drawFn
}, 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) {
@@ -658,6 +685,20 @@ const outlineRect = function (rect, thickness, context) {
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 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));
@@ -667,6 +708,16 @@ const outlineRect = function (rect, thickness, context) {
forEachLinePoint(startPoint, heightPoint, drawFn);
forEachLinePoint(endPoint, widthPoint, 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) {
@@ -773,6 +824,62 @@ const commitSelectionToBitmap = function (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.Raster} bitmap raster to draw selection
@@ -784,12 +891,12 @@ const commitOvalToBitmap = function (oval, bitmap) {
const context = bitmap.getContext('2d');
const filled = oval.strokeWidth === 0;
- const canvasColor = filled ? oval.fillColor : oval.strokeColor;
- // 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
+ 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 (!canvasColor) return;
- context.fillStyle = canvasColor.toCSS();
+ context.fillStyle = canvasColor;
+
const drew = drawEllipse({
position: oval.position,
radiusX,
@@ -811,12 +918,12 @@ const commitRectToBitmap = function (rect, bitmap) {
const context = tmpCanvas.getContext('2d');
const filled = rect.strokeWidth === 0;
- const canvasColor = filled ? rect.fillColor : rect.strokeColor;
- // 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
+ 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 (!canvasColor) return;
- context.fillStyle = canvasColor.toCSS();
+ context.fillStyle = canvasColor;
+
if (filled) {
fillRect(rect, context);
} else {
diff --git a/src/helper/hover.js b/src/helper/hover.js
index 314efe6845..60b97aeaa3 100644
--- a/src/helper/hover.js
+++ b/src/helper/hover.js
@@ -35,12 +35,17 @@ const getHoveredItem = function (event, hitOptions, subselect) {
return null;
}
+ let hoverGuide;
if (isBoundsItem(item)) {
- return hoverBounds(item);
+ hoverGuide = hoverBounds(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 {
diff --git a/src/helper/style-path.js b/src/helper/style-path.js
index 508c2f0c11..2f4ac02136 100644
--- a/src/helper/style-path.js
+++ b/src/helper/style-path.js
@@ -5,15 +5,15 @@ import {isGroup} from './group';
import {getItems} from './selection';
import GradientTypes from '../lib/gradient-types';
import parseColor from 'parse-color';
-import {DEFAULT_COLOR} from '../reducers/fill-color';
+import {DEFAULT_COLOR} from '../reducers/fill-style';
import {isCompoundPathChild} from '../helper/compound-path';
+import log from '../log/log';
const MIXED = 'scratch-paint/style-path/mixed';
// Check if the item color matches the incoming color. If the item color is a gradient, we assume
// that the incoming color never matches, since we don't support gradients yet.
const _colorMatch = function (itemColor, incomingColor) {
- // @todo colorMatch should not be called with gradients as arguments once stroke gradients are supported
if (itemColor && itemColor.type === 'gradient') return false;
// Either both are null or both are the same color when converted to CSS.
return (!itemColor && !incomingColor) ||
@@ -62,10 +62,12 @@ const getRotatedColor = function (firstColor) {
* @param {?string} color2 CSS string, or null for transparent
* @param {GradientType} gradientType gradient type
* @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.
+ * @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
*/
-const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter) {
+const createGradientObject = function (color1, color2, gradientType, bounds, radialCenter, minSize) {
if (gradientType === GradientTypes.SOLID) return color1;
if (color1 === null) {
color1 = getColorStringForTransparent(color2);
@@ -73,15 +75,50 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
if (color2 === null) {
color2 = getColorStringForTransparent(color1);
}
- const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2;
- const start = gradientType === GradientTypes.RADIAL ? radialCenter :
- gradientType === GradientTypes.VERTICAL ? bounds.topCenter :
- gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter :
- null;
- const end = gradientType === GradientTypes.RADIAL ? start.add(new paper.Point(halfLongestDimension, 0)) :
- gradientType === GradientTypes.VERTICAL ? bounds.bottomCenter :
- gradientType === GradientTypes.HORIZONTAL ? bounds.rightCenter :
- null;
+
+ // Force gradients to have a minimum length. If the gradient start and end points are the same or very close
+ // (e.g. applying a vertical gradient to a perfectly horizontal line or vice versa), the gradient will not appear.
+ if (!minSize) minSize = 1e-2;
+
+ let start;
+ let end;
+ switch (gradientType) {
+ 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 {
gradient: {
stops: [color1, color2],
@@ -93,17 +130,23 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad
};
/**
- * Called when setting fill color
+ * Called when setting an item's color
* @param {string} colorString color, css format, or null if completely transparent
* @param {number} colorIndex index of color being changed
* @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.
* When changing the color in this case, the solid gradient should override the existing gradient on the item.
- * @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 {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
-const applyFillColorToSelection = function (colorString, colorIndex, isSolidGradient, bitmapMode, textEditTargetId) {
+const applyColorToSelection = function (
+ colorString,
+ colorIndex,
+ isSolidGradient,
+ applyToStroke,
+ textEditTargetId
+) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
@@ -111,41 +154,38 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad
item = item.parent;
}
- // In bitmap mode, fill color applies to the stroke if there is a stroke
- if (bitmapMode && item.strokeColor !== null && item.strokeWidth) {
- if (!_colorMatch(item.strokeColor, colorString)) {
- changed = true;
- item.strokeColor = colorString;
- }
- } else if (isSolidGradient || !item.fillColor || !item.fillColor.gradient ||
- !item.fillColor.gradient.stops.length === 2) {
+ const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
+ const itemColor = item[itemColorProp];
+
+ if (isSolidGradient || !itemColor || !itemColor.gradient ||
+ !itemColor.gradient.stops.length === 2) {
// Applying a solid color
- if (!_colorMatch(item.fillColor, colorString)) {
+ if (!_colorMatch(itemColor, colorString)) {
changed = true;
if (isPointTextItem(item) && !colorString) {
// Allows transparent text to be hit
- item.fillColor = 'rgba(0,0,0,0)';
+ item[itemColorProp] = 'rgba(0,0,0,0)';
} else {
- item.fillColor = colorString;
+ item[itemColorProp] = colorString;
}
}
- } else if (!_colorMatch(item.fillColor.gradient.stops[colorIndex].color, colorString)) {
+ } else if (!_colorMatch(itemColor.gradient.stops[colorIndex].color, colorString)) {
// Changing one color of an existing gradient
changed = true;
const otherIndex = colorIndex === 0 ? 1 : 0;
if (colorString === null) {
- colorString = getColorStringForTransparent(item.fillColor.gradient.stops[otherIndex].color.toCSS());
+ colorString = getColorStringForTransparent(itemColor.gradient.stops[otherIndex].color.toCSS());
}
const colors = [0, 0];
colors[colorIndex] = colorString;
// If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth
- if (item.fillColor.gradient.stops[otherIndex].color.alpha === 0) {
+ if (itemColor.gradient.stops[otherIndex].color.alpha === 0) {
colors[otherIndex] = getColorStringForTransparent(colorString);
} else {
- colors[otherIndex] = item.fillColor.gradient.stops[otherIndex].color.toCSS();
+ colors[otherIndex] = itemColor.gradient.stops[otherIndex].color.toCSS();
}
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
- item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial};
+ itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial};
}
}
return changed;
@@ -153,11 +193,11 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad
/**
* 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 {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
-const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
+const swapColorsInSelection = function (applyToStroke, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (const item of items) {
@@ -166,21 +206,19 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
// that would leave us right where we started.
if (isCompoundPathChild(item)) continue;
- if (bitmapMode) {
- // @todo
- return;
- } else if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops.length === 2) {
+ const itemColor = applyToStroke ? item.strokeColor : item.fillColor;
+ if (!itemColor || !itemColor.gradient || !itemColor.gradient.stops.length === 2) {
// Only one color; nothing to swap
continue;
- } else if (!item.fillColor.gradient.stops[0].color.equals(item.fillColor.gradient.stops[1].color)) {
+ } else if (!itemColor.gradient.stops[0].color.equals(itemColor.gradient.stops[1].color)) {
// Changing one color of an existing gradient
changed = true;
const colors = [
- item.fillColor.gradient.stops[1].color.toCSS(),
- item.fillColor.gradient.stops[0].color.toCSS()
+ itemColor.gradient.stops[1].color.toCSS(),
+ itemColor.gradient.stops[0].color.toCSS()
];
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
- item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial};
+ itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial};
}
}
return changed;
@@ -189,11 +227,11 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
/**
* Called when setting 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 {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
-const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEditTargetId) {
+const applyGradientTypeToSelection = function (gradientType, applyToStroke, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
@@ -201,40 +239,42 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
item = item.parent;
}
+ const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor';
+ const itemColor = item[itemColorProp];
+
+ const hasGradient = itemColor && itemColor.gradient;
+
let itemColor1;
- if (item.fillColor === null || item.fillColor.alpha === 0) {
+ if (itemColor === null || itemColor.alpha === 0) {
// Transparent
itemColor1 = null;
- } else if (!item.fillColor.gradient) {
+ } else if (!hasGradient) {
// Solid color
- itemColor1 = item.fillColor.toCSS();
- } else if (!item.fillColor.gradient.stops[0] || item.fillColor.gradient.stops[0].color.alpha === 0) {
+ itemColor1 = itemColor.toCSS();
+ } else if (!itemColor.gradient.stops[0] || itemColor.gradient.stops[0].color.alpha === 0) {
// Gradient where first color is transparent
itemColor1 = null;
} else {
// Gradient where first color is not transparent
- itemColor1 = item.fillColor.gradient.stops[0].color.toCSS();
+ itemColor1 = itemColor.gradient.stops[0].color.toCSS();
}
let itemColor2;
- if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops[1]) {
+ if (!hasGradient || !itemColor.gradient.stops[1]) {
// If item color is solid or a gradient that has no 2nd color, set the 2nd color based on the first color
itemColor2 = getRotatedColor(itemColor1);
- } else if (item.fillColor.gradient.stops[1].color.alpha === 0) {
+ } else if (itemColor.gradient.stops[1].color.alpha === 0) {
// Gradient has 2nd color which is transparent
itemColor2 = null;
} else {
// Gradient has 2nd color which is not transparent
- itemColor2 = item.fillColor.gradient.stops[1].color.toCSS();
+ itemColor2 = itemColor.gradient.stops[1].color.toCSS();
}
- if (bitmapMode) {
- // @todo Add when we apply gradients to selections in bitmap mode
- continue;
- } else if (gradientType === GradientTypes.SOLID) {
- if (item.fillColor && item.fillColor.gradient) {
+ if (gradientType === GradientTypes.SOLID) {
+ if (itemColor && itemColor.gradient) {
changed = true;
- item.fillColor = itemColor1;
+ item[itemColorProp] = itemColor1;
}
continue;
}
@@ -245,72 +285,39 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi
if (itemColor2 === null) {
itemColor2 = getColorStringForTransparent(itemColor1);
}
- if (gradientType === GradientTypes.RADIAL) {
- const hasRadialGradient = item.fillColor && item.fillColor.gradient && item.fillColor.gradient.radial;
- if (!hasRadialGradient) {
- changed = true;
- const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2;
- item.fillColor = {
- gradient: {
- stops: [itemColor1, itemColor2],
- radial: true
- },
- origin: item.position,
- destination: item.position.add(new paper.Point(halfLongestDimension, 0))
- };
- }
- } else if (gradientType === GradientTypes.HORIZONTAL) {
- const hasHorizontalGradient = item.fillColor && item.fillColor.gradient &&
- !item.fillColor.gradient.radial &&
- Math.abs(item.fillColor.origin.y - item.fillColor.destination.y) < 1e-8;
- if (!hasHorizontalGradient) {
- changed = true;
- item.fillColor = {
- gradient: {
- stops: [itemColor1, itemColor2]
- },
- origin: item.bounds.leftCenter,
- destination: item.bounds.rightCenter
- };
- }
- } else if (gradientType === GradientTypes.VERTICAL) {
- const hasVerticalGradient = item.fillColor && item.fillColor.gradient && !item.fillColor.gradient.radial &&
- Math.abs(item.fillColor.origin.x - item.fillColor.destination.x) < 1e-8;
- if (!hasVerticalGradient) {
- changed = true;
- item.fillColor = {
- gradient: {
- stops: [itemColor1, itemColor2]
- },
- origin: item.bounds.topCenter,
- destination: item.bounds.bottomCenter
- };
- }
- }
- }
- return changed;
-};
-/**
- * Called when setting stroke color
- * @param {string} colorString New color, css format
- * @param {?boolean} bitmapMode True if the stroke color is being set in bitmap mode
- * @param {?string} textEditTargetId paper.Item.id of text editing target, if any
- * @return {boolean} Whether the color application actually changed visibly.
- */
-const applyStrokeColorToSelection = function (colorString, bitmapMode, textEditTargetId) {
- // Bitmap mode doesn't have stroke color
- if (bitmapMode) return false;
-
- const items = _getColorStateListeners(textEditTargetId);
- let changed = false;
- for (let item of items) {
- if (item.parent instanceof paper.CompoundPath) {
- item = item.parent;
+ let gradientTypeDiffers = false;
+ // If the item's gradient type differs from the gradient type we want to apply, then we change it
+ switch (gradientType) {
+ case GradientTypes.RADIAL: {
+ const hasRadialGradient = hasGradient && itemColor.gradient.radial;
+ gradientTypeDiffers = !hasRadialGradient;
+ break;
+ }
+ case GradientTypes.HORIZONTAL: {
+ const hasHorizontalGradient = hasGradient && !itemColor.gradient.radial &&
+ Math.abs(itemColor.origin.y - itemColor.destination.y) < 1e-8;
+ gradientTypeDiffers = !hasHorizontalGradient;
+ break;
}
- if (!_colorMatch(item.strokeColor, colorString)) {
+ case GradientTypes.VERTICAL: {
+ const hasVerticalGradient = hasGradient && !itemColor.gradient.radial &&
+ Math.abs(itemColor.origin.x - itemColor.destination.x) < 1e-8;
+ gradientTypeDiffers = !hasVerticalGradient;
+ break;
+ }
+ }
+
+ if (gradientTypeDiffers) {
changed = true;
- item.strokeColor = colorString;
+ item[itemColorProp] = createGradientObject(
+ itemColor1,
+ itemColor2,
+ gradientType,
+ item.bounds,
+ null, // radialCenter
+ item.strokeWidth
+ );
}
}
return changed;
@@ -339,6 +346,33 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
return changed;
};
+const _colorStateFromGradient = gradient => {
+ const colorState = {};
+ // Scratch only recognizes 2 color gradients
+ if (gradient.stops.length === 2) {
+ if (gradient.radial) {
+ colorState.gradientType = GradientTypes.RADIAL;
+ } else {
+ // Always use horizontal for linear gradients, since horizontal and vertical gradients
+ // are the same with rotation. We don't want to show MIXED just because anything is rotated.
+ colorState.gradientType = GradientTypes.HORIZONTAL;
+ }
+ colorState.primary = gradient.stops[0].color.alpha === 0 ?
+ null :
+ gradient.stops[0].color.toCSS();
+ colorState.secondary = gradient.stops[1].color.alpha === 0 ?
+ null :
+ gradient.stops[1].color.toCSS();
+ } else {
+ if (gradient.stops.length < 2) log.warn(`Gradient has ${gradient.stops.length} stop(s)`);
+
+ colorState.primary = MIXED;
+ colorState.secondary = MIXED;
+ }
+
+ return colorState;
+};
+
/**
* Get state of colors and stroke width for selection
* @param {!Array} selectedItems Selected paper items
@@ -349,12 +383,15 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
* Thickness is line thickness, used in the bitmap editor
*/
const getColorsFromSelection = function (selectedItems, bitmapMode) {
+ // TODO: DRY out this code
let selectionFillColorString;
let selectionFillColor2String;
let selectionStrokeColorString;
+ let selectionStrokeColor2String;
let selectionStrokeWidth;
let selectionThickness;
- let selectionGradientType;
+ let selectionFillGradientType;
+ let selectionStrokeGradientType;
let firstChild = true;
for (let item of selectedItems) {
@@ -365,7 +402,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
let itemFillColorString;
let itemFillColor2String;
let itemStrokeColorString;
- let itemGradientType = GradientTypes.SOLID;
+ let itemStrokeColor2String;
+ let itemFillGradientType = GradientTypes.SOLID;
+ let itemStrokeGradientType = GradientTypes.SOLID;
if (!isGroup(item)) {
if (item.fillColor) {
@@ -373,25 +412,10 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
if (isPointTextItem(item) && item.fillColor.alpha === 0) {
itemFillColorString = null;
} else if (item.fillColor.type === 'gradient') {
- // Scratch only recognizes 2 color gradients
- if (item.fillColor.gradient.stops.length === 2) {
- if (item.fillColor.gradient.radial) {
- itemGradientType = GradientTypes.RADIAL;
- } else {
- // Always use horizontal for linear gradients, since horizontal and vertical gradients
- // are the same with rotation. We don't want to show MIXED just because anything is rotated.
- itemGradientType = GradientTypes.HORIZONTAL;
- }
- itemFillColorString = item.fillColor.gradient.stops[0].color.alpha === 0 ?
- null :
- item.fillColor.gradient.stops[0].color.toCSS();
- itemFillColor2String = item.fillColor.gradient.stops[1].color.alpha === 0 ?
- null :
- item.fillColor.gradient.stops[1].color.toCSS();
- } else {
- itemFillColorString = MIXED;
- itemFillColor2String = MIXED;
- }
+ const {primary, secondary, gradientType} = _colorStateFromGradient(item.fillColor.gradient);
+ itemFillColorString = primary;
+ itemFillColor2String = secondary;
+ itemFillGradientType = gradientType;
} else {
itemFillColorString = item.fillColor.alpha === 0 ?
null :
@@ -399,15 +423,40 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
}
}
if (item.strokeColor) {
- // Stroke color is fill color in bitmap
- if (bitmapMode) {
- itemFillColorString = item.strokeColor.toCSS();
- } else if (item.strokeColor.type === 'gradient') {
- itemStrokeColorString = MIXED;
+ if (item.strokeColor.type === 'gradient') {
+ const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient);
+
+ let strokeColorString = primary;
+ const strokeColor2String = secondary;
+ 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
+ if (bitmapMode) {
+ itemFillColorString = strokeColorString;
+ itemFillColor2String = strokeColor2String;
+ itemFillGradientType = strokeGradientType;
+ } else {
+ itemStrokeColorString = strokeColorString;
+ itemStrokeColor2String = strokeColor2String;
+ itemStrokeGradientType = strokeGradientType;
+ }
} else {
- itemStrokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ?
+ const strokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ?
null :
item.strokeColor.toCSS();
+
+ // Stroke color is fill color in bitmap
+ if (bitmapMode) {
+ itemFillColorString = strokeColorString;
+ } else {
+ itemStrokeColorString = strokeColorString;
+ }
}
} else {
itemStrokeColorString = null;
@@ -418,7 +467,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
selectionFillColorString = itemFillColorString;
selectionFillColor2String = itemFillColor2String;
selectionStrokeColorString = itemStrokeColorString;
- selectionGradientType = itemGradientType;
+ selectionStrokeColor2String = itemStrokeColor2String;
+ selectionFillGradientType = itemFillGradientType;
+ selectionStrokeGradientType = itemStrokeGradientType;
selectionStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0;
if (item.strokeWidth && item.data && item.data.zoomLevel) {
selectionThickness = item.strokeWidth / item.data.zoomLevel;
@@ -430,14 +481,22 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
if (itemFillColor2String !== selectionFillColor2String) {
selectionFillColor2String = MIXED;
}
- if (itemGradientType !== selectionGradientType) {
- selectionGradientType = GradientTypes.SOLID;
+ if (itemFillGradientType !== selectionFillGradientType) {
+ selectionFillGradientType = GradientTypes.SOLID;
selectionFillColorString = MIXED;
selectionFillColor2String = MIXED;
}
+ if (itemStrokeGradientType !== selectionStrokeGradientType) {
+ selectionStrokeGradientType = GradientTypes.SOLID;
+ selectionStrokeColorString = MIXED;
+ selectionStrokeColor2String = MIXED;
+ }
if (itemStrokeColorString !== selectionStrokeColorString) {
selectionStrokeColorString = MIXED;
}
+ if (itemStrokeColor2String !== selectionStrokeColor2String) {
+ selectionStrokeColor2String = MIXED;
+ }
const itemStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0;
if (selectionStrokeWidth !== itemStrokeWidth) {
selectionStrokeWidth = null;
@@ -445,27 +504,46 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
}
}
// Convert selection gradient type from horizontal to vertical if first item is exactly vertical
- if (selectedItems && selectedItems.length && selectionGradientType !== GradientTypes.SOLID) {
+ // This is because up to this point, we assume all non-radial gradients are horizontal
+ // Otherwise, if there were a mix of horizontal/vertical gradient types in the selection, they would show as MIXED
+ // whereas we want them to show as horizontal (or vertical if the first item is vertical)
+ if (selectedItems && selectedItems.length) {
let firstItem = selectedItems[0];
if (firstItem.parent instanceof paper.CompoundPath) firstItem = firstItem.parent;
- const direction = firstItem.fillColor.destination.subtract(firstItem.fillColor.origin);
- if (Math.abs(direction.angle) === 90) {
- selectionGradientType = GradientTypes.VERTICAL;
+
+ if (selectionFillGradientType !== GradientTypes.SOLID) {
+ // Stroke color is fill color in bitmap if fill color is missing
+ // TODO: this whole "treat horizontal/vertical gradients specially" logic is janky; refactor at some point
+ const firstItemColor = (bitmapMode && firstItem.strokeColor) ? firstItem.strokeColor : firstItem.fillColor;
+ const direction = firstItemColor.destination.subtract(firstItemColor.origin);
+ if (Math.abs(direction.angle) === 90) {
+ selectionFillGradientType = GradientTypes.VERTICAL;
+ }
+ }
+
+ if (selectionStrokeGradientType !== GradientTypes.SOLID) {
+ const direction = firstItem.strokeColor.destination.subtract(firstItem.strokeColor.origin);
+ if (Math.abs(direction.angle) === 90) {
+ selectionStrokeGradientType = GradientTypes.VERTICAL;
+ }
}
}
if (bitmapMode) {
return {
fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
- gradientType: selectionGradientType,
+ fillGradientType: selectionFillGradientType,
thickness: selectionThickness
};
}
+
return {
fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
- gradientType: selectionGradientType,
+ fillGradientType: selectionFillGradientType,
strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null,
+ strokeColor2: selectionStrokeColor2String ? selectionStrokeColor2String : null,
+ strokeGradientType: selectionStrokeGradientType,
strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0
};
};
@@ -481,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) {
if (options.isEraser) {
path.fillColor = 'white';
@@ -503,15 +573,30 @@ const styleCursorPreview = function (path, options) {
};
const styleShape = function (path, options) {
- path.fillColor = options.fillColor;
- path.strokeColor = options.strokeColor;
- path.strokeWidth = options.strokeWidth;
+ for (const colorKey of ['fillColor', 'strokeColor']) {
+ if (options[colorKey] === null) {
+ 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 {
- applyFillColorToSelection,
+ applyColorToSelection,
applyGradientTypeToSelection,
- applyStrokeColorToSelection,
applyStrokeWidthToSelection,
createGradientObject,
getColorsFromSelection,
@@ -519,7 +604,6 @@ export {
MIXED,
styleBlob,
styleShape,
- stylePath,
styleCursorPreview,
swapColorsInSelection
};
diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js
index c82352d0b0..a34638b785 100644
--- a/src/helper/tools/fill-tool.js
+++ b/src/helper/tools/fill-tool.js
@@ -31,6 +31,8 @@ class FillTool extends paper.Tool {
// The path that's being hovered over.
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
// added item that's the same shape as the hole that's drawn over the hole when we fill a hole.
this.addedFillItem = null;
@@ -43,14 +45,17 @@ class FillTool extends paper.Tool {
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
};
return {
- segments: true,
+ segments: false,
stroke: true,
- curves: true,
+ curves: false,
fill: true,
guide: false,
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 &&
- (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;
}
if (hitResult.item instanceof paper.PointText) {
@@ -58,6 +63,12 @@ class FillTool extends paper.Tool {
}
},
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
};
}
@@ -89,8 +100,13 @@ class FillTool extends paper.Tool {
this.setHoveredItem(hoveredItem ? hoveredItem.id : 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
- if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) {
+ if (!hitTargetChanged) {
// Only radial gradient needs to be updated
if (this.gradientType === GradientTypes.RADIAL) {
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
@@ -106,14 +122,18 @@ class FillTool extends paper.Tool {
}
this.fillItemOrigColor = null;
this.fillItem = null;
+ this.fillProperty = null;
}
if (hitItem) {
this.fillItem = hitItem;
- this.fillItemOrigColor = hitItem.fillColor;
- if (hitItem.parent instanceof paper.CompoundPath && hitItem.area < 0) { // hole
+ this.fillProperty = hitType;
+ 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) {
// Hole filled with transparent is no-op
this.fillItem = null;
+ this.fillProperty = null;
this.fillItemOrigColor = null;
return;
}
@@ -127,7 +147,7 @@ class FillTool extends paper.Tool {
expandBy(this.addedFillItem, .1);
this.addedFillItem.insertAbove(hitItem.parent);
} 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);
}
@@ -163,6 +183,7 @@ class FillTool extends paper.Tool {
this.clearHoveredItem();
this.fillItem = null;
+ this.fillProperty = null;
this.addedFillItem = null;
this.fillItemOrigColor = null;
this.onUpdateImage();
@@ -178,12 +199,20 @@ class FillTool extends paper.Tool {
_setFillItemColor (color1, color2, gradientType, pointerLocation) {
const item = this._getFillItem();
if (!item) return;
+ const colorProp = this.fillProperty === 'fill' ? 'fillColor' : 'strokeColor';
// 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).
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 {
- item.fillColor = color1;
+ item[colorProp] = color1;
}
}
_getFillItem () {
@@ -199,6 +228,7 @@ class FillTool extends paper.Tool {
this._setFillItemColor(this.fillItemOrigColor);
this.fillItemOrigColor = null;
this.fillItem = null;
+ this.fillProperty = null;
}
this.clearHoveredItem();
this.setHoveredItem = null;
diff --git a/src/helper/tools/oval-tool.js b/src/helper/tools/oval-tool.js
index 1246a705b1..f1d3208a49 100644
--- a/src/helper/tools/oval-tool.js
+++ b/src/helper/tools/oval-tool.js
@@ -111,6 +111,8 @@ class OvalTool extends paper.Tool {
} else {
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
}
+
+ styleShape(this.oval, this.colorState);
}
handleMouseMove (event) {
this.boundingBoxTool.onMouseMove(event, this.getHitOptions());
diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js
index 3c20bc725b..bba8bda7a4 100644
--- a/src/helper/tools/text-tool.js
+++ b/src/helper/tools/text-tool.js
@@ -241,7 +241,9 @@ class TextTool extends paper.Tool {
content: '',
font: this.font,
fontSize: 40,
- fillColor: this.colorState.fillColor,
+ // TODO: style using gradient
+ // https://github.com/LLK/scratch-paint/issues/1164
+ fillColor: this.colorState.fillColor.primary,
// Default leading for both the HTML text area and paper.PointText
// is 120%, but for some reason they are slightly off from each other.
// This value was obtained experimentally.
diff --git a/src/lib/color-style-proptype.js b/src/lib/color-style-proptype.js
new file mode 100644
index 0000000000..6e59603f63
--- /dev/null
+++ b/src/lib/color-style-proptype.js
@@ -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
+});
diff --git a/src/lib/make-color-style-reducer.js b/src/lib/make-color-style-reducer.js
new file mode 100644
index 0000000000..db71697987
--- /dev/null
+++ b/src/lib/make-color-style-reducer.js
@@ -0,0 +1,95 @@
+import log from '../log/log';
+import {CHANGE_SELECTED_ITEMS} from '../reducers/selected-items';
+import {getColorsFromSelection, MIXED} from '../helper/style-path';
+import GradientTypes from './gradient-types';
+
+// Matches hex colors
+const hexRegex = /^#([0-9a-f]{3}){1,2}$/i;
+
+const isValidHexColor = color => {
+ if (!hexRegex.test(color) && color !== null && color !== MIXED) {
+ log.warn(`Invalid hex color code: ${color}`);
+ return false;
+ }
+ return true;
+};
+
+const makeColorStyleReducer = ({
+ // Action name for changing the primary color
+ changePrimaryColorAction,
+ // Action name for changing the secondary color
+ changeSecondaryColorAction,
+ // Action name for changing the gradient type
+ changeGradientTypeAction,
+ // Action name for clearing the gradient
+ clearGradientAction,
+ // Initial color when not set
+ defaultColor,
+ // The name of the property read from getColorsFromSelection to get the primary color.
+ // e.g. `fillColor` or `strokeColor`.
+ selectionPrimaryColorKey,
+ // The name of the property read from getColorsFromSelection to get the secondary color.
+ // e.g. `fillColor2` or `strokeColor2`.
+ selectionSecondaryColorKey,
+ // The name of the property read from getColorsFromSelection to get the gradient type.
+ // e.g. `fillGradientType` or `strokeGradientType`.
+ selectionGradientTypeKey
+}) => function colorReducer (state, action) {
+ if (typeof state === 'undefined') {
+ state = {
+ primary: defaultColor,
+ secondary: null,
+ gradientType: GradientTypes.SOLID
+ };
+ }
+ switch (action.type) {
+ case changePrimaryColorAction:
+ if (!isValidHexColor(action.color)) return state;
+ return {...state, primary: action.color};
+ case changeSecondaryColorAction:
+ if (!isValidHexColor(action.color)) return state;
+ return {...state, secondary: action.color};
+ case CHANGE_SELECTED_ITEMS: {
+ // Don't change state if no selection
+ if (!action.selectedItems || !action.selectedItems.length) {
+ return state;
+ }
+ const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode);
+
+ // Only set the primary color + gradient type if they exist in what getColorsFromSelection gave us.
+ // E.g. in bitmap mode, getColorsFromSelection will not return stroke color/gradient type. This allows us to
+ // preserve stroke swatch state across bitmap mode-- if getColorsFromSelection set them to null, then selecting
+ // anything in bitmap mode would overwrite the stroke state.
+ const newState = {...state};
+ if (selectionPrimaryColorKey in colors) {
+ newState.primary = colors[selectionPrimaryColorKey];
+ }
+ if (selectionGradientTypeKey in colors) {
+ newState.gradientType = colors[selectionGradientTypeKey];
+ }
+
+ // Gradient type may be solid when multiple gradient types are selected.
+ // In this case, changing the first color should not change the second color.
+ if (
+ selectionSecondaryColorKey in colors &&
+ (colors[selectionGradientTypeKey] !== GradientTypes.SOLID ||
+ colors[selectionSecondaryColorKey] === MIXED)
+ ) {
+ newState.secondary = colors[selectionSecondaryColorKey];
+ }
+ return newState;
+ }
+ case changeGradientTypeAction:
+ if (action.gradientType in GradientTypes) {
+ return {...state, gradientType: action.gradientType};
+ }
+ log.warn(`Gradient type does not exist: ${action.gradientType}`);
+ return state;
+ case clearGradientAction:
+ return {...state, secondary: null, gradientType: GradientTypes.SOLID};
+ default:
+ return state;
+ }
+};
+
+export default makeColorStyleReducer;
diff --git a/src/lib/modes.js b/src/lib/modes.js
index 48e9017cb0..3a0934ff6b 100644
--- a/src/lib/modes.js
+++ b/src/lib/modes.js
@@ -26,8 +26,23 @@ const VectorModes = keyMirror(vectorModesObj);
const BitmapModes = keyMirror(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 {
Modes as default,
VectorModes,
- BitmapModes
+ BitmapModes,
+ GradientToolsModes
};
diff --git a/src/reducers/color-index.js b/src/reducers/color-index.js
index c762ef8333..4520f627ac 100644
--- a/src/reducers/color-index.js
+++ b/src/reducers/color-index.js
@@ -1,5 +1,5 @@
import log from '../log/log';
-import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type';
+import {CHANGE_FILL_GRADIENT_TYPE} from './fill-style';
import GradientTypes from '../lib/gradient-types';
const CHANGE_COLOR_INDEX = 'scratch-paint/color-index/CHANGE_COLOR_INDEX';
@@ -14,7 +14,7 @@ const reducer = function (state, action) {
return state;
}
return action.index;
- case CHANGE_GRADIENT_TYPE:
+ case CHANGE_FILL_GRADIENT_TYPE:
if (action.gradientType === GradientTypes.SOLID) return 0;
/* falls through */
default:
diff --git a/src/reducers/color.js b/src/reducers/color.js
index 847c310bdb..4d50d49bed 100644
--- a/src/reducers/color.js
+++ b/src/reducers/color.js
@@ -1,16 +1,12 @@
import {combineReducers} from 'redux';
import eyeDropperReducer from './eye-dropper';
-import fillColorReducer from './fill-color';
-import fillColor2Reducer from './fill-color-2';
-import gradientTypeReducer from './selection-gradient-type';
-import strokeColorReducer from './stroke-color';
+import fillColorReducer from './fill-style';
+import strokeColorReducer from './stroke-style';
import strokeWidthReducer from './stroke-width';
export default combineReducers({
eyeDropper: eyeDropperReducer,
fillColor: fillColorReducer,
- fillColor2: fillColor2Reducer,
- gradientType: gradientTypeReducer,
strokeColor: strokeColorReducer,
strokeWidth: strokeWidthReducer
});
diff --git a/src/reducers/fill-color-2.js b/src/reducers/fill-color-2.js
deleted file mode 100644
index 5fe16d5af3..0000000000
--- a/src/reducers/fill-color-2.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import log from '../log/log';
-import {CHANGE_SELECTED_ITEMS} from './selected-items';
-import {CLEAR_GRADIENT} from './selection-gradient-type';
-import {MIXED, getColorsFromSelection} from '../helper/style-path';
-import GradientTypes from '../lib/gradient-types';
-
-const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2';
-// Matches hex colors
-const regExp = /^#([0-9a-f]{3}){1,2}$/i;
-
-const reducer = function (state, action) {
- if (typeof state === 'undefined') state = null;
- switch (action.type) {
- case CHANGE_FILL_COLOR_2:
- if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) {
- log.warn(`Invalid hex color code: ${action.fillColor}`);
- return state;
- }
- return action.fillColor;
- case CHANGE_SELECTED_ITEMS:
- {
- // Don't change state if no selection
- if (!action.selectedItems || !action.selectedItems.length) {
- return state;
- }
- const colors = getColorsFromSelection(action.selectedItems);
- if (colors.gradientType === GradientTypes.SOLID) {
- // Gradient type may be solid when multiple gradient types are selected.
- // In this case, changing the first color should not change the second color.
- if (colors.fillColor2 === MIXED) return MIXED;
- return state;
- }
- return colors.fillColor2;
- }
- case CLEAR_GRADIENT:
- return null;
- default:
- return state;
- }
-};
-
-// Action creators ==================================
-const changeFillColor2 = function (fillColor) {
- return {
- type: CHANGE_FILL_COLOR_2,
- fillColor: fillColor
- };
-};
-
-export {
- reducer as default,
- changeFillColor2
-};
diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js
deleted file mode 100644
index 8b194cae42..0000000000
--- a/src/reducers/fill-color.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import log from '../log/log';
-import {CHANGE_SELECTED_ITEMS} from './selected-items';
-import {getColorsFromSelection, MIXED} from '../helper/style-path';
-
-const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR';
-const DEFAULT_COLOR = '#9966FF';
-const initialState = DEFAULT_COLOR;
-// 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_FILL_COLOR:
- if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) {
- log.warn(`Invalid hex color code: ${action.fillColor}`);
- return state;
- }
- return action.fillColor;
- case CHANGE_SELECTED_ITEMS:
- // Don't change state if no selection
- if (!action.selectedItems || !action.selectedItems.length) {
- return state;
- }
- return getColorsFromSelection(action.selectedItems, action.bitmapMode).fillColor;
- default:
- return state;
- }
-};
-
-// Action creators ==================================
-const changeFillColor = function (fillColor) {
- return {
- type: CHANGE_FILL_COLOR,
- fillColor: fillColor
- };
-};
-
-export {
- reducer as default,
- changeFillColor,
- DEFAULT_COLOR
-};
diff --git a/src/reducers/fill-mode-gradient-type.js b/src/reducers/fill-mode-gradient-type.js
index b19753b3b0..571740f4ab 100644
--- a/src/reducers/fill-mode-gradient-type.js
+++ b/src/reducers/fill-mode-gradient-type.js
@@ -2,14 +2,14 @@
// and isn't overwritten by changing the selection.
import GradientTypes from '../lib/gradient-types';
import log from '../log/log';
+import {CHANGE_FILL_GRADIENT_TYPE} from './fill-style';
-const CHANGE_GRADIENT_TYPE = 'scratch-paint/fill-mode-gradient-type/CHANGE_GRADIENT_TYPE';
const initialState = null;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
- case CHANGE_GRADIENT_TYPE:
+ case CHANGE_FILL_GRADIENT_TYPE:
if (action.gradientType in GradientTypes) {
return action.gradientType;
}
@@ -22,16 +22,15 @@ const reducer = function (state, action) {
// Action creators ==================================
// Use this for user-initiated gradient type selections only.
-// See reducers/selection-gradient-type.js for other ways gradient type changes.
+// See reducers/fill-style.js for other ways gradient type changes.
const changeGradientType = function (gradientType) {
return {
- type: CHANGE_GRADIENT_TYPE,
+ type: CHANGE_FILL_GRADIENT_TYPE,
gradientType: gradientType
};
};
export {
reducer as default,
- CHANGE_GRADIENT_TYPE,
changeGradientType
};
diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js
new file mode 100644
index 0000000000..b97c4befdc
--- /dev/null
+++ b/src/reducers/fill-style.js
@@ -0,0 +1,56 @@
+import makeColorStyleReducer from '../lib/make-color-style-reducer';
+
+const CHANGE_FILL_COLOR = 'scratch-paint/fill-style/CHANGE_FILL_COLOR';
+const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-style/CHANGE_FILL_COLOR_2';
+const CHANGE_FILL_GRADIENT_TYPE = 'scratch-paint/fill-style/CHANGE_FILL_GRADIENT_TYPE';
+const CLEAR_FILL_GRADIENT = 'scratch-paint/fill-style/CLEAR_FILL_GRADIENT';
+const DEFAULT_COLOR = '#9966FF';
+
+const reducer = makeColorStyleReducer({
+ changePrimaryColorAction: CHANGE_FILL_COLOR,
+ changeSecondaryColorAction: CHANGE_FILL_COLOR_2,
+ changeGradientTypeAction: CHANGE_FILL_GRADIENT_TYPE,
+ clearGradientAction: CLEAR_FILL_GRADIENT,
+ defaultColor: DEFAULT_COLOR,
+ selectionPrimaryColorKey: 'fillColor',
+ selectionSecondaryColorKey: 'fillColor2',
+ selectionGradientTypeKey: 'fillGradientType'
+});
+
+// Action creators ==================================
+const changeFillColor = function (fillColor) {
+ return {
+ type: CHANGE_FILL_COLOR,
+ color: fillColor
+ };
+};
+
+const changeFillColor2 = function (fillColor) {
+ return {
+ type: CHANGE_FILL_COLOR_2,
+ color: fillColor
+ };
+};
+
+const changeFillGradientType = function (gradientType) {
+ return {
+ type: CHANGE_FILL_GRADIENT_TYPE,
+ gradientType
+ };
+};
+
+const clearFillGradient = function () {
+ return {
+ type: CLEAR_FILL_GRADIENT
+ };
+};
+
+export {
+ reducer as default,
+ changeFillColor,
+ changeFillColor2,
+ changeFillGradientType,
+ clearFillGradient,
+ DEFAULT_COLOR,
+ CHANGE_FILL_GRADIENT_TYPE
+};
diff --git a/src/reducers/selection-gradient-type.js b/src/reducers/selection-gradient-type.js
deleted file mode 100644
index c0d979186d..0000000000
--- a/src/reducers/selection-gradient-type.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Gradient type shown in the select tool
-import GradientTypes from '../lib/gradient-types';
-import {getColorsFromSelection} from '../helper/style-path';
-import {CHANGE_SELECTED_ITEMS} from './selected-items';
-import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type';
-import log from '../log/log';
-
-const CLEAR_GRADIENT = 'scratch-paint/selection-gradient-type/CLEAR_GRADIENT';
-const initialState = GradientTypes.SOLID;
-
-const reducer = function (state, action) {
- if (typeof state === 'undefined') state = initialState;
- switch (action.type) {
- case CHANGE_GRADIENT_TYPE:
- if (action.gradientType in GradientTypes) {
- return action.gradientType;
- }
- log.warn(`Gradient type does not exist: ${action.gradientType}`);
- return state;
- case CLEAR_GRADIENT:
- return GradientTypes.SOLID;
- case CHANGE_SELECTED_ITEMS:
- // Don't change state if no selection
- if (!action.selectedItems || !action.selectedItems.length) {
- return state;
- }
- return getColorsFromSelection(action.selectedItems, action.bitmapMode).gradientType;
- default:
- return state;
- }
-};
-
-// Action creators ==================================
-const clearGradient = function () {
- return {
- type: CLEAR_GRADIENT
- };
-};
-
-export {
- reducer as default,
- CLEAR_GRADIENT,
- clearGradient
-};
diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js
deleted file mode 100644
index 50cad337ff..0000000000
--- a/src/reducers/stroke-color.js
+++ /dev/null
@@ -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
-};
diff --git a/src/reducers/stroke-style.js b/src/reducers/stroke-style.js
new file mode 100644
index 0000000000..1d61135609
--- /dev/null
+++ b/src/reducers/stroke-style.js
@@ -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
+};
diff --git a/test/unit/color-reducer.test.js b/test/unit/color-reducer.test.js
new file mode 100644
index 0000000000..81183f8941
--- /dev/null
+++ b/test/unit/color-reducer.test.js
@@ -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);
+ });
+
+}
diff --git a/test/unit/fill-color-reducer.test.js b/test/unit/fill-color-reducer.test.js
deleted file mode 100644
index fb83043179..0000000000
--- a/test/unit/fill-color-reducer.test.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* eslint-env jest */
-import fillColorReducer from '../../src/reducers/fill-color';
-import {changeFillColor} from '../../src/reducers/fill-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(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 */))
- .toEqual(newFillColor);
- expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */))
- .toEqual(newFillColor);
-
- // 6 value hex code
- newFillColor = '#facade';
- expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */))
- .toEqual(newFillColor);
- expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */))
- .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 */))
- .toEqual(fillColor1);
- selectedItems = [mockPaperRootItem({fillColor: fillColor2})];
- expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
- .toEqual(fillColor2);
- selectedItems = [mockPaperRootItem({fillColor: fillColor1}), mockPaperRootItem({fillColor: fillColor2})];
- expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */))
- .toEqual(MIXED);
-});
-
-test('invalidChangeFillColor', () => {
- const origState = '#fff';
-
- 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);
-});
diff --git a/test/unit/stroke-color-reducer.test.js b/test/unit/stroke-color-reducer.test.js
deleted file mode 100644
index 25b5522237..0000000000
--- a/test/unit/stroke-color-reducer.test.js
+++ /dev/null
@@ -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);
-});