mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Initial prototype of color picker
This commit is contained in:
parent
8ffb95c02d
commit
3eaf8047aa
15 changed files with 550 additions and 36 deletions
|
@ -55,6 +55,7 @@
|
||||||
"minilog": "3.1.0",
|
"minilog": "3.1.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"paper": "0.11.4",
|
"paper": "0.11.4",
|
||||||
|
"parse-color": "1.0.0",
|
||||||
"postcss-import": "^10.0.0",
|
"postcss-import": "^10.0.0",
|
||||||
"postcss-loader": "^2.0.5",
|
"postcss-loader": "^2.0.5",
|
||||||
"postcss-simple-vars": "^4.0.0",
|
"postcss-simple-vars": "^4.0.0",
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
"react-dom": "16.0.0",
|
"react-dom": "16.0.0",
|
||||||
"react-intl": "2.4.0",
|
"react-intl": "2.4.0",
|
||||||
"react-intl-redux": "0.6.0",
|
"react-intl-redux": "0.6.0",
|
||||||
|
"react-popover": "0.5.4",
|
||||||
"react-redux": "5.0.5",
|
"react-redux": "5.0.5",
|
||||||
"react-test-renderer": "^16.0.0",
|
"react-test-renderer": "^16.0.0",
|
||||||
"redux": "3.7.0",
|
"redux": "3.7.0",
|
||||||
|
|
32
src/components/color-button.css
Normal file
32
src/components/color-button.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.color-button {
|
||||||
|
height: 2rem;
|
||||||
|
width: 3rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-button-swatch {
|
||||||
|
display: flex;
|
||||||
|
flex-basis: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-button-arrow {
|
||||||
|
display: flex;
|
||||||
|
flex-basis: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||||
|
border-left: none;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #575e75;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
26
src/components/color-button.jsx
Normal file
26
src/components/color-button.jsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import styles from './color-button.css';
|
||||||
|
|
||||||
|
const ColorButtonComponent = props => (
|
||||||
|
<div
|
||||||
|
className={styles.colorButton}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.colorButtonSwatch}
|
||||||
|
style={{
|
||||||
|
background: props.color
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={styles.colorButtonArrow}>▾</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ColorButtonComponent.propTypes = {
|
||||||
|
color: PropTypes.string,
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorButtonComponent;
|
50
src/components/color-picker.css
Normal file
50
src/components/color-picker.css
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/* Popover styles */
|
||||||
|
/* @TODO need to fix tip border issue */
|
||||||
|
:global(.Popover-body) {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
:global(.Popover-tipShape) {
|
||||||
|
fill: white;
|
||||||
|
stroke: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-header {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, sans-serif;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #575E75;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-readout {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-swatch {
|
||||||
|
border: 1px solid #4C97FF;
|
||||||
|
box-shadow: 0px 0px 0px 3px hsla(215, 100%, 65%, 0.2);
|
||||||
|
}
|
194
src/components/color-picker.jsx
Normal file
194
src/components/color-picker.jsx
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import parseColor from 'parse-color';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
|
import {MIXED} from '../helper/style-path';
|
||||||
|
|
||||||
|
import Slider from './forms/slider.jsx';
|
||||||
|
import styles from './color-picker.css';
|
||||||
|
|
||||||
|
const colorStringToHsv = hexString => {
|
||||||
|
const hsv = parseColor(hexString).hsv;
|
||||||
|
// Hue comes out in [0, 360], limit to [0, 100]
|
||||||
|
hsv[0] = hsv[0] / 3.6;
|
||||||
|
// Black is parsed as {0, 0, 0}, but turn saturation up to 100
|
||||||
|
// to make it easier to see slider values.
|
||||||
|
if (hsv[1] === 0 && hsv[2] === 0) {
|
||||||
|
hsv[1] = 100;
|
||||||
|
}
|
||||||
|
return hsv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hsvToHex = (h, s, v) =>
|
||||||
|
// Scale hue back up to [0, 360] from [0, 100]
|
||||||
|
parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex
|
||||||
|
;
|
||||||
|
|
||||||
|
// Important! This component ignores new color props and cannot be updated
|
||||||
|
// This is to make the HSV <=> RGB conversion stable. Because of this, the
|
||||||
|
// component MUST be unmounted in order to change the props externally.
|
||||||
|
class ColorPickerComponent extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleHueChange',
|
||||||
|
'handleSaturationChange',
|
||||||
|
'handleBrightnessChange',
|
||||||
|
'handleTransparent'
|
||||||
|
]);
|
||||||
|
const isTransparent = this.props.color === null;
|
||||||
|
const isMixed = this.props.color === MIXED;
|
||||||
|
const hsv = isTransparent || isMixed ?
|
||||||
|
[50, 100, 100] : colorStringToHsv(props.color);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hue: hsv[0],
|
||||||
|
saturation: hsv[1],
|
||||||
|
brightness: hsv[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps () {
|
||||||
|
// Just a reminder, new props do not update the hsv state
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHueChange (hue) {
|
||||||
|
this.setState({hue: hue});
|
||||||
|
this.handleColorChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaturationChange (saturation) {
|
||||||
|
this.setState({saturation: saturation});
|
||||||
|
this.handleColorChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBrightnessChange (brightness) {
|
||||||
|
this.setState({brightness: brightness});
|
||||||
|
this.handleColorChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorChange () {
|
||||||
|
this.props.onChangeColor(hsvToHex(
|
||||||
|
this.state.hue,
|
||||||
|
this.state.saturation,
|
||||||
|
this.state.brightness
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTransparent () {
|
||||||
|
this.props.onChangeColor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeBackground (channel) {
|
||||||
|
const stops = [];
|
||||||
|
for (let n = 100; n >= 0; n -= 10) {
|
||||||
|
switch (channel) {
|
||||||
|
case 'hue':
|
||||||
|
stops.push(hsvToHex(n, this.state.saturation, this.state.brightness));
|
||||||
|
break;
|
||||||
|
case 'saturation':
|
||||||
|
stops.push(hsvToHex(this.state.hue, n, this.state.brightness));
|
||||||
|
break;
|
||||||
|
case 'brightness':
|
||||||
|
stops.push(hsvToHex(this.state.hue, this.state.saturation, n));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown channel for color sliders: ${channel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `linear-gradient(to left, ${stops.join(',')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className={styles.colorPickerContainer}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.rowHeader}>
|
||||||
|
<span className={styles.labelName}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Hue"
|
||||||
|
description="Label for the hue component in the color picker"
|
||||||
|
id="paint.paintEditor.hue"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={styles.labelReadout}>
|
||||||
|
{Math.round(this.state.hue)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rowSlider}>
|
||||||
|
<Slider
|
||||||
|
background={this._makeBackground('hue')}
|
||||||
|
value={this.state.hue}
|
||||||
|
onChange={this.handleHueChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.rowHeader}>
|
||||||
|
<span className={styles.labelName}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Saturation"
|
||||||
|
description="Label for the saturation component in the color picker"
|
||||||
|
id="paint.paintEditor.saturation"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={styles.labelReadout}>
|
||||||
|
{Math.round(this.state.saturation)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rowSlider}>
|
||||||
|
<Slider
|
||||||
|
background={this._makeBackground('saturation')}
|
||||||
|
value={this.state.saturation}
|
||||||
|
onChange={this.handleSaturationChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.rowHeader}>
|
||||||
|
<span className={styles.labelName}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Brightness"
|
||||||
|
description="Label for the brightness component in the color picker"
|
||||||
|
id="paint.paintEditor.brightness"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className={styles.labelReadout}>
|
||||||
|
{Math.round(this.state.brightness)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rowSlider}>
|
||||||
|
<Slider
|
||||||
|
background={this._makeBackground('brightness')}
|
||||||
|
value={this.state.brightness}
|
||||||
|
onChange={this.handleBrightnessChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.swatches}>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
[styles.swatch]: true,
|
||||||
|
[styles.activeSwatch]: this.props.color === null
|
||||||
|
})}
|
||||||
|
onClick={this.handleTransparent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ColorPickerComponent.propTypes = {
|
||||||
|
color: PropTypes.string,
|
||||||
|
onChangeColor: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorPickerComponent;
|
|
@ -1,16 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Popover from 'react-popover';
|
||||||
|
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||||
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
|
|
||||||
import Label from './forms/label.jsx';
|
import Label from './forms/label.jsx';
|
||||||
import Input from './forms/input.jsx';
|
import ColorPicker from './color-picker.jsx';
|
||||||
|
import ColorButton from './color-button.jsx';
|
||||||
import {MIXED} from '../helper/style-path';
|
|
||||||
|
|
||||||
import styles from './paint-editor.css';
|
import styles from './paint-editor.css';
|
||||||
|
|
||||||
const BufferedInput = BufferedInputHOC(Input);
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
fill: {
|
fill: {
|
||||||
id: 'paint.paintEditor.fill',
|
id: 'paint.paintEditor.fill',
|
||||||
|
@ -18,23 +16,37 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Fill'
|
defaultMessage: 'Fill'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const FillColorIndicatorComponent = props => (
|
const FillColorIndicatorComponent = props => (
|
||||||
<div className={styles.inputGroup}>
|
<div className={styles.inputGroup}>
|
||||||
<Label text={props.intl.formatMessage(messages.fill)}>
|
<Popover
|
||||||
<BufferedInput
|
body={
|
||||||
type="text"
|
<ColorPicker
|
||||||
value={props.fillColor === MIXED ? 'mixed' :
|
color={props.fillColor}
|
||||||
props.fillColor === null ? 'transparent' : props.fillColor} // @todo Don't use text
|
onChangeColor={props.onChangeFillColor}
|
||||||
onSubmit={props.onChangeFillColor}
|
/>
|
||||||
/>
|
}
|
||||||
</Label>
|
isOpen={props.fillColorModalVisible}
|
||||||
|
preferPlace="below"
|
||||||
|
onOuterAction={props.onCloseFillColor}
|
||||||
|
>
|
||||||
|
<Label text={props.intl.formatMessage(messages.fill)}>
|
||||||
|
<ColorButton
|
||||||
|
color={props.fillColor}
|
||||||
|
onClick={props.onOpenFillColor}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
FillColorIndicatorComponent.propTypes = {
|
FillColorIndicatorComponent.propTypes = {
|
||||||
fillColor: PropTypes.string,
|
fillColor: PropTypes.string,
|
||||||
|
fillColorModalVisible: PropTypes.bool.isRequired,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
onChangeFillColor: PropTypes.func.isRequired
|
onChangeFillColor: PropTypes.func.isRequired,
|
||||||
|
onCloseFillColor: PropTypes.func.isRequired,
|
||||||
|
onOpenFillColor: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(FillColorIndicatorComponent);
|
export default injectIntl(FillColorIndicatorComponent);
|
||||||
|
|
20
src/components/forms/slider.css
Normal file
20
src/components/forms/slider.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.container {
|
||||||
|
margin: 8px;
|
||||||
|
height: 22px;
|
||||||
|
width: 150px;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 11px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
left: 100px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-top: -2px;
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
79
src/components/forms/slider.jsx
Normal file
79
src/components/forms/slider.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import bindAll from 'lodash.bindall';
|
||||||
|
|
||||||
|
import styles from './slider.css';
|
||||||
|
|
||||||
|
const CONTAINER_WIDTH = 150;
|
||||||
|
const HANDLE_WIDTH = 26;
|
||||||
|
|
||||||
|
class SliderComponent extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleMouseDown',
|
||||||
|
'handleMouseUp',
|
||||||
|
'handleMouseMove',
|
||||||
|
'setBackground'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown () {
|
||||||
|
document.addEventListener('mouseup', this.handleMouseUp);
|
||||||
|
document.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp () {
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const backgroundBBox = this.background.getBoundingClientRect();
|
||||||
|
const x = event.clientX - backgroundBBox.left;
|
||||||
|
this.props.onChange(Math.max(1, Math.min(99, 100 * x / backgroundBBox.width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackground (ref) {
|
||||||
|
this.background = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const halfHandleWidth = HANDLE_WIDTH / 2;
|
||||||
|
const pixelMin = halfHandleWidth;
|
||||||
|
const pixelMax = CONTAINER_WIDTH - halfHandleWidth;
|
||||||
|
const handleOffset = pixelMin +
|
||||||
|
((pixelMax - pixelMin) * (this.props.value / 100)) -
|
||||||
|
halfHandleWidth;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
ref={this.setBackground}
|
||||||
|
style={{
|
||||||
|
backgroundImage: this.props.background
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.handle}
|
||||||
|
style={{
|
||||||
|
left: `${handleOffset}px`
|
||||||
|
}}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderComponent.propTypes = {
|
||||||
|
background: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
SliderComponent.defaultProps = {
|
||||||
|
background: 'yellow'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SliderComponent;
|
|
@ -1,16 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Popover from 'react-popover';
|
||||||
|
|
||||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||||
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
|
|
||||||
import Label from './forms/label.jsx';
|
import Label from './forms/label.jsx';
|
||||||
import Input from './forms/input.jsx';
|
import ColorPicker from './color-picker.jsx';
|
||||||
|
import ColorButton from './color-button.jsx';
|
||||||
import {MIXED} from '../helper/style-path';
|
|
||||||
|
|
||||||
import styles from './paint-editor.css';
|
import styles from './paint-editor.css';
|
||||||
|
|
||||||
const BufferedInput = BufferedInputHOC(Input);
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
stroke: {
|
stroke: {
|
||||||
id: 'paint.paintEditor.stroke',
|
id: 'paint.paintEditor.stroke',
|
||||||
|
@ -18,24 +16,37 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Outline'
|
defaultMessage: 'Outline'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const StrokeColorIndicatorComponent = props => (
|
const StrokeColorIndicatorComponent = props => (
|
||||||
<div className={styles.inputGroup}>
|
<div className={styles.inputGroup}>
|
||||||
<Label text={props.intl.formatMessage(messages.stroke)}>
|
<Popover
|
||||||
<BufferedInput
|
body={
|
||||||
type="text"
|
<ColorPicker
|
||||||
// @todo Don't use text
|
color={props.strokeColor}
|
||||||
value={props.strokeColor === MIXED ? 'mixed' :
|
onChangeColor={props.onChangeStrokeColor}
|
||||||
props.strokeColor === null ? 'transparent' : props.strokeColor}
|
/>
|
||||||
onSubmit={props.onChangeStrokeColor}
|
}
|
||||||
/>
|
isOpen={props.strokeColorModalVisible}
|
||||||
</Label>
|
preferPlace="below"
|
||||||
|
onOuterAction={props.onCloseStrokeColor}
|
||||||
|
>
|
||||||
|
<Label text={props.intl.formatMessage(messages.stroke)}>
|
||||||
|
<ColorButton
|
||||||
|
color={props.strokeColor}
|
||||||
|
onClick={props.onOpenStrokeColor}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
StrokeColorIndicatorComponent.propTypes = {
|
StrokeColorIndicatorComponent.propTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||||
strokeColor: PropTypes.string
|
onCloseStrokeColor: PropTypes.func.isRequired,
|
||||||
|
onOpenStrokeColor: PropTypes.func.isRequired,
|
||||||
|
strokeColor: PropTypes.string,
|
||||||
|
strokeColorModalVisible: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(StrokeColorIndicatorComponent);
|
export default injectIntl(StrokeColorIndicatorComponent);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import {changeFillColor} from '../reducers/fill-color';
|
import {changeFillColor} from '../reducers/fill-color';
|
||||||
|
import {openFillColor, closeFillColor} from '../reducers/modals';
|
||||||
|
|
||||||
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
||||||
import {applyFillColorToSelection} from '../helper/style-path';
|
import {applyFillColorToSelection} from '../helper/style-path';
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ class FillColorIndicator extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<FillColorIndicatorComponent
|
<FillColorIndicatorComponent
|
||||||
fillColor={this.props.fillColor}
|
{...this.props}
|
||||||
onChangeFillColor={this.handleChangeFillColor}
|
onChangeFillColor={this.handleChangeFillColor}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -28,11 +30,19 @@ class FillColorIndicator extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
fillColor: state.scratchPaint.color.fillColor
|
fillColor: state.scratchPaint.color.fillColor,
|
||||||
|
fillColorModalVisible: state.scratchPaint.modals.fillColor
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeFillColor: fillColor => {
|
onChangeFillColor: fillColor => {
|
||||||
dispatch(changeFillColor(fillColor));
|
dispatch(changeFillColor(fillColor));
|
||||||
|
},
|
||||||
|
onOpenFillColor: () => {
|
||||||
|
dispatch(openFillColor());
|
||||||
|
},
|
||||||
|
onCloseFillColor: () => {
|
||||||
|
dispatch(closeFillColor());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import bindAll from 'lodash.bindall';
|
import bindAll from 'lodash.bindall';
|
||||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
import {changeStrokeColor} from '../reducers/stroke-color';
|
||||||
|
import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
|
||||||
|
|
||||||
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
||||||
import {applyStrokeColorToSelection} from '../helper/style-path';
|
import {applyStrokeColorToSelection} from '../helper/style-path';
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ class StrokeColorIndicator extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<StrokeColorIndicatorComponent
|
<StrokeColorIndicatorComponent
|
||||||
strokeColor={this.props.strokeColor}
|
{...this.props}
|
||||||
onChangeStrokeColor={this.handleChangeStrokeColor}
|
onChangeStrokeColor={this.handleChangeStrokeColor}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -28,11 +30,19 @@ class StrokeColorIndicator extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
strokeColor: state.scratchPaint.color.strokeColor
|
strokeColor: state.scratchPaint.color.strokeColor,
|
||||||
|
strokeColorModalVisible: state.scratchPaint.modals.strokeColor
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeStrokeColor: strokeColor => {
|
onChangeStrokeColor: strokeColor => {
|
||||||
dispatch(changeStrokeColor(strokeColor));
|
dispatch(changeStrokeColor(strokeColor));
|
||||||
|
},
|
||||||
|
onOpenStrokeColor: () => {
|
||||||
|
dispatch(openStrokeColor());
|
||||||
|
},
|
||||||
|
onCloseStrokeColor: () => {
|
||||||
|
dispatch(closeStrokeColor());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {CHANGE_SELECTED_ITEMS} from './selected-items';
|
||||||
import {getColorsFromSelection} from '../helper/style-path';
|
import {getColorsFromSelection} from '../helper/style-path';
|
||||||
|
|
||||||
const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR';
|
const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR';
|
||||||
const initialState = '#000';
|
const initialState = '#aa0551';
|
||||||
// Matches hex colors
|
// Matches hex colors
|
||||||
const regExp = /^#([0-9a-f]{3}){1,2}$/i;
|
const regExp = /^#([0-9a-f]{3}){1,2}$/i;
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ const reducer = function (state, action) {
|
||||||
if (typeof state === 'undefined') state = initialState;
|
if (typeof state === 'undefined') state = initialState;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CHANGE_FILL_COLOR:
|
case CHANGE_FILL_COLOR:
|
||||||
if (!regExp.test(action.fillColor)) {
|
if (!regExp.test(action.fillColor) && action.fillColor !== null) {
|
||||||
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
66
src/reducers/modals.js
Normal file
66
src/reducers/modals.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
const OPEN_MODAL = 'scratch-paint/modals/OPEN_MODAL';
|
||||||
|
const CLOSE_MODAL = 'scratch-paint/modals/CLOSE_MODAL';
|
||||||
|
|
||||||
|
const MODAL_FILL_COLOR = 'fillColor';
|
||||||
|
const MODAL_STROKE_COLOR = 'strokeColor';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
[MODAL_FILL_COLOR]: false,
|
||||||
|
[MODAL_STROKE_COLOR]: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case OPEN_MODAL:
|
||||||
|
return Object.assign({}, initialState, {
|
||||||
|
[action.modal]: true
|
||||||
|
});
|
||||||
|
case CLOSE_MODAL:
|
||||||
|
return Object.assign({}, initialState, {
|
||||||
|
[action.modal]: false
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = function (modal) {
|
||||||
|
return {
|
||||||
|
type: OPEN_MODAL,
|
||||||
|
modal: modal
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = function (modal) {
|
||||||
|
return {
|
||||||
|
type: CLOSE_MODAL,
|
||||||
|
modal: modal
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
|
||||||
|
const openFillColor = function () {
|
||||||
|
return openModal(MODAL_FILL_COLOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openStrokeColor = function () {
|
||||||
|
return openModal(MODAL_STROKE_COLOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFillColor = function () {
|
||||||
|
return closeModal(MODAL_FILL_COLOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeStrokeColor = function () {
|
||||||
|
return closeModal(MODAL_STROKE_COLOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
reducer as default,
|
||||||
|
openFillColor,
|
||||||
|
openStrokeColor,
|
||||||
|
closeFillColor,
|
||||||
|
closeStrokeColor
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ import brushModeReducer from './brush-mode';
|
||||||
import eraserModeReducer from './eraser-mode';
|
import eraserModeReducer from './eraser-mode';
|
||||||
import colorReducer from './color';
|
import colorReducer from './color';
|
||||||
import hoverReducer from './hover';
|
import hoverReducer from './hover';
|
||||||
|
import modalsReducer from './modals';
|
||||||
import selectedItemReducer from './selected-items';
|
import selectedItemReducer from './selected-items';
|
||||||
import undoReducer from './undo';
|
import undoReducer from './undo';
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ export default combineReducers({
|
||||||
eraserMode: eraserModeReducer,
|
eraserMode: eraserModeReducer,
|
||||||
color: colorReducer,
|
color: colorReducer,
|
||||||
hoveredItemId: hoverReducer,
|
hoveredItemId: hoverReducer,
|
||||||
|
modals: modalsReducer,
|
||||||
selectedItems: selectedItemReducer,
|
selectedItems: selectedItemReducer,
|
||||||
undo: undoReducer
|
undo: undoReducer
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,7 @@ const reducer = function (state, action) {
|
||||||
if (typeof state === 'undefined') state = initialState;
|
if (typeof state === 'undefined') state = initialState;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CHANGE_STROKE_COLOR:
|
case CHANGE_STROKE_COLOR:
|
||||||
if (!regExp.test(action.strokeColor)) {
|
if (!regExp.test(action.strokeColor) && action.strokeColor !== null) {
|
||||||
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue