mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Implement eye dropper for paint editor
This commit is contained in:
parent
5ffdd14ff0
commit
2183dc759f
18 changed files with 695 additions and 96 deletions
|
@ -69,6 +69,7 @@
|
|||
"react-popover": "0.5.4",
|
||||
"react-redux": "5.0.5",
|
||||
"react-responsive": "3.0.0",
|
||||
"react-style-proptype": "3.1.0",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"redux": "3.7.0",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
|
|
2
src/components/box/box.css
Normal file
2
src/components/box/box.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
.box {
|
||||
}
|
143
src/components/box/box.jsx
Normal file
143
src/components/box/box.jsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
/* DO NOT EDIT
|
||||
@todo This file is copied from GUI and should be pulled out into a shared library.
|
||||
See https://github.com/LLK/scratch-paint/issues/13 */
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import stylePropType from 'react-style-proptype';
|
||||
import styles from './box.css';
|
||||
|
||||
const getRandomColor = (function () {
|
||||
// In "DEBUG" mode this is used to output a random background color for each
|
||||
// box. The function gives the same "random" set for each seed, allowing re-
|
||||
// renders of the same content to give the same random display.
|
||||
const random = (function (seed) {
|
||||
let mW = seed;
|
||||
let mZ = 987654321;
|
||||
const mask = 0xffffffff;
|
||||
return function () {
|
||||
mZ = ((36969 * (mZ & 65535)) + (mZ >> 16)) & mask;
|
||||
mW = ((18000 * (mW & 65535)) + (mW >> 16)) & mask;
|
||||
let result = ((mZ << 16) + mW) & mask;
|
||||
result /= 4294967296;
|
||||
return result + 1;
|
||||
};
|
||||
}(601));
|
||||
return function () {
|
||||
const r = Math.max(parseInt(random() * 100, 10) % 256, 1);
|
||||
const g = Math.max(parseInt(random() * 100, 10) % 256, 1);
|
||||
const b = Math.max(parseInt(random() * 100, 10) % 256, 1);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
};
|
||||
}());
|
||||
|
||||
const Box = props => {
|
||||
const {
|
||||
alignContent,
|
||||
alignItems,
|
||||
alignSelf,
|
||||
basis,
|
||||
children,
|
||||
className,
|
||||
componentRef,
|
||||
direction,
|
||||
element,
|
||||
grow,
|
||||
height,
|
||||
justifyContent,
|
||||
width,
|
||||
wrap,
|
||||
shrink,
|
||||
style,
|
||||
...componentProps
|
||||
} = props;
|
||||
return React.createElement(element, {
|
||||
className: classNames(className, styles.box),
|
||||
ref: componentRef,
|
||||
style: Object.assign(
|
||||
{
|
||||
alignContent: alignContent,
|
||||
alignItems: alignItems,
|
||||
alignSelf: alignSelf,
|
||||
flexBasis: basis,
|
||||
flexDirection: direction,
|
||||
flexGrow: grow,
|
||||
flexShrink: shrink,
|
||||
flexWrap: wrap,
|
||||
justifyContent: justifyContent,
|
||||
width: width,
|
||||
height: height
|
||||
},
|
||||
process.env.DEBUG ? {
|
||||
backgroundColor: getRandomColor(),
|
||||
outline: `1px solid black`
|
||||
} : {},
|
||||
style
|
||||
),
|
||||
...componentProps
|
||||
}, children);
|
||||
};
|
||||
Box.propTypes = {
|
||||
/** Defines how the browser distributes space between and around content items vertically within this box. */
|
||||
alignContent: PropTypes.oneOf([
|
||||
'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'
|
||||
]),
|
||||
/** Defines how the browser distributes space between and around flex items horizontally within this box. */
|
||||
alignItems: PropTypes.oneOf([
|
||||
'flex-start', 'flex-end', 'center', 'baseline', 'stretch'
|
||||
]),
|
||||
/** Specifies how this box should be aligned inside of its container (requires the container to be flexable). */
|
||||
alignSelf: PropTypes.oneOf([
|
||||
'auto', 'flex-start', 'flex-end', 'center', 'baseline', 'stretch'
|
||||
]),
|
||||
/** Specifies the initial length of this box */
|
||||
basis: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.oneOf(['auto'])
|
||||
]),
|
||||
/** Specifies the the HTML nodes which will be child elements of this box. */
|
||||
children: PropTypes.node,
|
||||
/** Specifies the class name that will be set on this box */
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* A callback function whose first parameter is the underlying dom elements.
|
||||
* This call back will be executed immediately after the component is mounted or unmounted
|
||||
*/
|
||||
componentRef: PropTypes.func,
|
||||
/** https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction */
|
||||
direction: PropTypes.oneOf([
|
||||
'row', 'row-reverse', 'column', 'column-reverse'
|
||||
]),
|
||||
/** Specifies the type of HTML element of this box. Defaults to div. */
|
||||
element: PropTypes.string,
|
||||
/** Specifies the flex grow factor of a flex item. */
|
||||
grow: PropTypes.number,
|
||||
/** The height in pixels (if specified as a number) or a string if different units are required. */
|
||||
height: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string
|
||||
]),
|
||||
/** https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content */
|
||||
justifyContent: PropTypes.oneOf([
|
||||
'flex-start', 'flex-end', 'center', 'space-between', 'space-around'
|
||||
]),
|
||||
/** Specifies the flex shrink factor of a flex item. */
|
||||
shrink: PropTypes.number,
|
||||
/** An object whose keys are css property names and whose values correspond the the css property. */
|
||||
style: stylePropType,
|
||||
/** The width in pixels (if specified as a number) or a string if different units are required. */
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string
|
||||
]),
|
||||
/** How whitespace should wrap within this block. */
|
||||
wrap: PropTypes.oneOf([
|
||||
'nowrap', 'wrap', 'wrap-reverse'
|
||||
])
|
||||
};
|
||||
Box.defaultProps = {
|
||||
element: 'div',
|
||||
style: {}
|
||||
};
|
||||
export default Box;
|
|
@ -13,6 +13,12 @@
|
|||
stroke: #ddd;
|
||||
}
|
||||
|
||||
.swatch-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row-header {
|
||||
font-family: "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-size: 0.65rem;
|
||||
|
|
|
@ -3,86 +3,20 @@ 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';
|
||||
import noFillIcon from '../color-button/no-fill.svg';
|
||||
|
||||
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;
|
||||
};
|
||||
import styles from './color-picker.css';
|
||||
|
||||
import eyeDropperIcon from './eye-dropper.svg';
|
||||
import noFillIcon from '../color-button/no-fill.svg';
|
||||
|
||||
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 = [];
|
||||
// Generate the color slider background CSS gradients by adding
|
||||
|
@ -90,13 +24,13 @@ class ColorPickerComponent extends React.Component {
|
|||
for (let n = 100; n >= 0; n -= 10) {
|
||||
switch (channel) {
|
||||
case 'hue':
|
||||
stops.push(hsvToHex(n, this.state.saturation, this.state.brightness));
|
||||
stops.push(hsvToHex(n, this.props.saturation, this.props.brightness));
|
||||
break;
|
||||
case 'saturation':
|
||||
stops.push(hsvToHex(this.state.hue, n, this.state.brightness));
|
||||
stops.push(hsvToHex(this.props.hue, n, this.props.brightness));
|
||||
break;
|
||||
case 'brightness':
|
||||
stops.push(hsvToHex(this.state.hue, this.state.saturation, n));
|
||||
stops.push(hsvToHex(this.props.hue, this.props.saturation, n));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown channel for color sliders: ${channel}`);
|
||||
|
@ -104,7 +38,6 @@ class ColorPickerComponent extends React.Component {
|
|||
}
|
||||
return `linear-gradient(to left, ${stops.join(',')})`;
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.colorPickerContainer}>
|
||||
|
@ -118,14 +51,14 @@ class ColorPickerComponent extends React.Component {
|
|||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.hue)}
|
||||
{Math.round(this.props.hue)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('hue')}
|
||||
value={this.state.hue}
|
||||
onChange={this.handleHueChange}
|
||||
value={this.props.hue}
|
||||
onChange={this.props.onHueChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,14 +72,14 @@ class ColorPickerComponent extends React.Component {
|
|||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.saturation)}
|
||||
{Math.round(this.props.saturation)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('saturation')}
|
||||
value={this.state.saturation}
|
||||
onChange={this.handleSaturationChange}
|
||||
value={this.props.saturation}
|
||||
onChange={this.props.onSaturationChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -160,40 +93,58 @@ class ColorPickerComponent extends React.Component {
|
|||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.brightness)}
|
||||
{Math.round(this.props.brightness)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('brightness')}
|
||||
value={this.state.brightness}
|
||||
onChange={this.handleBrightnessChange}
|
||||
value={this.props.brightness}
|
||||
onChange={this.props.onBrightnessChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.row}>
|
||||
<div className={styles.swatchRow}>
|
||||
<div className={styles.swatches}>
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.swatch]: true,
|
||||
[styles.activeSwatch]: this.props.color === null
|
||||
})}
|
||||
onClick={this.handleTransparent}
|
||||
onClick={this.props.onTransparent}
|
||||
>
|
||||
<img src={noFillIcon} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.swatches}>
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.swatch]: true,
|
||||
[styles.activeSwatch]: this.props.isEyeDropping
|
||||
})}
|
||||
onClick={this.props.onActivateEyeDropper}
|
||||
>
|
||||
<img src={eyeDropperIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ColorPickerComponent.propTypes = {
|
||||
brightness: PropTypes.number.isRequired,
|
||||
color: PropTypes.string,
|
||||
onChangeColor: PropTypes.func.isRequired
|
||||
hue: PropTypes.number.isRequired,
|
||||
isEyeDropping: PropTypes.bool.isRequired,
|
||||
onActivateEyeDropper: PropTypes.func.isRequired,
|
||||
onBrightnessChange: PropTypes.func.isRequired,
|
||||
onHueChange: PropTypes.func.isRequired,
|
||||
onSaturationChange: PropTypes.func.isRequired,
|
||||
onTransparent: PropTypes.func.isRequired,
|
||||
saturation: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default ColorPickerComponent;
|
||||
|
|
12
src/components/color-picker/eye-dropper.svg
Normal file
12
src/components/color-picker/eye-dropper.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>eye-dropper</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="eye-dropper" fill="#575E75">
|
||||
<path d="M9.15334605,12.4824962 C9.03394044,12.6188737 8.88041895,12.7041096 8.60749186,12.7722983 C7.90811618,12.925723 7.24285639,13.5564688 7.03816107,14.2554033 C6.9699293,14.4770167 6.74817603,14.7156773 6.50936483,14.8350076 L4.73533871,15.6703196 C4.650049,15.704414 4.58181722,15.7214612 4.54770134,15.7214612 L4.27477424,15.4657534 C4.27477424,15.4487062 4.27477424,15.3805175 4.32594807,15.2611872 L5.1617873,13.4712329 C5.26413496,13.2496195 5.50294617,13.0280061 5.74175737,12.9598174 C6.44113305,12.738204 7.07227696,12.090411 7.25991433,11.2380518 C7.29403022,11.1016743 7.37931994,10.9652968 7.49872554,10.8289193 L11.4391105,6.90806697 L13.093731,8.56164384 L9.15334605,12.4824962 Z M16.6076673,5.28858447 C16.8635365,5.03287671 17,4.67488584 17,4.33394216 C17,3.99299848 16.8635365,3.65205479 16.6076673,3.39634703 C16.0788711,2.86788432 15.2430318,2.86788432 14.7142356,3.39634703 L13.2301945,4.87945205 L13.0596151,4.70898021 L12.5137609,4.16347032 C12.172602,3.82252664 11.6096899,3.82252664 11.268531,4.16347032 L10.6032712,4.81126332 C10.2791703,5.152207 10.2621124,5.64657534 10.5520974,5.98751903 L6.59465454,9.92541857 C6.30466951,10.2322679 6.09997418,10.5902588 5.98056858,11.1016743 C5.92939475,11.357382 5.63940971,11.6471842 5.36648262,11.7324201 C4.80357049,11.9028919 4.2577163,12.3802131 4.00184715,12.9427702 L3.16600792,14.7156773 C2.89308083,15.3123288 2.9613126,15.9260274 3.33658736,16.3181126 L3.67774623,16.6590563 C3.89949949,16.8806697 4.20654247,17 4.54770134,17 C4.7694546,17 5.02532375,16.9318113 5.26413496,16.8295282 L7.05521901,15.9942161 C7.61813114,15.7214612 8.09575356,15.1929985 8.26633299,14.6304414 C8.33456477,14.3576865 8.64160775,14.0678843 9.05099839,13.9826484 C9.4092152,13.8974125 9.76743201,13.6928463 10.057417,13.385997 L14.0148599,9.44809741 C14.3560188,9.73789954 14.8677571,9.70380518 15.1748001,9.37990868 L15.8400599,8.73211568 C16.1812187,8.39117199 16.1812187,7.82861492 15.8400599,7.48767123 L15.2600898,6.90806697 L15.1236262,6.7716895 L16.6076673,5.28858447 Z" id="eye-dropper-icon"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Popover from 'react-popover';
|
||||
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import ColorPicker from './color-picker/color-picker.jsx';
|
||||
|
||||
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';
|
||||
|
||||
|
|
5
src/components/loupe/loupe.css
Normal file
5
src/components/loupe/loupe.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.eye-dropper {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
border: 1px solid #222;
|
||||
}
|
108
src/components/loupe/loupe.jsx
Normal file
108
src/components/loupe/loupe.jsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
|
||||
import {LOUPE_RADIUS, CANVAS_SCALE} from '../../helper/eye-dropper';
|
||||
|
||||
import styles from './loupe.css';
|
||||
|
||||
const zoomScale = 3;
|
||||
|
||||
class LoupeComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'setCanvas'
|
||||
]);
|
||||
}
|
||||
componentDidUpdate () {
|
||||
this.draw();
|
||||
}
|
||||
draw () {
|
||||
const boxSize = 6 / zoomScale;
|
||||
const boxLineWidth = 1 / zoomScale;
|
||||
const colorRingWidth = 15 / zoomScale;
|
||||
|
||||
const color = this.props.colorInfo.color;
|
||||
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
this.canvas.width = zoomScale * (LOUPE_RADIUS * 2);
|
||||
this.canvas.height = zoomScale * (LOUPE_RADIUS * 2);
|
||||
|
||||
// In order to scale the image data, must draw to a tmp canvas first
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
tmpCanvas.width = LOUPE_RADIUS * 2;
|
||||
tmpCanvas.height = LOUPE_RADIUS * 2;
|
||||
const tmpCtx = tmpCanvas.getContext('2d');
|
||||
const imageData = tmpCtx.createImageData(
|
||||
LOUPE_RADIUS * 2, LOUPE_RADIUS * 2
|
||||
);
|
||||
imageData.data.set(this.props.colorInfo.data);
|
||||
tmpCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Scale the loupe canvas and draw the zoomed image
|
||||
ctx.save();
|
||||
ctx.scale(zoomScale, zoomScale);
|
||||
ctx.drawImage(tmpCanvas, 0, 0, LOUPE_RADIUS * 2, LOUPE_RADIUS * 2);
|
||||
|
||||
// Draw an outlined square at the cursor position (cursor is hidden)
|
||||
ctx.lineWidth = boxLineWidth;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
|
||||
ctx.beginPath();
|
||||
ctx.rect((20) - (boxSize / 2), (20) - (boxSize / 2), boxSize, boxSize);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Draw a thick ring around the loupe showing the current color
|
||||
ctx.strokeStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;
|
||||
ctx.lineWidth = colorRingWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(LOUPE_RADIUS * 2, LOUPE_RADIUS);
|
||||
ctx.arc(LOUPE_RADIUS, LOUPE_RADIUS, LOUPE_RADIUS, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
setCanvas (element) {
|
||||
this.canvas = element;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
colorInfo,
|
||||
...boxProps
|
||||
} = this.props;
|
||||
return (
|
||||
<Box
|
||||
{...boxProps}
|
||||
className={styles.eyeDropper}
|
||||
componentRef={this.setCanvas}
|
||||
element="canvas"
|
||||
height={LOUPE_RADIUS * 2}
|
||||
style={{
|
||||
top: (colorInfo.y / CANVAS_SCALE) - ((zoomScale * (LOUPE_RADIUS * 2)) / 2),
|
||||
left: (colorInfo.x / CANVAS_SCALE) - ((zoomScale * (LOUPE_RADIUS * 2)) / 2),
|
||||
width: (LOUPE_RADIUS * 2) * zoomScale,
|
||||
height: (LOUPE_RADIUS * 2) * zoomScale
|
||||
}}
|
||||
width={LOUPE_RADIUS * 2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoupeComponent.propTypes = {
|
||||
colorInfo: PropTypes.shape({
|
||||
color: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number
|
||||
}),
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
data: PropTypes.instanceOf(Uint8ClampedArray)
|
||||
})
|
||||
};
|
||||
|
||||
export default LoupeComponent;
|
|
@ -143,6 +143,16 @@ $border-radius: 0.25rem;
|
|||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.color-picker-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $full-size-paint) {
|
||||
.editor-container {
|
||||
padding: calc(3 * $grid-unit) $grid-unit;
|
||||
|
|
|
@ -10,6 +10,7 @@ import PaperCanvas from '../../containers/paper-canvas.jsx';
|
|||
import {shouldShowGroup, shouldShowUngroup} from '../../helper/group';
|
||||
import {shouldShowBringForward, shouldShowSendBackward} from '../../helper/order';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import Button from '../button/button.jsx';
|
||||
import ButtonGroup from '../button-group/button-group.jsx';
|
||||
import BrushMode from '../../containers/brush-mode.jsx';
|
||||
|
@ -22,6 +23,7 @@ import InputGroup from '../input-group/input-group.jsx';
|
|||
import Label from '../forms/label.jsx';
|
||||
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
|
||||
import LineMode from '../../containers/line-mode.jsx';
|
||||
import Loupe from '../loupe/loupe.jsx';
|
||||
import ModeToolsComponent from '../mode-tools/mode-tools.jsx';
|
||||
import OvalMode from '../../containers/oval-mode.jsx';
|
||||
import RectMode from '../../containers/rect-mode.jsx';
|
||||
|
@ -109,6 +111,7 @@ class PaintEditorComponent extends React.Component {
|
|||
}
|
||||
setCanvas (canvas) {
|
||||
this.setState({canvas: canvas});
|
||||
this.canvas = canvas;
|
||||
}
|
||||
render () {
|
||||
const redoDisabled = !this.props.canRedo();
|
||||
|
@ -368,6 +371,16 @@ class PaintEditorComponent extends React.Component {
|
|||
svgId={this.props.svgId}
|
||||
onUpdateSvg={this.props.onUpdateSvg}
|
||||
/>
|
||||
{(
|
||||
this.props.isEyeDropping &&
|
||||
this.props.colorInfo !== null &&
|
||||
!this.props.colorInfo.hideLoupe
|
||||
) ? (
|
||||
<Box className={styles.colorPickerWrapper}>
|
||||
<Loupe colorInfo={this.props.colorInfo} />
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
{/* Zoom controls */}
|
||||
<InputGroup className={styles.zoomControls}>
|
||||
<ButtonGroup>
|
||||
|
@ -413,7 +426,9 @@ class PaintEditorComponent extends React.Component {
|
|||
PaintEditorComponent.propTypes = {
|
||||
canRedo: PropTypes.func.isRequired,
|
||||
canUndo: PropTypes.func.isRequired,
|
||||
colorInfo: Loupe.propTypes.colorInfo,
|
||||
intl: intlShape,
|
||||
isEyeDropping: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
onCopyToClipboard: PropTypes.func.isRequired,
|
||||
onGroup: PropTypes.func.isRequired,
|
||||
|
@ -436,4 +451,4 @@ PaintEditorComponent.propTypes = {
|
|||
svgId: PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(PaintEditorComponent);
|
||||
export default injectIntl(PaintEditorComponent, {withRef: true});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Popover from 'react-popover';
|
||||
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import ColorPicker from './color-picker/color-picker.jsx';
|
||||
|
||||
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';
|
||||
|
||||
|
|
132
src/containers/color-picker.jsx
Normal file
132
src/containers/color-picker.jsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import bindAll from 'lodash.bindall';
|
||||
import {connect} from 'react-redux';
|
||||
import parseColor from 'parse-color';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {clearSelectedItems} from '../reducers/selected-items';
|
||||
import {activateEyeDropper} from '../reducers/eye-dropper';
|
||||
import {changeMode} from '../reducers/modes';
|
||||
|
||||
import ColorPickerComponent from '../components/color-picker/color-picker.jsx';
|
||||
import {MIXED} from '../helper/style-path';
|
||||
import Modes from '../lib/modes';
|
||||
|
||||
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 ColorPicker extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleHueChange',
|
||||
'handleSaturationChange',
|
||||
'handleBrightnessChange',
|
||||
'handleTransparent',
|
||||
'handleActivateEyeDropper'
|
||||
]);
|
||||
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);
|
||||
}
|
||||
handleActivateEyeDropper () {
|
||||
this.props.onActivateEyeDropper(
|
||||
this.props.currentMode,
|
||||
this.props.onChangeColor
|
||||
);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ColorPickerComponent
|
||||
brightness={this.state.brightness}
|
||||
color={this.props.color}
|
||||
hue={this.state.hue}
|
||||
isEyeDropping={this.props.isEyeDropping}
|
||||
saturation={this.state.saturation}
|
||||
onActivateEyeDropper={this.handleActivateEyeDropper}
|
||||
onBrightnessChange={this.handleBrightnessChange}
|
||||
onChangeColor={this.props.onChangeColor}
|
||||
onHueChange={this.handleHueChange}
|
||||
onSaturationChange={this.handleSaturationChange}
|
||||
onTransparent={this.handleTransparent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
color: PropTypes.string,
|
||||
currentMode: PropTypes.string,
|
||||
isEyeDropping: PropTypes.bool.isRequired,
|
||||
onActivateEyeDropper: PropTypes.func.isRequired,
|
||||
onChangeColor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentMode: state.scratchPaint.mode,
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
onActivateEyeDropper: (currentMode, callback) => {
|
||||
dispatch(changeMode(Modes.EYE_DROPPER));
|
||||
dispatch(activateEyeDropper(currentMode, callback));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ColorPicker);
|
|
@ -6,6 +6,7 @@ import {changeMode} from '../reducers/modes';
|
|||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
|
||||
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
||||
|
||||
import {hideGuideLayers, showGuideLayers} from '../helper/layer';
|
||||
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||
|
@ -13,6 +14,7 @@ import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/or
|
|||
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||
import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection';
|
||||
import {resetZoom, zoomOnSelection} from '../helper/view';
|
||||
import EyeDropperTool from '../helper/eye-dropper';
|
||||
|
||||
import Modes from '../lib/modes';
|
||||
import {connect} from 'react-redux';
|
||||
|
@ -38,14 +40,37 @@ class PaintEditor extends React.Component {
|
|||
'canRedo',
|
||||
'canUndo',
|
||||
'handleCopyToClipboard',
|
||||
'handlePasteFromClipboard'
|
||||
'handlePasteFromClipboard',
|
||||
'setPaintEditor',
|
||||
'onMouseDown',
|
||||
'startEyeDroppingLoop',
|
||||
'stopEyeDroppingLoop'
|
||||
]);
|
||||
this.state = {
|
||||
colorInfo: null
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('keydown', this.props.onKeyPress);
|
||||
}
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
return this.props.isEyeDropping !== nextProps.isEyeDropping ||
|
||||
this.state.colorInfo !== nextState.colorInfo ||
|
||||
this.props.clipboardItems !== nextProps.clipboardItems ||
|
||||
this.props.pasteOffset !== nextProps.pasteOffset ||
|
||||
this.props.selectedItems !== nextProps.selectedItems ||
|
||||
this.props.undoState !== nextProps.undoState;
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.isEyeDropping && !prevProps.isEyeDropping) {
|
||||
this.startEyeDroppingLoop();
|
||||
} else if (!this.props.isEyeDropping && prevProps.isEyeDropping) {
|
||||
this.stopEyeDroppingLoop();
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.props.onKeyPress);
|
||||
this.stopEyeDroppingLoop();
|
||||
}
|
||||
handleUpdateSvg (skipSnapshot) {
|
||||
// Store the zoom/pan and restore it after snapshotting
|
||||
|
@ -145,12 +170,58 @@ class PaintEditor extends React.Component {
|
|||
handleZoomReset () {
|
||||
resetZoom();
|
||||
}
|
||||
setPaintEditor (paintEditor) {
|
||||
this.paintEditor = paintEditor;
|
||||
}
|
||||
onMouseDown () {
|
||||
if (this.props.isEyeDropping) {
|
||||
const colorString = this.eyeDropper.colorString;
|
||||
const callback = this.props.changeColorToEyeDropper;
|
||||
|
||||
this.props.onDeactivateEyeDropper(this.props.previousMode);
|
||||
this.stopEyeDroppingLoop();
|
||||
if (!this.eyeDropper.hideLoupe) {
|
||||
// If not hide loupe, that means the click is inside the canvas,
|
||||
// so apply the new color
|
||||
callback(colorString);
|
||||
}
|
||||
this.setState({colorInfo: null});
|
||||
}
|
||||
}
|
||||
startEyeDroppingLoop () {
|
||||
const canvas = this.paintEditor.getWrappedInstance().canvas;
|
||||
this.eyeDropper = new EyeDropperTool(canvas);
|
||||
this.eyeDropper.activate();
|
||||
|
||||
// document listeners used to detect if a mouse is down outside of the
|
||||
// canvas, and should therefore stop the eye dropper
|
||||
document.addEventListener('mousedown', this.onMouseDown);
|
||||
document.addEventListener('touchstart', this.onMouseDown);
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
this.setState({
|
||||
colorInfo: this.eyeDropper.getColorInfo(
|
||||
this.eyeDropper.pickX,
|
||||
this.eyeDropper.pickY,
|
||||
this.eyeDropper.hideLoupe
|
||||
)
|
||||
});
|
||||
}, 30);
|
||||
}
|
||||
stopEyeDroppingLoop () {
|
||||
clearInterval(this.intervalId);
|
||||
document.removeEventListener('mousedown', this.onMouseDown);
|
||||
document.removeEventListener('touchstart', this.onMouseDown);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<PaintEditorComponent
|
||||
canRedo={this.canRedo}
|
||||
canUndo={this.canUndo}
|
||||
colorInfo={this.state.colorInfo}
|
||||
isEyeDropping={this.props.isEyeDropping}
|
||||
name={this.props.name}
|
||||
ref={this.setPaintEditor}
|
||||
rotationCenterX={this.props.rotationCenterX}
|
||||
rotationCenterY={this.props.rotationCenterY}
|
||||
svg={this.props.svg}
|
||||
|
@ -176,18 +247,23 @@ class PaintEditor extends React.Component {
|
|||
}
|
||||
|
||||
PaintEditor.propTypes = {
|
||||
changeColorToEyeDropper: PropTypes.func,
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
clipboardItems: PropTypes.arrayOf(PropTypes.array),
|
||||
incrementPasteOffset: PropTypes.func.isRequired,
|
||||
isEyeDropping: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
onDeactivateEyeDropper: PropTypes.func.isRequired,
|
||||
onKeyPress: PropTypes.func.isRequired,
|
||||
onRedo: PropTypes.func.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onUpdateName: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
pasteOffset: PropTypes.number,
|
||||
previousMode: PropTypes.string,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.object),
|
||||
setClipboardItems: PropTypes.func.isRequired,
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
|
@ -200,10 +276,14 @@ PaintEditor.propTypes = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
undoState: state.scratchPaint.undo,
|
||||
changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback,
|
||||
clipboardItems: state.scratchPaint.clipboard.items,
|
||||
pasteOffset: state.scratchPaint.clipboard.pasteOffset
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
||||
previousItems: state.scratchPaint.color.eyeDropper.previousItems,
|
||||
previousMode: state.scratchPaint.color.eyeDropper.previousMode,
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
undoState: state.scratchPaint.undo
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onKeyPress: event => {
|
||||
|
@ -237,6 +317,11 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
incrementPasteOffset: () => {
|
||||
dispatch(incrementPasteOffset());
|
||||
},
|
||||
onDeactivateEyeDropper: previousMode => {
|
||||
// deactivate the eye dropper, reset to previously selected mode
|
||||
dispatch(deactivateEyeDropper());
|
||||
dispatch(changeMode(previousMode));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
71
src/helper/eye-dropper.js
Normal file
71
src/helper/eye-dropper.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import paper from '@scratch/paper';
|
||||
|
||||
const PAPER_WIDTH = 864;
|
||||
const PAPER_HEIGHT = 648;
|
||||
const LOUPE_RADIUS = 20;
|
||||
const CANVAS_SCALE = 1.8;
|
||||
|
||||
class EyeDropperTool extends paper.Tool {
|
||||
constructor (canvas) {
|
||||
super();
|
||||
|
||||
this.onMouseDown = this.handleMouseDown;
|
||||
this.onMouseMove = this.handleMouseMove;
|
||||
|
||||
this.active = false;
|
||||
this.canvas = canvas;
|
||||
this.colorInfo = null;
|
||||
this.rect = canvas.getBoundingClientRect();
|
||||
this.colorString = '';
|
||||
}
|
||||
handleMouseMove (event) {
|
||||
// Set the pickX/Y for the color picker loop to pick up
|
||||
this.pickX = event.point.x * CANVAS_SCALE;
|
||||
this.pickY = event.point.y * CANVAS_SCALE;
|
||||
|
||||
// check if the x/y are outside of the canvas
|
||||
this.hideLoupe = this.pickX > PAPER_WIDTH ||
|
||||
this.pickX < 0 ||
|
||||
this.pickY > PAPER_HEIGHT ||
|
||||
this.pickY < 0;
|
||||
}
|
||||
handleMouseDown () {
|
||||
if (!this.hideLoupe) {
|
||||
const colorInfo = this.getColorInfo(this.pickX, this.pickY, this.hideLoupe);
|
||||
const r = colorInfo.color[0];
|
||||
const g = colorInfo.color[1];
|
||||
const b = colorInfo.color[2];
|
||||
|
||||
const componentToString = c => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
this.colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`;
|
||||
}
|
||||
}
|
||||
getColorInfo (x, y, hideLoupe) {
|
||||
const c = this.canvas.getContext('2d');
|
||||
const colors = c.getImageData(x, y, 1, 1);
|
||||
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
color: colors.data,
|
||||
data: c.getImageData(
|
||||
x - LOUPE_RADIUS,
|
||||
y - LOUPE_RADIUS,
|
||||
LOUPE_RADIUS * 2,
|
||||
LOUPE_RADIUS * 2
|
||||
).data,
|
||||
hideLoupe: hideLoupe
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
EyeDropperTool as default,
|
||||
PAPER_HEIGHT,
|
||||
PAPER_WIDTH,
|
||||
LOUPE_RADIUS,
|
||||
CANVAS_SCALE
|
||||
};
|
|
@ -2,6 +2,7 @@ import keyMirror from 'keymirror';
|
|||
|
||||
const Modes = keyMirror({
|
||||
BRUSH: null,
|
||||
EYE_DROPPER: null,
|
||||
ERASER: null,
|
||||
LINE: null,
|
||||
SELECT: null,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import {combineReducers} from 'redux';
|
||||
import eyeDropperReducer from './eye-dropper';
|
||||
import fillColorReducer from './fill-color';
|
||||
import strokeColorReducer from './stroke-color';
|
||||
import strokeWidthReducer from './stroke-width';
|
||||
|
||||
export default combineReducers({
|
||||
eyeDropper: eyeDropperReducer,
|
||||
fillColor: fillColorReducer,
|
||||
strokeColor: strokeColorReducer,
|
||||
strokeWidth: strokeWidthReducer
|
||||
|
|
55
src/reducers/eye-dropper.js
Normal file
55
src/reducers/eye-dropper.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
const ACTIVATE_EYE_DROPPER = 'scratch-paint/eye-dropper/ACTIVATE_COLOR_PICKER';
|
||||
const DEACTIVATE_EYE_DROPPER = 'scratch-paint/eye-dropper/DEACTIVATE_COLOR_PICKER';
|
||||
|
||||
const initialState = {
|
||||
active: false,
|
||||
callback: () => {}, // this will either be `onChangeFillColor` or `onChangeOutlineColor`
|
||||
previousMode: null // the previous mode that was active to go back to
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case ACTIVATE_EYE_DROPPER:
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
active: true,
|
||||
callback: action.callback,
|
||||
previousMode: action.previousMode
|
||||
}
|
||||
);
|
||||
case DEACTIVATE_EYE_DROPPER:
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
active: false,
|
||||
callback: () => {},
|
||||
previousMode: null
|
||||
}
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const activateEyeDropper = function (currentMode, callback) {
|
||||
return {
|
||||
type: ACTIVATE_EYE_DROPPER,
|
||||
callback: callback,
|
||||
previousMode: currentMode
|
||||
};
|
||||
};
|
||||
const deactivateEyeDropper = function () {
|
||||
return {
|
||||
type: DEACTIVATE_EYE_DROPPER
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
activateEyeDropper,
|
||||
deactivateEyeDropper
|
||||
};
|
Loading…
Reference in a new issue