mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-06-15 14:41:41 -04:00
Merge 5803c1d540
into a5fa94166a
This commit is contained in:
commit
29e7c4eeb4
9 changed files with 129 additions and 17 deletions
src
components
containers
helper/tools
lib
|
@ -17,6 +17,7 @@ const ColorIndicatorComponent = props => (
|
|||
<Popover
|
||||
body={
|
||||
<ColorPicker
|
||||
allowAlpha={props.allowAlpha}
|
||||
color={props.color}
|
||||
color2={props.color2}
|
||||
gradientType={props.gradientType}
|
||||
|
@ -44,6 +45,7 @@ const ColorIndicatorComponent = props => (
|
|||
);
|
||||
|
||||
ColorIndicatorComponent.propTypes = {
|
||||
allowAlpha: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
color: PropTypes.string,
|
||||
|
|
BIN
src/components/color-picker/checkerboard.png
Normal file
BIN
src/components/color-picker/checkerboard.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 145 B |
|
@ -19,12 +19,21 @@ 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';
|
||||
import checkerboard from './checkerboard.png'
|
||||
import Modes from '../../lib/modes';
|
||||
|
||||
const hsvToHex = (h, s, v) =>
|
||||
const hsvToHex = (h, s, v, alpha = 100) => {
|
||||
// Scale alpha from [0, 100] to [0, 1]
|
||||
const alphaNormalized = alpha / 100;
|
||||
// Scale hue back up to [0, 360] from [0, 100]
|
||||
parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex
|
||||
;
|
||||
const color = parseColor(`hsv(${3.6 * h}, ${s}, ${v})`);
|
||||
// Get the hex value without the alpha channel
|
||||
const hex = color.hex;
|
||||
// Calculate the alpha value in hex (0-255)
|
||||
const alphaHex = Math.round(alphaNormalized * 255).toString(16).padStart(2, '0');
|
||||
// Return the hex value with the alpha channel
|
||||
return `${hex}${alphaHex}`;
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
swap: {
|
||||
|
@ -41,13 +50,16 @@ class ColorPickerComponent extends React.Component {
|
|||
for (let n = 100; n >= 0; n -= 10) {
|
||||
switch (channel) {
|
||||
case 'hue':
|
||||
stops.push(hsvToHex(n, this.props.saturation, this.props.brightness));
|
||||
stops.push(hsvToHex(n, this.props.saturation, this.props.brightness, this.props.alpha));
|
||||
break;
|
||||
case 'saturation':
|
||||
stops.push(hsvToHex(this.props.hue, n, this.props.brightness));
|
||||
stops.push(hsvToHex(this.props.hue, n, this.props.brightness, this.props.alpha));
|
||||
break;
|
||||
case 'brightness':
|
||||
stops.push(hsvToHex(this.props.hue, this.props.saturation, n));
|
||||
stops.push(hsvToHex(this.props.hue, this.props.saturation, n, this.props.alpha));
|
||||
break;
|
||||
case 'alpha':
|
||||
stops.push(hsvToHex(this.props.hue, this.props.saturation, this.props.brightness, n));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown channel for color sliders: ${channel}`);
|
||||
|
@ -63,7 +75,7 @@ class ColorPickerComponent extends React.Component {
|
|||
stops[0] += ` 0 ${halfHandleWidth}px`;
|
||||
stops[stops.length - 1] += ` ${CONTAINER_WIDTH - halfHandleWidth}px 100%`;
|
||||
|
||||
return `linear-gradient(to left, ${stops.join(',')})`;
|
||||
return `linear-gradient(to left, ${stops.join(',')}), url("${checkerboard}")`;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
|
@ -245,13 +257,37 @@ class ColorPickerComponent extends React.Component {
|
|||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
lastSlider
|
||||
lastSlider={!this.props.allowAlpha}
|
||||
background={this._makeBackground('brightness')}
|
||||
value={this.props.brightness}
|
||||
onChange={this.props.onBrightnessChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.props.allowAlpha && (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowHeader}>
|
||||
<span className={styles.labelName}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Opacity"
|
||||
description="Label for the opacity component in the color picker"
|
||||
id="paint.paintEditor.alpha"
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.props.alpha)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
lastSlider
|
||||
background={this._makeBackground('alpha')}
|
||||
value={this.props.alpha}
|
||||
onChange={this.props.onAlphaChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.swatchRow}>
|
||||
<div className={styles.swatches}>
|
||||
{this.props.mode === Modes.BIT_LINE ||
|
||||
|
@ -299,7 +335,9 @@ class ColorPickerComponent extends React.Component {
|
|||
}
|
||||
|
||||
ColorPickerComponent.propTypes = {
|
||||
allowAlpha: PropTypes.bool,
|
||||
brightness: PropTypes.number.isRequired,
|
||||
alpha: PropTypes.number.isRequired,
|
||||
color: PropTypes.string,
|
||||
color2: PropTypes.string,
|
||||
colorIndex: PropTypes.number.isRequired,
|
||||
|
@ -310,6 +348,7 @@ ColorPickerComponent.propTypes = {
|
|||
mode: PropTypes.oneOf(Object.keys(Modes)),
|
||||
onActivateEyeDropper: PropTypes.func.isRequired,
|
||||
onBrightnessChange: PropTypes.func.isRequired,
|
||||
onAlphaChange: PropTypes.func.isRequired,
|
||||
onChangeGradientTypeHorizontal: PropTypes.func.isRequired,
|
||||
onChangeGradientTypeRadial: PropTypes.func.isRequired,
|
||||
onChangeGradientTypeSolid: PropTypes.func.isRequired,
|
||||
|
|
|
@ -88,10 +88,12 @@ const PaintEditorComponent = props => (
|
|||
<FillColorIndicatorComponent
|
||||
className={styles.modMarginAfter}
|
||||
onUpdateImage={props.onUpdateImage}
|
||||
allowAlpha
|
||||
/>
|
||||
{/* stroke */}
|
||||
<StrokeColorIndicatorComponent
|
||||
onUpdateImage={props.onUpdateImage}
|
||||
allowAlpha
|
||||
/>
|
||||
{/* stroke width */}
|
||||
<StrokeWidthIndicatorComponent
|
||||
|
|
|
@ -30,6 +30,16 @@ const makeColorIndicator = (label, isStroke) => {
|
|||
// Flag to track whether an svg-update-worthy change has been made
|
||||
this._hasChanged = false;
|
||||
}
|
||||
componentDidMount () {
|
||||
if (!this.props.allowAlpha) {
|
||||
this.removeAlpha();
|
||||
}
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!this.props.allowAlpha && prevProps.allowAlpha) {
|
||||
this.removeAlpha();
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
const {colorModalVisible, onUpdateImage} = this.props;
|
||||
if (colorModalVisible && !newProps.colorModalVisible) {
|
||||
|
@ -38,6 +48,14 @@ const makeColorIndicator = (label, isStroke) => {
|
|||
this._hasChanged = false;
|
||||
}
|
||||
}
|
||||
removeAlpha() {
|
||||
const parsedColor1 = parseColor(this.props.color)
|
||||
if (parsedColor1?.hex)
|
||||
this.props.onChangeColor(parsedColor1.hex.substr(0, 7), 0)
|
||||
const parsedColor2 = parseColor(this.props.color2)
|
||||
if (parsedColor2?.hex)
|
||||
this.props.onChangeColor(parsedColor2.hex.substr(0, 7), 1)
|
||||
}
|
||||
handleChangeColor (newColor) {
|
||||
// Stroke-selector-specific logic: if we change the stroke color from "none" to something visible, ensure
|
||||
// there's a nonzero stroke width. If we change the stroke color to "none", set the stroke width to zero.
|
||||
|
|
|
@ -26,10 +26,19 @@ const colorStringToHsv = hexString => {
|
|||
return hsv;
|
||||
};
|
||||
|
||||
const hsvToHex = (h, s, v) =>
|
||||
const hsvToHex = (h, s, v, alpha = 100) => {
|
||||
// Scale alpha from [0, 100] to [0, 1]
|
||||
const alphaNormalized = alpha / 100;
|
||||
// Scale hue back up to [0, 360] from [0, 100]
|
||||
parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex
|
||||
;
|
||||
const color = parseColor(`hsv(${3.6 * h}, ${s}, ${v})`);
|
||||
// Get the hex value without the alpha channel
|
||||
const hex = color.hex;
|
||||
// Calculate the alpha value in hex (0-255)
|
||||
const alphaHex = Math.round(alphaNormalized * 255).toString(16).padStart(2, '0');
|
||||
// Return the hex value with the alpha channel
|
||||
return `${hex}${alphaHex}`;
|
||||
};
|
||||
|
||||
|
||||
// Important! This component ignores new color props except when isEyeDropping
|
||||
// This is to make the HSV <=> RGB conversion stable. The sliders manage their
|
||||
|
@ -46,16 +55,20 @@ class ColorPicker extends React.Component {
|
|||
'handleHueChange',
|
||||
'handleSaturationChange',
|
||||
'handleBrightnessChange',
|
||||
'handleAlphaChange',
|
||||
'handleTransparent',
|
||||
'handleActivateEyeDropper'
|
||||
]);
|
||||
|
||||
const color = props.colorIndex === 0 ? props.color : props.color2;
|
||||
const hsv = this.getHsv(color);
|
||||
const alpha = this.getAlpha(color);
|
||||
|
||||
this.state = {
|
||||
hue: hsv[0],
|
||||
saturation: hsv[1],
|
||||
brightness: hsv[2]
|
||||
brightness: hsv[2],
|
||||
alpha: alpha * 100
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
|
@ -64,10 +77,13 @@ class ColorPicker extends React.Component {
|
|||
const colorSetByEyedropper = this.props.isEyeDropping && color !== newColor;
|
||||
if (colorSetByEyedropper || this.props.colorIndex !== newProps.colorIndex) {
|
||||
const hsv = this.getHsv(newColor);
|
||||
const alpha = this.getAlpha(newColor);
|
||||
|
||||
this.setState({
|
||||
hue: hsv[0],
|
||||
saturation: hsv[1],
|
||||
brightness: hsv[2]
|
||||
brightness: hsv[2],
|
||||
alpha: alpha * 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +93,26 @@ class ColorPicker extends React.Component {
|
|||
return isTransparent || isMixed ?
|
||||
[50, 100, 100] : colorStringToHsv(color);
|
||||
}
|
||||
getAlpha(color) {
|
||||
// TODO: need to find a way to get the alpha from all kinds of color strings (rgb, rgba, hex, hex with alpha, etc.)
|
||||
// parse-color returns a range of 0-255 for hex inputs, but 0-1 for any other input
|
||||
// (for hex codes without an alpha value, parse-color returns an alpha of 1)
|
||||
|
||||
if (!color) return 0; // transparent swatch
|
||||
|
||||
const result = parseColor(color)
|
||||
if (!result?.rgba) return 1; // no alpha value
|
||||
|
||||
let alpha = result.rgba[3]
|
||||
|
||||
if (color.startsWith('#') && alpha !== 1) {
|
||||
// We used a hex color, divide parse-color alpha value by 255
|
||||
|
||||
alpha = alpha / 255
|
||||
}
|
||||
|
||||
return alpha
|
||||
}
|
||||
handleHueChange (hue) {
|
||||
this.setState({hue: hue}, () => {
|
||||
this.handleColorChange();
|
||||
|
@ -92,15 +128,24 @@ class ColorPicker extends React.Component {
|
|||
this.handleColorChange();
|
||||
});
|
||||
}
|
||||
handleAlphaChange (alpha) {
|
||||
this.setState({alpha: alpha}, () => {
|
||||
this.handleColorChange();
|
||||
});
|
||||
}
|
||||
handleColorChange () {
|
||||
this.props.onChangeColor(hsvToHex(
|
||||
this.state.hue,
|
||||
this.state.saturation,
|
||||
this.state.brightness
|
||||
this.state.brightness,
|
||||
this.state.alpha
|
||||
));
|
||||
}
|
||||
handleTransparent () {
|
||||
this.props.onChangeColor(null);
|
||||
// TODO: UX - should this reset all sliders, or just the alpha?
|
||||
this.setState({alpha: 0}, () => {
|
||||
this.handleColorChange();
|
||||
});
|
||||
}
|
||||
handleActivateEyeDropper () {
|
||||
this.props.onActivateEyeDropper(
|
||||
|
@ -123,7 +168,9 @@ class ColorPicker extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<ColorPickerComponent
|
||||
allowAlpha={this.props.allowAlpha}
|
||||
brightness={this.state.brightness}
|
||||
alpha={this.state.alpha}
|
||||
color={this.props.color}
|
||||
color2={this.props.color2}
|
||||
colorIndex={this.props.colorIndex}
|
||||
|
@ -136,6 +183,7 @@ class ColorPicker extends React.Component {
|
|||
shouldShowGradientTools={this.props.shouldShowGradientTools}
|
||||
onActivateEyeDropper={this.handleActivateEyeDropper}
|
||||
onBrightnessChange={this.handleBrightnessChange}
|
||||
onAlphaChange={this.handleAlphaChange}
|
||||
onChangeGradientTypeHorizontal={this.handleChangeGradientTypeHorizontal}
|
||||
onChangeGradientTypeRadial={this.handleChangeGradientTypeRadial}
|
||||
onChangeGradientTypeSolid={this.handleChangeGradientTypeSolid}
|
||||
|
@ -152,6 +200,7 @@ class ColorPicker extends React.Component {
|
|||
}
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
allowAlpha: PropTypes.bool,
|
||||
color: PropTypes.string,
|
||||
color2: PropTypes.string,
|
||||
colorIndex: PropTypes.number.isRequired,
|
||||
|
|
|
@ -12,6 +12,7 @@ import UpdateImageHOC from '../hocs/update-image-hoc.jsx';
|
|||
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {changeFormat} from '../reducers/format';
|
||||
import {changeFillColor} from '../reducers/fill-style';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||
|
|
|
@ -77,6 +77,7 @@ class EyeDropperTool extends paper.Tool {
|
|||
const r = colorInfo.color[0];
|
||||
const g = colorInfo.color[1];
|
||||
const b = colorInfo.color[2];
|
||||
const a = colorInfo.color[3] / 255; // Normalize alpha to range [0, 1]
|
||||
|
||||
// from https://github.com/LLK/scratch-gui/blob/77e54a80a31b6cd4684d4b2a70f1aeec671f229e/src/containers/stage.jsx#L218-L222
|
||||
// formats the color info from the canvas into hex for parsing by the color picker
|
||||
|
@ -84,7 +85,7 @@ class EyeDropperTool extends paper.Tool {
|
|||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
this.colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`;
|
||||
this.colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}${Math.round(a * 255).toString(16).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
getColorInfo (x, y, hideLoupe) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {getColorsFromSelection, MIXED} from '../helper/style-path';
|
|||
import GradientTypes from './gradient-types';
|
||||
|
||||
// Matches hex colors
|
||||
const hexRegex = /^#([0-9a-f]{3}){1,2}$/i;
|
||||
const hexRegex = /^#([0-9a-f]{3}){1,2}([0-9a-f]{2})?$/i;
|
||||
|
||||
const isValidHexColor = color => {
|
||||
if (!hexRegex.test(color) && color !== null && color !== MIXED) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue