Mode tools
diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx
new file mode 100644
index 00000000..71531314
--- /dev/null
+++ b/src/components/stroke-color-indicator.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
+import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
+import Label from './forms/label.jsx';
+import Input from './forms/input.jsx';
+
+import styles from './paint-editor.css';
+
+const BufferedInput = BufferedInputHOC(Input);
+const messages = defineMessages({
+ stroke: {
+ id: 'paint.paintEditor.stroke',
+ description: 'Label for the color picker for the outline color',
+ defaultMessage: 'Outline'
+ }
+});
+const StrokeColorIndicatorComponent = props => (
+
+
+
+);
+
+StrokeColorIndicatorComponent.propTypes = {
+ intl: intlShape,
+ onChangeStrokeColor: PropTypes.func.isRequired,
+ strokeColor: PropTypes.string.isRequired
+};
+
+export default injectIntl(StrokeColorIndicatorComponent);
diff --git a/src/containers/blob/blob.js b/src/containers/blob/blob.js
index 22f4cda5..30bbdc95 100644
--- a/src/containers/blob/blob.js
+++ b/src/containers/blob/blob.js
@@ -39,6 +39,9 @@ class Blobbiness {
* @param {!number} options.brushSize Width of blob marking made by mouse
* @param {!boolean} options.isEraser Whether the stroke should be treated as an erase path. If false,
* the stroke is an additive path.
+ * @param {?string} options.fillColor Color of the brush stroke.
+ * @param {?string} options.strokeColor Color of the brush outline.
+ * @param {?number} options.strokeWidth Width of the brush outline.
*/
setOptions (options) {
this.options = options;
@@ -51,6 +54,9 @@ class Blobbiness {
* @param {!number} options.brushSize Width of blob marking made by mouse
* @param {!boolean} options.isEraser Whether the stroke should be treated as an erase path. If false,
* the stroke is an additive path.
+ * @param {?string} options.fillColor Color of the brush stroke.
+ * @param {?string} options.strokeColor Color of the brush outline.
+ * @param {?number} options.strokeWidth Width of the brush outline.
*/
activateTool (options) {
this.tool = new paper.Tool();
@@ -61,7 +67,7 @@ class Blobbiness {
const blob = this;
this.tool.onMouseMove = function (event) {
blob.resizeCursorIfNeeded(event.point);
- styleCursorPreview(blob.cursorPreview, blob.options.isEraser);
+ styleCursorPreview(blob.cursorPreview, blob.options);
blob.cursorPreview.bringToFront();
blob.cursorPreview.position = event.point;
};
@@ -141,7 +147,10 @@ class Blobbiness {
this.cursorPreviewLastPoint = point;
}
- if (this.cursorPreview && this.brushSize === this.options.brushSize) {
+ if (this.cursorPreview &&
+ this.brushSize === this.options.brushSize &&
+ this.fillColor === this.options.fillColor &&
+ this.strokeColor === this.options.strokeColor) {
return;
}
const newPreview = new paper.Path.Circle({
@@ -149,13 +158,13 @@ class Blobbiness {
radius: this.options.brushSize / 2
});
if (this.cursorPreview) {
- this.cursorPreview.segments = newPreview.segments;
- newPreview.remove();
- } else {
- this.cursorPreview = newPreview;
- styleCursorPreview(this.cursorPreview, this.options.isEraser);
+ this.cursorPreview.remove();
}
this.brushSize = this.options.brushSize;
+ this.fillColor = this.options.fillColor;
+ this.strokeColor = this.options.strokeColor;
+ this.cursorPreview = newPreview;
+ styleCursorPreview(this.cursorPreview, this.options);
}
mergeBrush (lastPath) {
diff --git a/src/containers/blob/broad-brush-helper.js b/src/containers/blob/broad-brush-helper.js
index c9cfee25..87af4f43 100644
--- a/src/containers/blob/broad-brush-helper.js
+++ b/src/containers/blob/broad-brush-helper.js
@@ -25,7 +25,7 @@ class BroadBrushHelper {
if (event.event.button > 0) return; // only first mouse button
this.finalPath = new paper.Path();
- stylePath(this.finalPath, options.isEraser);
+ stylePath(this.finalPath, options);
this.finalPath.add(event.point);
this.lastPoint = this.secondLastPoint = event.point;
}
@@ -77,7 +77,7 @@ class BroadBrushHelper {
center: event.point,
radius: options.brushSize / 2
});
- stylePath(this.finalPath, options.isEraser);
+ stylePath(this.finalPath, options);
} else {
const step = (event.point.subtract(this.lastPoint)).normalize(options.brushSize / 2);
step.angle += 90;
diff --git a/src/containers/blob/segment-brush-helper.js b/src/containers/blob/segment-brush-helper.js
index c745b9d8..6ccd48ce 100644
--- a/src/containers/blob/segment-brush-helper.js
+++ b/src/containers/blob/segment-brush-helper.js
@@ -32,7 +32,7 @@ class SegmentBrushHelper {
radius: options.brushSize / 2
});
this.finalPath = this.firstCircle;
- stylePath(this.finalPath, options.isEraser);
+ stylePath(this.finalPath, options);
this.lastPoint = event.point;
}
@@ -46,9 +46,7 @@ class SegmentBrushHelper {
const path = new paper.Path();
- // TODO: Add back brush styling
- // path = pg.stylebar.applyActiveToolbarStyle(path);
- path.fillColor = 'black';
+ stylePath(path, options);
// Add handles to round the end caps
path.add(new paper.Segment(this.lastPoint.subtract(step), handleVec.multiply(-1), handleVec));
diff --git a/src/containers/blob/style-path.js b/src/containers/blob/style-path.js
index e4380869..658d2f77 100644
--- a/src/containers/blob/style-path.js
+++ b/src/containers/blob/style-path.js
@@ -1,24 +1,22 @@
-const stylePath = function (path, isEraser) {
- if (isEraser) {
+const stylePath = function (path, options) {
+ if (options.isEraser) {
path.fillColor = 'white';
} else {
// TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen.
// path = pg.stylebar.applyActiveToolbarStyle(path);
- path.fillColor = 'black';
+ path.fillColor = options.fillColor;
}
};
-const styleCursorPreview = function (path, isEraser) {
- if (isEraser) {
+const styleCursorPreview = function (path, options) {
+ if (options.isEraser) {
path.fillColor = 'white';
path.strokeColor = 'cornflowerblue';
path.strokeWidth = 1;
} else {
// TODO: Add back brush styling. Keep a separate active toolbar style for brush vs pen.
// path = pg.stylebar.applyActiveToolbarStyle(path);
- path.fillColor = 'black';
- path.strokeColor = 'cornflowerblue';
- path.strokeWidth = 1;
+ path.fillColor = options.fillColor;
}
};
diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx
index c89e08f9..5ff4e8fb 100644
--- a/src/containers/brush-mode.jsx
+++ b/src/containers/brush-mode.jsx
@@ -29,7 +29,11 @@ class BrushMode extends React.Component {
} else if (!nextProps.isBrushModeActive && this.props.isBrushModeActive) {
this.deactivateTool();
} else if (nextProps.isBrushModeActive && this.props.isBrushModeActive) {
- this.blob.setOptions({isEraser: false, ...nextProps.brushModeState});
+ this.blob.setOptions({
+ isEraser: false,
+ ...nextProps.colorState,
+ ...nextProps.brushModeState
+ });
}
}
shouldComponentUpdate () {
@@ -42,7 +46,11 @@ class BrushMode extends React.Component {
// TODO: This is temporary until a component that provides the brush size is hooked up
this.props.canvas.addEventListener('mousewheel', this.onScroll);
- this.blob.activateTool({isEraser: false, ...this.props.brushModeState});
+ this.blob.activateTool({
+ isEraser: false,
+ ...this.props.colorState,
+ ...this.props.brushModeState
+ });
}
deactivateTool () {
this.props.canvas.removeEventListener('mousewheel', this.onScroll);
@@ -69,6 +77,10 @@ BrushMode.propTypes = {
}),
canvas: PropTypes.instanceOf(Element).isRequired,
changeBrushSize: PropTypes.func.isRequired,
+ colorState: PropTypes.shape({
+ fillColor: PropTypes.string.isRequired,
+ strokeColor: PropTypes.string.isRequired
+ }).isRequired,
handleMouseDown: PropTypes.func.isRequired,
isBrushModeActive: PropTypes.bool.isRequired,
onUpdateSvg: PropTypes.func.isRequired
@@ -76,6 +88,7 @@ BrushMode.propTypes = {
const mapStateToProps = state => ({
brushModeState: state.scratchPaint.brushMode,
+ colorState: state.scratchPaint.color,
isBrushModeActive: state.scratchPaint.mode === Modes.BRUSH
});
const mapDispatchToProps = dispatch => ({
diff --git a/src/containers/eraser-mode.jsx b/src/containers/eraser-mode.jsx
index 0972ed36..62d5b4fe 100644
--- a/src/containers/eraser-mode.jsx
+++ b/src/containers/eraser-mode.jsx
@@ -29,7 +29,10 @@ class EraserMode extends React.Component {
} else if (!nextProps.isEraserModeActive && this.props.isEraserModeActive) {
this.deactivateTool();
} else if (nextProps.isEraserModeActive && this.props.isEraserModeActive) {
- this.blob.setOptions({isEraser: true, ...nextProps.eraserModeState});
+ this.blob.setOptions({
+ isEraser: true,
+ ...nextProps.eraserModeState
+ });
}
}
shouldComponentUpdate () {
diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx
new file mode 100644
index 00000000..494be917
--- /dev/null
+++ b/src/containers/fill-color-indicator.jsx
@@ -0,0 +1,17 @@
+import {connect} from 'react-redux';
+import {changeFillColor} from '../reducers/fill-color';
+import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
+
+const mapStateToProps = state => ({
+ fillColor: state.scratchPaint.color.fillColor
+});
+const mapDispatchToProps = dispatch => ({
+ onChangeFillColor: fillColor => {
+ dispatch(changeFillColor(fillColor));
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(FillColorIndicatorComponent);
diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx
index 7648c045..fe2f0163 100644
--- a/src/containers/line-mode.jsx
+++ b/src/containers/line-mode.jsx
@@ -100,9 +100,8 @@ class LineMode extends React.Component {
if (!this.path) {
this.path = new paper.Path();
- // TODO add back style
- // this.path = pg.stylebar.applyActiveToolbarStyle(path);
- this.path.setStrokeColor('black');
+ // TODO add back stroke width styling
+ this.path.setStrokeColor(this.props.colorState.strokeColor);
this.path.setStrokeWidth(this.props.lineModeState.lineWidth);
this.path.setSelected(true);
@@ -277,6 +276,10 @@ class LineMode extends React.Component {
LineMode.propTypes = {
canvas: PropTypes.instanceOf(Element).isRequired,
changeLineWidth: PropTypes.func.isRequired,
+ colorState: PropTypes.shape({
+ fillColor: PropTypes.string.isRequired,
+ strokeColor: PropTypes.string.isRequired
+ }).isRequired,
handleMouseDown: PropTypes.func.isRequired,
isLineModeActive: PropTypes.bool.isRequired,
lineModeState: PropTypes.shape({
@@ -286,6 +289,7 @@ LineMode.propTypes = {
};
const mapStateToProps = state => ({
+ colorState: state.scratchPaint.color,
lineModeState: state.scratchPaint.lineMode,
isLineModeActive: state.scratchPaint.mode === Modes.LINE
});
diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx
new file mode 100644
index 00000000..add989bf
--- /dev/null
+++ b/src/containers/stroke-color-indicator.jsx
@@ -0,0 +1,17 @@
+import {connect} from 'react-redux';
+import {changeStrokeColor} from '../reducers/stroke-color';
+import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
+
+const mapStateToProps = state => ({
+ strokeColor: state.scratchPaint.color.strokeColor
+});
+const mapDispatchToProps = dispatch => ({
+ onChangeStrokeColor: strokeColor => {
+ dispatch(changeStrokeColor(strokeColor));
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(StrokeColorIndicatorComponent);
diff --git a/src/reducers/color.js b/src/reducers/color.js
new file mode 100644
index 00000000..62bba3a1
--- /dev/null
+++ b/src/reducers/color.js
@@ -0,0 +1,8 @@
+import {combineReducers} from 'redux';
+import fillColorReducer from './fill-color';
+import strokeColorReducer from './stroke-color';
+
+export default combineReducers({
+ fillColor: fillColorReducer,
+ strokeColor: strokeColorReducer
+});
diff --git a/src/reducers/fill-color.js b/src/reducers/fill-color.js
new file mode 100644
index 00000000..74bf4d6c
--- /dev/null
+++ b/src/reducers/fill-color.js
@@ -0,0 +1,33 @@
+import log from '../log/log';
+
+const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_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_FILL_COLOR:
+ if (!regExp.test(action.fillColor)) {
+ log.warn(`Invalid hex color code: ${action.fillColor}`);
+ return state;
+ }
+ return action.fillColor;
+ default:
+ return state;
+ }
+};
+
+// Action creators ==================================
+const changeFillColor = function (fillColor) {
+ return {
+ type: CHANGE_FILL_COLOR,
+ fillColor: fillColor
+ };
+};
+
+export {
+ reducer as default,
+ changeFillColor
+};
diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js
index 4f06e1bd..613bcfdb 100644
--- a/src/reducers/scratch-paint-reducer.js
+++ b/src/reducers/scratch-paint-reducer.js
@@ -3,10 +3,12 @@ import modeReducer from './modes';
import brushModeReducer from './brush-mode';
import eraserModeReducer from './eraser-mode';
import lineModeReducer from './line-mode';
+import colorReducer from './color';
export default combineReducers({
mode: modeReducer,
brushMode: brushModeReducer,
eraserMode: eraserModeReducer,
- lineMode: lineModeReducer
+ lineMode: lineModeReducer,
+ color: colorReducer
});
diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js
new file mode 100644
index 00000000..15efc21e
--- /dev/null
+++ b/src/reducers/stroke-color.js
@@ -0,0 +1,33 @@
+import log from '../log/log';
+
+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_COLOR:
+ if (!regExp.test(action.strokeColor)) {
+ log.warn(`Invalid hex color code: ${action.fillColor}`);
+ return state;
+ }
+ return action.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/test/unit/fill-color-reducer.test.js b/test/unit/fill-color-reducer.test.js
new file mode 100644
index 00000000..dddd9008
--- /dev/null
+++ b/test/unit/fill-color-reducer.test.js
@@ -0,0 +1,48 @@
+/* eslint-env jest */
+import fillColorReducer from '../../src/reducers/fill-color';
+import {changeFillColor} from '../../src/reducers/fill-color';
+
+test('initialState', () => {
+ let defaultState;
+
+ expect(fillColorReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
+});
+
+test('changeFillColor', () => {
+ let defaultState;
+
+ // 3 value hex code
+ let newFillColor = '#fff';
+ expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */))
+ .toEqual(newFillColor);
+ expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */))
+ .toEqual(newFillColor);
+
+ // 6 value hex code
+ newFillColor = '#facade';
+ expect(fillColorReducer(defaultState /* state */, changeFillColor(newFillColor) /* action */))
+ .toEqual(newFillColor);
+ expect(fillColorReducer('#010' /* state */, changeFillColor(newFillColor) /* action */))
+ .toEqual(newFillColor);
+});
+
+test('invalidChangeFillColor', () => {
+ const origState = '#fff';
+
+ expect(fillColorReducer(origState /* state */, changeFillColor() /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#1') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#12') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#1234') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#12345') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('#1234567') /* action */))
+ .toBe(origState);
+ expect(fillColorReducer(origState /* state */, changeFillColor('invalid argument') /* action */))
+ .toBe(origState);
+});
diff --git a/test/unit/stroke-color-reducer.test.js b/test/unit/stroke-color-reducer.test.js
new file mode 100644
index 00000000..7f812299
--- /dev/null
+++ b/test/unit/stroke-color-reducer.test.js
@@ -0,0 +1,48 @@
+/* eslint-env jest */
+import strokeColorReducer from '../../src/reducers/stroke-color';
+import {changeStrokeColor} from '../../src/reducers/stroke-color';
+
+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('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);
+});