Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opacity slider #2627

Draft
wants to merge 13 commits into
base: develop
Choose a base branch
from
Draft
2 changes: 2 additions & 0 deletions src/components/color-indicator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ColorIndicatorComponent = props => (
<Popover
body={
<ColorPicker
allowAlpha={props.allowAlpha}
color={props.color}
color2={props.color2}
gradientType={props.gradientType}
Expand Down Expand Up @@ -44,6 +45,7 @@ const ColorIndicatorComponent = props => (
);

ColorIndicatorComponent.propTypes = {
allowAlpha: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool.isRequired,
color: PropTypes.string,
Expand Down
Binary file added src/components/color-picker/checkerboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 47 additions & 8 deletions src/components/color-picker/color-picker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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}`);
Expand All @@ -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 (
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/components/paint-editor/paint-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ const PaintEditorComponent = props => (
<FillColorIndicatorComponent
className={styles.modMarginAfter}
onUpdateImage={props.onUpdateImage}
allowAlpha
/>
{/* stroke */}
<StrokeColorIndicatorComponent
onUpdateImage={props.onUpdateImage}
allowAlpha
/>
{/* stroke width */}
<StrokeWidthIndicatorComponent
Expand Down
18 changes: 18 additions & 0 deletions src/containers/color-indicator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
63 changes: 56 additions & 7 deletions src/containers/color-picker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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
});
}
}
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -152,6 +200,7 @@ class ColorPicker extends React.Component {
}

ColorPicker.propTypes = {
allowAlpha: PropTypes.bool,
color: PropTypes.string,
color2: PropTypes.string,
colorIndex: PropTypes.number.isRequired,
Expand Down
1 change: 1 addition & 0 deletions src/containers/paint-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/helper/tools/eye-dropper.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,15 @@ 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
const componentToString = c => {
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) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/make-color-style-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading