diff --git a/src/components/color-indicator.jsx b/src/components/color-indicator.jsx index d04ea2bbf2..be5b784c17 100644 --- a/src/components/color-indicator.jsx +++ b/src/components/color-indicator.jsx @@ -17,6 +17,7 @@ const ColorIndicatorComponent = props => ( ( ); ColorIndicatorComponent.propTypes = { + allowAlpha: PropTypes.bool, className: PropTypes.string, disabled: PropTypes.bool.isRequired, color: PropTypes.string, diff --git a/src/components/color-picker/checkerboard.png b/src/components/color-picker/checkerboard.png new file mode 100644 index 0000000000..d85f2d5826 Binary files /dev/null and b/src/components/color-picker/checkerboard.png differ diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 398c5cc6d8..ab2f0cf5c1 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -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 {
+ {this.props.allowAlpha && ( +
+
+ + + + + {Math.round(this.props.alpha)} + +
+
+ +
+
+ )}
{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, diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index c94d35853c..f1b53cad40 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -88,10 +88,12 @@ const PaintEditorComponent = props => ( {/* stroke */} {/* stroke width */} { // 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. diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 1411008847..cdc29ba051 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -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 ( { if (!hexRegex.test(color) && color !== null && color !== MIXED) {