{ ( isMovingInserter || isInsertingControlPoint ) && (
-
{
gradientBarStateDispatch( {
type: 'OPEN_INSERTER',
@@ -243,9 +153,8 @@ export default function CustomGradientBar( { value, onChange } ) {
? gradientBarState.insertPosition
: undefined
}
- markerPoints={ markerPoints }
- onChange={ onGradientStructureChange }
- gradientAST={ gradientAST }
+ value={ controlPoints }
+ onChange={ onChange }
onStartControlPointChange={ () => {
gradientBarStateDispatch( {
type: 'START_CONTROL_CHANGE',
diff --git a/packages/components/src/custom-gradient-bar/utils.js b/packages/components/src/custom-gradient-bar/utils.js
new file mode 100644
index 00000000000000..eb088b08ea0a55
--- /dev/null
+++ b/packages/components/src/custom-gradient-bar/utils.js
@@ -0,0 +1,199 @@
+/**
+ * Internal dependencies
+ */
+import {
+ MINIMUM_DISTANCE_BETWEEN_POINTS,
+ MINIMUM_ABSOLUTE_LEFT_POSITION,
+ INSERT_POINT_WIDTH,
+} from './constants';
+
+/**
+ * Control point for the gradient bar.
+ *
+ * @typedef {Object} ControlPoint
+ * @property {string} color Color of the control point.
+ * @property {number} position Integer position of the control point as a percentage.
+ */
+
+/**
+ * Color as parsed from the gradient by gradient-parser.
+ *
+ * @typedef {Object} Color
+ * @property {string} r Red component.
+ * @property {string} g Green component.
+ * @property {string} b Green component.
+ * @property {string} [a] Optional alpha component.
+ */
+
+/**
+ * Clamps a number between 0 and 100.
+ *
+ * @param {number} value Value to clamp.
+ *
+ * @return {number} Value clamped between 0 and 100.
+ */
+export function clampPercent( value ) {
+ return Math.max( 0, Math.min( 100, value ) );
+}
+
+/**
+ * Check if a control point is overlapping with another.
+ *
+ * @param {ControlPoint[]} value Array of control points.
+ * @param {number} initialIndex Index of the position to test.
+ * @param {number} newPosition New position of the control point.
+ * @param {number} minDistance Distance considered to be overlapping.
+ *
+ * @return {boolean} True if the point is overlapping.
+ */
+export function isOverlapping(
+ value,
+ initialIndex,
+ newPosition,
+ minDistance = MINIMUM_DISTANCE_BETWEEN_POINTS
+) {
+ const initialPosition = value[ initialIndex ].position;
+ const minPosition = Math.min( initialPosition, newPosition );
+ const maxPosition = Math.max( initialPosition, newPosition );
+
+ return value.some( ( { position }, index ) => {
+ return (
+ index !== initialIndex &&
+ ( Math.abs( position - newPosition ) < minDistance ||
+ ( minPosition < position && position < maxPosition ) )
+ );
+ } );
+}
+
+/**
+ * Adds a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} position Position to insert the new point.
+ * @param {Color} color Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function addControlPoint( points, position, color ) {
+ const nextIndex = points.findIndex(
+ ( point ) => point.position > position
+ );
+ const newPoint = { color, position };
+ const newPoints = points.slice();
+ newPoints.splice( nextIndex - 1, 0, newPoint );
+ return newPoints;
+}
+
+/**
+ * Removes a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to remove.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function removeControlPoint( points, index ) {
+ return points.filter( ( point, pointIndex ) => {
+ return pointIndex !== index;
+ } );
+}
+
+/**
+ * Updates a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {ControlPoint[]} newPoint New control point to replace the index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPoint( points, index, newPoint ) {
+ const newValue = points.slice();
+ newValue[ index ] = newPoint;
+ return newValue;
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {number} newPosition Position to move the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointPosition( points, index, newPosition ) {
+ if ( isOverlapping( points, index, newPosition ) ) {
+ return points;
+ }
+ const newPoint = {
+ ...points[ index ],
+ position: newPosition,
+ };
+ return updateControlPoint( points, index, newPoint );
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {Color} newColor Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointColor( points, index, newColor ) {
+ const newPoint = {
+ ...points[ index ],
+ color: newColor,
+ };
+ return updateControlPoint( points, index, newPoint );
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} position Position of the color stop.
+ * @param {string} newColor Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointColorByPosition(
+ points,
+ position,
+ newColor
+) {
+ const index = points.findIndex( ( point ) => point.position === position );
+ return updateControlPointColor( points, index, newColor );
+}
+
+/**
+ * Gets the horizontal coordinate when dragging a control point with the mouse.
+ *
+ * @param {number} mouseXCoordinate Horizontal coordinate of the mouse position.
+ * @param {Element} containerElement Container for the gradient picker.
+ * @param {number} positionedElementWidth Width of the positioned element.
+ *
+ * @return {number} Whole number percentage from the left.
+ */
+export function getHorizontalRelativeGradientPosition(
+ mouseXCoordinate,
+ containerElement,
+ positionedElementWidth
+) {
+ if ( ! containerElement ) {
+ return;
+ }
+ const { x, width } = containerElement.getBoundingClientRect();
+ const absolutePositionValue =
+ mouseXCoordinate -
+ x -
+ MINIMUM_ABSOLUTE_LEFT_POSITION -
+ positionedElementWidth / 2;
+ const availableWidth =
+ width - MINIMUM_ABSOLUTE_LEFT_POSITION - INSERT_POINT_WIDTH;
+ return Math.round(
+ clampPercent( ( absolutePositionValue * 100 ) / availableWidth )
+ );
+}
diff --git a/packages/components/src/custom-gradient-picker/constants.js b/packages/components/src/custom-gradient-picker/constants.js
index 8646c93471b190..9924ff2b3d6860 100644
--- a/packages/components/src/custom-gradient-picker/constants.js
+++ b/packages/components/src/custom-gradient-picker/constants.js
@@ -3,22 +3,11 @@
*/
import { __ } from '@wordpress/i18n';
-export const INSERT_POINT_WIDTH = 23;
-export const GRADIENT_MARKERS_WIDTH = 18;
-export const MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_MARKER =
- ( INSERT_POINT_WIDTH + GRADIENT_MARKERS_WIDTH ) / 2;
-export const MINIMUM_ABSOLUTE_LEFT_POSITION = 5;
-export const MINIMUM_DISTANCE_BETWEEN_POINTS = 0;
-export const MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT = 10;
-export const KEYBOARD_CONTROL_POINT_VARIATION = MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT;
-export const MINIMUM_SIGNIFICANT_MOVE = 5;
export const DEFAULT_GRADIENT =
'linear-gradient(135deg, rgba(6, 147, 227, 1) 0%, rgb(155, 81, 224) 100%)';
-export const COLOR_POPOVER_PROPS = {
- className: 'components-custom-gradient-picker__color-picker-popover',
- position: 'top',
-};
+
export const DEFAULT_LINEAR_GRADIENT_ANGLE = 180;
+
export const HORIZONTAL_GRADIENT_ORIENTATION = {
type: 'angular',
value: 90,
@@ -28,3 +17,18 @@ export const GRADIENT_OPTIONS = [
{ value: 'linear-gradient', label: __( 'Linear' ) },
{ value: 'radial-gradient', label: __( 'Radial' ) },
];
+
+export const DIRECTIONAL_ORIENTATION_ANGLE_MAP = {
+ top: 0,
+ 'top right': 45,
+ 'right top': 45,
+ right: 90,
+ 'right bottom': 135,
+ 'bottom right': 135,
+ bottom: 180,
+ 'bottom left': 225,
+ 'left bottom': 225,
+ left: 270,
+ 'top left': 315,
+ 'left top': 315,
+};
diff --git a/packages/components/src/custom-gradient-picker/index.js b/packages/components/src/custom-gradient-picker/index.js
index 39e63ab404ed2d..7d10440295afe9 100644
--- a/packages/components/src/custom-gradient-picker/index.js
+++ b/packages/components/src/custom-gradient-picker/index.js
@@ -12,15 +12,21 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import AnglePickerControl from '../angle-picker-control';
-import CustomGradientBar from './custom-gradient-bar';
+import CustomGradientBar from '../custom-gradient-bar';
import { Flex } from '../flex';
import SelectControl from '../select-control';
-import { getGradientParsed } from './utils';
+import {
+ getGradientAstWithDefault,
+ getLinearGradientRepresentationOfARadial,
+ getGradientAstWithControlPoints,
+ getStopCssColor,
+} from './utils';
import { serializeGradient } from './serializer';
import {
DEFAULT_LINEAR_GRADIENT_ANGLE,
HORIZONTAL_GRADIENT_ORIENTATION,
GRADIENT_OPTIONS,
+ DEFAULT_GRADIENT,
} from './constants';
import {
AccessoryWrapper,
@@ -98,11 +104,38 @@ const GradientTypePicker = ( { gradientAST, hasGradient, onChange } ) => {
};
export default function CustomGradientPicker( { value, onChange } ) {
- const { gradientAST, hasGradient } = getGradientParsed( value );
- const { type } = gradientAST;
+ const gradientAST = getGradientAstWithDefault( value );
+ // On radial gradients the bar should display a linear gradient.
+ // On radial gradients the bar represents a slice of the gradient from the center until the outside.
+ const background =
+ gradientAST.type === 'radial-gradient'
+ ? getLinearGradientRepresentationOfARadial( gradientAST )
+ : gradientAST.value;
+ const hasGradient = gradientAST.value !== DEFAULT_GRADIENT;
+ // Control points color option may be hex from presets, custom colors will be rgb.
+ // The position should always be a percentage.
+ const controlPoints = gradientAST.colorStops.map( ( colorStop ) => ( {
+ color: getStopCssColor( colorStop ),
+ position: parseInt( colorStop.length.value ),
+ } ) );
+
return (
-
+
{
+ onChange(
+ serializeGradient(
+ getGradientAstWithControlPoints(
+ gradientAST,
+ newControlPoints
+ )
+ )
+ );
+ } }
+ />
- { type === 'linear-gradient' && (
+ { gradientAST.type === 'linear-gradient' && (
{
- if ( colorStopIndex !== index ) {
- return colorStop;
- }
- return {
- ...colorStop,
- length: {
- ...colorStop.length,
- value: relativePosition,
- },
- };
- }
- ),
- };
-}
-
-export function isControlPointOverlapping(
- gradientAST,
- position,
- initialIndex
-) {
- const initialPosition = parseInt(
- gradientAST.colorStops[ initialIndex ].length.value
- );
- const minPosition = Math.min( initialPosition, position );
- const maxPosition = Math.max( initialPosition, position );
-
- return some( gradientAST.colorStops, ( { length }, index ) => {
- const itemPosition = parseInt( length.value );
- return (
- index !== initialIndex &&
- ( Math.abs( itemPosition - position ) <
- MINIMUM_DISTANCE_BETWEEN_POINTS ||
- ( minPosition < itemPosition && itemPosition < maxPosition ) )
- );
- } );
-}
-
-function getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- valueToSum
-) {
- const currentPosition = gradientAST.colorStops[ index ].length.value;
- const newPosition = Math.max(
- 0,
- Math.min( 100, parseInt( currentPosition ) + valueToSum )
- );
- if ( isControlPointOverlapping( gradientAST, newPosition, index ) ) {
- return gradientAST;
- }
- return getGradientWithPositionAtIndexChanged(
- gradientAST,
- index,
- newPosition
- );
-}
-
-export function getGradientWithPositionAtIndexIncreased( gradientAST, index ) {
- return getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- KEYBOARD_CONTROL_POINT_VARIATION
- );
-}
-
-export function getGradientWithPositionAtIndexDecreased( gradientAST, index ) {
- return getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- -KEYBOARD_CONTROL_POINT_VARIATION
- );
-}
-
-export function getGradientWithColorAtIndexChanged(
- gradientAST,
- index,
- rgbaColor
-) {
- return {
- ...gradientAST,
- colorStops: gradientAST.colorStops.map(
- ( colorStop, colorStopIndex ) => {
- if ( colorStopIndex !== index ) {
- return colorStop;
- }
- return {
- ...colorStop,
- ...tinyColorRgbToGradientColorStop( rgbaColor ),
- };
- }
- ),
- };
-}
-
-export function getGradientWithColorAtPositionChanged(
- gradientAST,
- relativePositionValue,
- rgbaColor
-) {
- const index = findIndex( gradientAST.colorStops, ( colorStop ) => {
- return (
- colorStop &&
- colorStop.length &&
- colorStop.length.type === '%' &&
- colorStop.length.value === relativePositionValue.toString()
- );
- } );
- return getGradientWithColorAtIndexChanged( gradientAST, index, rgbaColor );
-}
-
-export function getGradientWithControlPointRemoved( gradientAST, index ) {
- return {
- ...gradientAST,
- colorStops: gradientAST.colorStops.filter( ( elem, elemIndex ) => {
- return elemIndex !== index;
- } ),
- };
-}
-
-export function getHorizontalRelativeGradientPosition(
- mouseXCoordinate,
- containerElement,
- positionedElementWidth
-) {
- if ( ! containerElement ) {
- return;
- }
- const { x, width } = containerElement.getBoundingClientRect();
- const absolutePositionValue =
- mouseXCoordinate -
- x -
- MINIMUM_ABSOLUTE_LEFT_POSITION -
- positionedElementWidth / 2;
- const availableWidth =
- width - MINIMUM_ABSOLUTE_LEFT_POSITION - INSERT_POINT_WIDTH;
- return Math.round(
- Math.min(
- Math.max( ( absolutePositionValue * 100 ) / availableWidth, 0 ),
- 100
- )
- );
-}
-
-/**
- * Returns the marker points from a gradient AST.
- *
- * @param {Object} gradientAST An object representing the gradient AST.
- *
- * @return {Array.<{color: string, position: string, positionValue: number}>}
- * An array of markerPoint objects.
- * color: A string with the color code ready to be used in css style e.g: "rgba( 1, 2 , 3, 0.5)".
- * position: A string with the position ready to be used in css style e.g: "70%".
- * positionValue: A number with the relative position value e.g: 70.
- */
-export function getMarkerPoints( gradientAST ) {
- if ( ! gradientAST ) {
- return [];
- }
- return map( gradientAST.colorStops, ( colorStop ) => {
- if (
- ! colorStop ||
- ! colorStop.length ||
- colorStop.length.type !== '%'
- ) {
- return null;
- }
- return {
- color: serializeGradientColor( colorStop ),
- position: serializeGradientPosition( colorStop.length ),
- positionValue: parseInt( colorStop.length.value ),
- };
- } );
-}
+import { serializeGradient } from './serializer';
export function getLinearGradientRepresentationOfARadial( gradientAST ) {
return serializeGradient( {
@@ -241,55 +22,24 @@ export function getLinearGradientRepresentationOfARadial( gradientAST ) {
} );
}
-const DIRECTIONAL_ORIENTATION_ANGLE_MAP = {
- top: 0,
- 'top right': 45,
- 'right top': 45,
- right: 90,
- 'right bottom': 135,
- 'bottom right': 135,
- bottom: 180,
- 'bottom left': 225,
- 'left bottom': 225,
- left: 270,
- 'top left': 315,
- 'left top': 315,
-};
-
function hasUnsupportedLength( item ) {
return item.length === undefined || item.length.type !== '%';
}
-function assignColorStopLengths( gradientAST ) {
- const { colorStops } = gradientAST;
- const step = 100 / ( colorStops.length - 1 );
- colorStops.forEach( ( stop, index ) => {
- stop.length = {
- value: step * index,
- type: '%',
- };
- } );
-}
-
-export function getGradientParsed( value ) {
- let hasGradient = !! value;
+export function getGradientAstWithDefault( value ) {
// gradientAST will contain the gradient AST as parsed by gradient-parser npm module.
// More information of its structure available at https://www.npmjs.com/package/gradient-parser#ast.
let gradientAST;
- let gradientValue;
+
try {
- gradientAST = gradientParser.parse( value || DEFAULT_GRADIENT )[ 0 ];
- gradientValue = value || DEFAULT_GRADIENT;
+ gradientAST = gradientParser.parse( value )[ 0 ];
+ gradientAST.value = value;
} catch ( error ) {
- hasGradient = false;
gradientAST = gradientParser.parse( DEFAULT_GRADIENT )[ 0 ];
- gradientValue = DEFAULT_GRADIENT;
+ gradientAST.value = DEFAULT_GRADIENT;
}
- if (
- gradientAST.orientation &&
- gradientAST.orientation.type === 'directional'
- ) {
+ if ( gradientAST.orientation?.type === 'directional' ) {
gradientAST.orientation.type = 'angular';
gradientAST.orientation.value = DIRECTIONAL_ORIENTATION_ANGLE_MAP[
gradientAST.orientation.value
@@ -297,13 +47,52 @@ export function getGradientParsed( value ) {
}
if ( gradientAST.colorStops.some( hasUnsupportedLength ) ) {
- assignColorStopLengths( gradientAST );
- gradientValue = serializeGradient( gradientAST );
+ const { colorStops } = gradientAST;
+ const step = 100 / ( colorStops.length - 1 );
+ colorStops.forEach( ( stop, index ) => {
+ stop.length = {
+ value: step * index,
+ type: '%',
+ };
+ } );
+ gradientAST.value = serializeGradient( gradientAST );
}
+ return gradientAST;
+}
+
+export function getGradientAstWithControlPoints(
+ gradientAST,
+ newControlPoints
+) {
return {
- hasGradient,
- gradientAST,
- gradientValue,
+ ...gradientAST,
+ colorStops: newControlPoints.map( ( { position, color } ) => {
+ const { r, g, b, a } = tinycolor( color ).toRgb();
+ return {
+ length: {
+ type: '%',
+ value: position.toString(),
+ },
+ type: a < 1 ? 'rgba' : 'rgb',
+ value: a < 1 ? [ r, g, b, a ] : [ r, g, b ],
+ };
+ } ),
};
}
+
+export function getStopCssColor( colorStop ) {
+ switch ( colorStop.type ) {
+ case 'hex':
+ return `#${ colorStop.value }`;
+ case 'literal':
+ return colorStop.value;
+ case 'rgb':
+ case 'rgba':
+ return `${ colorStop.type }(${ colorStop.value.join( ',' ) })`;
+ default:
+ // Should be unreachable if passing an AST from gradient-parser.
+ // See https://github.com/rafaelcaricio/gradient-parser#ast.
+ return 'transparent';
+ }
+}