This commit is contained in:
varun 2025-05-04 22:26:20 +00:00 committed by GitHub
commit 29e7c4eeb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 129 additions and 17 deletions

View file

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

Binary file not shown.

After

(image error) Size: 145 B

View file

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

View file

@ -88,10 +88,12 @@ const PaintEditorComponent = props => (
<FillColorIndicatorComponent
className={styles.modMarginAfter}
onUpdateImage={props.onUpdateImage}
allowAlpha
/>
{/* stroke */}
<StrokeColorIndicatorComponent
onUpdateImage={props.onUpdateImage}
allowAlpha
/>
{/* stroke width */}
<StrokeWidthIndicatorComponent

View file

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

View file

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

View file

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

View file

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

View file

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