From 604b450b5d6da9c1136898cdd810ded63834db37 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 14 Apr 2020 16:12:29 -0400 Subject: [PATCH 01/25] Simplify fill-color-2 reducer logic why my brain hurt??? --- src/reducers/fill-color-2.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/reducers/fill-color-2.js b/src/reducers/fill-color-2.js index 5fe16d5af3..c655123e61 100644 --- a/src/reducers/fill-color-2.js +++ b/src/reducers/fill-color-2.js @@ -24,13 +24,12 @@ const reducer = function (state, action) { return state; } const colors = getColorsFromSelection(action.selectedItems); - if (colors.gradientType === GradientTypes.SOLID) { - // Gradient type may be solid when multiple gradient types are selected. - // In this case, changing the first color should not change the second color. - if (colors.fillColor2 === MIXED) return MIXED; - return state; + // Gradient type may be solid when multiple gradient types are selected. + // In this case, changing the first color should not change the second color. + if (colors.gradientType !== GradientTypes.SOLID || colors.fillColor2 === MIXED) { + return colors.fillColor2; } - return colors.fillColor2; + return state; } case CLEAR_GRADIENT: return null; From 018958ce7b38d9d4eab169e41f9eff79784636b1 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 14 Apr 2020 16:29:15 -0400 Subject: [PATCH 02/25] Combine fill-color and fill-color-2 reducers --- src/containers/bit-brush-mode.jsx | 2 +- src/containers/bit-fill-mode.jsx | 7 ++-- src/containers/bit-line-mode.jsx | 2 +- src/containers/bit-oval-mode.jsx | 2 +- src/containers/bit-rect-mode.jsx | 2 +- src/containers/brush-mode.jsx | 12 ++++-- src/containers/fill-color-indicator.jsx | 7 ++-- src/containers/fill-mode.jsx | 7 ++-- src/containers/line-mode.jsx | 5 ++- src/containers/oval-mode.jsx | 8 +++- src/containers/rect-mode.jsx | 8 +++- src/containers/text-mode.jsx | 8 +++- src/helper/style-path.js | 3 +- src/helper/tools/text-tool.js | 3 +- src/reducers/color.js | 2 - src/reducers/fill-color-2.js | 52 ----------------------- src/reducers/fill-color.js | 55 ++++++++++++++++++++----- 17 files changed, 93 insertions(+), 92 deletions(-) delete mode 100644 src/reducers/fill-color-2.js diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 0697821625..3d627822d1 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -94,7 +94,7 @@ BitBrushMode.propTypes = { const mapStateToProps = state => ({ bitBrushSize: state.scratchPaint.bitBrushSize, - color: state.scratchPaint.color.fillColor, + color: state.scratchPaint.color.fillColor.primary, isBitBrushModeActive: state.scratchPaint.mode === Modes.BIT_BRUSH }); const mapDispatchToProps = dispatch => ({ diff --git a/src/containers/bit-fill-mode.jsx b/src/containers/bit-fill-mode.jsx index 4987bae8aa..a6d528c733 100644 --- a/src/containers/bit-fill-mode.jsx +++ b/src/containers/bit-fill-mode.jsx @@ -7,8 +7,7 @@ import GradientTypes from '../lib/gradient-types'; import FillModeComponent from '../components/bit-fill-mode/bit-fill-mode.jsx'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; -import {changeFillColor2} from '../reducers/fill-color-2'; +import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-color'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {changeGradientType} from '../reducers/fill-mode-gradient-type'; @@ -115,8 +114,8 @@ BitFillMode.propTypes = { const mapStateToProps = state => ({ fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type - color: state.scratchPaint.color.fillColor, - color2: state.scratchPaint.color.fillColor2, + color: state.scratchPaint.color.fillColor.primary, + color2: state.scratchPaint.color.fillColor.secondary, isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL, selectModeGradientType: state.scratchPaint.color.gradientType }); diff --git a/src/containers/bit-line-mode.jsx b/src/containers/bit-line-mode.jsx index c2a9e9bf59..1adef62533 100644 --- a/src/containers/bit-line-mode.jsx +++ b/src/containers/bit-line-mode.jsx @@ -94,7 +94,7 @@ BitLineMode.propTypes = { const mapStateToProps = state => ({ bitBrushSize: state.scratchPaint.bitBrushSize, - color: state.scratchPaint.color.fillColor, + color: state.scratchPaint.color.fillColor.primary, isBitLineModeActive: state.scratchPaint.mode === Modes.BIT_LINE }); const mapDispatchToProps = dispatch => ({ diff --git a/src/containers/bit-oval-mode.jsx b/src/containers/bit-oval-mode.jsx index 0a7f356bf9..edf6d902c2 100644 --- a/src/containers/bit-oval-mode.jsx +++ b/src/containers/bit-oval-mode.jsx @@ -111,7 +111,7 @@ BitOvalMode.propTypes = { }; const mapStateToProps = state => ({ - color: state.scratchPaint.color.fillColor, + color: state.scratchPaint.color.fillColor.primary, filled: state.scratchPaint.fillBitmapShapes, isOvalModeActive: state.scratchPaint.mode === Modes.BIT_OVAL, selectedItems: state.scratchPaint.selectedItems, diff --git a/src/containers/bit-rect-mode.jsx b/src/containers/bit-rect-mode.jsx index 97c1f5cb98..c07d3a9f61 100644 --- a/src/containers/bit-rect-mode.jsx +++ b/src/containers/bit-rect-mode.jsx @@ -111,7 +111,7 @@ BitRectMode.propTypes = { }; const mapStateToProps = state => ({ - color: state.scratchPaint.color.fillColor, + color: state.scratchPaint.color.fillColor.primary, filled: state.scratchPaint.fillBitmapShapes, isRectModeActive: state.scratchPaint.mode === Modes.BIT_RECT, selectedItems: state.scratchPaint.selectedItems, diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index f47a71a96b..ba78de6592 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -35,9 +35,12 @@ class BrushMode extends React.Component { } else if (!nextProps.isBrushModeActive && this.props.isBrushModeActive) { this.deactivateTool(); } else if (nextProps.isBrushModeActive && this.props.isBrushModeActive) { + const {fillColor, strokeColor, strokeWidth} = nextProps.colorState; this.blob.setOptions({ isEraser: false, - ...nextProps.colorState, + fillColor: fillColor.primary, + strokeColor, + strokeWidth, ...nextProps.brushModeState }); } @@ -56,7 +59,7 @@ class BrushMode extends React.Component { clearSelection(this.props.clearSelectedItems); this.props.clearGradient(); // Force the default brush color if fill is MIXED or transparent - const {fillColor} = this.props.colorState; + const fillColor = this.props.colorState.fillColor.primary; if (fillColor === MIXED || fillColor === null) { this.props.onChangeFillColor(DEFAULT_COLOR); } @@ -86,7 +89,10 @@ BrushMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string, + fillColor: PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string + }), strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 757e84737f..0a4de156ce 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -5,8 +5,7 @@ import bindAll from 'lodash.bindall'; import parseColor from 'parse-color'; import {changeColorIndex} from '../reducers/color-index'; -import {changeFillColor} from '../reducers/fill-color'; -import {changeFillColor2} from '../reducers/fill-color-2'; +import {changeFillColor, changeFillColor2} from '../reducers/fill-color'; import {changeGradientType} from '../reducers/fill-mode-gradient-type'; import {openFillColor, closeFillColor} from '../reducers/modals'; import {getSelectedLeafItems} from '../helper/selection'; @@ -121,8 +120,8 @@ class FillColorIndicator extends React.Component { const mapStateToProps = state => ({ colorIndex: state.scratchPaint.fillMode.colorIndex, disabled: state.scratchPaint.mode === Modes.LINE, - fillColor: state.scratchPaint.color.fillColor, - fillColor2: state.scratchPaint.color.fillColor2, + fillColor: state.scratchPaint.color.fillColor.primary, + fillColor2: state.scratchPaint.color.fillColor.secondary, fillColorModalVisible: state.scratchPaint.modals.fillColor, format: state.scratchPaint.format, gradientType: state.scratchPaint.color.gradientType, diff --git a/src/containers/fill-mode.jsx b/src/containers/fill-mode.jsx index 1abc9c3709..e0f468ae86 100644 --- a/src/containers/fill-mode.jsx +++ b/src/containers/fill-mode.jsx @@ -7,8 +7,7 @@ import GradientTypes from '../lib/gradient-types'; import FillTool from '../helper/tools/fill-tool'; import {getRotatedColor, MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; -import {changeFillColor2} from '../reducers/fill-color-2'; +import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-color'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; @@ -127,8 +126,8 @@ FillMode.propTypes = { const mapStateToProps = state => ({ fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type - fillColor: state.scratchPaint.color.fillColor, - fillColor2: state.scratchPaint.color.fillColor2, + fillColor: state.scratchPaint.color.fillColor.primary, + fillColor2: state.scratchPaint.color.fillColor.secondary, hoveredItemId: state.scratchPaint.hoveredItemId, isFillModeActive: state.scratchPaint.mode === Modes.FILL, selectModeGradientType: state.scratchPaint.color.gradientType diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index ebe011e51f..9f2d675ec5 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -255,7 +255,10 @@ class LineMode extends React.Component { LineMode.propTypes = { clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string, + fillColor: PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string + }), strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index b525ec9a1c..445c20efa8 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -58,7 +58,8 @@ 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 {fillColor, strokeColor, strokeWidth} = this.props.colorState; + const {strokeColor, strokeWidth} = this.props.colorState; + const fillColor = this.props.colorState.fillColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -98,7 +99,10 @@ OvalMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string, + fillColor: PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string + }), strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index 9675b7f910..14fa80104f 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -58,7 +58,8 @@ 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 {fillColor, strokeColor, strokeWidth} = this.props.colorState; + const {strokeColor, strokeWidth} = this.props.colorState; + const fillColor = this.props.colorState.fillColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -98,7 +99,10 @@ RectMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string, + fillColor: PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string + }), strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index 15a10bddf3..d075eb772b 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -82,7 +82,8 @@ class TextMode extends React.Component { // If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent. // If exactly one of fill or stroke color is set, set the other one to transparent. // This way the tool won't draw an invisible state, or be unclear about what will be drawn. - const {fillColor, strokeColor, strokeWidth} = nextProps.colorState; + const {strokeColor, strokeWidth} = nextProps.colorState; + const fillColor = this.props.colorState.fillColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = nextProps.isBitmap ? false : strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -143,7 +144,10 @@ TextMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.string, + fillColor: PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string + }), strokeColor: PropTypes.string, strokeWidth: PropTypes.number }).isRequired, diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 508c2f0c11..319c2ea372 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -502,8 +502,9 @@ const styleCursorPreview = function (path, options) { } }; +// TODO: style using gradient? const styleShape = function (path, options) { - path.fillColor = options.fillColor; + path.fillColor = options.fillColor.primary; path.strokeColor = options.strokeColor; path.strokeWidth = options.strokeWidth; }; diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 3c20bc725b..d7188846ee 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -241,7 +241,8 @@ class TextTool extends paper.Tool { content: '', font: this.font, fontSize: 40, - fillColor: this.colorState.fillColor, + // TODO: style using gradient? + fillColor: this.colorState.fillColor.primary, // Default leading for both the HTML text area and paper.PointText // is 120%, but for some reason they are slightly off from each other. // This value was obtained experimentally. diff --git a/src/reducers/color.js b/src/reducers/color.js index 847c310bdb..ed641f8dc5 100644 --- a/src/reducers/color.js +++ b/src/reducers/color.js @@ -1,7 +1,6 @@ import {combineReducers} from 'redux'; import eyeDropperReducer from './eye-dropper'; import fillColorReducer from './fill-color'; -import fillColor2Reducer from './fill-color-2'; import gradientTypeReducer from './selection-gradient-type'; import strokeColorReducer from './stroke-color'; import strokeWidthReducer from './stroke-width'; @@ -9,7 +8,6 @@ import strokeWidthReducer from './stroke-width'; export default combineReducers({ eyeDropper: eyeDropperReducer, fillColor: fillColorReducer, - fillColor2: fillColor2Reducer, gradientType: gradientTypeReducer, strokeColor: strokeColorReducer, strokeWidth: strokeWidthReducer diff --git a/src/reducers/fill-color-2.js b/src/reducers/fill-color-2.js deleted file mode 100644 index c655123e61..0000000000 --- a/src/reducers/fill-color-2.js +++ /dev/null @@ -1,52 +0,0 @@ -import log from '../log/log'; -import {CHANGE_SELECTED_ITEMS} from './selected-items'; -import {CLEAR_GRADIENT} from './selection-gradient-type'; -import {MIXED, getColorsFromSelection} from '../helper/style-path'; -import GradientTypes from '../lib/gradient-types'; - -const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2'; -// Matches hex colors -const regExp = /^#([0-9a-f]{3}){1,2}$/i; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = null; - switch (action.type) { - case CHANGE_FILL_COLOR_2: - if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) { - log.warn(`Invalid hex color code: ${action.fillColor}`); - return state; - } - return action.fillColor; - case CHANGE_SELECTED_ITEMS: - { - // Don't change state if no selection - if (!action.selectedItems || !action.selectedItems.length) { - return state; - } - const colors = getColorsFromSelection(action.selectedItems); - // Gradient type may be solid when multiple gradient types are selected. - // In this case, changing the first color should not change the second color. - if (colors.gradientType !== GradientTypes.SOLID || colors.fillColor2 === MIXED) { - return colors.fillColor2; - } - return state; - } - case CLEAR_GRADIENT: - return null; - default: - return state; - } -}; - -// Action creators ================================== -const changeFillColor2 = function (fillColor) { - return { - type: CHANGE_FILL_COLOR_2, - fillColor: fillColor - }; -}; - -export { - reducer as default, - changeFillColor2 -}; diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js index 8b194cae42..795c87bb99 100644 --- a/src/reducers/fill-color.js +++ b/src/reducers/fill-color.js @@ -1,28 +1,55 @@ import log from '../log/log'; import {CHANGE_SELECTED_ITEMS} from './selected-items'; +import {CLEAR_GRADIENT} from './selection-gradient-type'; import {getColorsFromSelection, MIXED} from '../helper/style-path'; +import GradientTypes from '../lib/gradient-types'; const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR'; +const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2'; const DEFAULT_COLOR = '#9966FF'; -const initialState = DEFAULT_COLOR; +const initialState = { + primary: DEFAULT_COLOR, + secondary: null +}; + // Matches hex colors -const regExp = /^#([0-9a-f]{3}){1,2}$/i; +const hexRegex = /^#([0-9a-f]{3}){1,2}$/i; + +const isValidHexColor = color => { + if (!hexRegex.test(color) && color !== null && color !== MIXED) { + log.warn(`Invalid hex color code: ${color}`); + return false; + } + return true; +}; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { case CHANGE_FILL_COLOR: - if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) { - log.warn(`Invalid hex color code: ${action.fillColor}`); - return state; - } - return action.fillColor; - case CHANGE_SELECTED_ITEMS: + if (!isValidHexColor(action.fillColor)) return state; + return {...state, primary: action.fillColor}; + case CHANGE_FILL_COLOR_2: + if (!isValidHexColor(action.fillColor)) return state; + return {...state, secondary: action.fillColor}; + case CHANGE_SELECTED_ITEMS: { // Don't change state if no selection if (!action.selectedItems || !action.selectedItems.length) { return state; } - return getColorsFromSelection(action.selectedItems, action.bitmapMode).fillColor; + const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode); + + const newState = {...state, primary: colors.fillColor}; + + // Gradient type may be solid when multiple gradient types are selected. + // In this case, changing the first color should not change the second color. + if (colors.gradientType !== GradientTypes.SOLID || colors.fillColor2 === MIXED) { + newState.secondary = colors.fillColor2; + } + return newState; + } + case CLEAR_GRADIENT: + return {...state, secondary: null}; default: return state; } @@ -32,12 +59,20 @@ const reducer = function (state, action) { const changeFillColor = function (fillColor) { return { type: CHANGE_FILL_COLOR, - fillColor: fillColor + fillColor + }; +}; + +const changeFillColor2 = function (fillColor) { + return { + type: CHANGE_FILL_COLOR_2, + fillColor }; }; export { reducer as default, changeFillColor, + changeFillColor2, DEFAULT_COLOR }; From 7bab4d5ed8734b792f678238d10d458ba9c956c2 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 14 Apr 2020 16:50:55 -0400 Subject: [PATCH 03/25] Abstract color reducer into reducer-generator --- src/lib/make-color-reducer.js | 62 +++++++++++++++++++++++++++++++++ src/reducers/fill-color.js | 64 ++++++----------------------------- 2 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 src/lib/make-color-reducer.js diff --git a/src/lib/make-color-reducer.js b/src/lib/make-color-reducer.js new file mode 100644 index 0000000000..786f24caef --- /dev/null +++ b/src/lib/make-color-reducer.js @@ -0,0 +1,62 @@ +import log from '../log/log'; +import {CHANGE_SELECTED_ITEMS} from '../reducers/selected-items'; +import {CLEAR_GRADIENT} from '../reducers/selection-gradient-type'; +import {getColorsFromSelection, MIXED} from '../helper/style-path'; +import GradientTypes from './gradient-types'; + +// Matches hex colors +const hexRegex = /^#([0-9a-f]{3}){1,2}$/i; + +const isValidHexColor = color => { + if (!hexRegex.test(color) && color !== null && color !== MIXED) { + log.warn(`Invalid hex color code: ${color}`); + return false; + } + return true; +}; + +const makeColorReducer = ({ + changePrimaryColorAction, + changeSecondaryColorAction, + defaultColor, + selectionPrimaryColorKey, + selectionSecondaryColorKey, + selectionGradientTypeKey +}) => function colorReducer (state, action) { + if (typeof state === 'undefined') { + state = { + primary: defaultColor, + secondary: null + }; + } + switch (action.type) { + case changePrimaryColorAction: + if (!isValidHexColor(action.color)) return state; + return {...state, primary: action.color}; + case changeSecondaryColorAction: + if (!isValidHexColor(action.color)) return state; + return {...state, secondary: action.color}; + case CHANGE_SELECTED_ITEMS: { + // Don't change state if no selection + if (!action.selectedItems || !action.selectedItems.length) { + return state; + } + const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode); + + const newState = {...state, primary: colors[selectionPrimaryColorKey]}; + + // Gradient type may be solid when multiple gradient types are selected. + // In this case, changing the first color should not change the second color. + if (colors[selectionGradientTypeKey] !== GradientTypes.SOLID || colors[selectionSecondaryColorKey] === MIXED) { + newState.secondary = colors[selectionSecondaryColorKey]; + } + return newState; + } + case CLEAR_GRADIENT: + return {...state, secondary: null}; + default: + return state; + } +}; + +export default makeColorReducer; diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js index 795c87bb99..4c234b661d 100644 --- a/src/reducers/fill-color.js +++ b/src/reducers/fill-color.js @@ -1,72 +1,30 @@ -import log from '../log/log'; -import {CHANGE_SELECTED_ITEMS} from './selected-items'; -import {CLEAR_GRADIENT} from './selection-gradient-type'; -import {getColorsFromSelection, MIXED} from '../helper/style-path'; -import GradientTypes from '../lib/gradient-types'; +import makeColorReducer from '../lib/make-color-reducer'; const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR'; const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2'; const DEFAULT_COLOR = '#9966FF'; -const initialState = { - primary: DEFAULT_COLOR, - secondary: null -}; - -// Matches hex colors -const hexRegex = /^#([0-9a-f]{3}){1,2}$/i; - -const isValidHexColor = color => { - if (!hexRegex.test(color) && color !== null && color !== MIXED) { - log.warn(`Invalid hex color code: ${color}`); - return false; - } - return true; -}; -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case CHANGE_FILL_COLOR: - if (!isValidHexColor(action.fillColor)) return state; - return {...state, primary: action.fillColor}; - case CHANGE_FILL_COLOR_2: - if (!isValidHexColor(action.fillColor)) return state; - return {...state, secondary: action.fillColor}; - case CHANGE_SELECTED_ITEMS: { - // Don't change state if no selection - if (!action.selectedItems || !action.selectedItems.length) { - return state; - } - const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode); - - const newState = {...state, primary: colors.fillColor}; - - // Gradient type may be solid when multiple gradient types are selected. - // In this case, changing the first color should not change the second color. - if (colors.gradientType !== GradientTypes.SOLID || colors.fillColor2 === MIXED) { - newState.secondary = colors.fillColor2; - } - return newState; - } - case CLEAR_GRADIENT: - return {...state, secondary: null}; - default: - return state; - } -}; +const reducer = makeColorReducer({ + changePrimaryColorAction: CHANGE_FILL_COLOR, + changeSecondaryColorAction: CHANGE_FILL_COLOR_2, + defaultColor: DEFAULT_COLOR, + selectionPrimaryColorKey: 'fillColor', + selectionSecondaryColorKey: 'fillColor2', + selectionGradientTypeKey: 'gradientType' +}); // Action creators ================================== const changeFillColor = function (fillColor) { return { type: CHANGE_FILL_COLOR, - fillColor + color: fillColor }; }; const changeFillColor2 = function (fillColor) { return { type: CHANGE_FILL_COLOR_2, - fillColor + color: fillColor }; }; From 1017911fd4353bc7af7ac9a3ecd172dbb5c507d3 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 15 Apr 2020 05:13:59 -0400 Subject: [PATCH 04/25] color -> style, move selection gradient into it A "style" refers to something that can fill/stroke a shape. Currently that's either a solid color or a gradient of some orientation. The selection gradient type reducer has been removed and folded into the "fill style" reducer. --- src/containers/bit-brush-mode.jsx | 5 +- src/containers/bit-fill-mode.jsx | 16 +++--- src/containers/bit-line-mode.jsx | 5 +- src/containers/bit-oval-mode.jsx | 5 +- src/containers/bit-rect-mode.jsx | 5 +- src/containers/bit-select-mode.jsx | 4 +- src/containers/brush-mode.jsx | 5 +- src/containers/fill-color-indicator.jsx | 4 +- src/containers/fill-mode.jsx | 14 ++--- src/containers/oval-mode.jsx | 5 +- src/containers/rect-mode.jsx | 5 +- src/containers/text-mode.jsx | 5 +- src/helper/style-path.js | 2 +- ...reducer.js => make-color-style-reducer.js} | 37 +++++++++--- src/reducers/color-index.js | 4 +- src/reducers/color.js | 4 +- src/reducers/fill-color.js | 36 ------------ src/reducers/fill-mode-gradient-type.js | 9 ++- src/reducers/fill-style.js | 56 +++++++++++++++++++ src/reducers/selection-gradient-type.js | 44 --------------- 20 files changed, 129 insertions(+), 141 deletions(-) rename src/lib/{make-color-reducer.js => make-color-style-reducer.js} (57%) delete mode 100644 src/reducers/fill-color.js create mode 100644 src/reducers/fill-style.js delete mode 100644 src/reducers/selection-gradient-type.js diff --git a/src/containers/bit-brush-mode.jsx b/src/containers/bit-brush-mode.jsx index 3d627822d1..652395d8eb 100644 --- a/src/containers/bit-brush-mode.jsx +++ b/src/containers/bit-brush-mode.jsx @@ -5,11 +5,10 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; -import {clearGradient} from '../reducers/selection-gradient-type'; import BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx'; import BitBrushTool from '../helper/bit-tools/brush-tool'; @@ -102,7 +101,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_BRUSH)); diff --git a/src/containers/bit-fill-mode.jsx b/src/containers/bit-fill-mode.jsx index a6d528c733..85dbd11881 100644 --- a/src/containers/bit-fill-mode.jsx +++ b/src/containers/bit-fill-mode.jsx @@ -7,7 +7,7 @@ import GradientTypes from '../lib/gradient-types'; import FillModeComponent from '../components/bit-fill-mode/bit-fill-mode.jsx'; -import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {changeGradientType} from '../reducers/fill-mode-gradient-type'; @@ -65,10 +65,10 @@ class BitFillMode extends React.Component { this.props.onChangeFillColor(DEFAULT_COLOR, 0); } const gradientType = this.props.fillModeGradientType ? - this.props.fillModeGradientType : this.props.selectModeGradientType; + this.props.fillModeGradientType : this.props.styleGradientType; let color2 = this.props.color2; - if (gradientType !== this.props.selectModeGradientType) { - if (this.props.selectModeGradientType === GradientTypes.SOLID) { + if (gradientType !== this.props.styleGradientType) { + if (this.props.styleGradientType === GradientTypes.SOLID) { color2 = getRotatedColor(color); this.props.onChangeFillColor(color2, 1); } @@ -104,20 +104,20 @@ BitFillMode.propTypes = { clearSelectedItems: PropTypes.func.isRequired, color: PropTypes.string, color2: PropTypes.string, + styleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)), handleMouseDown: PropTypes.func.isRequired, isFillModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, - onUpdateImage: PropTypes.func.isRequired, - selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired + onUpdateImage: PropTypes.func.isRequired }; const mapStateToProps = state => ({ fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type color: state.scratchPaint.color.fillColor.primary, color2: state.scratchPaint.color.fillColor.secondary, - isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL, - selectModeGradientType: state.scratchPaint.color.gradientType + styleGradientType: state.scratchPaint.color.fillColor.gradientType, + isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { diff --git a/src/containers/bit-line-mode.jsx b/src/containers/bit-line-mode.jsx index 1adef62533..5b57cdafb1 100644 --- a/src/containers/bit-line-mode.jsx +++ b/src/containers/bit-line-mode.jsx @@ -5,11 +5,10 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; -import {clearGradient} from '../reducers/selection-gradient-type'; import BitLineModeComponent from '../components/bit-line-mode/bit-line-mode.jsx'; import BitLineTool from '../helper/bit-tools/line-tool'; @@ -102,7 +101,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, handleMouseDown: () => { dispatch(changeMode(Modes.BIT_LINE)); diff --git a/src/containers/bit-oval-mode.jsx b/src/containers/bit-oval-mode.jsx index edf6d902c2..edd7ebb223 100644 --- a/src/containers/bit-oval-mode.jsx +++ b/src/containers/bit-oval-mode.jsx @@ -6,10 +6,9 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -123,7 +122,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(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 c07d3a9f61..61aa53fa3f 100644 --- a/src/containers/bit-rect-mode.jsx +++ b/src/containers/bit-rect-mode.jsx @@ -6,10 +6,9 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -123,7 +122,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, setCursor: cursorString => { dispatch(setCursor(cursorString)); diff --git a/src/containers/bit-select-mode.jsx b/src/containers/bit-select-mode.jsx index 189799f5fa..7f049b1416 100644 --- a/src/containers/bit-select-mode.jsx +++ b/src/containers/bit-select-mode.jsx @@ -5,9 +5,9 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import {clearFillGradient} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {getSelectedLeafItems} from '../helper/selection'; @@ -88,7 +88,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, clearSelectedItems: () => { dispatch(clearSelectedItems()); diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index ba78de6592..32a80640b5 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -6,10 +6,9 @@ import Modes from '../lib/modes'; import Blobbiness from '../helper/blob-tools/blob'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {clearSelection} from '../helper/selection'; import BrushModeComponent from '../components/brush-mode/brush-mode.jsx'; @@ -112,7 +111,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, handleMouseDown: () => { dispatch(changeMode(Modes.BRUSH)); diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 0a4de156ce..8ff198ef93 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -5,7 +5,7 @@ import bindAll from 'lodash.bindall'; import parseColor from 'parse-color'; import {changeColorIndex} from '../reducers/color-index'; -import {changeFillColor, changeFillColor2} from '../reducers/fill-color'; +import {changeFillColor, changeFillColor2} from '../reducers/fill-style'; import {changeGradientType} from '../reducers/fill-mode-gradient-type'; import {openFillColor, closeFillColor} from '../reducers/modals'; import {getSelectedLeafItems} from '../helper/selection'; @@ -124,7 +124,7 @@ const mapStateToProps = state => ({ fillColor2: state.scratchPaint.color.fillColor.secondary, fillColorModalVisible: state.scratchPaint.modals.fillColor, format: state.scratchPaint.format, - gradientType: state.scratchPaint.color.gradientType, + gradientType: state.scratchPaint.color.fillColor.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, mode: state.scratchPaint.mode, shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || diff --git a/src/containers/fill-mode.jsx b/src/containers/fill-mode.jsx index e0f468ae86..63d10815b2 100644 --- a/src/containers/fill-mode.jsx +++ b/src/containers/fill-mode.jsx @@ -7,7 +7,7 @@ import GradientTypes from '../lib/gradient-types'; import FillTool from '../helper/tools/fill-tool'; import {getRotatedColor, MIXED} from '../helper/style-path'; -import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, changeFillColor2, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; import {clearSelection} from '../helper/selection'; @@ -69,10 +69,10 @@ class FillMode extends React.Component { this.props.onChangeFillColor(DEFAULT_COLOR, 0); } const gradientType = this.props.fillModeGradientType ? - this.props.fillModeGradientType : this.props.selectModeGradientType; + this.props.fillModeGradientType : this.props.fillStyleGradientType; let fillColor2 = this.props.fillColor2; - if (gradientType !== this.props.selectModeGradientType) { - if (this.props.selectModeGradientType === GradientTypes.SOLID) { + if (gradientType !== this.props.fillStyleGradientType) { + if (this.props.fillStyleGradientType === GradientTypes.SOLID) { fillColor2 = getRotatedColor(fillColor); this.props.onChangeFillColor(fillColor2, 1); } @@ -114,13 +114,13 @@ FillMode.propTypes = { clearSelectedItems: PropTypes.func.isRequired, fillColor: PropTypes.string, fillColor2: PropTypes.string, + fillStyleGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)), handleMouseDown: PropTypes.func.isRequired, hoveredItemId: PropTypes.number, isFillModeActive: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, - selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, setHoveredItem: PropTypes.func.isRequired }; @@ -128,9 +128,9 @@ const mapStateToProps = state => ({ fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type fillColor: state.scratchPaint.color.fillColor.primary, fillColor2: state.scratchPaint.color.fillColor.secondary, + fillStyleGradientType: state.scratchPaint.color.fillColor.gradientType, // Selected item(s)' gradient type hoveredItemId: state.scratchPaint.hoveredItemId, - isFillModeActive: state.scratchPaint.mode === Modes.FILL, - selectModeGradientType: state.scratchPaint.color.gradientType + isFillModeActive: state.scratchPaint.mode === Modes.FILL }); const mapDispatchToProps = dispatch => ({ setHoveredItem: hoveredItemId => { diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index 445c20efa8..c5b99d99ff 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -6,11 +6,10 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeStrokeColor} from '../reducers/stroke-color'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -126,7 +125,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, setCursor: cursorString => { dispatch(setCursor(cursorString)); diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index 14fa80104f..f4929f9347 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -6,11 +6,10 @@ import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeStrokeColor} from '../reducers/stroke-color'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -126,7 +125,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, setSelectedItems: () => { dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */)); diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index d075eb772b..eda064d7c2 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -8,12 +8,11 @@ import Modes from '../lib/modes'; import {MIXED} from '../helper/style-path'; import {changeFont} from '../reducers/font'; -import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color'; +import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; import {changeStrokeColor} from '../reducers/stroke-color'; import {changeMode} from '../reducers/modes'; import {setTextEditTarget} from '../reducers/text-edit-target'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; -import {clearGradient} from '../reducers/selection-gradient-type'; import {setCursor} from '../reducers/cursor'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -188,7 +187,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ dispatch(clearSelectedItems()); }, clearGradient: () => { - dispatch(clearGradient()); + dispatch(clearFillGradient()); }, handleChangeModeBitText: () => { dispatch(changeMode(Modes.BIT_TEXT)); diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 319c2ea372..1a2a394a65 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -5,7 +5,7 @@ import {isGroup} from './group'; import {getItems} from './selection'; import GradientTypes from '../lib/gradient-types'; import parseColor from 'parse-color'; -import {DEFAULT_COLOR} from '../reducers/fill-color'; +import {DEFAULT_COLOR} from '../reducers/fill-style'; import {isCompoundPathChild} from '../helper/compound-path'; const MIXED = 'scratch-paint/style-path/mixed'; diff --git a/src/lib/make-color-reducer.js b/src/lib/make-color-style-reducer.js similarity index 57% rename from src/lib/make-color-reducer.js rename to src/lib/make-color-style-reducer.js index 786f24caef..79ca899cc4 100644 --- a/src/lib/make-color-reducer.js +++ b/src/lib/make-color-style-reducer.js @@ -1,6 +1,5 @@ import log from '../log/log'; import {CHANGE_SELECTED_ITEMS} from '../reducers/selected-items'; -import {CLEAR_GRADIENT} from '../reducers/selection-gradient-type'; import {getColorsFromSelection, MIXED} from '../helper/style-path'; import GradientTypes from './gradient-types'; @@ -15,18 +14,32 @@ const isValidHexColor = color => { return true; }; -const makeColorReducer = ({ +const makeColorStyleReducer = ({ + // Action name for changing the primary color changePrimaryColorAction, + // Action name for changing the secondary color changeSecondaryColorAction, + // Action name for changing the gradient type + changeGradientTypeAction, + // Action name for clearing the gradient + clearGradientAction, + // Initial color when not set defaultColor, + // The name of the property read from getColorsFromSelection to get the primary color. + // e.g. `fillColor` or `strokeColor`. selectionPrimaryColorKey, + // The name of the property read from getColorsFromSelection to get the secondary color. + // e.g. `fillColor2` or `strokeColor2`. selectionSecondaryColorKey, + // The name of the property read from getColorsFromSelection to get the gradient type. + // e.g. `fillGradientType` or `strokeGradientType`. selectionGradientTypeKey }) => function colorReducer (state, action) { if (typeof state === 'undefined') { state = { primary: defaultColor, - secondary: null + secondary: null, + gradientType: GradientTypes.SOLID }; } switch (action.type) { @@ -43,7 +56,11 @@ const makeColorReducer = ({ } const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode); - const newState = {...state, primary: colors[selectionPrimaryColorKey]}; + const newState = { + ...state, + primary: colors[selectionPrimaryColorKey], + gradientType: colors[selectionGradientTypeKey] + }; // Gradient type may be solid when multiple gradient types are selected. // In this case, changing the first color should not change the second color. @@ -52,11 +69,17 @@ const makeColorReducer = ({ } return newState; } - case CLEAR_GRADIENT: - return {...state, secondary: null}; + case changeGradientTypeAction: + if (action.gradientType in GradientTypes) { + return {...state, gradientType: action.gradientType}; + } + log.warn(`Gradient type does not exist: ${action.gradientType}`); + return state; + case clearGradientAction: + return {...state, secondary: null, gradientType: GradientTypes.SOLID}; default: return state; } }; -export default makeColorReducer; +export default makeColorStyleReducer; diff --git a/src/reducers/color-index.js b/src/reducers/color-index.js index c762ef8333..4520f627ac 100644 --- a/src/reducers/color-index.js +++ b/src/reducers/color-index.js @@ -1,5 +1,5 @@ import log from '../log/log'; -import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type'; +import {CHANGE_FILL_GRADIENT_TYPE} from './fill-style'; import GradientTypes from '../lib/gradient-types'; const CHANGE_COLOR_INDEX = 'scratch-paint/color-index/CHANGE_COLOR_INDEX'; @@ -14,7 +14,7 @@ const reducer = function (state, action) { return state; } return action.index; - case CHANGE_GRADIENT_TYPE: + case CHANGE_FILL_GRADIENT_TYPE: if (action.gradientType === GradientTypes.SOLID) return 0; /* falls through */ default: diff --git a/src/reducers/color.js b/src/reducers/color.js index ed641f8dc5..1ceb3aa435 100644 --- a/src/reducers/color.js +++ b/src/reducers/color.js @@ -1,14 +1,12 @@ import {combineReducers} from 'redux'; import eyeDropperReducer from './eye-dropper'; -import fillColorReducer from './fill-color'; -import gradientTypeReducer from './selection-gradient-type'; +import fillColorReducer from './fill-style'; import strokeColorReducer from './stroke-color'; import strokeWidthReducer from './stroke-width'; export default combineReducers({ eyeDropper: eyeDropperReducer, fillColor: fillColorReducer, - gradientType: gradientTypeReducer, strokeColor: strokeColorReducer, strokeWidth: strokeWidthReducer }); diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js deleted file mode 100644 index 4c234b661d..0000000000 --- a/src/reducers/fill-color.js +++ /dev/null @@ -1,36 +0,0 @@ -import makeColorReducer from '../lib/make-color-reducer'; - -const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR'; -const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2'; -const DEFAULT_COLOR = '#9966FF'; - -const reducer = makeColorReducer({ - changePrimaryColorAction: CHANGE_FILL_COLOR, - changeSecondaryColorAction: CHANGE_FILL_COLOR_2, - defaultColor: DEFAULT_COLOR, - selectionPrimaryColorKey: 'fillColor', - selectionSecondaryColorKey: 'fillColor2', - selectionGradientTypeKey: 'gradientType' -}); - -// Action creators ================================== -const changeFillColor = function (fillColor) { - return { - type: CHANGE_FILL_COLOR, - color: fillColor - }; -}; - -const changeFillColor2 = function (fillColor) { - return { - type: CHANGE_FILL_COLOR_2, - color: fillColor - }; -}; - -export { - reducer as default, - changeFillColor, - changeFillColor2, - DEFAULT_COLOR -}; diff --git a/src/reducers/fill-mode-gradient-type.js b/src/reducers/fill-mode-gradient-type.js index b19753b3b0..571740f4ab 100644 --- a/src/reducers/fill-mode-gradient-type.js +++ b/src/reducers/fill-mode-gradient-type.js @@ -2,14 +2,14 @@ // and isn't overwritten by changing the selection. import GradientTypes from '../lib/gradient-types'; import log from '../log/log'; +import {CHANGE_FILL_GRADIENT_TYPE} from './fill-style'; -const CHANGE_GRADIENT_TYPE = 'scratch-paint/fill-mode-gradient-type/CHANGE_GRADIENT_TYPE'; const initialState = null; const reducer = function (state, action) { if (typeof state === 'undefined') state = initialState; switch (action.type) { - case CHANGE_GRADIENT_TYPE: + case CHANGE_FILL_GRADIENT_TYPE: if (action.gradientType in GradientTypes) { return action.gradientType; } @@ -22,16 +22,15 @@ const reducer = function (state, action) { // Action creators ================================== // Use this for user-initiated gradient type selections only. -// See reducers/selection-gradient-type.js for other ways gradient type changes. +// See reducers/fill-style.js for other ways gradient type changes. const changeGradientType = function (gradientType) { return { - type: CHANGE_GRADIENT_TYPE, + type: CHANGE_FILL_GRADIENT_TYPE, gradientType: gradientType }; }; export { reducer as default, - CHANGE_GRADIENT_TYPE, changeGradientType }; diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js new file mode 100644 index 0000000000..b98b73f857 --- /dev/null +++ b/src/reducers/fill-style.js @@ -0,0 +1,56 @@ +import makeColorStyleReducer from '../lib/make-color-style-reducer'; + +const CHANGE_FILL_COLOR = 'scratch-paint/fill-style/CHANGE_FILL_COLOR'; +const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-style/CHANGE_FILL_COLOR_2'; +const CHANGE_FILL_GRADIENT_TYPE = 'scratch-paint/fill-style/CHANGE_FILL_GRADIENT_TYPE'; +const CLEAR_FILL_GRADIENT = 'scratch-paint/fill-style/CLEAR_FILL_GRADIENT'; +const DEFAULT_COLOR = '#9966FF'; + +const reducer = makeColorStyleReducer({ + changePrimaryColorAction: CHANGE_FILL_COLOR, + changeSecondaryColorAction: CHANGE_FILL_COLOR_2, + changeGradientTypeAction: CHANGE_FILL_GRADIENT_TYPE, + clearGradientAction: CLEAR_FILL_GRADIENT, + defaultColor: DEFAULT_COLOR, + selectionPrimaryColorKey: 'fillColor', + selectionSecondaryColorKey: 'fillColor2', + selectionGradientTypeKey: 'gradientType' +}); + +// Action creators ================================== +const changeFillColor = function (fillColor) { + return { + type: CHANGE_FILL_COLOR, + color: fillColor + }; +}; + +const changeFillColor2 = function (fillColor) { + return { + type: CHANGE_FILL_COLOR_2, + color: fillColor + }; +}; + +const changeFillGradientType = function (gradientType) { + return { + type: CHANGE_FILL_GRADIENT_TYPE, + gradientType: gradientType + }; +}; + +const clearFillGradient = function () { + return { + type: CLEAR_FILL_GRADIENT + }; +}; + +export { + reducer as default, + changeFillColor, + changeFillColor2, + changeFillGradientType, + clearFillGradient, + DEFAULT_COLOR, + CHANGE_FILL_GRADIENT_TYPE +}; diff --git a/src/reducers/selection-gradient-type.js b/src/reducers/selection-gradient-type.js deleted file mode 100644 index c0d979186d..0000000000 --- a/src/reducers/selection-gradient-type.js +++ /dev/null @@ -1,44 +0,0 @@ -// Gradient type shown in the select tool -import GradientTypes from '../lib/gradient-types'; -import {getColorsFromSelection} from '../helper/style-path'; -import {CHANGE_SELECTED_ITEMS} from './selected-items'; -import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type'; -import log from '../log/log'; - -const CLEAR_GRADIENT = 'scratch-paint/selection-gradient-type/CLEAR_GRADIENT'; -const initialState = GradientTypes.SOLID; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case CHANGE_GRADIENT_TYPE: - if (action.gradientType in GradientTypes) { - return action.gradientType; - } - log.warn(`Gradient type does not exist: ${action.gradientType}`); - return state; - case CLEAR_GRADIENT: - return GradientTypes.SOLID; - case CHANGE_SELECTED_ITEMS: - // Don't change state if no selection - if (!action.selectedItems || !action.selectedItems.length) { - return state; - } - return getColorsFromSelection(action.selectedItems, action.bitmapMode).gradientType; - default: - return state; - } -}; - -// Action creators ================================== -const clearGradient = function () { - return { - type: CLEAR_GRADIENT - }; -}; - -export { - reducer as default, - CLEAR_GRADIENT, - clearGradient -}; From dc430a0111b04439c44c82e3f24566889ba41dcc Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 25 May 2020 07:55:36 -0400 Subject: [PATCH 05/25] Fix setting of stroke colors in bitmap mode --- src/lib/make-color-style-reducer.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/make-color-style-reducer.js b/src/lib/make-color-style-reducer.js index 79ca899cc4..db71697987 100644 --- a/src/lib/make-color-style-reducer.js +++ b/src/lib/make-color-style-reducer.js @@ -56,15 +56,25 @@ const makeColorStyleReducer = ({ } const colors = getColorsFromSelection(action.selectedItems, action.bitmapMode); - const newState = { - ...state, - primary: colors[selectionPrimaryColorKey], - gradientType: colors[selectionGradientTypeKey] - }; + // Only set the primary color + gradient type if they exist in what getColorsFromSelection gave us. + // E.g. in bitmap mode, getColorsFromSelection will not return stroke color/gradient type. This allows us to + // preserve stroke swatch state across bitmap mode-- if getColorsFromSelection set them to null, then selecting + // anything in bitmap mode would overwrite the stroke state. + const newState = {...state}; + if (selectionPrimaryColorKey in colors) { + newState.primary = colors[selectionPrimaryColorKey]; + } + if (selectionGradientTypeKey in colors) { + newState.gradientType = colors[selectionGradientTypeKey]; + } // Gradient type may be solid when multiple gradient types are selected. // In this case, changing the first color should not change the second color. - if (colors[selectionGradientTypeKey] !== GradientTypes.SOLID || colors[selectionSecondaryColorKey] === MIXED) { + if ( + selectionSecondaryColorKey in colors && + (colors[selectionGradientTypeKey] !== GradientTypes.SOLID || + colors[selectionSecondaryColorKey] === MIXED) + ) { newState.secondary = colors[selectionSecondaryColorKey]; } return newState; From 1ecab99cfbaea28db8dbc8d44fa50e07c28a19a2 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 15 Apr 2020 09:51:07 -0400 Subject: [PATCH 06/25] Make style-path helpers generic over fill & stroke --- src/containers/fill-color-indicator.jsx | 7 +- src/containers/stroke-color-indicator.jsx | 11 +- src/containers/stroke-width-indicator.jsx | 10 +- src/helper/style-path.js | 302 ++++++++++++++-------- src/helper/tools/text-tool.js | 3 +- src/reducers/fill-style.js | 2 +- 6 files changed, 215 insertions(+), 120 deletions(-) diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 8ff198ef93..4329146286 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -16,7 +16,7 @@ import {isBitmap} from '../lib/format'; import GradientTypes from '../lib/gradient-types'; import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; -import {applyFillColorToSelection, +import {applyColorToSelection, applyGradientTypeToSelection, getRotatedColor, swapColorsInSelection, @@ -45,11 +45,12 @@ class FillColorIndicator extends React.Component { } handleChangeFillColor (newColor) { // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyFillColorToSelection( + 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); @@ -59,6 +60,7 @@ class FillColorIndicator extends React.Component { const isDifferent = applyGradientTypeToSelection( gradientType, isBitmap(this.props.format), + false, // applyToStroke this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; const hasSelectedItems = getSelectedLeafItems().length > 0; @@ -92,6 +94,7 @@ class FillColorIndicator extends React.Component { if (getSelectedLeafItems().length) { const isDifferent = swapColorsInSelection( isBitmap(this.props.format), + false, // applyToStroke this.props.textEditTarget); this.props.setSelectedItems(); this._hasChanged = this._hasChanged || isDifferent; diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index ea78e0cb83..57f68e3e9f 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -10,7 +10,7 @@ import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; -import {applyStrokeColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path'; +import {applyColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path'; class StrokeColorIndicator extends React.Component { constructor (props) { @@ -40,8 +40,13 @@ class StrokeColorIndicator extends React.Component { this.props.onChangeStrokeWidth(0); } // Apply color and update redux, but do not update svg until picker closes. - this._hasChanged = - applyStrokeColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget) || + this._hasChanged = applyColorToSelection( + newColor, + 0, // colorIndex, + true, // isSolidGradient + isBitmap(this.props.format), + true, // applyToStroke + this.props.textEditTarget) || this._hasChanged; this.props.onChangeStrokeColor(newColor); } diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index d224f44d59..164126770b 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -7,7 +7,7 @@ import {changeStrokeColor} from '../reducers/stroke-color'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {getSelectedLeafItems} from '../helper/selection'; -import {applyStrokeColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED} +import {applyColorToSelection, applyStrokeWidthToSelection, getColorsFromSelection, MIXED} from '../helper/style-path'; import Modes from '../lib/modes'; import Formats from '../lib/format'; @@ -25,7 +25,13 @@ class StrokeWidthIndicator extends React.Component { if ((!this.props.strokeWidth || this.props.strokeWidth === 0) && newWidth > 0) { let currentColor = getColorsFromSelection(getSelectedLeafItems(), isBitmap(this.props.format)).strokeColor; if (currentColor === null) { - changed = applyStrokeColorToSelection('#000', isBitmap(this.props.format), this.props.textEditTarget) || + changed = applyColorToSelection( + '#000', + 0, // colorIndex, + true, // isSolidGradient + isBitmap(this.props.format), + true, // applyToStroke + this.props.textEditTarget) || changed; currentColor = '#000'; } else if (currentColor !== MIXED) { diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 1a2a394a65..33cadf460c 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -7,13 +7,13 @@ import GradientTypes from '../lib/gradient-types'; import parseColor from 'parse-color'; import {DEFAULT_COLOR} from '../reducers/fill-style'; import {isCompoundPathChild} from '../helper/compound-path'; +import log from '../log/log'; const MIXED = 'scratch-paint/style-path/mixed'; // Check if the item color matches the incoming color. If the item color is a gradient, we assume // that the incoming color never matches, since we don't support gradients yet. const _colorMatch = function (itemColor, incomingColor) { - // @todo colorMatch should not be called with gradients as arguments once stroke gradients are supported if (itemColor && itemColor.type === 'gradient') return false; // Either both are null or both are the same color when converted to CSS. return (!itemColor && !incomingColor) || @@ -62,7 +62,8 @@ const getRotatedColor = function (firstColor) { * @param {?string} color2 CSS string, or null for transparent * @param {GradientType} gradientType gradient type * @param {paper.Rectangle} bounds Bounds of the object - * @param {paper.Point} radialCenter Where the center of a radial gradient should be, if the gradient is radial + * @param {?paper.Point} [radialCenter] Where the center of a radial gradient should be, if the gradient is radial. + * Defaults to center of bounds. * @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) { @@ -74,7 +75,7 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad color2 = getColorStringForTransparent(color1); } const halfLongestDimension = Math.max(bounds.width, bounds.height) / 2; - const start = gradientType === GradientTypes.RADIAL ? radialCenter : + const start = gradientType === GradientTypes.RADIAL ? (radialCenter || bounds.center) : gradientType === GradientTypes.VERTICAL ? bounds.topCenter : gradientType === GradientTypes.HORIZONTAL ? bounds.leftCenter : null; @@ -93,17 +94,25 @@ const createGradientObject = function (color1, color2, gradientType, bounds, rad }; /** - * Called when setting fill color + * Called when setting an item's color * @param {string} colorString color, css format, or null if completely transparent * @param {number} colorIndex index of color being changed * @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color * picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected. * When changing the color in this case, the solid gradient should override the existing gradient on the item. - * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode + * @param {?boolean} 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. */ -const applyFillColorToSelection = function (colorString, colorIndex, isSolidGradient, bitmapMode, textEditTargetId) { +const applyColorToSelection = function ( + colorString, + colorIndex, + isSolidGradient, + bitmapMode, + applyToStroke, + textEditTargetId +) { const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { @@ -112,40 +121,51 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad } // In bitmap mode, fill color applies to the stroke if there is a stroke - if (bitmapMode && item.strokeColor !== null && item.strokeWidth) { + if ( + bitmapMode && + !applyToStroke && + item.strokeColor !== null && + item.strokeWidth + ) { if (!_colorMatch(item.strokeColor, colorString)) { changed = true; item.strokeColor = colorString; } - } else if (isSolidGradient || !item.fillColor || !item.fillColor.gradient || - !item.fillColor.gradient.stops.length === 2) { + continue; + } + + const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor'; + const itemColor = item[itemColorProp]; + + if (isSolidGradient || !itemColor || !itemColor.gradient || + !itemColor.gradient.stops.length === 2) { // Applying a solid color - if (!_colorMatch(item.fillColor, colorString)) { + if (!_colorMatch(itemColor, colorString)) { changed = true; if (isPointTextItem(item) && !colorString) { // Allows transparent text to be hit - item.fillColor = 'rgba(0,0,0,0)'; + item[itemColorProp] = 'rgba(0,0,0,0)'; } else { - item.fillColor = colorString; + item[itemColorProp] = colorString; } } - } else if (!_colorMatch(item.fillColor.gradient.stops[colorIndex].color, colorString)) { + } else if (!_colorMatch(itemColor.gradient.stops[colorIndex].color, colorString)) { // Changing one color of an existing gradient changed = true; const otherIndex = colorIndex === 0 ? 1 : 0; if (colorString === null) { - colorString = getColorStringForTransparent(item.fillColor.gradient.stops[otherIndex].color.toCSS()); + colorString = getColorStringForTransparent(itemColor.gradient.stops[otherIndex].color.toCSS()); } const colors = [0, 0]; colors[colorIndex] = colorString; // If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth - if (item.fillColor.gradient.stops[otherIndex].color.alpha === 0) { + if (itemColor.gradient.stops[otherIndex].color.alpha === 0) { colors[otherIndex] = getColorStringForTransparent(colorString); } else { - colors[otherIndex] = item.fillColor.gradient.stops[otherIndex].color.toCSS(); + colors[otherIndex] = itemColor.gradient.stops[otherIndex].color.toCSS(); } // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. - item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial}; + itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial}; } } return changed; @@ -154,10 +174,13 @@ const applyFillColorToSelection = function (colorString, colorIndex, isSolidGrad /** * Called to swap gradient colors * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode + * @param {?boolean} applyToStroke True if changing the selection's stroke, false if changing its fill. * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const swapColorsInSelection = function (bitmapMode, textEditTargetId) { +const swapColorsInSelection = function (bitmapMode, applyToStroke, textEditTargetId) { + if (bitmapMode) return; // @todo + const items = _getColorStateListeners(textEditTargetId); let changed = false; for (const item of items) { @@ -166,21 +189,19 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) { // that would leave us right where we started. if (isCompoundPathChild(item)) continue; - if (bitmapMode) { - // @todo - return; - } else if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops.length === 2) { + const itemColor = applyToStroke ? item.strokeColor : item.fillColor; + if (!itemColor || !itemColor.gradient || !itemColor.gradient.stops.length === 2) { // Only one color; nothing to swap continue; - } else if (!item.fillColor.gradient.stops[0].color.equals(item.fillColor.gradient.stops[1].color)) { + } else if (!itemColor.gradient.stops[0].color.equals(itemColor.gradient.stops[1].color)) { // Changing one color of an existing gradient changed = true; const colors = [ - item.fillColor.gradient.stops[1].color.toCSS(), - item.fillColor.gradient.stops[0].color.toCSS() + itemColor.gradient.stops[1].color.toCSS(), + itemColor.gradient.stops[0].color.toCSS() ]; // There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient. - item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial}; + itemColor.gradient = {stops: colors, radial: itemColor.gradient.radial}; } } return changed; @@ -190,10 +211,11 @@ const swapColorsInSelection = function (bitmapMode, textEditTargetId) { * Called when setting gradient type * @param {GradientType} gradientType gradient type * @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode + * @param {boolean} applyToStroke True if changing the selection's stroke, false if changing its fill. * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEditTargetId) { +const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyToStroke, textEditTargetId) { const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { @@ -201,40 +223,45 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi item = item.parent; } + const itemColorProp = applyToStroke ? 'strokeColor' : 'fillColor'; + const itemColor = item[itemColorProp]; + + const hasGradient = itemColor && itemColor.gradient; + let itemColor1; - if (item.fillColor === null || item.fillColor.alpha === 0) { + if (itemColor === null || itemColor.alpha === 0) { // Transparent itemColor1 = null; - } else if (!item.fillColor.gradient) { + } else if (!hasGradient) { // Solid color - itemColor1 = item.fillColor.toCSS(); - } else if (!item.fillColor.gradient.stops[0] || item.fillColor.gradient.stops[0].color.alpha === 0) { + itemColor1 = itemColor.toCSS(); + } else if (!itemColor.gradient.stops[0] || itemColor.gradient.stops[0].color.alpha === 0) { // Gradient where first color is transparent itemColor1 = null; } else { // Gradient where first color is not transparent - itemColor1 = item.fillColor.gradient.stops[0].color.toCSS(); + itemColor1 = itemColor.gradient.stops[0].color.toCSS(); } let itemColor2; - if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops[1]) { + if (!hasGradient || !itemColor.gradient.stops[1]) { // If item color is solid or a gradient that has no 2nd color, set the 2nd color based on the first color itemColor2 = getRotatedColor(itemColor1); - } else if (item.fillColor.gradient.stops[1].color.alpha === 0) { + } else if (itemColor.gradient.stops[1].color.alpha === 0) { // Gradient has 2nd color which is transparent itemColor2 = null; } else { // Gradient has 2nd color which is not transparent - itemColor2 = item.fillColor.gradient.stops[1].color.toCSS(); + itemColor2 = itemColor.gradient.stops[1].color.toCSS(); } if (bitmapMode) { // @todo Add when we apply gradients to selections in bitmap mode continue; } else if (gradientType === GradientTypes.SOLID) { - if (item.fillColor && item.fillColor.gradient) { + if (itemColor && itemColor.gradient) { changed = true; - item.fillColor = itemColor1; + item[itemColorProp] = itemColor1; } continue; } @@ -245,12 +272,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi if (itemColor2 === null) { itemColor2 = getColorStringForTransparent(itemColor1); } - if (gradientType === GradientTypes.RADIAL) { - const hasRadialGradient = item.fillColor && item.fillColor.gradient && item.fillColor.gradient.radial; + + switch (gradientType) { + case GradientTypes.RADIAL: { + const hasRadialGradient = hasGradient && itemColor.gradient.radial; if (!hasRadialGradient) { changed = true; const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2; - item.fillColor = { + + item[itemColorProp] = { gradient: { stops: [itemColor1, itemColor2], radial: true @@ -259,13 +289,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi destination: item.position.add(new paper.Point(halfLongestDimension, 0)) }; } - } else if (gradientType === GradientTypes.HORIZONTAL) { - const hasHorizontalGradient = item.fillColor && item.fillColor.gradient && - !item.fillColor.gradient.radial && - Math.abs(item.fillColor.origin.y - item.fillColor.destination.y) < 1e-8; + break; + } + case GradientTypes.HORIZONTAL: { + const hasHorizontalGradient = hasGradient && !itemColor.gradient.radial && + Math.abs(itemColor.origin.y - itemColor.destination.y) < 1e-8; if (!hasHorizontalGradient) { changed = true; - item.fillColor = { + + item[itemColorProp] = { gradient: { stops: [itemColor1, itemColor2] }, @@ -273,12 +305,15 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi destination: item.bounds.rightCenter }; } - } else if (gradientType === GradientTypes.VERTICAL) { - const hasVerticalGradient = item.fillColor && item.fillColor.gradient && !item.fillColor.gradient.radial && - Math.abs(item.fillColor.origin.x - item.fillColor.destination.x) < 1e-8; + break; + } + case GradientTypes.VERTICAL: { + const hasVerticalGradient = hasGradient && !itemColor.gradient.radial && + Math.abs(itemColor.origin.x - itemColor.destination.x) < 1e-8; if (!hasVerticalGradient) { changed = true; - item.fillColor = { + + item[itemColorProp] = { gradient: { stops: [itemColor1, itemColor2] }, @@ -286,31 +321,8 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEdi destination: item.bounds.bottomCenter }; } + break; } - } - return changed; -}; - -/** - * Called when setting stroke color - * @param {string} colorString New color, css format - * @param {?boolean} bitmapMode True if the stroke color is being set in bitmap mode - * @param {?string} textEditTargetId paper.Item.id of text editing target, if any - * @return {boolean} Whether the color application actually changed visibly. - */ -const applyStrokeColorToSelection = function (colorString, bitmapMode, textEditTargetId) { - // Bitmap mode doesn't have stroke color - if (bitmapMode) return false; - - const items = _getColorStateListeners(textEditTargetId); - let changed = false; - for (let item of items) { - if (item.parent instanceof paper.CompoundPath) { - item = item.parent; - } - if (!_colorMatch(item.strokeColor, colorString)) { - changed = true; - item.strokeColor = colorString; } } return changed; @@ -339,6 +351,33 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) { return changed; }; +const _colorStateFromGradient = gradient => { + const colorState = {}; + // Scratch only recognizes 2 color gradients + if (gradient.stops.length === 2) { + if (gradient.radial) { + colorState.gradientType = GradientTypes.RADIAL; + } else { + // Always use horizontal for linear gradients, since horizontal and vertical gradients + // are the same with rotation. We don't want to show MIXED just because anything is rotated. + colorState.gradientType = GradientTypes.HORIZONTAL; + } + colorState.primary = gradient.stops[0].color.alpha === 0 ? + null : + gradient.stops[0].color.toCSS(); + colorState.secondary = gradient.stops[1].color.alpha === 0 ? + null : + gradient.stops[1].color.toCSS(); + } else { + if (gradient.stops.length < 2) log.warn(`Gradient has ${gradient.stops.length} stop(s)`); + + colorState.primary = MIXED; + colorState.secondary = MIXED; + } + + return colorState; +}; + /** * Get state of colors and stroke width for selection * @param {!Array} selectedItems Selected paper items @@ -349,12 +388,15 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) { * Thickness is line thickness, used in the bitmap editor */ const getColorsFromSelection = function (selectedItems, bitmapMode) { + // TODO: DRY out this code let selectionFillColorString; let selectionFillColor2String; let selectionStrokeColorString; + let selectionStrokeColor2String; let selectionStrokeWidth; let selectionThickness; - let selectionGradientType; + let selectionFillGradientType; + let selectionStrokeGradientType; let firstChild = true; for (let item of selectedItems) { @@ -365,7 +407,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { let itemFillColorString; let itemFillColor2String; let itemStrokeColorString; - let itemGradientType = GradientTypes.SOLID; + let itemStrokeColor2String; + let itemFillGradientType = GradientTypes.SOLID; + let itemStrokeGradientType = GradientTypes.SOLID; if (!isGroup(item)) { if (item.fillColor) { @@ -373,25 +417,10 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { if (isPointTextItem(item) && item.fillColor.alpha === 0) { itemFillColorString = null; } else if (item.fillColor.type === 'gradient') { - // Scratch only recognizes 2 color gradients - if (item.fillColor.gradient.stops.length === 2) { - if (item.fillColor.gradient.radial) { - itemGradientType = GradientTypes.RADIAL; - } else { - // Always use horizontal for linear gradients, since horizontal and vertical gradients - // are the same with rotation. We don't want to show MIXED just because anything is rotated. - itemGradientType = GradientTypes.HORIZONTAL; - } - itemFillColorString = item.fillColor.gradient.stops[0].color.alpha === 0 ? - null : - item.fillColor.gradient.stops[0].color.toCSS(); - itemFillColor2String = item.fillColor.gradient.stops[1].color.alpha === 0 ? - null : - item.fillColor.gradient.stops[1].color.toCSS(); - } else { - itemFillColorString = MIXED; - itemFillColor2String = MIXED; - } + const {primary, secondary, gradientType} = _colorStateFromGradient(item.fillColor.gradient); + itemFillColorString = primary; + itemFillColor2String = secondary; + itemFillGradientType = gradientType; } else { itemFillColorString = item.fillColor.alpha === 0 ? null : @@ -399,15 +428,34 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { } } if (item.strokeColor) { - // Stroke color is fill color in bitmap - if (bitmapMode) { - itemFillColorString = item.strokeColor.toCSS(); - } else if (item.strokeColor.type === 'gradient') { - itemStrokeColorString = MIXED; + + if (item.strokeColor.type === 'gradient') { + const {primary, secondary, gradientType} = _colorStateFromGradient(item.strokeColor.gradient); + const strokeColorString = primary; + const strokeColor2String = secondary; + const strokeGradientType = gradientType; + + // Stroke color is fill color in bitmap + if (bitmapMode) { + itemFillColorString = strokeColorString; + itemFillColor2String = strokeColor2String; + itemFillGradientType = strokeGradientType; + } else { + itemStrokeColorString = strokeColorString; + itemStrokeColor2String = strokeColor2String; + itemStrokeGradientType = strokeGradientType; + } } else { - itemStrokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ? + const strokeColorString = item.strokeColor.alpha === 0 || !item.strokeWidth ? null : item.strokeColor.toCSS(); + + // Stroke color is fill color in bitmap + if (bitmapMode) { + itemFillColorString = strokeColorString; + } else { + itemStrokeColorString = strokeColorString; + } } } else { itemStrokeColorString = null; @@ -418,7 +466,9 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { selectionFillColorString = itemFillColorString; selectionFillColor2String = itemFillColor2String; selectionStrokeColorString = itemStrokeColorString; - selectionGradientType = itemGradientType; + selectionStrokeColor2String = itemStrokeColor2String; + selectionFillGradientType = itemFillGradientType; + selectionStrokeGradientType = itemStrokeGradientType; selectionStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; if (item.strokeWidth && item.data && item.data.zoomLevel) { selectionThickness = item.strokeWidth / item.data.zoomLevel; @@ -430,14 +480,22 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { if (itemFillColor2String !== selectionFillColor2String) { selectionFillColor2String = MIXED; } - if (itemGradientType !== selectionGradientType) { - selectionGradientType = GradientTypes.SOLID; + if (itemFillGradientType !== selectionFillGradientType) { + selectionFillGradientType = GradientTypes.SOLID; selectionFillColorString = MIXED; selectionFillColor2String = MIXED; } + if (itemStrokeGradientType !== selectionStrokeGradientType) { + selectionStrokeGradientType = GradientTypes.SOLID; + selectionStrokeColorString = MIXED; + selectionStrokeColor2String = MIXED; + } if (itemStrokeColorString !== selectionStrokeColorString) { selectionStrokeColorString = MIXED; } + if (itemStrokeColor2String !== selectionStrokeColor2String) { + selectionStrokeColor2String = MIXED; + } const itemStrokeWidth = itemStrokeColorString ? item.strokeWidth : 0; if (selectionStrokeWidth !== itemStrokeWidth) { selectionStrokeWidth = null; @@ -445,27 +503,50 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { } } // Convert selection gradient type from horizontal to vertical if first item is exactly vertical - if (selectedItems && selectedItems.length && selectionGradientType !== GradientTypes.SOLID) { + // This is because up to this point, we assume all non-radial gradients are horizontal + // Otherwise, if there were a mix of horizontal/vertical gradient types in the selection, they would show as MIXED + // whereas we want them to show as horizontal (or vertical if the first item is vertical) + if (selectedItems && selectedItems.length) { let firstItem = selectedItems[0]; if (firstItem.parent instanceof paper.CompoundPath) firstItem = firstItem.parent; - const direction = firstItem.fillColor.destination.subtract(firstItem.fillColor.origin); - if (Math.abs(direction.angle) === 90) { - selectionGradientType = GradientTypes.VERTICAL; + + if (selectionFillGradientType !== GradientTypes.SOLID) { + // Stroke color is fill color in bitmap if fill color is missing + // TODO: this whole "treat horizontal/vertical gradients specially" logic is janky; refactor at some point + const firstItemColor = (bitmapMode && firstItem.strokeColor) ? firstItem.strokeColor : firstItem.fillColor; + const direction = firstItemColor.destination.subtract(firstItemColor.origin); + if (Math.abs(direction.angle) === 90) { + selectionFillGradientType = GradientTypes.VERTICAL; + } + } + + if (selectionStrokeGradientType !== GradientTypes.SOLID) { + const direction = firstItem.strokeColor.destination.subtract(firstItem.strokeColor.origin); + if (Math.abs(direction.angle) === 90) { + selectionStrokeGradientType = GradientTypes.VERTICAL; + } } } if (bitmapMode) { return { fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor2: selectionFillColor2String ? selectionFillColor2String : null, - gradientType: selectionGradientType, + fillGradientType: selectionFillGradientType, thickness: selectionThickness }; } + + // 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, - gradientType: selectionGradientType, + fillGradientType: selectionFillGradientType, strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null, + strokeColor2: selectionStrokeColor2String ? selectionStrokeColor2String : null, + strokeGradientType: selectionStrokeGradientType, strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0 }; }; @@ -510,9 +591,8 @@ const styleShape = function (path, options) { }; export { - applyFillColorToSelection, + applyColorToSelection, applyGradientTypeToSelection, - applyStrokeColorToSelection, applyStrokeWidthToSelection, createGradientObject, getColorsFromSelection, diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index d7188846ee..bba8bda7a4 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -241,7 +241,8 @@ class TextTool extends paper.Tool { content: '', font: this.font, fontSize: 40, - // TODO: style using gradient? + // TODO: style using gradient + // https://github.com/LLK/scratch-paint/issues/1164 fillColor: this.colorState.fillColor.primary, // Default leading for both the HTML text area and paper.PointText // is 120%, but for some reason they are slightly off from each other. diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js index b98b73f857..98046c49a1 100644 --- a/src/reducers/fill-style.js +++ b/src/reducers/fill-style.js @@ -14,7 +14,7 @@ const reducer = makeColorStyleReducer({ defaultColor: DEFAULT_COLOR, selectionPrimaryColorKey: 'fillColor', selectionSecondaryColorKey: 'fillColor2', - selectionGradientTypeKey: 'gradientType' + selectionGradientTypeKey: 'fillGradientType' }); // Action creators ================================== From f8da45500544c457ae88168335d949509a03e62e Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 23 Jul 2020 05:56:41 -0400 Subject: [PATCH 07/25] Use createGradientObject in applyGradientTypeToSelection --- src/helper/style-path.js | 50 ++++++++++++---------------------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/src/helper/style-path.js b/src/helper/style-path.js index 33cadf460c..b6af421ac9 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -273,57 +273,37 @@ const applyGradientTypeToSelection = function (gradientType, bitmapMode, applyTo itemColor2 = getColorStringForTransparent(itemColor1); } + let gradientTypeDiffers = false; + // If the item's gradient type differs from the gradient type we want to apply, then we change it switch (gradientType) { case GradientTypes.RADIAL: { const hasRadialGradient = hasGradient && itemColor.gradient.radial; - if (!hasRadialGradient) { - changed = true; - const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2; - - item[itemColorProp] = { - gradient: { - stops: [itemColor1, itemColor2], - radial: true - }, - origin: item.position, - destination: item.position.add(new paper.Point(halfLongestDimension, 0)) - }; - } + gradientTypeDiffers = !hasRadialGradient; break; } case GradientTypes.HORIZONTAL: { const hasHorizontalGradient = hasGradient && !itemColor.gradient.radial && Math.abs(itemColor.origin.y - itemColor.destination.y) < 1e-8; - if (!hasHorizontalGradient) { - changed = true; - - item[itemColorProp] = { - gradient: { - stops: [itemColor1, itemColor2] - }, - origin: item.bounds.leftCenter, - destination: item.bounds.rightCenter - }; - } + gradientTypeDiffers = !hasHorizontalGradient; break; } case GradientTypes.VERTICAL: { const hasVerticalGradient = hasGradient && !itemColor.gradient.radial && Math.abs(itemColor.origin.x - itemColor.destination.x) < 1e-8; - if (!hasVerticalGradient) { - changed = true; - - item[itemColorProp] = { - gradient: { - stops: [itemColor1, itemColor2] - }, - origin: item.bounds.topCenter, - destination: item.bounds.bottomCenter - }; - } + gradientTypeDiffers = !hasVerticalGradient; break; } } + + if (gradientTypeDiffers) { + changed = true; + item[itemColorProp] = createGradientObject( + itemColor1, + itemColor2, + gradientType, + item.bounds + ); + } } return changed; }; From a23fabd77815e81364eb5b86bce14c6655077e8f Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Jul 2020 18:00:12 -0400 Subject: [PATCH 08/25] Fix fill color reducer test --- test/unit/fill-color-reducer.test.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/unit/fill-color-reducer.test.js b/test/unit/fill-color-reducer.test.js index fb83043179..35fa60b095 100644 --- a/test/unit/fill-color-reducer.test.js +++ b/test/unit/fill-color-reducer.test.js @@ -1,8 +1,9 @@ /* eslint-env jest */ -import fillColorReducer from '../../src/reducers/fill-color'; -import {changeFillColor} from '../../src/reducers/fill-color'; +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', () => { @@ -16,16 +17,16 @@ test('changeFillColor', () => { // 3 value hex code let newFillColor = '#fff'; - expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */)) + expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary) .toEqual(newFillColor); - expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */)) + expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary) .toEqual(newFillColor); // 6 value hex code newFillColor = '#facade'; - expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */)) + expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */).primary) .toEqual(newFillColor); - expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */)) + expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */).primary) .toEqual(newFillColor); }); @@ -35,18 +36,18 @@ test('changefillColorViaSelectedItems', () => { const fillColor1 = 6; const fillColor2 = null; // transparent let selectedItems = [mockPaperRootItem({fillColor: fillColor1})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) .toEqual(fillColor1); selectedItems = [mockPaperRootItem({fillColor: fillColor2})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) .toEqual(fillColor2); selectedItems = [mockPaperRootItem({fillColor: fillColor1}), mockPaperRootItem({fillColor: fillColor2})]; - expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) + expect(fillColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) .toEqual(MIXED); }); test('invalidChangeFillColor', () => { - const origState = '#fff'; + const origState = {primary: '#fff', secondary: null, gradientType: GradientTypes.SOLID}; expect(fillColorReducer(origState /* state */, changeFillColor() /* action */)) .toBe(origState); From 6094953ef424fb512f59c2946b929923333ba597 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 15 Apr 2020 11:48:11 -0400 Subject: [PATCH 09/25] 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 | 146 ++-------------- 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, 337 insertions(+), 397 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 0a0fa0cf05..d04ea2bbf2 100644 --- a/src/components/fill-color-indicator.jsx +++ b/src/components/color-indicator.jsx @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Popover from 'react-popover'; -import {defineMessages, injectIntl, intlShape} from 'react-intl'; import ColorButton from './color-button/color-button.jsx'; import ColorPicker from '../containers/color-picker.jsx'; @@ -10,15 +9,7 @@ import Label from './forms/label.jsx'; import GradientTypes from '../lib/gradient-types'; -const messages = defineMessages({ - fill: { - id: 'paint.paintEditor.fill', - description: 'Label for the color picker for the fill color', - defaultMessage: 'Fill' - } -}); - -const FillColorIndicatorComponent = props => ( +const ColorIndicatorComponent = props => ( ( } - isOpen={props.fillColorModalVisible} + isOpen={props.colorModalVisible} preferPlace="below" - onOuterAction={props.onCloseFillColor} + onOuterAction={props.onCloseColor} > - ); -FillColorIndicatorComponent.propTypes = { +ColorIndicatorComponent.propTypes = { className: PropTypes.string, disabled: PropTypes.bool.isRequired, - fillColor: PropTypes.string, - fillColor2: PropTypes.string, - fillColorModalVisible: PropTypes.bool.isRequired, + color: PropTypes.string, + color2: PropTypes.string, + colorModalVisible: PropTypes.bool.isRequired, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, - intl: intlShape, - onChangeFillColor: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + onChangeColor: PropTypes.func.isRequired, onChangeGradientType: PropTypes.func.isRequired, - onCloseFillColor: PropTypes.func.isRequired, - onOpenFillColor: PropTypes.func.isRequired, + onCloseColor: PropTypes.func.isRequired, + onOpenColor: PropTypes.func.isRequired, onSwap: PropTypes.func.isRequired, + outline: PropTypes.bool.isRequired, shouldShowGradientTools: PropTypes.bool.isRequired }; -export default injectIntl(FillColorIndicatorComponent); +export default ColorIndicatorComponent; diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx deleted file mode 100644 index 15c40d9bd7..0000000000 --- a/src/components/stroke-color-indicator.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Popover from 'react-popover'; -import {defineMessages, injectIntl, intlShape} from 'react-intl'; - -import ColorButton from './color-button/color-button.jsx'; -import ColorPicker from '../containers/color-picker.jsx'; -import InputGroup from './input-group/input-group.jsx'; -import Label from './forms/label.jsx'; -import GradientTypes from '../lib/gradient-types'; - -const messages = defineMessages({ - stroke: { - id: 'paint.paintEditor.stroke', - description: 'Label for the color picker for the outline color', - defaultMessage: 'Outline' - } -}); - -const StrokeColorIndicatorComponent = props => ( - - - } - isOpen={props.strokeColorModalVisible} - preferPlace="below" - onOuterAction={props.onCloseStrokeColor} - > - - - -); - -StrokeColorIndicatorComponent.propTypes = { - className: PropTypes.string, - disabled: PropTypes.bool.isRequired, - intl: intlShape, - onChangeStrokeColor: PropTypes.func.isRequired, - onCloseStrokeColor: PropTypes.func.isRequired, - onOpenStrokeColor: PropTypes.func.isRequired, - strokeColor: PropTypes.string, - strokeColorModalVisible: PropTypes.bool.isRequired -}; - -export default injectIntl(StrokeColorIndicatorComponent); diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 32a80640b5..f1936ad617 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 0000000000..37e2b40a63 --- /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 4329146286..9d88070d54 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' - ]); - - // 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); +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.fill', + description: 'Label for the color picker for the fill color', + defaultMessage: 'Fill' } - handleCloseFillColor () { - // If the eyedropper is currently being used, don't - // close the fill color menu. - if (this.props.isEyeDropping) return; +}); - // Otherwise, close the fill color menu and - // also reset the color index to indicate - // that `color1` is selected. - this.props.onCloseFillColor(); - this.props.onChangeColorIndex(0); - } - handleSwap () { - if (getSelectedLeafItems().length) { - const isDifferent = swapColorsInSelection( - isBitmap(this.props.format), - 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 9f2d675ec5..2368c43b55 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 c5b99d99ff..8333b3f29c 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 f4929f9347..5730b0a64f 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 57f68e3e9f..409064c14d 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'; - -class StrokeColorIndicator extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleChangeStrokeColor', - 'handleCloseStrokeColor' - ]); +import makeColorIndicator from './color-indicator.jsx'; - // Flag to track whether an svg-update-worthy change has been made - this._hasChanged = false; - } - componentWillReceiveProps (newProps) { - const {strokeColorModalVisible, onUpdateImage} = this.props; - if (strokeColorModalVisible && !newProps.strokeColorModalVisible) { - // Submit the new SVG, which also stores a single undo/redo action. - if (this._hasChanged) onUpdateImage(); - this._hasChanged = false; - } - } - handleChangeStrokeColor (newColor) { - if (this.props.strokeColor === null && newColor !== null) { - this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged; - this.props.onChangeStrokeWidth(1); - } else if (this.props.strokeColor !== null && newColor === null) { - this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged; - this.props.onChangeStrokeWidth(0); - } - // Apply color and update redux, but do not update svg until picker closes. - this._hasChanged = 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(); - } +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.stroke', + description: 'Label for the color picker for the outline color', + defaultMessage: 'Outline' } - render () { - return ( - - ); - } -} +}); + +const StrokeColorIndicator = makeColorIndicator(messages.label, true); const mapStateToProps = state => ({ + colorIndex: state.scratchPaint.fillMode.colorIndex, disabled: state.scratchPaint.mode === Modes.BRUSH || state.scratchPaint.mode === Modes.TEXT || state.scratchPaint.mode === Modes.FILL, + color: state.scratchPaint.color.strokeColor.primary, + color2: state.scratchPaint.color.strokeColor.secondary, + 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 164126770b..31e4d88f79 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 eda064d7c2..ce270cd5aa 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 b6af421ac9..53ad35e3a3 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 0000000000..6e59603f63 --- /dev/null +++ b/src/lib/color-style-proptype.js @@ -0,0 +1,9 @@ +import {PropTypes} from 'prop-types'; + +import GradientTypes from './gradient-types'; + +export default PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string, + gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired +}); diff --git a/src/reducers/color.js b/src/reducers/color.js index 1ceb3aa435..4d50d49bed 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 98046c49a1..b97c4befdc 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 50cad337ff..0000000000 --- a/src/reducers/stroke-color.js +++ /dev/null @@ -1,51 +0,0 @@ -import log from '../log/log'; -import {CHANGE_SELECTED_ITEMS} from './selected-items'; -import {CHANGE_STROKE_WIDTH} from './stroke-width'; -import {getColorsFromSelection, MIXED} from '../helper/style-path'; - -const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-color/CHANGE_STROKE_COLOR'; -const initialState = '#000'; -// Matches hex colors -const regExp = /^#([0-9a-f]{3}){1,2}$/i; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case CHANGE_STROKE_WIDTH: - if (Math.max(0, action.strokeWidth) === 0) { - return null; - } - return state; - case CHANGE_STROKE_COLOR: - if (!regExp.test(action.strokeColor) && action.strokeColor !== null && action.strokeColor !== MIXED) { - log.warn(`Invalid hex color code: ${action.fillColor}`); - return state; - } - return action.strokeColor; - case CHANGE_SELECTED_ITEMS: - // Don't change state if no selection - if (!action.selectedItems || !action.selectedItems.length) { - return state; - } - // Bitmap mode doesn't have stroke color - if (action.bitmapMode) { - return state; - } - return getColorsFromSelection(action.selectedItems, action.bitmapMode).strokeColor; - default: - return state; - } -}; - -// Action creators ================================== -const changeStrokeColor = function (strokeColor) { - return { - type: CHANGE_STROKE_COLOR, - strokeColor: strokeColor - }; -}; - -export { - reducer as default, - changeStrokeColor -}; diff --git a/src/reducers/stroke-style.js b/src/reducers/stroke-style.js new file mode 100644 index 0000000000..72a1516896 --- /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 10/25] 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 0000000000..81183f8941 --- /dev/null +++ b/test/unit/color-reducer.test.js @@ -0,0 +1,86 @@ +/* eslint-env jest */ +import fillColorReducer, {changeFillColor} from '../../src/reducers/fill-style'; +import strokeColorReducer, {changeStrokeColor} from '../../src/reducers/stroke-style'; +import {setSelectedItems} from '../../src/reducers/selected-items'; +import {MIXED} from '../../src/helper/style-path'; +import GradientTypes from '../../src/lib/gradient-types'; +import {mockPaperRootItem} from '../__mocks__/paperMocks'; + +for (const [colorReducer, changeColor, colorProp] of [ + [fillColorReducer, changeFillColor, 'fillColor'], + [strokeColorReducer, changeStrokeColor, 'strokeColor'] +]) { + test('initialState', () => { + let defaultState; + + expect(colorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); + }); + + test('changeColor', () => { + let defaultState; + + // 3 value hex code + let newColor = '#fff'; + expect(colorReducer(defaultState /* state */, changeColor(newColor) /* action */).primary) + .toEqual(newColor); + expect(colorReducer({ + primary: '#010', + secondary: null, + gradientType: GradientTypes.SOLID + } /* state */, changeColor(newColor) /* action */).primary) + .toEqual(newColor); + + // 6 value hex code + newColor = '#facade'; + expect(colorReducer(defaultState /* state */, changeColor(newColor) /* action */).primary) + .toEqual(newColor); + expect(colorReducer({ + primary: '#010', + secondary: null, + gradientType: GradientTypes.SOLID + } /* state */, changeColor(newColor) /* action */).primary) + .toEqual(newColor); + }); + + test('changeColorViaSelectedItems', () => { + let defaultState; + + const color1 = 6; + const color2 = null; // transparent + let selectedItems = [mockPaperRootItem({[colorProp]: color1, strokeWidth: 1})]; + + expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) + .toEqual(color1); + selectedItems = [mockPaperRootItem({[colorProp]: color2, strokeWidth: 1})]; + expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) + .toEqual(color2); + selectedItems = [ + mockPaperRootItem({[colorProp]: color1, strokeWidth: 1}), + mockPaperRootItem({[colorProp]: color2, strokeWidth: 1}) + ]; + expect(colorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */).primary) + .toEqual(MIXED); + }); + + test('invalidChangeColor', () => { + const origState = {primary: '#fff', secondary: null, gradientType: GradientTypes.SOLID}; + + expect(colorReducer(origState /* state */, changeColor() /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#1') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#12') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#1234') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#12345') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('#1234567') /* action */)) + .toBe(origState); + expect(colorReducer(origState /* state */, changeColor('invalid argument') /* action */)) + .toBe(origState); + }); + +} diff --git a/test/unit/fill-color-reducer.test.js b/test/unit/fill-color-reducer.test.js deleted file mode 100644 index 35fa60b095..0000000000 --- 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 25b5522237..0000000000 --- a/test/unit/stroke-color-reducer.test.js +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-env jest */ -import strokeColorReducer from '../../src/reducers/stroke-color'; -import {changeStrokeColor} from '../../src/reducers/stroke-color'; -import {setSelectedItems} from '../../src/reducers/selected-items'; -import {MIXED} from '../../src/helper/style-path'; -import {mockPaperRootItem} from '../__mocks__/paperMocks'; - -test('initialState', () => { - let defaultState; - - expect(strokeColorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); -}); - -test('changeStrokeColor', () => { - let defaultState; - - // 3 value hex code - let newStrokeColor = '#fff'; - expect(strokeColorReducer(defaultState /* state */, changeStrokeColor(newStrokeColor) /* action */)) - .toEqual(newStrokeColor); - expect(strokeColorReducer('#010' /* state */, changeStrokeColor(newStrokeColor) /* action */)) - .toEqual(newStrokeColor); - - // 6 value hex code - newStrokeColor = '#facade'; - expect(strokeColorReducer(defaultState /* state */, changeStrokeColor(newStrokeColor) /* action */)) - .toEqual(newStrokeColor); - expect(strokeColorReducer('#010' /* state */, changeStrokeColor(newStrokeColor) /* action */)) - .toEqual(newStrokeColor); -}); - -test('changeStrokeColorViaSelectedItems', () => { - let defaultState; - - const strokeColor1 = 6; - const strokeColor2 = null; // transparent - let selectedItems = [mockPaperRootItem({strokeColor: strokeColor1, strokeWidth: 1})]; - expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) - .toEqual(strokeColor1); - selectedItems = [mockPaperRootItem({strokeColor: strokeColor2, strokeWidth: 1})]; - expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) - .toEqual(strokeColor2); - selectedItems = [mockPaperRootItem({strokeColor: strokeColor1, strokeWidth: 1}), - mockPaperRootItem({strokeColor: strokeColor2, strokeWidth: 1})]; - expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) - .toEqual(MIXED); -}); - -test('showNoStrokeColorIfNoStrokeWidth', () => { - let defaultState; - - let selectedItems = [mockPaperRootItem({strokeColor: '#fff', strokeWidth: null})]; - expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) - .toEqual(null); - selectedItems = [mockPaperRootItem({strokeColor: '#fff', strokeWidth: 0})]; - expect(strokeColorReducer(defaultState /* state */, setSelectedItems(selectedItems) /* action */)) - .toEqual(null); -}); - -test('invalidChangeStrokeColor', () => { - const origState = '#fff'; - - expect(strokeColorReducer(origState /* state */, changeStrokeColor() /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#12') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1234') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#12345') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('#1234567') /* action */)) - .toBe(origState); - expect(strokeColorReducer(origState /* state */, changeStrokeColor('invalid argument') /* action */)) - .toBe(origState); -}); From df1989d0b0acb849283811a125c93b980835dc57 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Mon, 25 May 2020 08:20:43 -0400 Subject: [PATCH 11/25] 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 9d88070d54..3d9276c870 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 8333b3f29c..03678acd7c 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 5730b0a64f..df8c8a3efc 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 409064c14d..45360c3cfd 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 53ad35e3a3..c1222e0b44 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 1246a705b1..f1d3208a49 100644 --- a/src/helper/tools/oval-tool.js +++ b/src/helper/tools/oval-tool.js @@ -111,6 +111,8 @@ class OvalTool extends paper.Tool { } else { this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5)); } + + styleShape(this.oval, this.colorState); } handleMouseMove (event) { this.boundingBoxTool.onMouseMove(event, this.getHitOptions()); From 2eab5048ace34d945d951c06c919da181c4dc129 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 5 Jun 2020 15:18:39 -0400 Subject: [PATCH 12/25] 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 314efe6845..60b97aeaa3 100644 --- a/src/helper/hover.js +++ b/src/helper/hover.js @@ -35,12 +35,17 @@ const getHoveredItem = function (event, hitOptions, subselect) { return null; } + let hoverGuide; if (isBoundsItem(item)) { - return hoverBounds(item); + hoverGuide = hoverBounds(item); } else if (!subselect && isGroupChild(item)) { - return hoverBounds(getRootItem(item)); + hoverGuide = hoverBounds(getRootItem(item)); + } else { + hoverGuide = hoverItem(item); } - return hoverItem(item); + hoverGuide.data.hitResult = hitResult; + + return hoverGuide; }; export { From c81853b1b7b073e6ff59fd029b74bd128f3604d1 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 5 Jun 2020 16:03:03 -0400 Subject: [PATCH 13/25] 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 c82352d0b0..05bb998eeb 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 14/25] 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 c1222e0b44..c9482ca9c7 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 15/25] 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 2afdba68bd..c7ae45a2ec 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 f65af52568..64aa6b7873 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 16/25] 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 edd7ebb223..bf3ae380c1 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 61aa53fa3f..6263953a6b 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 37e2b40a63..762b57ddee 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 3d9276c870..f370427615 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 45360c3cfd..5fafaff162 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 c7ae45a2ec..f13f595b69 100644 --- a/src/helper/bit-tools/oval-tool.js +++ b/src/helper/bit-tools/oval-tool.js @@ -1,5 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; +import {styleShape} from '../style-path'; import {commitOvalToBitmap} from '../bitmap'; import {getRaster} from '../layer'; import {clearSelection} from '../selection'; @@ -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 64aa6b7873..e31aeedbaa 100644 --- a/src/helper/bit-tools/rect-tool.js +++ b/src/helper/bit-tools/rect-tool.js @@ -1,5 +1,6 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; +import {styleShape} from '../../helper/style-path'; import {commitRectToBitmap} from '../bitmap'; import {getRaster} from '../layer'; import {clearSelection} from '../selection'; @@ -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 b4b7303c3d..b070e2a215 100644 --- a/src/helper/bitmap.js +++ b/src/helper/bitmap.js @@ -275,8 +275,24 @@ const drawEllipse = function (options, context) { if (!matrix.isInvertible()) return false; const inverse = matrix.clone().invert(); + const isGradient = context.fillStyle instanceof CanvasGradient; + + // If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas + // only where the shape appears. drawShearedEllipse draws some pixels twice, which would be a problem if the + // gradient fades to transparent as those pixels would end up looking more opaque. Instead, mask in the gradient. + // https://github.com/LLK/scratch-paint/issues/1152 + // Outlines are drawn as a series of brush mark images and as such can't be drawn as gradients in the first place. + let origContext; + let tmpCanvas; + const {width: canvasWidth, height: canvasHeight} = context.canvas; + if (isGradient) { + tmpCanvas = createCanvas(canvasWidth, canvasHeight); + origContext = context; + context = tmpCanvas.getContext('2d'); + } + if (!isFilled) { - const brushMark = getBrushMark(thickness, context.fillStyle); + const brushMark = getBrushMark(thickness, isGradient ? 'black' : context.fillStyle); const roundedUpRadius = Math.ceil(thickness / 2); drawFn = (x, y) => { context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius); @@ -295,7 +311,7 @@ const drawEllipse = function (options, context) { const radiusA = Math.sqrt(-4 * C / ((B * B) - (4 * A * C))); const slope = B / 2 / C; - return drawShearedEllipse_({ + const wasDrawn = drawShearedEllipse_({ centerX: positionX, centerY: positionY, radiusX: radiusA, @@ -304,6 +320,17 @@ const drawEllipse = function (options, context) { isFilled: isFilled, drawFn: drawFn }, context); + + // Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the + // original canvas normally. + if (isGradient && wasDrawn) { + context.globalCompositeOperation = 'source-in'; + context.fillStyle = origContext.fillStyle; + context.fillRect(0, 0, canvasWidth, canvasHeight); + origContext.drawImage(tmpCanvas, 0, 0); + } + + return wasDrawn; }; const rowBlank_ = function (imageData, width, y) { @@ -658,6 +685,20 @@ const outlineRect = function (rect, thickness, context) { context.drawImage(brushMark, ~~x - roundedUpRadius, ~~y - roundedUpRadius); }; + const isGradient = context.fillStyle instanceof CanvasGradient; + + // If drawing a gradient, we need to draw the shape onto a temporary canvas, then draw the gradient atop that canvas + // only where the shape appears. Outlines are drawn as a series of brush mark images and as such can't be drawn as + // gradients. + let origContext; + let tmpCanvas; + const {width: canvasWidth, height: canvasHeight} = context.canvas; + if (isGradient) { + tmpCanvas = createCanvas(canvasWidth, canvasHeight); + origContext = context; + context = tmpCanvas.getContext('2d'); + } + const startPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, -rect.size.height / 2)); const widthPoint = rect.matrix.transform(new paper.Point(rect.size.width / 2, -rect.size.height / 2)); const heightPoint = rect.matrix.transform(new paper.Point(-rect.size.width / 2, rect.size.height / 2)); @@ -667,6 +708,16 @@ const outlineRect = function (rect, thickness, context) { forEachLinePoint(startPoint, heightPoint, drawFn); forEachLinePoint(endPoint, widthPoint, drawFn); forEachLinePoint(endPoint, heightPoint, drawFn); + + // Mask in the gradient only where the shape was drawn, and draw it. Then draw the gradientified shape onto the + // original canvas normally. + if (isGradient) { + context.globalCompositeOperation = 'source-in'; + context.fillStyle = origContext.fillStyle; + context.fillRect(0, 0, canvasWidth, canvasHeight); + origContext.drawImage(tmpCanvas, 0, 0); + } + }; const flipBitmapHorizontal = function (canvas) { @@ -773,6 +824,62 @@ const commitSelectionToBitmap = function (selection, bitmap) { commitArbitraryTransformation_(selection, bitmap); }; +/** + * Converts a Paper.js color style (an item's fillColor or strokeColor) into a canvas-applicable color style. + * Note that a "color style" as applied to an item is different from a plain paper.Color or paper.Gradient. + * For instance, a gradient "color style" has origin and destination points whereas an unattached paper.Gradient + * does not. + * @param {paper.Color} color The color to convert to a canvas color/gradient + * @param {CanvasRenderingContext2D} context The rendering context on which the style will be used + * @returns {string|CanvasGradient} The canvas fill/stroke style. + */ +const _paperColorToCanvasStyle = function (color, context) { + if (!color) return null; + if (color.type === 'gradient') { + let canvasGradient; + const {origin, destination} = color; + if (color.gradient.radial) { + // Adapted from: + // https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L926-L935 + let {highlight} = color; + const start = highlight || origin; + const radius = destination.getDistance(origin); + if (highlight) { + const vector = highlight.subtract(origin); + if (vector.getLength() > radius) { + // Paper ¯\_(ツ)_/¯ + highlight = origin.add(vector.normalize(radius - 0.1)); + } + } + canvasGradient = context.createRadialGradient( + start.x, start.y, + 0, + origin.x, origin.y, + radius + ); + } else { + canvasGradient = context.createLinearGradient( + origin.x, origin.y, + destination.x, destination.y + ); + } + + const {stops} = color.gradient; + // Adapted from: + // https://github.com/paperjs/paper.js/blob/b081fd72c72cd61331313c3961edb48f3dfaffbd/src/style/Color.js#L940-L950 + for (let i = 0, len = stops.length; i < len; i++) { + const stop = stops[i]; + const offset = stop.offset; + canvasGradient.addColorStop( + offset || i / (len - 1), + stop.color.toCSS() + ); + } + return canvasGradient; + } + return color.toCSS(); +}; + /** * @param {paper.Shape.Ellipse} oval Vector oval to convert * @param {paper.Raster} bitmap raster to draw selection @@ -784,12 +891,12 @@ const commitOvalToBitmap = function (oval, bitmap) { const context = bitmap.getContext('2d'); const filled = oval.strokeWidth === 0; - const canvasColor = filled ? oval.fillColor : oval.strokeColor; - // If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything, - // and especially don't try calling `toCSS` on it + const canvasColor = _paperColorToCanvasStyle(filled ? oval.fillColor : oval.strokeColor, context); + // If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything if (!canvasColor) return; - context.fillStyle = canvasColor.toCSS(); + context.fillStyle = canvasColor; + const drew = drawEllipse({ position: oval.position, radiusX, @@ -811,12 +918,12 @@ const commitRectToBitmap = function (rect, bitmap) { const context = tmpCanvas.getContext('2d'); const filled = rect.strokeWidth === 0; - const canvasColor = filled ? rect.fillColor : rect.strokeColor; - // If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything, - // and especially don't try calling `toCSS` on it + const canvasColor = _paperColorToCanvasStyle(filled ? rect.fillColor : rect.strokeColor, context); + // If the color is null (e.g. fully transparent/"no fill"), don't bother drawing anything if (!canvasColor) return; - context.fillStyle = canvasColor.toCSS(); + context.fillStyle = canvasColor; + if (filled) { fillRect(rect, context); } else { diff --git a/src/helper/style-path.js b/src/helper/style-path.js index c9482ca9c7..98561f621a 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 17/25] 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 762b57ddee..b825c3c417 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 31e4d88f79..30d9c0a9a2 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 98561f621a..2d023136c9 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 18/25] 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 f370427615..fc86f31f72 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 5fafaff162..b912bcc3dd 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 48e9017cb0..dc7145ad1c 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 19/25] 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 b825c3c417..a6cabe50e7 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 20/25] 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 2d023136c9..1c75653707 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 21/25] 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 2368c43b55..4a67111073 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 1c75653707..17e0ae771b 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 dc7145ad1c..3a0934ff6b 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 22/25] 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 17e0ae771b..7b258e8042 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 23/25] 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 30d9c0a9a2..ed27ff967e 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -3,12 +3,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import parseColor from 'parse-color'; -import {changeStrokeColor} from '../reducers/stroke-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 72a1516896..1d61135609 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 24/25] 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 05bb998eeb..a34638b785 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 25/25] 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 7b258e8042..2f4ac02136 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 ); } }