Merge pull request #11 from fsih/fillStrokeColor

Add fill and stroke color state
This commit is contained in:
DD Liu 2017-09-11 14:22:16 -04:00 committed by GitHub
commit 1d9760a476
18 changed files with 339 additions and 59 deletions

View file

@ -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({
fill: {
id: 'paint.paintEditor.fill',
description: 'Label for the color picker for the fill color',
defaultMessage: 'Fill'
}
});
const FillColorIndicatorComponent = props => (
<div className={styles.inputGroup}>
<Label text={props.intl.formatMessage(messages.fill)}>
<BufferedInput
type="text"
value={props.fillColor}
onSubmit={props.onChangeFillColor}
/>
</Label>
</div>
);
FillColorIndicatorComponent.propTypes = {
fillColor: PropTypes.string.isRequired,
intl: intlShape,
onChangeFillColor: PropTypes.func.isRequired
};
export default injectIntl(FillColorIndicatorComponent);

View file

@ -5,6 +5,8 @@ import BrushMode from '../containers/brush-mode.jsx';
import EraserMode from '../containers/eraser-mode.jsx';
import PropTypes from 'prop-types';
import LineMode from '../containers/line-mode.jsx';
import FillColorIndicatorComponent from '../containers/fill-color-indicator.jsx';
import StrokeColorIndicatorComponent from '../containers/stroke-color-indicator.jsx';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
@ -19,16 +21,6 @@ const messages = defineMessages({
id: 'paint.paintEditor.costume',
description: 'Label for the name of a sound',
defaultMessage: 'Costume'
},
fill: {
id: 'paint.paintEditor.fill',
description: 'Label for the color picker for the fill color',
defaultMessage: 'Fill'
},
outline: {
id: 'paint.paintEditor.outline',
description: 'Label for the color picker for the outline color',
defaultMessage: 'Outline'
}
});
@ -52,7 +44,6 @@ class PaintEditorComponent extends React.Component {
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.costume)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
@ -106,26 +97,10 @@ class PaintEditorComponent extends React.Component {
{/* Second Row */}
<div className={styles.row}>
{/* To be fill */}
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.fill)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
</Label>
</div>
{/* To be stroke */}
<div className={styles.inputGroup}>
<Label text={this.props.intl.formatMessage(messages.outline)}>
<BufferedInput
tabIndex="1"
type="text"
value="meow"
/>
</Label>
</div>
{/* fill */}
<FillColorIndicatorComponent />
{/* stroke */}
<StrokeColorIndicatorComponent />
<div className={styles.inputGroup}>
Mode tools

View file

@ -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 => (
<div className={styles.inputGroup}>
<Label text={props.intl.formatMessage(messages.stroke)}>
<BufferedInput
type="text"
value={props.strokeColor}
onSubmit={props.onChangeStrokeColor}
/>
</Label>
</div>
);
StrokeColorIndicatorComponent.propTypes = {
intl: intlShape,
onChangeStrokeColor: PropTypes.func.isRequired,
strokeColor: PropTypes.string.isRequired
};
export default injectIntl(StrokeColorIndicatorComponent);

View file

@ -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) {

View file

@ -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;

View file

@ -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));

View file

@ -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;
}
};

View file

@ -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 => ({

View file

@ -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 () {

View file

@ -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);

View file

@ -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
});

View file

@ -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);

8
src/reducers/color.js Normal file
View file

@ -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
});

View file

@ -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
};

View file

@ -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
});

View file

@ -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
};

View file

@ -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);
});

View file

@ -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);
});