Vector gradient (#558)

This commit is contained in:
DD Liu 2018-07-17 16:37:03 -04:00 committed by GitHub
parent 9be50ffaf6
commit 4ba79cacbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1092 additions and 99 deletions

View file

@ -7,10 +7,20 @@ import {MIXED} from '../../helper/style-path';
import noFillIcon from './no-fill.svg';
import mixedFillIcon from './mixed-fill.svg';
import styles from './color-button.css';
import GradientTypes from '../../lib/gradient-types';
import log from '../../log/log';
const colorToBackground = color => {
if (color === MIXED || color === null) return 'white';
return color;
const colorToBackground = (color, color2, gradientType) => {
if (color === MIXED || color2 === MIXED) return 'white';
if (color === null) color = 'white';
if (color2 === null) color2 = 'white';
switch (gradientType) {
case GradientTypes.SOLID: return color;
case GradientTypes.HORIZONTAL: return `linear-gradient(to right, ${color}, ${color2})`;
case GradientTypes.VERTICAL: return `linear-gradient(${color}, ${color2})`;
case GradientTypes.RADIAL: return `radial-gradient(${color}, ${color2})`;
default: log.error(`Unrecognized gradient type: ${gradientType}`);
}
};
const ColorButtonComponent = props => (
@ -23,16 +33,16 @@ const ColorButtonComponent = props => (
[styles.outlineSwatch]: props.outline && !(props.color === MIXED)
})}
style={{
background: colorToBackground(props.color)
background: colorToBackground(props.color, props.color2, props.gradientType)
}}
>
{props.color === null ? (
{props.color === null && (props.gradientType === GradientTypes.SOLID || props.color2 === null) ? (
<img
className={styles.swatchIcon}
draggable={false}
src={noFillIcon}
/>
) : ((props.color === MIXED ? (
) : ((props.color === MIXED || (props.gradientType !== GradientTypes.SOLID && props.color2 === MIXED) ? (
<img
className={styles.swatchIcon}
draggable={false}
@ -46,6 +56,8 @@ const ColorButtonComponent = props => (
ColorButtonComponent.propTypes = {
color: PropTypes.string,
color2: PropTypes.string,
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
onClick: PropTypes.func.isRequired,
outline: PropTypes.bool.isRequired
};

View file

@ -1,3 +1,5 @@
@import "../../css/units";
/* Popover styles */
:global(.Popover-body) {
background: white;
@ -13,6 +15,10 @@
stroke: #ddd;
}
.clickable {
cursor: pointer;
}
.swatch-row {
display: flex;
flex-direction: row;
@ -39,6 +45,11 @@
margin: 8px;
}
.swap-button {
margin-left: 8px;
margin-right: 8px;
}
.swatches {
margin: 8px;
}
@ -49,6 +60,18 @@
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: content-box;
display: flex;
align-items: center;
}
.large-swatch-icon {
width: 1.75rem;
margin: auto;
}
.large-swatch {
width: 2rem;
height: 2rem;
}
.active-swatch {
@ -56,7 +79,23 @@
box-shadow: 0px 0px 0px 3px hsla(215, 100%, 65%, 0.2);
}
.swatch > img {
.swatch-icon {
width: 1.5rem;
height: 1.5rem;
}
.inactive-gradient {
filter: saturate(0%);
}
.gradient-picker-row {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
margin: 8px;
}
.gradient-picker-row > img + img {
margin-left: calc(2 * $grid-unit);
}

View file

@ -1,21 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
import classNames from 'classnames';
import parseColor from 'parse-color';
import Slider from '../forms/slider.jsx';
import LabeledIconButton from '../labeled-icon-button/labeled-icon-button.jsx';
import styles from './color-picker.css';
import GradientTypes from '../../lib/gradient-types';
import {MIXED} from '../../helper/style-path';
import eyeDropperIcon from './eye-dropper.svg';
import eyeDropperIcon from './icons/eye-dropper.svg';
import noFillIcon from '../color-button/no-fill.svg';
import mixedFillIcon from '../color-button/mixed-fill.svg';
import fillHorzGradientIcon from './icons/fill-horz-gradient-enabled.svg';
import fillRadialIcon from './icons/fill-radial-enabled.svg';
import fillSolidIcon from './icons/fill-solid-enabled.svg';
import fillVertGradientIcon from './icons/fill-vert-gradient-enabled.svg';
import swapIcon from './icons/swap.svg';
const hsvToHex = (h, s, v) =>
// Scale hue back up to [0, 360] from [0, 100]
parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex
;
const messages = defineMessages({
swap: {
defaultMessage: 'Swap',
description: 'Label for button that swaps the two colors in a gradient',
id: 'paint.colorPicker.swap'
}
});
class ColorPickerComponent extends React.Component {
_makeBackground (channel) {
const stops = [];
@ -41,6 +57,118 @@ class ColorPickerComponent extends React.Component {
render () {
return (
<div className={styles.colorPickerContainer}>
{this.props.shouldShowGradientTools ? (
<div>
<div className={styles.row}>
<div className={styles.gradientPickerRow}>
<img
className={classNames({
[styles.inactiveGradient]: this.props.gradientType !== GradientTypes.SOLID,
[styles.clickable]: true
})}
draggable={false}
src={fillSolidIcon}
onClick={this.props.onChangeGradientTypeSolid}
/>
<img
className={classNames({
[styles.inactiveGradient]:
this.props.gradientType !== GradientTypes.HORIZONTAL,
[styles.clickable]: true
})}
draggable={false}
src={fillHorzGradientIcon}
onClick={this.props.onChangeGradientTypeHorizontal}
/>
<img
className={classNames({
[styles.inactiveGradient]: this.props.gradientType !== GradientTypes.VERTICAL,
[styles.clickable]: true
})}
draggable={false}
src={fillVertGradientIcon}
onClick={this.props.onChangeGradientTypeVertical}
/>
<img
className={classNames({
[styles.inactiveGradient]: this.props.gradientType !== GradientTypes.RADIAL,
[styles.clickable]: true
})}
draggable={false}
src={fillRadialIcon}
onClick={this.props.onChangeGradientTypeRadial}
/>
</div>
</div>
<div className={styles.divider} />
{this.props.gradientType === GradientTypes.SOLID ? null : (
<div className={styles.row}>
<div className={styles.gradientPickerRow}>
<div
className={classNames({
[styles.clickable]: true,
[styles.swatch]: true,
[styles.largeSwatch]: true,
[styles.activeSwatch]: this.props.colorIndex === 0
})}
style={{
backgroundColor: this.props.color === null || this.props.color === MIXED ?
'white' : this.props.color
}}
onClick={this.props.onSelectColor}
>
{this.props.color === null ? (
<img
className={styles.largeSwatchIcon}
draggable={false}
src={noFillIcon}
/>
) : this.props.color === MIXED ? (
<img
className={styles.largeSwatchIcon}
draggable={false}
src={mixedFillIcon}
/>
) : null}
</div>
<LabeledIconButton
className={styles.swapButton}
imgSrc={swapIcon}
title={this.props.intl.formatMessage(messages.swap)}
onClick={this.props.onSwap}
/>
<div
className={classNames({
[styles.clickable]: true,
[styles.swatch]: true,
[styles.largeSwatch]: true,
[styles.activeSwatch]: this.props.colorIndex === 1
})}
style={{
backgroundColor: this.props.color2 === null || this.props.color2 === MIXED ?
'white' : this.props.color2
}}
onClick={this.props.onSelectColor2}
>
{this.props.color2 === null ? (
<img
className={styles.largeSwatchIcon}
draggable={false}
src={noFillIcon}
/>
) : this.props.color2 === MIXED ? (
<img
className={styles.largeSwatchIcon}
draggable={false}
src={mixedFillIcon}
/>
) : null}
</div>
</div>
</div>
)}
</div>
) : null}
<div className={styles.row}>
<div className={styles.rowHeader}>
<span className={styles.labelName}>
@ -98,23 +226,27 @@ class ColorPickerComponent extends React.Component {
</div>
<div className={styles.rowSlider}>
<Slider
lastSlider
background={this._makeBackground('brightness')}
value={this.props.brightness}
onChange={this.props.onBrightnessChange}
/>
</div>
</div>
<div className={styles.divider} />
<div className={styles.swatchRow}>
<div className={styles.swatches}>
<div
className={classNames({
[styles.clickable]: true,
[styles.swatch]: true,
[styles.activeSwatch]: this.props.color === null
[styles.activeSwatch]:
(this.props.colorIndex === 0 && this.props.color === null) ||
(this.props.colorIndex === 1 && this.props.color2 === null)
})}
onClick={this.props.onTransparent}
>
<img
className={styles.swatchIcon}
draggable={false}
src={noFillIcon}
/>
@ -123,12 +255,14 @@ class ColorPickerComponent extends React.Component {
<div className={styles.swatches}>
<div
className={classNames({
[styles.clickable]: true,
[styles.swatch]: true,
[styles.activeSwatch]: this.props.isEyeDropping
})}
onClick={this.props.onActivateEyeDropper}
>
<img
className={styles.swatchIcon}
draggable={false}
src={eyeDropperIcon}
/>
@ -143,14 +277,26 @@ class ColorPickerComponent extends React.Component {
ColorPickerComponent.propTypes = {
brightness: PropTypes.number.isRequired,
color: PropTypes.string,
color2: PropTypes.string,
colorIndex: PropTypes.number.isRequired,
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
hue: PropTypes.number.isRequired,
intl: intlShape.isRequired,
isEyeDropping: PropTypes.bool.isRequired,
onActivateEyeDropper: PropTypes.func.isRequired,
onBrightnessChange: PropTypes.func.isRequired,
onChangeGradientTypeHorizontal: PropTypes.func.isRequired,
onChangeGradientTypeRadial: PropTypes.func.isRequired,
onChangeGradientTypeSolid: PropTypes.func.isRequired,
onChangeGradientTypeVertical: PropTypes.func.isRequired,
onHueChange: PropTypes.func.isRequired,
onSaturationChange: PropTypes.func.isRequired,
onSelectColor: PropTypes.func.isRequired,
onSelectColor2: PropTypes.func.isRequired,
onSwap: PropTypes.func,
onTransparent: PropTypes.func.isRequired,
saturation: PropTypes.number.isRequired
saturation: PropTypes.number.isRequired,
shouldShowGradientTools: PropTypes.bool.isRequired
};
export default ColorPickerComponent;
export default injectIntl(ColorPickerComponent);

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>fill-horz-gradient-enabled</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="linearGradient-1">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#4C97FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="fill-horz-gradient-enabled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-opacity="0.15">
<rect id="Horizontal" stroke="#000000" fill="url(#linearGradient-1)" x="0.5" y="0.5" width="19" height="19" rx="4"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 51 (57462) - http://www.bohemiancoding.com/sketch -->
<title>fill-radial-enabled</title>
<desc>Created with Sketch.</desc>
<defs>
<radialGradient cx="50%" cy="50%" fx="50%" fy="50%" r="39.3896484%" id="radialGradient-1">
<stop stop-color="#4C97FF" offset="0%"></stop>
<stop stop-color="#FFFFFF" offset="100%"></stop>
</radialGradient>
</defs>
<g id="fill-radial-enabled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-opacity="0.15">
<rect id="Radial" stroke="#000000" fill="url(#radialGradient-1)" x="0.5" y="0.5" width="19" height="19" rx="4"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 870 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>fill-solid-enabled</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="fill-solid-enabled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-opacity="0.15">
<rect id="Solid" stroke="#000000" fill="#4C97FF" x="0.5" y="0.5" width="19" height="19" rx="4"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 604 B

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 51 (57462) - http://www.bohemiancoding.com/sketch -->
<title>fill-vert-gradient-enabled</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2="3.061617e-15%" id="linearGradient-1">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#4C97FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="fill-vert-gradient-enabled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-opacity="0.15">
<rect id="Vertical" stroke="#000000" fill="url(#linearGradient-1)" x="0.5" y="0.5" width="19" height="19" rx="4"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>swap</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="swap" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Swap-v2" transform="translate(2.000000, 2.000000)" fill="#4C97FF">
<path d="M3.69424597,2.00682151 L7.95663608,2.66498231 C8.44536222,2.73588913 8.7900887,3.24405473 8.7176525,3.79585529 C8.66179809,4.24129561 8.34150792,4.58764819 7.95663608,4.64128284 L3.69424597,5.30126176 L3.69424597,6.79666856 C3.69424597,7.22392764 3.23781067,7.42846657 2.97250219,7.13483958 L0.120436084,3.97403142 C-0.0401453614,3.78767373 -0.0401453614,3.48404706 0.120436084,3.30587093 L2.97250219,0.135972153 C3.23781067,-0.157654834 3.69424597,0.0559747078 3.69424597,0.474143173 L3.69424597,2.00682151 Z M12.305754,10.7340942 L12.305754,9.2014159 C12.305754,8.78324744 12.7621893,8.56961789 13.0274978,8.86324488 L15.8795639,12.0331437 C16.0401454,12.2113198 16.0401454,12.5149465 15.8795639,12.7013041 L13.0274978,15.8621123 C12.7621893,16.1557393 12.305754,15.9512004 12.305754,15.5239413 L12.305754,14.0285345 L8.04336392,13.3685556 C7.65849208,13.3149209 7.33820191,12.9685683 7.2823475,12.523128 C7.2099113,11.9713275 7.55463778,11.4631619 8.04336392,11.392255 L12.305754,10.7340942 Z" id="Swap-v1"></path>
<path d="M11.2727273,1.45454545 L13.4545455,1.45454545 C14.0567273,1.45454545 14.5454545,1.94327273 14.5454545,2.54545455 L14.5454545,4.72727273 C14.5454545,5.33054545 14.0567273,5.81818182 13.4545455,5.81818182 L11.2727273,5.81818182 C10.6705455,5.81818182 10.1818182,5.33054545 10.1818182,4.72727273 L10.1818182,2.54545455 C10.1818182,1.94327273 10.6705455,1.45454545 11.2727273,1.45454545" id="Fill-6" fill-opacity="0.5"></path>
<path d="M2.54545455,10.1818182 L4.72727273,10.1818182 C5.32945455,10.1818182 5.81818182,10.6705455 5.81818182,11.2727273 L5.81818182,13.4545455 C5.81818182,14.0578182 5.32945455,14.5454545 4.72727273,14.5454545 L2.54545455,14.5454545 C1.94327273,14.5454545 1.45454545,14.0578182 1.45454545,13.4545455 L1.45454545,11.2727273 C1.45454545,10.6705455 1.94327273,10.1818182 2.54545455,10.1818182" id="Fill-6-Copy" fill-opacity="0.5"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -8,6 +8,8 @@ import ColorPicker from '../containers/color-picker.jsx';
import InputGroup from './input-group/input-group.jsx';
import Label from './forms/label.jsx';
import GradientTypes from '../lib/gradient-types';
const messages = defineMessages({
fill: {
id: 'paint.paintEditor.fill',
@ -25,7 +27,12 @@ const FillColorIndicatorComponent = props => (
body={
<ColorPicker
color={props.fillColor}
color2={props.fillColor2}
gradientType={props.gradientType}
shouldShowGradientTools={props.shouldShowGradientTools}
onChangeColor={props.onChangeFillColor}
onChangeGradientType={props.onChangeGradientType}
onSwap={props.onSwap}
/>
}
isOpen={props.fillColorModalVisible}
@ -35,6 +42,8 @@ const FillColorIndicatorComponent = props => (
<Label text={props.intl.formatMessage(messages.fill)}>
<ColorButton
color={props.fillColor}
color2={props.fillColor2}
gradientType={props.gradientType}
onClick={props.onOpenFillColor}
/>
</Label>
@ -46,11 +55,16 @@ FillColorIndicatorComponent.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool.isRequired,
fillColor: PropTypes.string,
fillColor2: PropTypes.string,
fillColorModalVisible: PropTypes.bool.isRequired,
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
intl: intlShape,
onChangeFillColor: PropTypes.func.isRequired,
onChangeGradientType: PropTypes.func.isRequired,
onCloseFillColor: PropTypes.func.isRequired,
onOpenFillColor: PropTypes.func.isRequired
onOpenFillColor: PropTypes.func.isRequired,
onSwap: PropTypes.func.isRequired,
shouldShowGradientTools: PropTypes.bool.isRequired
};
export default injectIntl(FillColorIndicatorComponent);

View file

@ -8,6 +8,10 @@
margin-bottom: 20px;
}
.last {
margin-bottom: 4px;
}
.handle {
left: 100px;
width: 26px;

View file

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import classNames from 'classnames';
import {getEventXY} from '../../lib/touch-utils';
import styles from './slider.css';
@ -63,7 +64,10 @@ class SliderComponent extends React.Component {
halfHandleWidth;
return (
<div
className={styles.container}
className={classNames({
[styles.container]: true,
[styles.last]: this.props.lastSlider
})}
ref={this.setBackground}
style={{
backgroundImage: this.props.background
@ -85,6 +89,7 @@ class SliderComponent extends React.Component {
SliderComponent.propTypes = {
background: PropTypes.string,
lastSlider: PropTypes.bool,
onChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};

View file

@ -7,6 +7,7 @@ import ColorButton from './color-button/color-button.jsx';
import ColorPicker from '../containers/color-picker.jsx';
import InputGroup from './input-group/input-group.jsx';
import Label from './forms/label.jsx';
import GradientTypes from '../lib/gradient-types';
const messages = defineMessages({
stroke: {
@ -25,6 +26,10 @@ const StrokeColorIndicatorComponent = props => (
body={
<ColorPicker
color={props.strokeColor}
color2={null}
gradientType={GradientTypes.SOLID}
shouldShowGradientTools={false}
// @todo handle stroke gradient
onChangeColor={props.onChangeStrokeColor}
/>
}
@ -36,6 +41,9 @@ const StrokeColorIndicatorComponent = props => (
<ColorButton
outline
color={props.strokeColor}
color2={null}
gradientType={GradientTypes.SOLID}
// @todo handle stroke gradient
onClick={props.onOpenStrokeColor}
/>
</Label>

View file

@ -9,6 +9,7 @@ import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
import {clearGradient} from '../reducers/selection-gradient-type';
import BitBrushModeComponent from '../components/bit-brush-mode/bit-brush-mode.jsx';
import BitBrushTool from '../helper/bit-tools/brush-tool';
@ -33,7 +34,7 @@ class BitBrushMode extends React.Component {
if (this.tool && nextProps.bitBrushSize !== this.props.bitBrushSize) {
this.tool.setBrushSize(nextProps.bitBrushSize);
}
if (nextProps.isBitBrushModeActive && !this.props.isBitBrushModeActive) {
this.activateTool();
} else if (!nextProps.isBitBrushModeActive && this.props.isBitBrushModeActive) {
@ -45,6 +46,7 @@ class BitBrushMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
let color = this.props.color;
if (!color || color === MIXED) {
@ -76,6 +78,7 @@ class BitBrushMode extends React.Component {
BitBrushMode.propTypes = {
bitBrushSize: PropTypes.number.isRequired,
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
@ -93,6 +96,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_BRUSH));
},

View file

@ -9,6 +9,7 @@ import FillModeComponent from '../components/bit-fill-mode/bit-fill-mode.jsx';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection} from '../helper/selection';
import FillTool from '../helper/bit-tools/fill-tool';
import {MIXED} from '../helper/style-path';
@ -42,6 +43,7 @@ class BitFillMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
if (!fillColorPresent) {
@ -67,6 +69,7 @@ class BitFillMode extends React.Component {
}
BitFillMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
@ -80,6 +83,9 @@ const mapStateToProps = state => ({
isFillModeActive: state.scratchPaint.mode === Modes.BIT_FILL
});
const mapDispatchToProps = dispatch => ({
clearGradient: () => {
dispatch(clearGradient());
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},

View file

@ -9,6 +9,7 @@ import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
import {clearGradient} from '../reducers/selection-gradient-type';
import BitLineModeComponent from '../components/bit-line-mode/bit-line-mode.jsx';
import BitLineTool from '../helper/bit-tools/line-tool';
@ -33,7 +34,7 @@ class BitLineMode extends React.Component {
if (this.tool && nextProps.bitBrushSize !== this.props.bitBrushSize) {
this.tool.setLineSize(nextProps.bitBrushSize);
}
if (nextProps.isBitLineModeActive && !this.props.isBitLineModeActive) {
this.activateTool();
} else if (!nextProps.isBitLineModeActive && this.props.isBitLineModeActive) {
@ -45,6 +46,7 @@ class BitLineMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default line color if fill is MIXED or transparent
let color = this.props.color;
if (!color || color === MIXED) {
@ -76,6 +78,7 @@ class BitLineMode extends React.Component {
BitLineMode.propTypes = {
bitBrushSize: PropTypes.number.isRequired,
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
handleMouseDown: PropTypes.func.isRequired,
@ -93,6 +96,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BIT_LINE));
},

View file

@ -9,6 +9,7 @@ import {MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import OvalTool from '../helper/bit-tools/oval-tool';
import OvalModeComponent from '../components/bit-oval-mode/bit-oval-mode.jsx';
@ -54,6 +55,7 @@ class BitOvalMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
if (!fillColorPresent) {
@ -84,6 +86,7 @@ class BitOvalMode extends React.Component {
}
BitOvalMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
filled: PropTypes.bool,
@ -109,6 +112,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */));
},

View file

@ -9,6 +9,7 @@ import {MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import RectTool from '../helper/bit-tools/rect-tool';
import RectModeComponent from '../components/bit-rect-mode/bit-rect-mode.jsx';
@ -54,6 +55,7 @@ class BitRectMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
const fillColorPresent = this.props.color !== MIXED && this.props.color !== null;
if (!fillColorPresent) {
@ -84,6 +86,7 @@ class BitRectMode extends React.Component {
}
BitRectMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
color: PropTypes.string,
filled: PropTypes.bool,
@ -109,6 +112,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems(), true /* bitmapMode */));
},

View file

@ -7,6 +7,7 @@ import Modes from '../lib/modes';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {getSelectedLeafItems} from '../helper/selection';
import BitSelectTool from '../helper/bit-tools/select-tool';
import SelectModeComponent from '../components/bit-select-mode/bit-select-mode.jsx';
@ -39,6 +40,7 @@ class BitSelectMode extends React.Component {
return nextProps.isSelectModeActive !== this.props.isSelectModeActive;
}
activateTool () {
this.props.clearGradient();
this.tool = new BitSelectTool(
this.props.setSelectedItems,
this.props.clearSelectedItems,
@ -62,6 +64,7 @@ class BitSelectMode extends React.Component {
}
BitSelectMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
handleMouseDown: PropTypes.func.isRequired,
isSelectModeActive: PropTypes.bool.isRequired,
@ -75,6 +78,9 @@ const mapStateToProps = state => ({
selectedItems: state.scratchPaint.selectedItems
});
const mapDispatchToProps = dispatch => ({
clearGradient: () => {
dispatch(clearGradient());
},
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},

View file

@ -9,6 +9,7 @@ import {MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection} from '../helper/selection';
import BrushModeComponent from '../components/brush-mode/brush-mode.jsx';
@ -48,6 +49,7 @@ class BrushMode extends React.Component {
// TODO: Instead of clearing selection, consider a kind of "draw inside"
// analogous to how selection works with eraser
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// Force the default brush color if fill is MIXED or transparent
const {fillColor} = this.props.colorState;
if (fillColor === MIXED || fillColor === null) {
@ -76,6 +78,7 @@ BrushMode.propTypes = {
brushModeState: PropTypes.shape({
brushSize: PropTypes.number.isRequired
}),
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
fillColor: PropTypes.string,
@ -97,6 +100,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
handleMouseDown: () => {
dispatch(changeMode(Modes.BRUSH));
},

View file

@ -5,8 +5,10 @@ import parseColor from 'parse-color';
import PropTypes from 'prop-types';
import React from 'react';
import {changeColorIndex} from '../reducers/color-index';
import {clearSelectedItems} from '../reducers/selected-items';
import {activateEyeDropper} from '../reducers/eye-dropper';
import GradientTypes from '../lib/gradient-types';
import ColorPickerComponent from '../components/color-picker/color-picker.jsx';
import {MIXED} from '../helper/style-path';
@ -36,6 +38,10 @@ class ColorPicker extends React.Component {
super(props);
bindAll(this, [
'getHsv',
'handleChangeGradientTypeHorizontal',
'handleChangeGradientTypeRadial',
'handleChangeGradientTypeSolid',
'handleChangeGradientTypeVertical',
'handleHueChange',
'handleSaturationChange',
'handleBrightnessChange',
@ -43,7 +49,8 @@ class ColorPicker extends React.Component {
'handleActivateEyeDropper'
]);
const hsv = this.getHsv(props.color);
const color = props.colorIndex === 0 ? props.color : props.color2;
const hsv = this.getHsv(color);
this.state = {
hue: hsv[0],
saturation: hsv[1],
@ -51,9 +58,11 @@ class ColorPicker extends React.Component {
};
}
componentWillReceiveProps (newProps) {
if (this.props.isEyeDropping && this.props.color !== newProps.color) {
// color set by eye dropper, so update slider states
const hsv = this.getHsv(newProps.color);
const color = newProps.colorIndex === 0 ? this.props.color : this.props.color2;
const newColor = newProps.colorIndex === 0 ? newProps.color : newProps.color2;
const colorSetByEyedropper = this.props.isEyeDropping && color !== newColor;
if (colorSetByEyedropper || this.props.colorIndex !== newProps.colorIndex) {
const hsv = this.getHsv(newColor);
this.setState({
hue: hsv[0],
saturation: hsv[1],
@ -98,19 +107,41 @@ class ColorPicker extends React.Component {
this.props.onChangeColor
);
}
handleChangeGradientTypeHorizontal () {
this.props.onChangeGradientType(GradientTypes.HORIZONTAL);
}
handleChangeGradientTypeRadial () {
this.props.onChangeGradientType(GradientTypes.RADIAL);
}
handleChangeGradientTypeSolid () {
this.props.onChangeGradientType(GradientTypes.SOLID);
}
handleChangeGradientTypeVertical () {
this.props.onChangeGradientType(GradientTypes.VERTICAL);
}
render () {
return (
<ColorPickerComponent
brightness={this.state.brightness}
color={this.props.color}
color2={this.props.color2}
colorIndex={this.props.colorIndex}
gradientType={this.props.gradientType}
hue={this.state.hue}
isEyeDropping={this.props.isEyeDropping}
saturation={this.state.saturation}
shouldShowGradientTools={this.props.shouldShowGradientTools}
onActivateEyeDropper={this.handleActivateEyeDropper}
onBrightnessChange={this.handleBrightnessChange}
onChangeColor={this.props.onChangeColor}
onChangeGradientTypeHorizontal={this.handleChangeGradientTypeHorizontal}
onChangeGradientTypeRadial={this.handleChangeGradientTypeRadial}
onChangeGradientTypeSolid={this.handleChangeGradientTypeSolid}
onChangeGradientTypeVertical={this.handleChangeGradientTypeVertical}
onHueChange={this.handleHueChange}
onSaturationChange={this.handleSaturationChange}
onSelectColor={this.props.onSelectColor}
onSelectColor2={this.props.onSelectColor2}
onSwap={this.props.onSwap}
onTransparent={this.handleTransparent}
/>
);
@ -119,12 +150,21 @@ class ColorPicker extends React.Component {
ColorPicker.propTypes = {
color: PropTypes.string,
color2: PropTypes.string,
colorIndex: PropTypes.number.isRequired,
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
isEyeDropping: PropTypes.bool.isRequired,
onActivateEyeDropper: PropTypes.func.isRequired,
onChangeColor: PropTypes.func.isRequired
onChangeColor: PropTypes.func.isRequired,
onChangeGradientType: PropTypes.func,
onSelectColor: PropTypes.func.isRequired,
onSelectColor2: PropTypes.func.isRequired,
onSwap: PropTypes.func,
shouldShowGradientTools: PropTypes.bool.isRequired
};
const mapStateToProps = state => ({
colorIndex: state.scratchPaint.fillMode.colorIndex,
isEyeDropping: state.scratchPaint.color.eyeDropper.active
});
@ -134,6 +174,12 @@ const mapDispatchToProps = dispatch => ({
},
onActivateEyeDropper: (currentTool, callback) => {
dispatch(activateEyeDropper(currentTool, callback));
},
onSelectColor: () => {
dispatch(changeColorIndex(0));
},
onSelectColor2: () => {
dispatch(changeColorIndex(1));
}
});

View file

@ -2,21 +2,35 @@ import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import bindAll from 'lodash.bindall';
import parseColor from 'parse-color';
import {changeColorIndex} from '../reducers/color-index';
import {changeFillColor} from '../reducers/fill-color';
import {changeFillColor2} from '../reducers/fill-color-2';
import {changeGradientType} from '../reducers/fill-mode-gradient-type';
import {openFillColor, closeFillColor} from '../reducers/modals';
import {getSelectedLeafItems} from '../helper/selection';
import {setSelectedItems} from '../reducers/selected-items';
import Modes from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap} from '../lib/format';
import {isBitmap, isVector} from '../lib/format';
import GradientTypes from '../lib/gradient-types';
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
import {applyFillColorToSelection} from '../helper/style-path';
import {applyFillColorToSelection,
applyGradientTypeToSelection,
getRotatedColor,
swapColorsInSelection,
MIXED} from '../helper/style-path';
class FillColorIndicator extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeFillColor',
'handleCloseFillColor'
'handleChangeGradientType',
'handleCloseFillColor',
'handleSwap'
]);
// Flag to track whether an svg-update-worthy change has been made
@ -32,56 +46,129 @@ class FillColorIndicator extends React.Component {
}
handleChangeFillColor (newColor) {
// Apply color and update redux, but do not update svg until picker closes.
const isDifferent = applyFillColorToSelection(newColor, isBitmap(this.props.format), this.props.textEditTarget);
const isDifferent = applyFillColorToSelection(
newColor,
this.props.colorIndex,
this.props.gradientType === GradientTypes.SOLID,
isBitmap(this.props.format),
this.props.textEditTarget);
this._hasChanged = this._hasChanged || isDifferent;
this.props.onChangeFillColor(newColor);
this.props.onChangeFillColor(newColor, this.props.colorIndex);
}
handleChangeGradientType (gradientType) {
// Apply color and update redux, but do not update svg until picker closes.
const isDifferent = applyGradientTypeToSelection(
gradientType,
isBitmap(this.props.format),
this.props.textEditTarget);
this._hasChanged = this._hasChanged || isDifferent;
const hasSelectedItems = getSelectedLeafItems().length > 0;
if (hasSelectedItems) {
if (isDifferent) {
// Recalculates the swatch colors
this.props.setSelectedItems();
}
}
if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) {
// Generate color 2 and change to the 2nd swatch when switching from solid to gradient
if (!hasSelectedItems) {
this.props.onChangeFillColor(getRotatedColor(this.props.fillColor), 1);
}
this.props.onChangeColorIndex(1);
}
this.props.onChangeGradientType(gradientType);
}
handleCloseFillColor () {
if (!this.props.isEyeDropping) {
this.props.onCloseFillColor();
}
this.props.onChangeColorIndex(0);
}
handleSwap () {
if (getSelectedLeafItems().length) {
swapColorsInSelection(
isBitmap(this.props.format),
this.props.textEditTarget);
this.props.setSelectedItems();
} else {
let color1 = this.props.fillColor;
let color2 = this.props.fillColor2;
color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex;
color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex;
this.props.onChangeFillColor(color1, 1);
this.props.onChangeFillColor(color2, 0);
}
}
render () {
return (
<FillColorIndicatorComponent
{...this.props}
onChangeFillColor={this.handleChangeFillColor}
onChangeGradientType={this.handleChangeGradientType}
onCloseFillColor={this.handleCloseFillColor}
onSwap={this.handleSwap}
/>
);
}
}
const mapStateToProps = state => ({
colorIndex: state.scratchPaint.fillMode.colorIndex,
disabled: state.scratchPaint.mode === Modes.LINE,
fillColor: state.scratchPaint.color.fillColor,
fillColor2: state.scratchPaint.color.fillColor2,
fillColorModalVisible: state.scratchPaint.modals.fillColor,
format: state.scratchPaint.format,
gradientType: state.scratchPaint.color.gradientType,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
mode: state.scratchPaint.mode,
shouldShowGradientTools: isVector(state.scratchPaint.format) &&
(state.scratchPaint.mode === Modes.SELECT ||
state.scratchPaint.mode === Modes.RESHAPE ||
state.scratchPaint.mode === Modes.FILL),
textEditTarget: state.scratchPaint.textEditTarget
});
const mapDispatchToProps = dispatch => ({
onChangeFillColor: fillColor => {
dispatch(changeFillColor(fillColor));
onChangeColorIndex: index => {
dispatch(changeColorIndex(index));
},
onChangeFillColor: (fillColor, index) => {
if (index === 0) {
dispatch(changeFillColor(fillColor));
} else if (index === 1) {
dispatch(changeFillColor2(fillColor));
}
},
onOpenFillColor: () => {
dispatch(openFillColor());
},
onCloseFillColor: () => {
dispatch(closeFillColor());
},
onChangeGradientType: gradientType => {
dispatch(changeGradientType(gradientType));
},
setSelectedItems: format => {
dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
}
});
FillColorIndicator.propTypes = {
colorIndex: PropTypes.number.isRequired,
disabled: PropTypes.bool.isRequired,
fillColor: PropTypes.string,
fillColor2: PropTypes.string,
fillColorModalVisible: PropTypes.bool.isRequired,
format: PropTypes.oneOf(Object.keys(Formats)),
gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
isEyeDropping: PropTypes.bool.isRequired,
onChangeColorIndex: PropTypes.func.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onChangeGradientType: PropTypes.func.isRequired,
onCloseFillColor: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired,
textEditTarget: PropTypes.number
};

View file

@ -3,14 +3,17 @@ import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import Modes from '../lib/modes';
import GradientTypes from '../lib/gradient-types';
import FillTool from '../helper/tools/fill-tool';
import {MIXED} from '../helper/style-path';
import {getRotatedColor, MIXED} from '../helper/style-path';
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeFillColor2} from '../reducers/fill-color-2';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems} from '../reducers/selected-items';
import {clearSelection} from '../helper/selection';
import {clearHoveredItem, setHoveredItem} from '../reducers/hover';
import {changeGradientType} from '../reducers/fill-mode-gradient-type';
import FillModeComponent from '../components/fill-mode/fill-mode.jsx';
@ -28,11 +31,19 @@ class FillMode extends React.Component {
}
}
componentWillReceiveProps (nextProps) {
if (this.tool && nextProps.fillColor !== this.props.fillColor) {
this.tool.setFillColor(nextProps.fillColor);
}
if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) {
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
if (this.tool) {
if (nextProps.fillColor !== this.props.fillColor) {
this.tool.setFillColor(nextProps.fillColor);
}
if (nextProps.fillColor2 !== this.props.fillColor2) {
this.tool.setFillColor2(nextProps.fillColor2);
}
if (nextProps.hoveredItemId !== this.props.hoveredItemId) {
this.tool.setPrevHoveredItemId(nextProps.hoveredItemId);
}
if (nextProps.fillModeGradientType !== this.props.fillModeGradientType) {
this.tool.setGradientType(nextProps.fillModeGradientType);
}
}
if (nextProps.isFillModeActive && !this.props.isFillModeActive) {
@ -46,16 +57,35 @@ class FillMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
// Force the default fill color if fill is MIXED
let fillColor = this.props.fillColor;
if (this.props.fillColor === MIXED) {
this.props.onChangeFillColor(DEFAULT_COLOR);
fillColor = DEFAULT_COLOR;
this.props.onChangeFillColor(DEFAULT_COLOR, 0);
}
const gradientType = this.props.fillModeGradientType ?
this.props.fillModeGradientType : this.props.selectModeGradientType;
let fillColor2 = this.props.fillColor2;
if (gradientType !== this.props.selectModeGradientType) {
if (this.props.selectModeGradientType === GradientTypes.SOLID) {
fillColor2 = getRotatedColor(fillColor);
this.props.onChangeFillColor(fillColor2, 1);
}
this.props.changeGradientType(gradientType);
}
if (this.props.fillColor2 === MIXED) {
fillColor2 = getRotatedColor(fillColor);
this.props.onChangeFillColor(fillColor2, 1);
}
this.tool = new FillTool(
this.props.setHoveredItem,
this.props.clearHoveredItem,
this.props.onUpdateImage
);
this.tool.setFillColor(this.props.fillColor === MIXED ? DEFAULT_COLOR : this.props.fillColor);
this.tool.setFillColor(fillColor);
this.tool.setFillColor2(fillColor2);
this.tool.setGradientType(gradientType);
this.tool.setPrevHoveredItemId(this.props.hoveredItemId);
this.tool.activate();
}
@ -75,22 +105,28 @@ class FillMode extends React.Component {
}
FillMode.propTypes = {
changeGradientType: PropTypes.func.isRequired,
clearHoveredItem: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
fillColor: PropTypes.string,
fillColor2: PropTypes.string,
fillModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)),
handleMouseDown: PropTypes.func.isRequired,
hoveredItemId: PropTypes.number,
isFillModeActive: PropTypes.bool.isRequired,
onChangeFillColor: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
selectModeGradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired,
setHoveredItem: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
fillModeState: state.scratchPaint.fillMode,
fillModeGradientType: state.scratchPaint.fillMode.gradientType, // Last user-selected gradient type
fillColor: state.scratchPaint.color.fillColor,
fillColor2: state.scratchPaint.color.fillColor2,
hoveredItemId: state.scratchPaint.hoveredItemId,
isFillModeActive: state.scratchPaint.mode === Modes.FILL
isFillModeActive: state.scratchPaint.mode === Modes.FILL,
selectModeGradientType: state.scratchPaint.color.gradientType
});
const mapDispatchToProps = dispatch => ({
setHoveredItem: hoveredItemId => {
@ -102,11 +138,18 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
changeGradientType: gradientType => {
dispatch(changeGradientType(gradientType));
},
handleMouseDown: () => {
dispatch(changeMode(Modes.FILL));
},
onChangeFillColor: fillColor => {
dispatch(changeFillColor(fillColor));
onChangeFillColor: (fillColor, index) => {
if (index === 0) {
dispatch(changeFillColor(fillColor));
} else if (index === 1) {
dispatch(changeFillColor2(fillColor));
}
}
});

View file

@ -10,6 +10,7 @@ import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeStrokeColor} from '../reducers/stroke-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import OvalTool from '../helper/tools/oval-tool';
@ -47,6 +48,7 @@ class OvalMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
// If exactly one of fill or stroke color is set, set the other one to transparent.
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
@ -86,6 +88,7 @@ class OvalMode extends React.Component {
}
OvalMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
fillColor: PropTypes.string,
@ -110,6 +113,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},

View file

@ -10,6 +10,7 @@ import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
import {changeStrokeColor} from '../reducers/stroke-color';
import {changeMode} from '../reducers/modes';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import RectTool from '../helper/tools/rect-tool';
@ -47,6 +48,7 @@ class RectMode extends React.Component {
}
activateTool () {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
// If exactly one of fill or stroke color is set, set the other one to transparent.
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
@ -86,6 +88,7 @@ class RectMode extends React.Component {
}
RectMode.propTypes = {
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
fillColor: PropTypes.string,
@ -110,6 +113,9 @@ const mapDispatchToProps = dispatch => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
setSelectedItems: () => {
dispatch(setSelectedItems(getSelectedLeafItems(), false /* bitmapMode */));
},

View file

@ -55,7 +55,8 @@ class StrokeColorIndicator extends React.Component {
const mapStateToProps = state => ({
disabled: state.scratchPaint.mode === Modes.BRUSH ||
state.scratchPaint.mode === Modes.TEXT,
state.scratchPaint.mode === Modes.TEXT ||
state.scratchPaint.mode === Modes.FILL,
format: state.scratchPaint.format,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
strokeColor: state.scratchPaint.color.strokeColor,

View file

@ -33,7 +33,8 @@ class StrokeWidthIndicator extends React.Component {
const mapStateToProps = state => ({
disabled: state.scratchPaint.mode === Modes.BRUSH ||
state.scratchPaint.mode === Modes.TEXT,
state.scratchPaint.mode === Modes.TEXT ||
state.scratchPaint.mode === Modes.FILL,
strokeWidth: state.scratchPaint.color.strokeWidth,
textEditTarget: state.scratchPaint.textEditTarget
});

View file

@ -13,6 +13,7 @@ import {changeStrokeColor} from '../reducers/stroke-color';
import {changeMode} from '../reducers/modes';
import {setTextEditTarget} from '../reducers/text-edit-target';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {clearGradient} from '../reducers/selection-gradient-type';
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
import TextTool from '../helper/tools/text-tool';
@ -60,6 +61,7 @@ class TextMode extends React.Component {
}
activateTool (nextProps) {
clearSelection(this.props.clearSelectedItems);
this.props.clearGradient();
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
// If exactly one of fill or stroke color is set, set the other one to transparent.
@ -116,6 +118,7 @@ class TextMode extends React.Component {
TextMode.propTypes = {
changeFont: PropTypes.func.isRequired,
clearGradient: PropTypes.func.isRequired,
clearSelectedItems: PropTypes.func.isRequired,
colorState: PropTypes.shape({
fillColor: PropTypes.string,
@ -155,6 +158,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
clearSelectedItems: () => {
dispatch(clearSelectedItems());
},
clearGradient: () => {
dispatch(clearGradient());
},
handleChangeModeBitText: () => {
dispatch(changeMode(Modes.BIT_TEXT));
},

View file

@ -3,13 +3,16 @@ import {getSelectedLeafItems} from './selection';
import {isPGTextItem, isPointTextItem} from './item';
import {isGroup} from './group';
import {getItems} from './selection';
import GradientTypes from '../lib/gradient-types';
import parseColor from 'parse-color';
import {DEFAULT_COLOR} from '../reducers/fill-color';
const MIXED = 'scratch-paint/style-path/mixed';
// Check if the item color matches the incoming color. If the item color is a gradient, we assume
// that the incoming color never matches, since we don't support gradients yet.
const _colorMatch = function (itemColor, incomingColor) {
// @todo check whether the gradient has changed when we support gradients
// @todo colorMatch should not be called with gradients as arguments once stroke gradients are supported
if (itemColor && itemColor.type === 'gradient') return false;
// Either both are null or both are the same color when converted to CSS.
return (!itemColor && !incomingColor) ||
@ -30,31 +33,221 @@ const _getColorStateListeners = function (textEditTargetId) {
return items;
};
/**
* Transparent R, G, B values need to match the other color of the gradient
* in order to form a smooth gradient, otherwise it fades through black. This
* function gets the transparent color for a given color string.
* @param {?string} colorToMatch CSS string of other color of gradient, or null for transparent
* @return {string} CSS string for matching color of transparent
*/
const getColorStringForTransparent = function (colorToMatch) {
const color = new paper.Color(colorToMatch);
color.alpha = 0;
return color.toCSS();
};
// Returns a color shift by 72 of the given color, DEFAULT_COLOR if the given color is null, or null if it is MIXED.
const getRotatedColor = function (firstColor) {
if (firstColor === MIXED) return null;
const color = new paper.Color(firstColor);
if (!firstColor || color.alpha === 0) return DEFAULT_COLOR;
return parseColor(
`hsl(${(color.hue - 72) % 360}, ${color.saturation * 100}, ${Math.max(color.lightness * 100, 10)})`).hex;
};
/**
* Called when setting fill color
* @param {string} colorString New color, css format
* @param {string} colorString color, css format, or null if completely transparent
* @param {number} colorIndex index of color being changed
* @param {boolean} isSolidGradient True if is solid gradient. Sometimes the item has a gradient but the color
* picker is set to a solid gradient. This happens when a mix of colors and gradient types is selected.
* When changing the color in this case, the solid gradient should override the existing gradient on the item.
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
const applyFillColorToSelection = function (colorString, bitmapMode, textEditTargetId) {
const applyFillColorToSelection = function (colorString, colorIndex, isSolidGradient, bitmapMode, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
if (isPointTextItem(item) && !colorString) {
colorString = 'rgba(0,0,0,0)';
} else if (item.parent instanceof paper.CompoundPath) {
if (item.parent instanceof paper.CompoundPath) {
item = item.parent;
}
// In bitmap mode, fill color applies to the stroke if there is a stroke
if (bitmapMode && item.strokeColor !== null && item.strokeWidth !== 0) {
if (!_colorMatch(item.strokeColor, colorString)) {
changed = true;
item.strokeColor = colorString;
}
} else if (!_colorMatch(item.fillColor, colorString)) {
} else if (isSolidGradient || !item.fillColor || !item.fillColor.gradient ||
!item.fillColor.gradient.stops.length === 2) {
// Applying a solid color
if (!_colorMatch(item.fillColor, colorString)) {
changed = true;
if (isPointTextItem(item) && !colorString) {
// Allows transparent text to be hit
item.fillColor = 'rgba(0,0,0,0)';
} else {
item.fillColor = colorString;
}
}
} else if (!_colorMatch(item.fillColor.gradient.stops[colorIndex].color, colorString)) {
// Changing one color of an existing gradient
changed = true;
item.fillColor = colorString;
const otherIndex = colorIndex === 0 ? 1 : 0;
if (colorString === null) {
colorString = getColorStringForTransparent(item.fillColor.gradient.stops[otherIndex].color.toCSS());
}
const colors = [0, 0];
colors[colorIndex] = colorString;
// If the other color is transparent, its RGB values need to be adjusted for the gradient to be smooth
if (item.fillColor.gradient.stops[otherIndex].color.alpha === 0) {
colors[otherIndex] = getColorStringForTransparent(colorString);
} else {
colors[otherIndex] = item.fillColor.gradient.stops[otherIndex].color.toCSS();
}
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial};
}
}
return changed;
};
/**
* Called to swap gradient colors
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
const swapColorsInSelection = function (bitmapMode, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
if (item.parent instanceof paper.CompoundPath) {
item = item.parent;
}
if (bitmapMode) {
// @todo
return;
} else if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops.length === 2) {
// Only one color; nothing to swap
continue;
} else if (!item.fillColor.gradient.stops[0].color.equals(item.fillColor.gradient.stops[1].color)) {
// Changing one color of an existing gradient
changed = true;
const colors = [
item.fillColor.gradient.stops[1].color.toCSS(),
item.fillColor.gradient.stops[0].color.toCSS()
];
// There seems to be a bug where setting colors on stops doesn't always update the view, so set gradient.
item.fillColor.gradient = {stops: colors, radial: item.fillColor.gradient.radial};
}
}
return changed;
};
/**
* Called when setting gradient type
* @param {GradientType} gradientType gradient type
* @param {?boolean} bitmapMode True if the fill color is being set in bitmap mode
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
* @return {boolean} Whether the color application actually changed visibly.
*/
const applyGradientTypeToSelection = function (gradientType, bitmapMode, textEditTargetId) {
const items = _getColorStateListeners(textEditTargetId);
let changed = false;
for (let item of items) {
if (item.parent instanceof paper.CompoundPath) {
item = item.parent;
}
let itemColor1;
if (item.fillColor === null || item.fillColor.alpha === 0) {
// Transparent
itemColor1 = null;
} else if (!item.fillColor.gradient) {
// Solid color
itemColor1 = item.fillColor.toCSS();
} else if (!item.fillColor.gradient.stops[0] || item.fillColor.gradient.stops[0].color.alpha === 0) {
// Gradient where first color is transparent
itemColor1 = null;
} else {
// Gradient where first color is not transparent
itemColor1 = item.fillColor.gradient.stops[0].color.toCSS();
}
let itemColor2;
if (!item.fillColor || !item.fillColor.gradient || !item.fillColor.gradient.stops[1]) {
// If item color is solid or a gradient that has no 2nd color, set the 2nd color based on the first color
itemColor2 = getRotatedColor(itemColor1);
} else if (item.fillColor.gradient.stops[1].color.alpha === 0) {
// Gradient has 2nd color which is transparent
itemColor2 = null;
} else {
// Gradient has 2nd color which is not transparent
itemColor2 = item.fillColor.gradient.stops[1].color.toCSS();
}
if (bitmapMode) {
// @todo Add when we apply gradients to selections in bitmap mode
continue;
} else if (gradientType === GradientTypes.SOLID) {
if (item.fillColor && item.fillColor.gradient) {
changed = true;
item.fillColor = itemColor1;
}
continue;
}
if (itemColor1 === null) {
itemColor1 = getColorStringForTransparent(itemColor2);
}
if (itemColor2 === null) {
itemColor2 = getColorStringForTransparent(itemColor1);
}
if (gradientType === GradientTypes.RADIAL) {
const hasRadialGradient = item.fillColor && item.fillColor.gradient && item.fillColor.gradient.radial;
if (!hasRadialGradient) {
changed = true;
const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2;
item.fillColor = {
gradient: {
stops: [itemColor1, itemColor2],
radial: true
},
origin: item.position,
destination: item.position.add(new paper.Point(halfLongestDimension, 0))
};
}
} else if (gradientType === GradientTypes.HORIZONTAL) {
const hasHorizontalGradient = item.fillColor && item.fillColor.gradient &&
!item.fillColor.gradient.radial &&
Math.abs(item.fillColor.origin.y - item.fillColor.destination.y) < 1e-8;
if (!hasHorizontalGradient) {
changed = true;
item.fillColor = {
gradient: {
stops: [itemColor1, itemColor2]
},
origin: item.bounds.leftCenter,
destination: item.bounds.rightCenter
};
}
} else if (gradientType === GradientTypes.VERTICAL) {
const hasVerticalGradient = item.fillColor && item.fillColor.gradient && !item.fillColor.gradient.radial &&
Math.abs(item.fillColor.origin.x - item.fillColor.destination.x) < 1e-8;
if (!hasVerticalGradient) {
changed = true;
item.fillColor = {
gradient: {
stops: [itemColor1, itemColor2]
},
origin: item.bounds.topCenter,
destination: item.bounds.bottomCenter
};
}
}
}
return changed;
@ -144,9 +337,11 @@ const applyStrokeWidthToSelection = function (value, textEditTargetId) {
*/
const getColorsFromSelection = function (selectedItems, bitmapMode) {
let selectionFillColorString;
let selectionFillColor2String;
let selectionStrokeColorString;
let selectionStrokeWidth;
let selectionThickness;
let selectionGradientType;
let firstChild = true;
for (let item of selectedItems) {
@ -155,47 +350,39 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
item = item.parent;
}
let itemFillColorString;
let itemFillColor2String;
let itemStrokeColorString;
let itemGradientType = GradientTypes.SOLID;
// handle pgTextItems differently by going through their children
if (isPGTextItem(item)) {
for (const child of item.children) {
for (const path of child.children) {
if (!path.data.isPGGlyphRect) {
if (path.fillColor) {
itemFillColorString = path.fillColor.toCSS();
}
if (path.strokeColor) {
itemStrokeColorString = path.strokeColor.toCSS();
}
// check every style against the first of the items
if (firstChild) {
firstChild = false;
selectionFillColorString = itemFillColorString;
selectionStrokeColorString = itemStrokeColorString;
selectionStrokeWidth = path.strokeWidth;
}
if (itemFillColorString !== selectionFillColorString) {
selectionFillColorString = MIXED;
}
if (itemStrokeColorString !== selectionStrokeColorString) {
selectionStrokeColorString = MIXED;
}
if (selectionStrokeWidth !== path.strokeWidth) {
selectionStrokeWidth = null;
}
}
}
}
} else if (!isGroup(item)) {
if (!isGroup(item)) {
if (item.fillColor) {
// hack bc text items with null fill can't be detected by fill-hitTest anymore
if (isPointTextItem(item) && item.fillColor.toCSS() === 'rgba(0,0,0,0)') {
if (isPointTextItem(item) && item.fillColor.alpha === 0) {
itemFillColorString = null;
} else if (item.fillColor.type === 'gradient') {
itemFillColorString = MIXED;
// Scratch only recognizes 2 color gradients
if (item.fillColor.gradient.stops.length === 2) {
if (item.fillColor.gradient.radial) {
itemGradientType = GradientTypes.RADIAL;
} else {
// Always use horizontal for linear gradients, since horizontal and vertical gradients
// are the same with rotation. We don't want to show MIXED just because anything is rotated.
itemGradientType = GradientTypes.HORIZONTAL;
}
itemFillColorString = item.fillColor.gradient.stops[0].color.alpha === 0 ?
null :
item.fillColor.gradient.stops[0].color.toCSS();
itemFillColor2String = item.fillColor.gradient.stops[1].color.alpha === 0 ?
null :
item.fillColor.gradient.stops[1].color.toCSS();
} else {
itemFillColorString = MIXED;
itemFillColor2String = MIXED;
}
} else {
itemFillColorString = item.fillColor.toCSS();
itemFillColorString = item.fillColor.alpha === 0 ?
null :
item.fillColor.toCSS();
}
}
if (item.strokeColor) {
@ -205,14 +392,18 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
} else if (item.strokeColor.type === 'gradient') {
itemStrokeColorString = MIXED;
} else {
itemStrokeColorString = item.strokeColor.toCSS();
itemStrokeColorString = item.strokeColor.alpha === 0 ?
null :
item.strokeColor.toCSS();
}
}
// check every style against the first of the items
if (firstChild) {
firstChild = false;
selectionFillColorString = itemFillColorString;
selectionFillColor2String = itemFillColor2String;
selectionStrokeColorString = itemStrokeColorString;
selectionGradientType = itemGradientType;
selectionStrokeWidth = item.strokeWidth;
if (item.strokeWidth && item.data && item.data.zoomLevel) {
selectionThickness = item.strokeWidth / item.data.zoomLevel;
@ -221,6 +412,14 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
if (itemFillColorString !== selectionFillColorString) {
selectionFillColorString = MIXED;
}
if (itemFillColor2String !== selectionFillColor2String) {
selectionFillColor2String = MIXED;
}
if (itemGradientType !== selectionGradientType) {
selectionGradientType = GradientTypes.SOLID;
selectionFillColorString = MIXED;
selectionFillColor2String = MIXED;
}
if (itemStrokeColorString !== selectionStrokeColorString) {
selectionStrokeColorString = MIXED;
}
@ -229,14 +428,27 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) {
}
}
}
// Convert selection gradient type from horizontal to vertical if first item is exactly vertical
if (selectionGradientType !== GradientTypes.SOLID) {
let firstItem = selectedItems[0];
if (firstItem.parent instanceof paper.CompoundPath) firstItem = firstItem.parent;
const direction = firstItem.fillColor.destination.subtract(firstItem.fillColor.origin);
if (Math.abs(direction.angle) === 90) {
selectionGradientType = GradientTypes.VERTICAL;
}
}
if (bitmapMode) {
return {
fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
gradientType: selectionGradientType,
thickness: selectionThickness
};
}
return {
fillColor: selectionFillColorString ? selectionFillColorString : null,
fillColor2: selectionFillColor2String ? selectionFillColor2String : null,
gradientType: selectionGradientType,
strokeColor: selectionStrokeColorString ? selectionStrokeColorString : null,
strokeWidth: selectionStrokeWidth || (selectionStrokeWidth === null) ? selectionStrokeWidth : 0
};
@ -282,12 +494,16 @@ const styleShape = function (path, options) {
export {
applyFillColorToSelection,
applyGradientTypeToSelection,
applyStrokeColorToSelection,
applyStrokeWidthToSelection,
getColorsFromSelection,
getColorStringForTransparent,
getRotatedColor,
MIXED,
styleBlob,
styleShape,
stylePath,
styleCursorPreview
styleCursorPreview,
swapColorsInSelection
};

View file

@ -1,6 +1,8 @@
import paper from '@scratch/paper';
import {getHoveredItem} from '../hover';
import {expandBy} from '../math';
import {getColorStringForTransparent} from '../style-path';
import GradientTypes from '../../lib/gradient-types';
class FillTool extends paper.Tool {
static get TOLERANCE () {
@ -16,7 +18,7 @@ class FillTool extends paper.Tool {
this.setHoveredItem = setHoveredItem;
this.clearHoveredItem = clearHoveredItem;
this.onUpdateImage = onUpdateImage;
// We have to set these functions instead of just declaring them because
// paper.js tools hook up the listeners in the setter functions.
this.onMouseMove = this.handleMouseMove;
@ -24,6 +26,9 @@ class FillTool extends paper.Tool {
// Color to fill with
this.fillColor = null;
this.fillColor2 = null;
this.gradientType = null;
// The path that's being hovered over.
this.fillItem = null;
// If we're hovering over a hole in a compound path, we can't just recolor it. This is the
@ -59,6 +64,12 @@ class FillTool extends paper.Tool {
setFillColor (fillColor) {
this.fillColor = fillColor;
}
setFillColor2 (fillColor2) {
this.fillColor2 = fillColor2;
}
setGradientType (gradientType) {
this.gradientType = gradientType;
}
/**
* To be called when the hovered item changes. When the select tool hovers over a
* new item, it compares against this to see if a hover item change event needs to
@ -80,6 +91,10 @@ class FillTool extends paper.Tool {
const hitItem = hoveredItem ? hoveredItem.data.origItem : null;
// Still hitting the same thing
if ((!hitItem && !this.fillItem) || this.fillItem === hitItem) {
// Only radial gradient needs to be updated
if (this.gradientType === GradientTypes.RADIAL) {
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
}
return;
}
if (this.fillItem) {
@ -114,7 +129,7 @@ class FillTool extends paper.Tool {
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
this.fillItemOrigColor = hitItem.parent.fillColor;
}
this._setFillItemColor(this.fillColor);
this._setFillItemColor(this.fillColor, this.fillColor2, this.gradientType, event.point);
}
}
handleMouseUp (event) {
@ -158,14 +173,48 @@ class FillTool extends paper.Tool {
item.strokeColor.alpha === 0 ||
item.strokeWidth === 0;
}
_setFillItemColor (color) {
if (this.addedFillItem) {
this.addedFillItem.fillColor = color;
} else if (this.fillItem.parent instanceof paper.CompoundPath) {
this.fillItem.parent.fillColor = color;
// Either pass in a fully defined paper.Color as color1,
// or pass in 2 color strings, a gradient type, and a pointer location
_setFillItemColor (color1, color2, gradientType, pointerLocation) {
let fillColor;
const item = this._getFillItem();
if (!item) return;
if (color1 instanceof paper.Color || gradientType === GradientTypes.SOLID) {
fillColor = color1;
} else {
this.fillItem.fillColor = color;
if (color1 === null) {
color1 = getColorStringForTransparent(color2);
}
if (color2 === null) {
color2 = getColorStringForTransparent(color1);
}
const halfLongestDimension = Math.max(item.bounds.width, item.bounds.height) / 2;
const start = gradientType === GradientTypes.RADIAL ? pointerLocation :
gradientType === GradientTypes.VERTICAL ? item.bounds.topCenter :
gradientType === GradientTypes.HORIZONTAL ? item.bounds.leftCenter :
null;
const end = gradientType === GradientTypes.RADIAL ? start.add(new paper.Point(halfLongestDimension, 0)) :
gradientType === GradientTypes.VERTICAL ? item.bounds.bottomCenter :
gradientType === GradientTypes.HORIZONTAL ? item.bounds.rightCenter :
null;
fillColor = {
gradient: {
stops: [color1, color2],
radial: gradientType === GradientTypes.RADIAL
},
origin: start,
destination: end
};
}
item.fillColor = fillColor;
}
_getFillItem () {
if (this.addedFillItem) {
return this.addedFillItem;
} else if (this.fillItem && this.fillItem.parent instanceof paper.CompoundPath) {
return this.fillItem.parent;
}
return this.fillItem;
}
deactivateTool () {
if (this.fillItem) {

View file

@ -0,0 +1,9 @@
import keyMirror from 'keymirror';
const GradientTypes = keyMirror({
SOLID: null,
HORIZONTAL: null,
VERTICAL: null,
RADIAL: null
});
export default GradientTypes;

View file

@ -0,0 +1,36 @@
import log from '../log/log';
import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type';
import GradientTypes from '../lib/gradient-types';
const CHANGE_COLOR_INDEX = 'scratch-paint/color-index/CHANGE_COLOR_INDEX';
const initialState = 0;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case CHANGE_COLOR_INDEX:
if (action.index !== 1 && action.index !== 0) {
log.warn(`Invalid color index: ${action.index}`);
return state;
}
return action.index;
case CHANGE_GRADIENT_TYPE:
if (action.gradientType === GradientTypes.SOLID) return 0;
/* falls through */
default:
return state;
}
};
// Action creators ==================================
const changeColorIndex = function (index) {
return {
type: CHANGE_COLOR_INDEX,
index: index
};
};
export {
reducer as default,
changeColorIndex
};

View file

@ -1,12 +1,16 @@
import {combineReducers} from 'redux';
import eyeDropperReducer from './eye-dropper';
import fillColorReducer from './fill-color';
import fillColor2Reducer from './fill-color-2';
import gradientTypeReducer from './selection-gradient-type';
import strokeColorReducer from './stroke-color';
import strokeWidthReducer from './stroke-width';
export default combineReducers({
eyeDropper: eyeDropperReducer,
fillColor: fillColorReducer,
fillColor2: fillColor2Reducer,
gradientType: gradientTypeReducer,
strokeColor: strokeColorReducer,
strokeWidth: strokeWidthReducer
});

View file

@ -0,0 +1,53 @@
import log from '../log/log';
import {CHANGE_SELECTED_ITEMS} from './selected-items';
import {CLEAR_GRADIENT} from './selection-gradient-type';
import {MIXED, getColorsFromSelection} from '../helper/style-path';
import GradientTypes from '../lib/gradient-types';
const CHANGE_FILL_COLOR_2 = 'scratch-paint/fill-color/CHANGE_FILL_COLOR_2';
// Matches hex colors
const regExp = /^#([0-9a-f]{3}){1,2}$/i;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = null;
switch (action.type) {
case CHANGE_FILL_COLOR_2:
if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) {
log.warn(`Invalid hex color code: ${action.fillColor}`);
return state;
}
return action.fillColor;
case CHANGE_SELECTED_ITEMS:
{
// Don't change state if no selection
if (!action.selectedItems || !action.selectedItems.length) {
return state;
}
const colors = getColorsFromSelection(action.selectedItems);
if (colors.gradientType === GradientTypes.SOLID) {
// Gradient type may be solid when multiple gradient types are selected.
// In this case, changing the first color should not change the second color.
if (colors.fillColor2 === MIXED) return MIXED;
return state;
}
return colors.fillColor2;
}
case CLEAR_GRADIENT:
return null;
default:
return state;
}
};
// Action creators ==================================
const changeFillColor2 = function (fillColor) {
return {
type: CHANGE_FILL_COLOR_2,
fillColor: fillColor
};
};
export {
reducer as default,
changeFillColor2
};

View file

@ -1,6 +1,6 @@
import log from '../log/log';
import {CHANGE_SELECTED_ITEMS} from './selected-items';
import {getColorsFromSelection} from '../helper/style-path';
import {getColorsFromSelection, MIXED} from '../helper/style-path';
const CHANGE_FILL_COLOR = 'scratch-paint/fill-color/CHANGE_FILL_COLOR';
const DEFAULT_COLOR = '#9966FF';
@ -12,7 +12,7 @@ const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case CHANGE_FILL_COLOR:
if (!regExp.test(action.fillColor) && action.fillColor !== null) {
if (!regExp.test(action.fillColor) && action.fillColor !== null && action.fillColor !== MIXED) {
log.warn(`Invalid hex color code: ${action.fillColor}`);
return state;
}

View file

@ -0,0 +1,37 @@
// Gradient type shown in the fill tool. This is the last gradient type explicitly chosen by the user,
// and isn't overwritten by changing the selection.
import GradientTypes from '../lib/gradient-types';
import log from '../log/log';
const CHANGE_GRADIENT_TYPE = 'scratch-paint/fill-mode-gradient-type/CHANGE_GRADIENT_TYPE';
const initialState = null;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case CHANGE_GRADIENT_TYPE:
if (action.gradientType in GradientTypes) {
return action.gradientType;
}
log.warn(`Gradient type does not exist: ${action.gradientType}`);
/* falls through */
default:
return state;
}
};
// Action creators ==================================
// Use this for user-initiated gradient type selections only.
// See reducers/selection-gradient-type.js for other ways gradient type changes.
const changeGradientType = function (gradientType) {
return {
type: CHANGE_GRADIENT_TYPE,
gradientType: gradientType
};
};
export {
reducer as default,
CHANGE_GRADIENT_TYPE,
changeGradientType
};

View file

@ -0,0 +1,8 @@
import {combineReducers} from 'redux';
import fillModeGradientTypeReducer from './fill-mode-gradient-type';
import colorIndexReducer from './color-index';
export default combineReducers({
gradientType: fillModeGradientTypeReducer,
colorIndex: colorIndexReducer
});

View file

@ -7,6 +7,7 @@ import eraserModeReducer from './eraser-mode';
import colorReducer from './color';
import clipboardReducer from './clipboard';
import fillBitmapShapesReducer from './fill-bitmap-shapes';
import fillModeReducer from './fill-mode';
import fontReducer from './font';
import formatReducer from './format';
import hoverReducer from './hover';
@ -25,6 +26,7 @@ export default combineReducers({
clipboard: clipboardReducer,
eraserMode: eraserModeReducer,
fillBitmapShapes: fillBitmapShapesReducer,
fillMode: fillModeReducer,
font: fontReducer,
format: formatReducer,
hoveredItemId: hoverReducer,

View file

@ -0,0 +1,44 @@
// Gradient type shown in the select tool
import GradientTypes from '../lib/gradient-types';
import {getColorsFromSelection} from '../helper/style-path';
import {CHANGE_SELECTED_ITEMS} from './selected-items';
import {CHANGE_GRADIENT_TYPE} from './fill-mode-gradient-type';
import log from '../log/log';
const CLEAR_GRADIENT = 'scratch-paint/selection-gradient-type/CLEAR_GRADIENT';
const initialState = GradientTypes.SOLID;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case CHANGE_GRADIENT_TYPE:
if (action.gradientType in GradientTypes) {
return action.gradientType;
}
log.warn(`Gradient type does not exist: ${action.gradientType}`);
return state;
case CLEAR_GRADIENT:
return GradientTypes.SOLID;
case CHANGE_SELECTED_ITEMS:
// Don't change state if no selection
if (!action.selectedItems || !action.selectedItems.length) {
return state;
}
return getColorsFromSelection(action.selectedItems, action.bitmapMode).gradientType;
default:
return state;
}
};
// Action creators ==================================
const clearGradient = function () {
return {
type: CLEAR_GRADIENT
};
};
export {
reducer as default,
CLEAR_GRADIENT,
clearGradient
};