From 802581e0ed0dd4ceba7bd1da05920bc286647749 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 10 Feb 2021 16:39:38 -1000 Subject: [PATCH] Add duotone toolbar --- .../duotone-toolbar/custom-duotone-bar.js | 45 +++++ .../duotone-toolbar/custom-duotone-picker.js | 109 ++++++++++ .../duotone-toolbar/duotone-picker-popover.js | 100 +++++++++ .../duotone-toolbar/duotone-swatch.js | 10 + .../src/components/duotone-toolbar/index.js | 64 ++++++ .../src/components/duotone-toolbar/style.scss | 59 ++++++ .../src/components/duotone-toolbar/utils.js | 190 ++++++++++++++++++ packages/block-editor/src/components/index.js | 1 + packages/block-editor/src/style.scss | 1 + .../src/custom-gradient-bar/control-points.js | 37 ++-- .../src/custom-gradient-bar/index.js | 40 ++-- .../src/custom-gradient-picker/style.scss | 4 +- packages/components/src/index.js | 2 + 13 files changed, 628 insertions(+), 34 deletions(-) create mode 100644 packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js create mode 100644 packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js create mode 100644 packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js create mode 100644 packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js create mode 100644 packages/block-editor/src/components/duotone-toolbar/index.js create mode 100644 packages/block-editor/src/components/duotone-toolbar/style.scss create mode 100644 packages/block-editor/src/components/duotone-toolbar/utils.js diff --git a/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js new file mode 100644 index 0000000000000..75410ca876b2e --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-bar.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { __experimentalCustomGradientBar as CustomGradientBar } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + getColorStopsFromValues, + getCustomDuotoneIdFromColorStops, + getGradientFromValues, + getValuesFromColorStops, +} from './utils'; + +const PLACEHOLDER_VALUES = { + r: [ 0.2, 0.8 ], + g: [ 0.2, 0.8 ], + b: [ 0.2, 0.8 ], +}; + +export default function CustomDuotoneBar( { value, onChange } ) { + const hasGradient = !! value?.values; + const values = hasGradient ? value.values : PLACEHOLDER_VALUES; + const background = getGradientFromValues( values ); + const controlPoints = getColorStopsFromValues( values ); + return ( +
+ { + const newDuotone = { + id: getCustomDuotoneIdFromColorStops( newColorStops ), + values: getValuesFromColorStops( newColorStops ), + }; + onChange( newDuotone ); + } } + /> +
+ ); +} diff --git a/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js new file mode 100644 index 0000000000000..57343e5500eb1 --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/custom-duotone-picker.js @@ -0,0 +1,109 @@ +/** + * WordPress dependencies + */ +import { Button, ColorPalette, Icon } from '@wordpress/components'; +import { useMemo, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { swatch } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import Swatch from './duotone-swatch'; +import { + getCustomDuotoneIdFromHexColors, + getDefaultColors, + getHexColorsFromValues, + getValuesFromColors, +} from './utils'; + +function CustomColorOption( { label, value, colors, onChange } ) { + const [ isOpen, setIsOpen ] = useState( false ); + const icon = value ? : ; + return ( + <> + + { isOpen && ( + + ) } + + ); +} + +function CustomColorPicker( { colors, palette, onChange } ) { + const [ defaultDark, defaultLight ] = useMemo( + () => getDefaultColors( palette ), + [ palette ] + ); + + return ( +
+ { + const newColors = colors.slice(); + newColors[ 0 ] = newColor; + if ( ! newColors[ 0 ] ) { + newColors[ 0 ] = defaultDark; + } + if ( ! newColors[ 1 ] ) { + newColors[ 1 ] = defaultLight; + } + onChange( newColors ); + } } + /> + { + const newColors = colors.slice(); + newColors[ 1 ] = newColor; + if ( ! newColors[ 0 ] ) { + newColors[ 0 ] = defaultDark; + } + if ( ! newColors[ 1 ] ) { + newColors[ 1 ] = defaultLight; + } + onChange( newColors ); + } } + /> +
+ ); +} + +function CustomDuotonePicker( { colorPalette, value, onChange } ) { + return ( + + onChange( + newColors.length >= 2 + ? { + values: getValuesFromColors( newColors ), + id: getCustomDuotoneIdFromHexColors( + newColors + ), + } + : undefined + ) + } + /> + ); +} + +export default CustomDuotonePicker; diff --git a/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js b/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js new file mode 100644 index 0000000000000..1b4d66f56aded --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/duotone-picker-popover.js @@ -0,0 +1,100 @@ +/** + * WordPress dependencies + */ +import { + CircularOptionPicker, + Popover, + MenuGroup, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import CustomDuotoneBar from './custom-duotone-bar'; +import CustomDuotonePicker from './custom-duotone-picker'; +import { getGradientFromCSSColors, getValuesFromColors } from './utils'; + +function DuotonePickerPopover( { + value, + onChange, + onToggle, + duotonePalette, + colorPalette, +} ) { + return ( + + + { + const isSelected = option.slug === value?.slug; + const style = { + background: getGradientFromCSSColors( + option.colors, + '135deg' + ), + color: 'transparent', + }; + const code = sprintf( + // translators: %s: duotone code e.g: "dark-grayscale" or "7f7f7f-ffffff". + __( 'Duotone code: %s' ), + option.slug + ); + const label = sprintf( + // translators: %s: The name of the option e.g: "Dark grayscale". + __( 'Duotone: %s' ), + option.name + ); + + return ( + { + const newValue = { + values: getValuesFromColors( + option.colors + ), + id: `duotone-filter-${ option.slug }`, + }; + onChange( + isSelected ? undefined : newValue + ); + } } + aria-label={ option.name ? label : code } + /> + ); + } ) } + actions={ + onChange( undefined ) } + > + { __( 'Clear' ) } + + } + > + + + + +
+ { __( + 'The duotone filter creates a two-color version of your image, where you choose the colors.' + ) } +
+
+ ); +} + +export default DuotonePickerPopover; diff --git a/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js b/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js new file mode 100644 index 0000000000000..64d5fcc26753a --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/duotone-swatch.js @@ -0,0 +1,10 @@ +function DuotoneSwatch( { fill } ) { + return ( + + ); +} + +export default DuotoneSwatch; diff --git a/packages/block-editor/src/components/duotone-toolbar/index.js b/packages/block-editor/src/components/duotone-toolbar/index.js new file mode 100644 index 0000000000000..3e0dfc663ecce --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/index.js @@ -0,0 +1,64 @@ +/** + * WordPress dependencies + */ +import { ToolbarButton } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { swatch } from '@wordpress/icons'; +import { DOWN } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import DuotonePickerPopover from './duotone-picker-popover'; +import Swatch from './duotone-swatch'; +import { getGradientFromValues } from './utils'; + +function DuotoneToolbar( { value, onChange, duotonePalette, colorPalette } ) { + const [ isOpen, setIsOpen ] = useState( false ); + const onToggle = () => { + setIsOpen( ( prev ) => ! prev ); + }; + const openOnArrowDown = ( event ) => { + if ( ! isOpen && event.keyCode === DOWN ) { + event.preventDefault(); + event.stopPropagation(); + onToggle(); + } + }; + return ( + <> + + ) : ( + swatch + ) + } + /> + { isOpen && ( + + ) } + + ); +} + +export default DuotoneToolbar; diff --git a/packages/block-editor/src/components/duotone-toolbar/style.scss b/packages/block-editor/src/components/duotone-toolbar/style.scss new file mode 100644 index 0000000000000..1e0c44a4c49d0 --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/style.scss @@ -0,0 +1,59 @@ +.block-editor-duotone-toolbar__swatch { + width: 18px; + height: 18px; + border-radius: 50%; + color: transparent; + background: transparent; + + // Regular border doesn't seem to work in the toolbar button, but pseudo-selector border does. + &::after { + content: ""; + display: block; + width: 100%; + height: 100%; + border: $border-width solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + } +} + +.components-button.has-icon.has-text .block-editor-duotone-toolbar__swatch { + margin-right: $grid-unit; +} + +.block-editor-duotone-toolbar__popover { + .components-popover__content { + border: $border-width solid $gray-900; + min-width: 214px; + } + + .components-circular-option-picker, + .block-editor-duotone-toolbar__description { + padding: $grid-unit-15; + } + + .components-menu-group__label { + padding: $grid-unit-15 $grid-unit-15 0 $grid-unit-15; + } + + .components-menu-group__label, + .block-editor-duotone-toolbar__custom-colors, + .block-editor-duotone-toolbar__color-button { + width: 100%; + } + + .block-editor-duotone-toolbar__description { + border-top: $border-width solid $gray-900; + color: $gray-700; + } +} + +.block-editor-duotone-toolbar__popover > .components-popover__content { + // Matches 8 swatches in width. + width: 334px; +} + +// Better align the popover under the swatch. +// @todo: when the positioning for popovers gets refactored, this can presumably be removed. +.block-editor-duotone-toolbar__popover:not([data-y-axis="middle"][data-x-axis="right"]) > .components-popover__content { + margin-left: -14px; +} diff --git a/packages/block-editor/src/components/duotone-toolbar/utils.js b/packages/block-editor/src/components/duotone-toolbar/utils.js new file mode 100644 index 0000000000000..2b5ed968f5686 --- /dev/null +++ b/packages/block-editor/src/components/duotone-toolbar/utils.js @@ -0,0 +1,190 @@ +/** + * External dependencies + */ +import tinycolor from 'tinycolor2'; + +/** + * Object representation for a color. + * + * @typedef {Object} RGBColor + * @property {number} r Red component of the color in the range [0,1]. + * @property {number} g Green component of the color in the range [0,1]. + * @property {number} b Blue component of the color in the range [0,1]. + */ + +/** + * Arrays of values in convenient format for SVG feComponentTransfer. + * + * @typedef {Object} RGBValues + * @property {number[]} r Array of red components of the colors in the range [0,1]. + * @property {number[]} g Array of green components of the colors in the range [0,1]. + * @property {number[]} b Array of blue components of the colors in the range [0,1]. + */ + +/** + * Calculate the brightest and darkest values from a color palette. + * + * @param {Object[]} palette Color palette for the theme. + * + * @return {string[]} Tuple of the darkest color and brightest color. + */ +export function getDefaultColors( palette ) { + // A default dark and light color are required. + if ( ! palette || palette.length < 2 ) return [ '#000', '#fff' ]; + + return palette + .map( ( { color } ) => ( { + color, + brightness: tinycolor( color ).getBrightness() / 255, + } ) ) + .reduce( + ( [ min, max ], current ) => { + return [ + current.brightness <= min.brightness ? current : min, + current.brightness >= max.brightness ? current : max, + ]; + }, + [ { brightness: 1 }, { brightness: 0 } ] + ) + .map( ( { color } ) => color ); +} + +/** + * Generate a duotone gradient from a list of colors. + * + * @param {string[]} colors CSS color strings. + * @param {string} angle CSS gradient angle. + * + * @return {string} CSS gradient string for the duotone swatch. + */ +export function getGradientFromCSSColors( colors = [], angle = '90deg' ) { + const l = 100 / colors.length; + + const stops = colors + .map( ( c, i ) => `${ c } ${ i * l }%, ${ c } ${ ( i + 1 ) * l }%` ) + .join( ', ' ); + + return `linear-gradient( ${ angle }, ${ stops } )`; +} + +/** + * Create a CSS gradient for duotone swatches. + * + * @param {RGBValues} values R, G, and B values. + * @param {string} angle CSS gradient angle. + * + * @return {string} CSS gradient string for the duotone swatch. + */ +export function getGradientFromValues( + values = { r: [], g: [], b: [] }, + angle +) { + return getGradientFromCSSColors( + getColorsFromValues( values ).map( ( tcolor ) => tcolor.toRgbString() ), + angle + ); +} + +/** + * Convert a list of colors to an object of R, G, and B values. + * + * @param {string[]} colors Array of RBG color strings. + * + * @return {RGBValues} R, G, and B values. + */ +export function getValuesFromColors( colors = [] ) { + const values = { r: [], g: [], b: [] }; + + colors.forEach( ( color ) => { + // Access values directly to skip rounding that tinycolor.toRgb() does. + const tcolor = tinycolor( color ); + values.r.push( tcolor._r / 255 ); + values.g.push( tcolor._g / 255 ); + values.b.push( tcolor._b / 255 ); + } ); + + return values; +} + +/** + * Convert a color values object to an array of colors. + * + * @param {RGBValues} values R, G, and B values. + * + * @return {Object[]} Tinycolor object array. + */ +function getColorsFromValues( values = { r: [], g: [], b: [] } ) { + // R, G, and B should all be the same length, so we only need to map over one. + return values.r.map( ( x, i ) => { + return tinycolor( { + r: values.r[ i ] * 255, + g: values.g[ i ] * 255, + b: values.b[ i ] * 255, + } ); + } ); +} + +/** + * Convert a color values object to an array of color stops. + * + * @param {RGBValues} values R, G, and B values. + * + * @return {Object[]} Color stop information. + */ +export function getColorStopsFromValues( values = { r: [], g: [], b: [] } ) { + const colors = getColorsFromValues( values ); + return colors.map( ( tcolor, i ) => ( { + position: ( i * 100 ) / ( colors.length - 1 ), + color: tcolor.toRgbString(), + } ) ); +} + +/** + * Convert a color values object to an array of color stops. + * + * @param {Object[]} colorStops Color stop information. + * + * @return {RGBValues} R, G, and B values. + */ +export function getValuesFromColorStops( colorStops = [] ) { + return getValuesFromColors( colorStops.map( ( { color } ) => color ) ); +} + +/** + * Convert a color values object to an array of colors. + * + * @param {RGBValues} values R, G, and B values. + * + * @return {string[]} Hex color array. + */ +export function getHexColorsFromValues( values = { r: [], g: [], b: [] } ) { + return getColorsFromValues( values ).map( ( tcolor ) => + tcolor.toHexString() + ); +} + +/** + * Gets a duotone id from custom hex colors. + * + * @param {string[]} colors Hex color array. + * + * @return {string} Custom duotone id. + */ +export function getCustomDuotoneIdFromHexColors( colors ) { + return `duotone-filter-custom-${ colors + .map( ( hex ) => hex.slice( 1 ).toLowerCase() ) + .join( '-' ) }`; +} + +/** + * Convert a color stops array to a custom duotone id. + * + * @param {Object[]} colorStops Color stop information. + * + * @return {string} Custom duotone id. + */ +export function getCustomDuotoneIdFromColorStops( colorStops = [] ) { + return getCustomDuotoneIdFromHexColors( + colorStops.map( ( { color } ) => tinycolor( color ).toHexString() ) + ); +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 4d965d88d9c97..5d0416309eb4a 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -31,6 +31,7 @@ export { default as ButtonBlockerAppender } from './button-block-appender'; export { default as ColorPalette } from './color-palette'; export { default as ColorPaletteControl } from './color-palette/control'; export { default as ContrastChecker } from './contrast-checker'; +export { default as __experimentalDuotoneToolbar } from './duotone-toolbar'; export { default as __experimentalGradientPicker } from './gradient-picker'; export { default as __experimentalGradientPickerControl } from './gradient-picker/control'; export { default as __experimentalGradientPickerPanel } from './gradient-picker/panel'; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 9469ed8cdf909..fec56bc7683ea 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -31,6 +31,7 @@ @import "./components/colors-gradients/style.scss"; @import "./components/contrast-checker/style.scss"; @import "./components/default-block-appender/style.scss"; +@import "./components/duotone-toolbar/style.scss"; @import "./components/font-appearance-control/style.scss"; @import "./components/justify-toolbar/style.scss"; @import "./components/link-control/style.scss"; diff --git a/packages/components/src/custom-gradient-bar/control-points.js b/packages/components/src/custom-gradient-bar/control-points.js index d8d5ec16c2fa7..5bec42a2f2ae6 100644 --- a/packages/components/src/custom-gradient-bar/control-points.js +++ b/packages/components/src/custom-gradient-bar/control-points.js @@ -109,6 +109,8 @@ function ControlPointButton( { } function ControlPoints( { + disableRemove, + disableAlpha, gradientPickerDomRef, ignoreMarkerPosition, value: controlPoints, @@ -223,6 +225,7 @@ function ControlPoints( { renderContent={ ( { onClose } ) => ( <> { onChange( @@ -234,21 +237,23 @@ function ControlPoints( { ); } } /> - + { ! disableRemove && ( + + ) } ) } popoverProps={ COLOR_POPOVER_PROPS } @@ -264,6 +269,7 @@ function InsertPoint( { onOpenInserter, onCloseInserter, insertPosition, + disableAlpha, } ) { const [ alreadyInsertedPoint, setAlreadyInsertedPoint ] = useState( false ); return ( @@ -297,6 +303,7 @@ function InsertPoint( { ) } renderContent={ () => ( { if ( ! alreadyInsertedPoint ) { onChange( diff --git a/packages/components/src/custom-gradient-bar/index.js b/packages/components/src/custom-gradient-bar/index.js index bd8b1922cdf6e..823e3d00992a3 100644 --- a/packages/components/src/custom-gradient-bar/index.js +++ b/packages/components/src/custom-gradient-bar/index.js @@ -76,6 +76,8 @@ export default function CustomGradientBar( { hasGradient, value: controlPoints, onChange, + disableInserter = false, + disableAlpha = false, } ) { const gradientPickerDomRef = useRef(); @@ -129,24 +131,28 @@ export default function CustomGradientBar( { onMouseLeave={ onMouseLeave } >
- { ( isMovingInserter || isInsertingControlPoint ) && ( - { - gradientBarStateDispatch( { - type: 'OPEN_INSERTER', - } ); - } } - onCloseInserter={ () => { - gradientBarStateDispatch( { - type: 'CLOSE_INSERTER', - } ); - } } - /> - ) } + { ! disableInserter && + ( isMovingInserter || isInsertingControlPoint ) && ( + { + gradientBarStateDispatch( { + type: 'OPEN_INSERTER', + } ); + } } + onCloseInserter={ () => { + gradientBarStateDispatch( { + type: 'CLOSE_INSERTER', + } ); + } } + /> + ) }