From 6094953ef424fb512f59c2946b929923333ba597 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 15 Apr 2020 11:48:11 -0400 Subject: [PATCH 01/17] Wire up stroke gradient controls * Abstract FillColorIndicator and StrokeColorIndicator to ColorIndicator * Replace stroke color reducer with stroke style reducer * Add color style proptype * Clear stroke gradient in line mode --- ...olor-indicator.jsx => color-indicator.jsx} | 49 +++--- src/components/stroke-color-indicator.jsx | 65 -------- src/containers/brush-mode.jsx | 10 +- src/containers/color-indicator.jsx | 156 ++++++++++++++++++ src/containers/fill-color-indicator.jsx | 148 ++--------------- src/containers/line-mode.jsx | 21 ++- src/containers/oval-mode.jsx | 13 +- src/containers/rect-mode.jsx | 13 +- src/containers/stroke-color-indicator.jsx | 118 +++++-------- src/containers/stroke-width-indicator.jsx | 2 +- src/containers/text-mode.jsx | 15 +- src/helper/style-path.js | 6 +- src/lib/color-style-proptype.js | 9 + src/reducers/color.js | 2 +- src/reducers/fill-style.js | 2 +- src/reducers/stroke-color.js | 51 ------ src/reducers/stroke-style.js | 56 +++++++ 17 files changed, 338 insertions(+), 398 deletions(-) rename src/components/{fill-color-indicator.jsx => color-indicator.jsx} (53%) delete mode 100644 src/components/stroke-color-indicator.jsx create mode 100644 src/containers/color-indicator.jsx create mode 100644 src/lib/color-style-proptype.js delete mode 100644 src/reducers/stroke-color.js create mode 100644 src/reducers/stroke-style.js 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 0a0fa0cf..d04ea2bb 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 15c40d9b..00000000 --- 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/brush-mode.jsx b/src/containers/brush-mode.jsx index 32a80640..f1936ad6 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -3,6 +3,7 @@ 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'; @@ -38,7 +39,7 @@ class BrushMode extends React.Component { this.blob.setOptions({ isEraser: false, fillColor: fillColor.primary, - strokeColor, + strokeColor: strokeColor.primary, strokeWidth, ...nextProps.brushModeState }); @@ -88,11 +89,8 @@ BrushMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx new file mode 100644 index 00000000..37e2b40a --- /dev/null +++ b/src/containers/color-indicator.jsx @@ -0,0 +1,156 @@ +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) { + if (this.props.color === null && newColor !== null) { + this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged; + this.props.onChangeStrokeWidth(1); + } else if (this.props.color !== 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. + const isDifferent = applyColorToSelection( + newColor, + this.props.colorIndex, + this.props.gradientType === GradientTypes.SOLID, + isBitmap(this.props.format), + isStroke, + this.props.textEditTarget); + this._hasChanged = this._hasChanged || isDifferent; + this.props.onChangeColor(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), + isStroke, + 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.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 isDifferent = swapColorsInSelection( + isBitmap(this.props.format), + isStroke, + this.props.textEditTarget); + this.props.setSelectedItems(); + 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, + 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 43291462..9d88070d 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -1,8 +1,5 @@ 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, changeFillColor2} from '../reducers/fill-style'; @@ -11,121 +8,26 @@ 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 {isBitmap} from '../lib/format'; -import GradientTypes from '../lib/gradient-types'; -import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; -import {applyColorToSelection, - 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' - ]); +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.fill', + description: 'Label for the color picker for the fill color', + defaultMessage: 'Fill' + } +}); - // 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 = applyColorToSelection( - newColor, - this.props.colorIndex, - this.props.gradientType === GradientTypes.SOLID, - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this._hasChanged = this._hasChanged || isDifferent; - this.props.onChangeFillColor(newColor, this.props.colorIndex); - } - handleChangeGradientType (gradientType) { - // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyGradientTypeToSelection( - gradientType, - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this._hasChanged = this._hasChanged || isDifferent; - const hasSelectedItems = getSelectedLeafItems().length > 0; - if (hasSelectedItems) { - if (isDifferent) { - // Recalculates the swatch colors - this.props.setSelectedItems(); - } - } - if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) { - // Generate color 2 and change to the 2nd swatch when switching from solid to gradient - if (!hasSelectedItems) { - this.props.onChangeFillColor(getRotatedColor(this.props.fillColor), 1); - } - this.props.onChangeColorIndex(1); - } - this.props.onChangeGradientType(gradientType); - } - handleCloseFillColor () { - // If the eyedropper is currently being used, don't - // close the fill color menu. - if (this.props.isEyeDropping) return; - - // Otherwise, close the fill color menu and - // also reset the color index to indicate - // that `color1` is selected. - this.props.onCloseFillColor(); - this.props.onChangeColorIndex(0); - } - handleSwap () { - if (getSelectedLeafItems().length) { - const isDifferent = swapColorsInSelection( - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this.props.setSelectedItems(); - this._hasChanged = this._hasChanged || isDifferent; - } else { - let color1 = this.props.fillColor; - let color2 = this.props.fillColor2; - color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex; - color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex; - this.props.onChangeFillColor(color1, 1); - this.props.onChangeFillColor(color2, 0); - } - } - render () { - return ( - - ); - } -} +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.primary, - fillColor2: state.scratchPaint.color.fillColor.secondary, - fillColorModalVisible: state.scratchPaint.modals.fillColor, + color: state.scratchPaint.color.fillColor.primary, + color2: state.scratchPaint.color.fillColor.secondary, + colorModalVisible: state.scratchPaint.modals.fillColor, format: state.scratchPaint.format, gradientType: state.scratchPaint.color.fillColor.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, @@ -142,17 +44,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 => { @@ -163,24 +65,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/line-mode.jsx b/src/containers/line-mode.jsx index 9f2d675e..2368c43b 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 {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,7 @@ 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); + stylePath(this.path, this.props.colorState.strokeColor.primary, this.props.colorState.strokeWidth); if (this.hitResult.isFirst) { this.path.reverse(); } @@ -114,7 +116,7 @@ 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); + stylePath(this.path, this.props.colorState.strokeColor.primary, this.props.colorState.strokeWidth); this.path.add(event.point); this.path.add(event.point); // Add second point, which is what will move when dragged @@ -253,13 +255,11 @@ class LineMode extends React.Component { } LineMode.propTypes = { + clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, @@ -274,6 +274,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 c5b99d99..8333b3f2 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -4,10 +4,11 @@ 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, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -57,8 +58,9 @@ class OvalMode 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 {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; @@ -98,11 +100,8 @@ OvalMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index f4929f93..5730b0a6 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -4,10 +4,11 @@ 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, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -57,8 +58,9 @@ class RectMode 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 {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; @@ -98,11 +100,8 @@ RectMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index 57f68e3e..409064c1 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -1,109 +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 {getSelectedLeafItems} from '../helper/selection'; +import {setSelectedItems} from '../reducers/selected-items'; import Modes from '../lib/modes'; -import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; -import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; -import {applyColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path'; +import makeColorIndicator from './color-indicator.jsx'; -class StrokeColorIndicator extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleChangeStrokeColor', - 'handleCloseStrokeColor' - ]); +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.stroke', + description: 'Label for the color picker for the outline color', + defaultMessage: 'Outline' + } +}); - // 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 = applyColorToSelection( - newColor, - 0, // colorIndex, - true, // isSolidGradient - isBitmap(this.props.format), - true, // applyToStroke - this.props.textEditTarget) || - this._hasChanged; - this.props.onChangeStrokeColor(newColor); - } - handleCloseStrokeColor () { - if (!this.props.isEyeDropping) { - this.props.onCloseStrokeColor(); - } - } - render () { - return ( - - ); - } -} +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, + 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 === Modes.SELECT || + state.scratchPaint.mode === Modes.RESHAPE, 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 16412677..31e4d88f 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -3,7 +3,7 @@ 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} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {getSelectedLeafItems} from '../helper/selection'; diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index eda064d7..ce270cd5 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -5,11 +5,12 @@ 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, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {setTextEditTarget} from '../reducers/text-edit-target'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; @@ -81,8 +82,9 @@ class TextMode extends React.Component { // If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent. // If 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 {strokeColor, strokeWidth} = nextProps.colorState; - const fillColor = this.props.colorState.fillColor.primary; + 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,11 +145,8 @@ TextMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, font: PropTypes.string, diff --git a/src/helper/style-path.js b/src/helper/style-path.js index b6af421a..53ad35e3 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -516,10 +516,6 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { }; } - // Treat stroke gradients as MIXED - // TODO: remove this once stroke gradients are supported - if (selectionStrokeGradientType !== GradientTypes.SOLID) selectionStrokeColorString = MIXED; - return { fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor2: selectionFillColor2String ? selectionFillColor2String : null, @@ -566,7 +562,7 @@ const styleCursorPreview = function (path, options) { // TODO: style using gradient? const styleShape = function (path, options) { path.fillColor = options.fillColor.primary; - path.strokeColor = options.strokeColor; + path.strokeColor = options.strokeColor.primary; path.strokeWidth = options.strokeWidth; }; diff --git a/src/lib/color-style-proptype.js b/src/lib/color-style-proptype.js new file mode 100644 index 00000000..6e59603f --- /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/reducers/color.js b/src/reducers/color.js index 1ceb3aa4..4d50d49b 100644 --- a/src/reducers/color.js +++ b/src/reducers/color.js @@ -1,7 +1,7 @@ import {combineReducers} from 'redux'; import eyeDropperReducer from './eye-dropper'; import fillColorReducer from './fill-style'; -import strokeColorReducer from './stroke-color'; +import strokeColorReducer from './stroke-style'; import strokeWidthReducer from './stroke-width'; export default combineReducers({ diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js index 98046c49..b97c4bef 100644 --- a/src/reducers/fill-style.js +++ b/src/reducers/fill-style.js @@ -35,7 +35,7 @@ const changeFillColor2 = function (fillColor) { const changeFillGradientType = function (gradientType) { return { type: CHANGE_FILL_GRADIENT_TYPE, - gradientType: gradientType + gradientType }; }; diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js deleted file mode 100644 index 50cad337..00000000 --- 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 00000000..72a15168 --- /dev/null +++ b/src/reducers/stroke-style.js @@ -0,0 +1,56 @@ +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'; + +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' +}); + +// 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 { + reducer as default, + changeStrokeColor, + changeStrokeColor2, + changeStrokeGradientType, + clearStrokeGradient, + DEFAULT_COLOR, + CHANGE_STROKE_GRADIENT_TYPE +}; From 2df22838b4ab9bc742d2f1a91dc250fce71852ab Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 15 Apr 2020 12:28:52 -0400 Subject: [PATCH 02/17] Update color reducer tests --- test/unit/color-reducer.test.js | 86 ++++++++++++++++++++++++++ test/unit/fill-color-reducer.test.js | 68 -------------------- test/unit/stroke-color-reducer.test.js | 79 ----------------------- 3 files changed, 86 insertions(+), 147 deletions(-) create mode 100644 test/unit/color-reducer.test.js delete mode 100644 test/unit/fill-color-reducer.test.js delete mode 100644 test/unit/stroke-color-reducer.test.js diff --git a/test/unit/color-reducer.test.js b/test/unit/color-reducer.test.js new file mode 100644 index 00000000..81183f89 --- /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 35fa60b0..00000000 --- a/test/unit/fill-color-reducer.test.js +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-env jest */ -import fillColorReducer from '../../src/reducers/fill-style'; -import {changeFillColor} from '../../src/reducers/fill-style'; -import {setSelectedItems} from '../../src/reducers/selected-items'; -import {MIXED} from '../../src/helper/style-path'; -import GradientTypes from '../../src/lib/gradient-types'; -import {mockPaperRootItem} from '../__mocks__/paperMocks'; - -test('initialState', () => { - let defaultState; - - expect(fillColorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); -}); - -test('changeFillColor', () => { - let defaultState; - - // 3 value hex code - let newFillColor = '#fff'; - expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary) - .toEqual(newFillColor); - expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary) - .toEqual(newFillColor); - - // 6 value hex code - newFillColor = '#facade'; - expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary) - .toEqual(newFillColor); - expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary) - .toEqual(newFillColor); -}); - -test('changefillColorViaSelectedItems', () => { - let defaultState; - - const fillColor1 = 6; - const fillColor2 = null; // transparent - let selectedItems = [mockPaperRootItem({fillColor: fillColor1})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) - .toEqual(fillColor1); - selectedItems = [mockPaperRootItem({fillColor: fillColor2})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) - .toEqual(fillColor2); - selectedItems = [mockPaperRootItem({fillColor: fillColor1}), mockPaperRootItem({fillColor: fillColor2})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) - .toEqual(MIXED); -}); - -test('invalidChangeFillColor', () => { - const origState = {primary: '#fff', secondary: null, gradientType: GradientTypes.SOLID}; - - expect(fillColorReducer(origState /* state */, changeFillColor() /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#1') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#12') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#1234') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#12345') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('#1234567') /* action */)) - .toBe(origState); - expect(fillColorReducer(origState /* state */, changeFillColor('invalid argument') /* action */)) - .toBe(origState); -}); diff --git a/test/unit/stroke-color-reducer.test.js b/test/unit/stroke-color-reducer.test.js deleted file mode 100644 index 25b55222..00000000 --- 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); -}); From df1989d0b0acb849283811a125c93b980835dc57 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 25 May 2020 08:20:43 -0400 Subject: [PATCH 03/17] Add gradients to vector shape tools --- src/containers/fill-color-indicator.jsx | 2 ++ src/containers/oval-mode.jsx | 7 +------ src/containers/rect-mode.jsx | 7 +------ src/containers/stroke-color-indicator.jsx | 4 +++- src/helper/style-path.js | 12 +++++++++--- src/helper/tools/oval-tool.js | 2 ++ 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 9d88070d..3d9276c8 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -35,6 +35,8 @@ const mapStateToProps = state => ({ shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || state.scratchPaint.mode === Modes.RESHAPE || state.scratchPaint.mode === Modes.FILL || + state.scratchPaint.mode === Modes.RECT || + state.scratchPaint.mode === Modes.OVAL || state.scratchPaint.mode === Modes.BIT_SELECT || state.scratchPaint.mode === Modes.BIT_FILL, textEditTarget: state.scratchPaint.textEditTarget diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index 8333b3f2..03678acd 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -7,7 +7,7 @@ import Modes from '../lib/modes'; import ColorStyleProptype from '../lib/color-style-proptype'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; +import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; @@ -54,7 +54,6 @@ 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. @@ -97,7 +96,6 @@ class OvalMode extends React.Component { } OvalMode.propTypes = { - clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ fillColor: ColorStyleProptype, @@ -123,9 +121,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - clearGradient: () => { - dispatch(clearFillGradient()); - }, setCursor: cursorString => { dispatch(setCursor(cursorString)); }, diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index 5730b0a6..df8c8a3e 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -7,7 +7,7 @@ import Modes from '../lib/modes'; import ColorStyleProptype from '../lib/color-style-proptype'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; +import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; @@ -54,7 +54,6 @@ 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. @@ -97,7 +96,6 @@ class RectMode extends React.Component { } RectMode.propTypes = { - clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ fillColor: ColorStyleProptype, @@ -123,9 +121,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - clearGradient: () => { - dispatch(clearFillGradient()); - }, setSelectedItems: () => { dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); }, diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index 409064c1..45360c3c 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -36,7 +36,9 @@ const mapStateToProps = state => ({ 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.RESHAPE || + state.scratchPaint.mode === Modes.RECT || + state.scratchPaint.mode === Modes.OVAL, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 53ad35e3..c1222e0b 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -559,10 +559,16 @@ const styleCursorPreview = function (path, options) { } }; -// TODO: style using gradient? const styleShape = function (path, options) { - path.fillColor = options.fillColor.primary; - path.strokeColor = options.strokeColor.primary; + for (const colorKey of ['fillColor', 'strokeColor']) { + 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); + } + } + path.strokeWidth = options.strokeWidth; }; diff --git a/src/helper/tools/oval-tool.js b/src/helper/tools/oval-tool.js index 1246a705..f1d3208a 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()); From 2eab5048ace34d945d951c06c919da181c4dc129 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 5 Jun 2020 15:18:39 -0400 Subject: [PATCH 04/17] Add hit result to hovered item data --- src/helper/hover.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/helper/hover.js b/src/helper/hover.js index 314efe68..60b97aea 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 { From c81853b1b7b073e6ff59fd029b74bd128f3604d1 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 5 Jun 2020 16:03:03 -0400 Subject: [PATCH 05/17] Make fill tool work for outlines --- src/helper/tools/fill-tool.js | 41 +++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js index c82352d0..05bb998e 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,13 @@ 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); } else { - item.fillColor = color1; + item[colorProp] = color1; } } _getFillItem () { @@ -199,6 +221,7 @@ class FillTool extends paper.Tool { this._setFillItemColor(this.fillItemOrigColor); this.fillItemOrigColor = null; this.fillItem = null; + this.fillProperty = null; } this.clearHoveredItem(); this.setHoveredItem = null; From f625109c67615b411b7c9e53c59004a9132ad8d2 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Jul 2020 17:05:43 -0400 Subject: [PATCH 06/17] Make styleShape more flexible Now, you can pass null in for a color instead of {primary: null, secondary: null, gradientType: GradientTypes.SOLID} and it'll still clear the color. Passing strokeWidth is also optional now. --- src/helper/style-path.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index c1222e0b..c9482ca9 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -561,7 +561,9 @@ const styleCursorPreview = function (path, options) { const styleShape = function (path, options) { for (const colorKey of ['fillColor', 'strokeColor']) { - if (options[colorKey].gradientType === GradientTypes.SOLID) { + 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]; @@ -569,7 +571,7 @@ const styleShape = function (path, options) { } } - path.strokeWidth = options.strokeWidth; + if (options.hasOwnProperty('strokeWidth')) path.strokeWidth = options.strokeWidth; }; export { From 9f77faf5c16f633fccdf126a58a2d91a02b55172 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 8 Jun 2020 22:53:26 -0400 Subject: [PATCH 07/17] Don't set color in bitmap shape onSelectionChanged This *should* be safe because OvalTool and RectTool's wrapper "mode" components, the only places that call onSelectionChanged, also update the tools' color, and the color state reducers will always set the color every time the selection changes, meaning it'll be updated anyway. --- src/helper/bit-tools/oval-tool.js | 4 ++-- src/helper/bit-tools/rect-tool.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js index 2afdba68..c7ae45a2 100644 --- a/src/helper/bit-tools/oval-tool.js +++ b/src/helper/bit-tools/oval-tool.js @@ -76,8 +76,8 @@ 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(); diff --git a/src/helper/bit-tools/rect-tool.js b/src/helper/bit-tools/rect-tool.js index f65af525..64aa6b78 100644 --- a/src/helper/bit-tools/rect-tool.js +++ b/src/helper/bit-tools/rect-tool.js @@ -76,8 +76,6 @@ 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(); From a304dea3384c0bde5c6c362643c7f3fc856e1cee Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 9 Jun 2020 15:02:36 -0400 Subject: [PATCH 08/17] Add gradients to bitmap shape tools --- src/containers/bit-oval-mode.jsx | 14 +-- src/containers/bit-rect-mode.jsx | 14 +-- src/containers/color-indicator.jsx | 22 ++-- src/containers/fill-color-indicator.jsx | 3 + src/containers/stroke-color-indicator.jsx | 5 +- src/helper/bit-tools/oval-tool.js | 51 +++------ src/helper/bit-tools/rect-tool.js | 35 ++---- src/helper/bitmap.js | 127 ++++++++++++++++++++-- src/helper/style-path.js | 21 +--- 9 files changed, 179 insertions(+), 113 deletions(-) diff --git a/src/containers/bit-oval-mode.jsx b/src/containers/bit-oval-mode.jsx index edd7ebb2..bf3ae380 100644 --- a/src/containers/bit-oval-mode.jsx +++ b/src/containers/bit-oval-mode.jsx @@ -4,9 +4,10 @@ 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, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; +import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -61,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); } @@ -94,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, @@ -110,7 +109,7 @@ BitOvalMode.propTypes = { }; const mapStateToProps = state => ({ - color: state.scratchPaint.color.fillColor.primary, + color: state.scratchPaint.color.fillColor, filled: state.scratchPaint.fillBitmapShapes, isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL, selectedItems: state.scratchPaint.selectedItems, @@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - clearGradient: () => { - dispatch(clearFillGradient()); - }, setCursor: cursorString => { dispatch(setCursor(cursorString)); }, diff --git a/src/containers/bit-rect-mode.jsx b/src/containers/bit-rect-mode.jsx index 61aa53fa..6263953a 100644 --- a/src/containers/bit-rect-mode.jsx +++ b/src/containers/bit-rect-mode.jsx @@ -4,9 +4,10 @@ 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, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; +import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -61,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); } @@ -94,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, @@ -110,7 +109,7 @@ BitRectMode.propTypes = { }; const mapStateToProps = state => ({ - color: state.scratchPaint.color.fillColor.primary, + color: state.scratchPaint.color.fillColor, filled: state.scratchPaint.fillBitmapShapes, isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT, selectedItems: state.scratchPaint.selectedItems, @@ -121,9 +120,6 @@ const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { dispatch(clearSelectedItems()); }, - clearGradient: () => { - dispatch(clearFillGradient()); - }, setCursor: cursorString => { dispatch(setCursor(cursorString)); }, diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx index 37e2b40a..762b57dd 100644 --- a/src/containers/color-indicator.jsx +++ b/src/containers/color-indicator.jsx @@ -52,30 +52,34 @@ const makeColorIndicator = (label, isStroke) => { } } + 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, - isBitmap(this.props.format), - isStroke, + formatIsBitmap, + // 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, - isBitmap(this.props.format), - isStroke, + formatIsBitmap, + 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.setSelectedItems(this.props.format); } } if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) { @@ -100,11 +104,12 @@ const makeColorIndicator = (label, isStroke) => { } handleSwap () { if (getSelectedLeafItems().length) { + const formatIsBitmap = isBitmap(this.props.format); const isDifferent = swapColorsInSelection( - isBitmap(this.props.format), - isStroke, + formatIsBitmap, + isStroke || (formatIsBitmap && !this.props.fillBitmapShapes), this.props.textEditTarget); - this.props.setSelectedItems(); + this.props.setSelectedItems(this.props.format); this._hasChanged = this._hasChanged || isDifferent; } else { let color1 = this.props.color; @@ -136,6 +141,7 @@ const makeColorIndicator = (label, isStroke) => { 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, diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 3d9276c8..f3704276 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -28,6 +28,7 @@ const mapStateToProps = state => ({ 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.fillColor.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, @@ -38,6 +39,8 @@ const mapStateToProps = state => ({ state.scratchPaint.mode === Modes.RECT || state.scratchPaint.mode === Modes.OVAL || state.scratchPaint.mode === Modes.BIT_SELECT || + state.scratchPaint.mode === Modes.BIT_RECT || + state.scratchPaint.mode === Modes.BIT_OVAL || state.scratchPaint.mode === Modes.BIT_FILL, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index 45360c3c..5fafaff1 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -30,6 +30,7 @@ const mapStateToProps = state => ({ 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, @@ -38,7 +39,9 @@ const mapStateToProps = state => ({ shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || state.scratchPaint.mode === Modes.RESHAPE || state.scratchPaint.mode === Modes.RECT || - state.scratchPaint.mode === Modes.OVAL, + state.scratchPaint.mode === Modes.OVAL || + state.scratchPaint.mode === Modes.BIT_RECT || + state.scratchPaint.mode === Modes.BIT_OVAL, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/helper/bit-tools/oval-tool.js b/src/helper/bit-tools/oval-tool.js index c7ae45a2..f13f595b 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'; @@ -83,29 +84,22 @@ class OvalTool extends paper.Tool { 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 64aa6b78..e31aeedb 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'; @@ -81,29 +82,22 @@ class RectTool extends paper.Tool { 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(); } } @@ -148,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; @@ -188,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 b4b7303c..b070e2a2 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/style-path.js b/src/helper/style-path.js index c9482ca9..98561f62 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -120,20 +120,6 @@ const applyColorToSelection = function ( item = item.parent; } - // In bitmap mode, fill color applies to the stroke if there is a stroke - if ( - bitmapMode && - !applyToStroke && - item.strokeColor !== null && - item.strokeWidth - ) { - if (!_colorMatch(item.strokeColor, colorString)) { - changed = true; - item.strokeColor = colorString; - } - continue; - } - const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor'; const itemColor = item[itemColorProp]; @@ -179,8 +165,6 @@ const applyColorToSelection = function ( * @return {boolean} Whether the color application actually changed visibly. */ const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) { - if (bitmapMode) return; // @todo - const items = _getColorStateListeners(textEditTargetId); let changed = false; for (const item of items) { @@ -255,10 +239,7 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyTo 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 (gradientType === GradientTypes.SOLID) { if (itemColor && itemColor.gradient) { changed = true; item[itemColorProp] = itemColor1; From 6ab7b4c67de5b978eb1ede0ae79b4abfd39ded93 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 19 Jun 2020 11:23:37 -0400 Subject: [PATCH 09/17] Remove unnecessary bitmapMode params --- src/containers/color-indicator.jsx | 3 --- src/containers/stroke-width-indicator.jsx | 1 - src/helper/style-path.js | 10 +++------- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx index 762b57dd..b825c3c4 100644 --- a/src/containers/color-indicator.jsx +++ b/src/containers/color-indicator.jsx @@ -58,7 +58,6 @@ const makeColorIndicator = (label, isStroke) => { newColor, this.props.colorIndex, this.props.gradientType === GradientTypes.SOLID, - formatIsBitmap, // 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), @@ -71,7 +70,6 @@ const makeColorIndicator = (label, isStroke) => { // Apply color and update redux, but do not update svg until picker closes. const isDifferent = applyGradientTypeToSelection( gradientType, - formatIsBitmap, isStroke || (formatIsBitmap && !this.props.fillBitmapShapes), this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; @@ -106,7 +104,6 @@ const makeColorIndicator = (label, isStroke) => { if (getSelectedLeafItems().length) { const formatIsBitmap = isBitmap(this.props.format); const isDifferent = swapColorsInSelection( - formatIsBitmap, isStroke || (formatIsBitmap && !this.props.fillBitmapShapes), this.props.textEditTarget); this.props.setSelectedItems(this.props.format); diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 31e4d88f..30d9c0a9 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -29,7 +29,6 @@ class StrokeWidthIndicator extends React.Component { '#000', 0, // colorIndex, true, // isSolidGradient - isBitmap(this.props.format), true, // applyToStroke this.props.textEditTarget) || changed; diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 98561f62..2d023136 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -100,7 +100,6 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad * @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color * 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 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. @@ -109,7 +108,6 @@ const applyColorToSelection = function ( colorString, colorIndex, isSolidGradient, - bitmapMode, applyToStroke, textEditTargetId ) { @@ -159,12 +157,11 @@ const applyColorToSelection = function ( /** * 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, applyToStroke, textEditTargetId) { +const swapColorsInSelection = function (applyToStroke, textEditTargetId) { const items = _getColorStateListeners(textEditTargetId); let changed = false; for (const item of items) { @@ -194,12 +191,11 @@ const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTarge /** * 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 {?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, applyToStroke, textEditTargetId) { +const applyGradientTypeToSelection = function (gradientType, applyToStroke, textEditTargetId) { const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { From 696e35582c20e19cfb817efde6b354481bdfc683 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 6 Jul 2020 17:27:46 -0400 Subject: [PATCH 10/17] Add GradientToolsModes for shouldShowGradientTools This means that gradient tools will also be enabled for the stroke color indicator even in fill and bitmap modes, but that's okay because the stroke color indicator will be disabled or hidden in those modes anyway. --- src/containers/fill-color-indicator.jsx | 12 ++---------- src/containers/stroke-color-indicator.jsx | 9 ++------- src/lib/modes.js | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index f3704276..fc86f31f 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -7,7 +7,7 @@ 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 Modes, {GradientToolsModes} from '../lib/modes'; import {isBitmap} from '../lib/format'; import makeColorIndicator from './color-indicator.jsx'; @@ -33,15 +33,7 @@ const mapStateToProps = state => ({ 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.RECT || - state.scratchPaint.mode === Modes.OVAL || - state.scratchPaint.mode === Modes.BIT_SELECT || - state.scratchPaint.mode === Modes.BIT_RECT || - state.scratchPaint.mode === Modes.BIT_OVAL || - state.scratchPaint.mode === Modes.BIT_FILL, + shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index 5fafaff1..b912bcc3 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -8,7 +8,7 @@ import {changeStrokeGradientType} from '../reducers/stroke-style'; import {openStrokeColor, closeStrokeColor} from '../reducers/modals'; import {getSelectedLeafItems} from '../helper/selection'; import {setSelectedItems} from '../reducers/selected-items'; -import Modes from '../lib/modes'; +import Modes, {GradientToolsModes} from '../lib/modes'; import {isBitmap} from '../lib/format'; import makeColorIndicator from './color-indicator.jsx'; @@ -36,12 +36,7 @@ const mapStateToProps = state => ({ gradientType: state.scratchPaint.color.strokeColor.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.RECT || - state.scratchPaint.mode === Modes.OVAL || - state.scratchPaint.mode === Modes.BIT_RECT || - state.scratchPaint.mode === Modes.BIT_OVAL, + shouldShowGradientTools: state.scratchPaint.mode in GradientToolsModes, textEditTarget: state.scratchPaint.textEditTarget }); diff --git a/src/lib/modes.js b/src/lib/modes.js index 48e9017c..dc7145ad 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -26,8 +26,22 @@ 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, + + BIT_OVAL: null, + BIT_RECT: null, + BIT_SELECT: null, + BIT_FILL: null +}); + export { Modes as default, VectorModes, - BitmapModes + BitmapModes, + GradientToolsModes }; From f0b1881fb0449c03a100fc8e98112bddf681c79d Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 16 Jul 2020 06:52:39 -0400 Subject: [PATCH 11/17] Fix changing gradient strokes to/from null Hopefully the comments help; the logic is kinda convoluted here --- src/containers/color-indicator.jsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx index b825c3c4..a6cabe50 100644 --- a/src/containers/color-indicator.jsx +++ b/src/containers/color-indicator.jsx @@ -43,10 +43,24 @@ const makeColorIndicator = (label, isStroke) => { // 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) { - if (this.props.color === null && newColor !== null) { + + // 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 (this.props.color !== null && newColor === null) { + } else if (!oldStyleWasNull && newStyleIsNull) { this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged; this.props.onChangeStrokeWidth(0); } From 689532f26964ac600ed32b0942376980e4b0bd81 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 23 Jul 2020 06:07:05 -0400 Subject: [PATCH 12/17] Enforce minimum gradient size This fixes the bug where percertly horizontal gradients on perfectly vertical lines and vice versa would not be rendered at all --- src/helper/style-path.js | 65 +++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 2d023136..1c756537 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -64,9 +64,10 @@ const getRotatedColor = function (firstColor) { * @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. * 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); @@ -74,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 || bounds.center) : - 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], @@ -544,7 +580,14 @@ const styleShape = function (path, options) { path[colorKey] = options[colorKey].primary; } else { const {primary, secondary, gradientType} = options[colorKey]; - path[colorKey] = createGradientObject(primary, secondary, gradientType, path.bounds); + path[colorKey] = createGradientObject( + primary, + secondary, + gradientType, + path.bounds, + null, // radialCenter + options.strokeWidth // minimum gradient size is stroke width + ); } } From 98daa5ee5ddeb472c9d569a14f22a4d363fad6ae Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 23 Jul 2020 06:15:03 -0400 Subject: [PATCH 13/17] Gradients in line mode The UX here isn't the best thing in the world but I think having the functionality is important judging from the playtest --- src/containers/line-mode.jsx | 26 +++++++++++++++++++++++--- src/helper/style-path.js | 9 --------- src/lib/modes.js | 1 + 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 2368c43b..4a671110 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -8,7 +8,7 @@ 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 {styleShape} from '../helper/style-path'; import {changeStrokeColor, clearStrokeGradient} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; import {changeMode} from '../reducers/modes'; @@ -103,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.primary, 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(); } @@ -116,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.primary, 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 @@ -188,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 @@ -227,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; diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 1c756537..17e0ae77 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -551,14 +551,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'; @@ -604,7 +596,6 @@ export { MIXED, styleBlob, styleShape, - stylePath, styleCursorPreview, swapColorsInSelection }; diff --git a/src/lib/modes.js b/src/lib/modes.js index dc7145ad..3a0934ff 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -32,6 +32,7 @@ const GradientToolsModes = keyMirror({ RESHAPE: null, OVAL: null, RECT: null, + LINE: null, BIT_OVAL: null, BIT_RECT: null, From 852eefc2d7ac19bd41fe1ca93dbcd67d66e4b31d Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 28 Jul 2020 08:27:38 -0400 Subject: [PATCH 14/17] Fix gradient outlines with 0 width in select mode Previously, if you selected a shape with a gradient outline that had a width of 0, its outline color wouldn't be null when it should have been. --- src/helper/style-path.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 17e0ae77..7b258e80 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -421,12 +421,18 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { } } if (item.strokeColor) { - if (item.strokeColor.type === 'gradient') { const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient); - const strokeColorString = primary; + + let strokeColorString = primary; const strokeColor2String = secondary; - const strokeGradientType = gradientType; + let strokeGradientType = gradientType; + + // If the item's stroke width is 0, pretend the stroke color is null + if (!item.strokeWidth) { + strokeColorString = null; + strokeGradientType = GradientTypes.SOLID; + } // Stroke color is fill color in bitmap if (bitmapMode) { From cca0832f0dd6ad8fe07717468da72bb901e87843 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 28 Jul 2020 08:36:34 -0400 Subject: [PATCH 15/17] Fix zero-width-outline logic - Set the stroke color to "null" when the width is set to 0 - Properly set the stroke color state when the width is increased from 0 --- src/containers/stroke-width-indicator.jsx | 34 ++++++++++++++++++----- src/reducers/stroke-style.js | 20 ++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 30d9c0a9..ed27ff96 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-style'; +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 {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,8 +24,15 @@ 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) { + 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, @@ -32,11 +40,15 @@ class StrokeWidthIndicator extends React.Component { 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(); @@ -64,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)); } @@ -73,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/reducers/stroke-style.js b/src/reducers/stroke-style.js index 72a15168..1d611356 100644 --- a/src/reducers/stroke-style.js +++ b/src/reducers/stroke-style.js @@ -6,6 +6,8 @@ const CHANGE_STROKE_GRADIENT_TYPE = 'scratch-paint/stroke-style/CHANGE_STROKE_GR 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, @@ -17,6 +19,22 @@ const reducer = makeColorStyleReducer({ 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 { @@ -46,7 +64,7 @@ const clearStrokeGradient = function () { }; export { - reducer as default, + strokeReducer as default, changeStrokeColor, changeStrokeColor2, changeStrokeGradientType, From 379599905aa61df751d2dbd90bb5165a14f2ea6b Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 28 Jul 2020 16:01:49 -0400 Subject: [PATCH 16/17] Enforce minimum gradient width for fill tool --- src/helper/tools/fill-tool.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js index 05bb998e..a34638b7 100644 --- a/src/helper/tools/fill-tool.js +++ b/src/helper/tools/fill-tool.js @@ -203,7 +203,14 @@ class FillTool extends paper.Tool { // 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[colorProp] = createGradientObject(color1, color2, gradientType, item.bounds, pointerLocation); + item[colorProp] = createGradientObject( + color1, + color2, + gradientType, + item.bounds, + pointerLocation, + item.strokeWidth + ); } else { item[colorProp] = color1; } From 272a10c91f57706b208e53a33547e550b490bb2c Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 28 Jul 2020 16:32:46 -0400 Subject: [PATCH 17/17] Also enforce minimum gradient size in select mode Oops --- src/helper/style-path.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 7b258e80..2f4ac021 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -314,7 +314,9 @@ const applyGradientTypeToSelection = function (gradientType, applyToStroke, text itemColor1, itemColor2, gradientType, - item.bounds + item.bounds, + null, // radialCenter + item.strokeWidth ); } }