From 661718e5918e0f9f7b53ef9fa868a92de7d453bd Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:05:05 +0200 Subject: [PATCH 01/12] Initial opacity slider work --- src/components/color-picker/color-picker.jsx | 25 ++++++++++++- src/containers/color-picker.jsx | 39 ++++++++++++++++---- src/lib/make-color-style-reducer.js | 2 +- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 398c5cc6d8..d86d04a33f 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -245,13 +245,34 @@ class ColorPickerComponent extends React.Component {
+
+
+ + + + + {Math.round(this.props.alpha)} + +
+
+ +
+
{this.props.mode === Modes.BIT_LINE || @@ -300,6 +321,7 @@ class ColorPickerComponent extends React.Component { ColorPickerComponent.propTypes = { brightness: PropTypes.number.isRequired, + alpha: PropTypes.number.isRequired, color: PropTypes.string, color2: PropTypes.string, colorIndex: PropTypes.number.isRequired, @@ -310,6 +332,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/containers/color-picker.jsx b/src/containers/color-picker.jsx index 1411008847..7aab173da9 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,6 +55,7 @@ class ColorPicker extends React.Component { 'handleHueChange', 'handleSaturationChange', 'handleBrightnessChange', + 'handleAlphaChange', 'handleTransparent', 'handleActivateEyeDropper' ]); @@ -55,7 +65,8 @@ class ColorPicker extends React.Component { this.state = { hue: hsv[0], saturation: hsv[1], - brightness: hsv[2] + brightness: hsv[2], + alpha: color.length === 9 ? (parseInt(color.slice(7, 9), 16) / 255) * 100 : 100 }; } componentWillReceiveProps (newProps) { @@ -67,7 +78,8 @@ class ColorPicker extends React.Component { this.setState({ hue: hsv[0], saturation: hsv[1], - brightness: hsv[2] + brightness: hsv[2], + alpha: 100 }); } } @@ -92,15 +104,26 @@ 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); + this.props.onChangeColor(hsvToHex( + this.state.hue, + this.state.saturation, + this.state.brightness, + 0.5 + )); } handleActivateEyeDropper () { this.props.onActivateEyeDropper( @@ -124,6 +147,7 @@ class ColorPicker extends React.Component { return ( { if (!hexRegex.test(color) && color !== null && color !== MIXED) { From dd86455db529d92637fae3c996184b9f91248ff6 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:13:22 +0200 Subject: [PATCH 02/12] Don't assume color exists --- src/containers/color-picker.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 7aab173da9..27fc7aa965 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -66,7 +66,7 @@ class ColorPicker extends React.Component { hue: hsv[0], saturation: hsv[1], brightness: hsv[2], - alpha: color.length === 9 ? (parseInt(color.slice(7, 9), 16) / 255) * 100 : 100 + alpha: color?.length === 9 ? (parseInt(color.slice(7, 9), 16) / 255) * 100 : 100 }; } componentWillReceiveProps (newProps) { From c11ff8f64f0dc47d1accdfd6e07f4fd9a4035aef Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:20:48 +0200 Subject: [PATCH 03/12] Set alpha 0 on transparent swatch --- src/containers/color-picker.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 27fc7aa965..7b7138d37b 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -118,11 +118,12 @@ class ColorPicker extends React.Component { )); } handleTransparent () { + // TODO: UX - should this reset all sliders, or just the alpha? this.props.onChangeColor(hsvToHex( this.state.hue, this.state.saturation, this.state.brightness, - 0.5 + 0 )); } handleActivateEyeDropper () { From 47cb365a1e10769148a26e4034d811beeb536ccd Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:24:47 +0200 Subject: [PATCH 04/12] _makeBackground for alpha slider --- src/components/color-picker/color-picker.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index d86d04a33f..3d1780c1a0 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -21,10 +21,18 @@ import fillVertGradientIcon from './icons/fill-vert-gradient-enabled.svg'; import swapIcon from './icons/swap.svg'; 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: { @@ -49,6 +57,9 @@ class ColorPickerComponent extends React.Component { case 'brightness': stops.push(hsvToHex(this.props.hue, this.props.saturation, n)); 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}`); } @@ -267,7 +278,7 @@ class ColorPickerComponent extends React.Component {
From 5494449076d73d978ff52fbf070494c83e964934 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:33:23 +0200 Subject: [PATCH 05/12] Alpha support for eyedropper --- src/helper/tools/eye-dropper.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/helper/tools/eye-dropper.js b/src/helper/tools/eye-dropper.js index 447509154c..90a92482ad 100644 --- a/src/helper/tools/eye-dropper.js +++ b/src/helper/tools/eye-dropper.js @@ -77,6 +77,9 @@ 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] + + console.log({ r, g, b, a }) // 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 +87,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) { From 7abfa1eec8c7032e7f55beade540f3901e49c7d0 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 12:46:47 +0200 Subject: [PATCH 06/12] Handle non hex colour codes --- src/containers/color-picker.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 7b7138d37b..6534da71a0 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -62,11 +62,13 @@ class ColorPicker extends React.Component { 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], - alpha: color?.length === 9 ? (parseInt(color.slice(7, 9), 16) / 255) * 100 : 100 + alpha: alpha * 100 || 100 }; } componentWillReceiveProps (newProps) { @@ -89,6 +91,10 @@ class ColorPicker extends React.Component { return isTransparent || isMixed ? [50, 100, 100] : colorStringToHsv(color); } + getAlpha(color) { + const result = parseColor(color) + return result.rgba[3] || 1 + } handleHueChange (hue) { this.setState({hue: hue}, () => { this.handleColorChange(); From 51c7c193db3a8b2dc3b9b8763096e894a02dd51b Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 13:55:43 +0200 Subject: [PATCH 07/12] Hacky fix for eyedropper by dividing hex alpha value --- src/containers/color-picker.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 6534da71a0..85dedd4f9b 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -77,11 +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], - alpha: 100 + alpha: alpha * 100 || 100 }); } } @@ -92,8 +94,19 @@ class ColorPicker extends React.Component { [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 doesn't work great (incorrectly parsing alpha from hex color codes, rgba from 0-1, but hex 0-255) + const result = parseColor(color) - return result.rgba[3] || 1 + let alpha = result.rgba[3] + + if (color.startsWith('#')) { + // We used a hex color, divide parse-color alpha value by 255 + + alpha = alpha / 255 + } + + return alpha || 1 } handleHueChange (hue) { this.setState({hue: hue}, () => { From 9826b16755a835730da9ad636d27134d2137b703 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 25 Jun 2024 22:24:51 +0200 Subject: [PATCH 08/12] More parse-color strangeness --- src/containers/color-picker.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index 85dedd4f9b..a47702c9a7 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -95,12 +95,15 @@ class ColorPicker extends React.Component { } 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 doesn't work great (incorrectly parsing alpha from hex color codes, rgba from 0-1, but hex 0-255) + // 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) const result = parseColor(color) + if (!result?.rgba) return 1 + let alpha = result.rgba[3] - if (color.startsWith('#')) { + if (color.startsWith('#') && alpha !== 1) { // We used a hex color, divide parse-color alpha value by 255 alpha = alpha / 255 From f38213ed3f00e3181f1aba33d2cb6a8dad618174 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Wed, 26 Jun 2024 00:01:01 +0200 Subject: [PATCH 09/12] add checkerboard background, improve transparent swatch --- src/components/color-picker/checkerboard.png | Bin 0 -> 145 bytes src/components/color-picker/color-picker.jsx | 3 ++- src/containers/color-picker.jsx | 9 +++------ 3 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 src/components/color-picker/checkerboard.png diff --git a/src/components/color-picker/checkerboard.png b/src/components/color-picker/checkerboard.png new file mode 100644 index 0000000000000000000000000000000000000000..d85f2d58260527b85e98f33416c140d5d8e2788b GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaT3?y&uT)!JgF%}28J2BoosZ$T+a29w(7Bet# z3xhBt!>l44o^Chr0`kN>T^vIy<|L=oy*}T; i`(N;0hv?)Latu{{D!f{E^6h~t7(8A5T-G@yGywox;wF#) literal 0 HcmV?d00001 diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 3d1780c1a0..3c333c0fc4 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -19,6 +19,7 @@ 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, alpha = 100) => { @@ -74,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 ( diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index a47702c9a7..f6528401cf 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -141,12 +141,9 @@ class ColorPicker extends React.Component { } handleTransparent () { // TODO: UX - should this reset all sliders, or just the alpha? - this.props.onChangeColor(hsvToHex( - this.state.hue, - this.state.saturation, - this.state.brightness, - 0 - )); + this.setState({alpha: 0}, () => { + this.handleColorChange(); + }); } handleActivateEyeDropper () { this.props.onActivateEyeDropper( From 357465ce81f0a1c2a9916026ffa902be484a526a Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Wed, 26 Jun 2024 00:14:21 +0200 Subject: [PATCH 10/12] Allow opacity 0 (work better with transparent swatch) --- src/containers/color-picker.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/containers/color-picker.jsx b/src/containers/color-picker.jsx index f6528401cf..f860aa8be0 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -68,7 +68,7 @@ class ColorPicker extends React.Component { hue: hsv[0], saturation: hsv[1], brightness: hsv[2], - alpha: alpha * 100 || 100 + alpha: alpha * 100 }; } componentWillReceiveProps (newProps) { @@ -83,7 +83,7 @@ class ColorPicker extends React.Component { hue: hsv[0], saturation: hsv[1], brightness: hsv[2], - alpha: alpha * 100 || 100 + alpha: alpha * 100 }); } } @@ -97,9 +97,11 @@ class ColorPicker extends React.Component { // 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 + if (!result?.rgba) return 1; // no alpha value let alpha = result.rgba[3] @@ -109,7 +111,7 @@ class ColorPicker extends React.Component { alpha = alpha / 255 } - return alpha || 1 + return alpha } handleHueChange (hue) { this.setState({hue: hue}, () => { From 77051b768b7a6601c321279804cc6db5b9329551 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Wed, 26 Jun 2024 11:16:41 +0200 Subject: [PATCH 11/12] Make other sliders match opacity style --- src/components/color-picker/color-picker.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/color-picker/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 3c333c0fc4..26bb141ceb 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -50,13 +50,13 @@ 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)); From 5803c1d5405760b9c8674e2adffd28504adf2456 Mon Sep 17 00:00:00 2001 From: Varun Biniwale Date: Tue, 2 Jul 2024 16:56:26 +0200 Subject: [PATCH 12/12] Make transparency only usable in vector - add allowAlpha props to pass around whether alpha should be allowed - remove transparency if exists when switching to bitmap --- src/components/color-indicator.jsx | 2 + src/components/color-picker/color-picker.jsx | 44 +++++++++++--------- src/components/paint-editor/paint-editor.jsx | 2 + src/containers/color-indicator.jsx | 18 ++++++++ src/containers/color-picker.jsx | 2 + src/containers/paint-editor.jsx | 1 + src/helper/tools/eye-dropper.js | 2 - 7 files changed, 49 insertions(+), 22 deletions(-) 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/color-picker.jsx b/src/components/color-picker/color-picker.jsx index 26bb141ceb..ab2f0cf5c1 100644 --- a/src/components/color-picker/color-picker.jsx +++ b/src/components/color-picker/color-picker.jsx @@ -257,34 +257,37 @@ class ColorPickerComponent extends React.Component {
-
-
- - +
+ + + + + {Math.round(this.props.alpha)} + +
+
+ - - - {Math.round(this.props.alpha)} - -
-
- +
-
+ )}
{this.props.mode === Modes.BIT_LINE || @@ -332,6 +335,7 @@ class ColorPickerComponent extends React.Component { } ColorPickerComponent.propTypes = { + allowAlpha: PropTypes.bool, brightness: PropTypes.number.isRequired, alpha: PropTypes.number.isRequired, color: PropTypes.string, 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 f860aa8be0..cdc29ba051 100644 --- a/src/containers/color-picker.jsx +++ b/src/containers/color-picker.jsx @@ -168,6 +168,7 @@ class ColorPicker extends React.Component { render () { return ( {