Skip to content

Commit

Permalink
feat(style): add support for fill color gradient in the Update Style …
Browse files Browse the repository at this point in the history
…API (#2760)

Major changes include:
- Introduction of new types, `FillColorGradient` and `Direction`, to handle gradient fill colors.
The `FillColorGradient` type defines properties for the `startColor`, `endColor` and `direction`.
The `Direction' type provides options such as `left-to-right` and`bottom-to-top` for specifying the gradient direction.
- Updated the existing `color' property of the `Fill' type, to accept either a solid color string, or a new `FillColorGradient' type.
- Changed the functionality for resetting the fill color property. Currently, resetting the fill color removes the gradient.
- Added visual and integration tests.
- Updated the TS documentation to clearly explain the new gradient type.
- Modified the "elements identification" page to display the `CallActivity` with the fill gradient color.
  • Loading branch information
csouchet authored Jul 12, 2023
1 parent 44866da commit 7ecdd37
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 30 deletions.
2 changes: 1 addition & 1 deletion dev/public/static/js/elements-identification.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function computeStyleUpdateByKind(bpmnKind) {
style.font.isItalic = true;
style.font.isStrikeThrough = true;

style.fill.color = 'LimeGreen';
style.fill.color = { startColor: 'LightYellow', endColor: 'LimeGreen', direction: 'left-to-right' };
} else if (ShapeUtil.isSubProcess(bpmnKind)) {
style.font.color = 'white';
style.font.size = 14;
Expand Down
14 changes: 13 additions & 1 deletion dev/ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type {
StyleUpdate,
Version,
ZoomType,
FillColorGradient,
GradientDirection,
} from '../../src/bpmn-visualization';
import { FlowKind, ShapeBpmnElementKind } from '../../src/bpmn-visualization';
import { fetchBpmnContent, logDownload, logError, logErrorAndOpenAlert, logStartup } from './utils/internal-helpers';
Expand Down Expand Up @@ -262,9 +264,19 @@ function configureStyleFromParameters(parameters: URLSearchParams): void {
style = { stroke: {}, font: {}, fill: {} };

parameters.get('style.api.stroke.color') && (style.stroke.color = parameters.get('style.api.stroke.color'));

parameters.get('style.api.font.color') && (style.font.color = parameters.get('style.api.font.color'));
parameters.get('style.api.font.opacity') && (style.font.opacity = Number(parameters.get('style.api.font.opacity')));
parameters.get('style.api.fill.color') && (style.fill.color = parameters.get('style.api.fill.color'));

if (parameters.get('style.api.fill.color')) {
style.fill.color = parameters.get('style.api.fill.color');
} else if (parameters.get('style.api.fill.color.startColor') && parameters.get('style.api.fill.color.endColor') && parameters.get('style.api.fill.color.direction')) {
style.fill.color = {
startColor: parameters.get('style.api.fill.color.startColor'),
endColor: parameters.get('style.api.fill.color.endColor'),
direction: parameters.get('style.api.fill.color.direction') as GradientDirection,
} as FillColorGradient;
}
parameters.get('style.api.fill.opacity') && (style.fill.opacity = Number(parameters.get('style.api.fill.opacity')));

logStartup(`Prepared "Update Style" API object`, style);
Expand Down
38 changes: 34 additions & 4 deletions src/component/mxgraph/style/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import { ensureOpacityValue, ensureStrokeWidthValue } from '../../helpers/validators';
import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate } from '../../registry';
import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate, GradientDirection, FillColorGradient } from '../../registry';
import { ShapeBpmnElementKind } from '../../../model/bpmn/internal';
import { mxConstants, mxUtils } from '../initializer';
import { BpmnStyleIdentifier } from './identifiers';
Expand Down Expand Up @@ -110,12 +110,38 @@ export const updateFont = (cellStyle: string, font: Font): string => {
return cellStyle;
};

const convertDirection = (direction: GradientDirection): string => {
switch (direction) {
case 'right-to-left':
return mxConstants.DIRECTION_WEST;
case 'bottom-to-top':
return mxConstants.DIRECTION_NORTH;
case 'top-to-bottom':
return mxConstants.DIRECTION_SOUTH;
default:
return mxConstants.DIRECTION_EAST;
}
};

export const updateFill = (cellStyle: string, fill: Fill): string => {
if (fill.color) {
cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fill.color, convertDefaultValue);
const color = fill.color;
if (color) {
const isGradient = isFillColorGradient(color);

const fillColor = isGradient ? color.startColor : color;
cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fillColor, convertDefaultValue);

if (isGradient) {
// The values of the color are mandatory. So, no need to check if it's undefined.
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, color.endColor);
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, convertDirection(color.direction));
} else if (color === 'default') {
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, undefined);
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, undefined);
}

if (cellStyle.includes(ShapeBpmnElementKind.POOL) || cellStyle.includes(ShapeBpmnElementKind.LANE)) {
cellStyle = setStyle(cellStyle, mxConstants.STYLE_SWIMLANE_FILLCOLOR, fill.color, convertDefaultValue);
cellStyle = setStyle(cellStyle, mxConstants.STYLE_SWIMLANE_FILLCOLOR, fillColor, convertDefaultValue);
}
}

Expand All @@ -127,3 +153,7 @@ export const updateFill = (cellStyle: string, fill: Fill): string => {
export const isShapeStyleUpdate = (style: StyleUpdate): style is ShapeStyleUpdate => {
return style && typeof style === 'object' && 'fill' in style;
};

const isFillColorGradient = (color: string | FillColorGradient): color is FillColorGradient => {
return color && typeof color === 'object';
};
47 changes: 45 additions & 2 deletions src/component/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export type Font = StyleWithOpacity & {
*/
export type Fill = StyleWithOpacity & {
/**
* Possible values are all HTML color names or HEX codes, as well as special keywords such as:
* Possible values are all HTML color names, HEX codes, {@link FillColorGradient}, as well as special keywords such as:
* - `default` to use the color defined in the BPMN element default style.
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
Expand All @@ -264,10 +264,53 @@ export type Fill = StyleWithOpacity & {
* **Notes about the `default` special keyword**:
* - It can be used when the style is first updated and then needs to be reset to its initial value.
* - It doesn't use the color set in the BPMN source when the "BPMN in Color" support is enabled. It uses the color defined in the BPMN element default style.
* - If a gradient was set, it will be completely reverted.
*/
color?: 'default' | 'inherit' | 'none' | 'swimlane' | string;
color?: FillColorGradient | 'default' | 'inherit' | 'none' | 'swimlane' | string;
};

/**
* It is a linear gradient managed by `mxGraph`.
* For more information about mxGraph, refer to the documentation at:
* {@link https://jgraph.github.io/mxgraph/docs/js-api/files/util/mxConstants-js.html#mxConstants.STYLE_GRADIENTCOLOR}
*
* **Notes**:
* Using the same color for the start color and end color will have the same effect as setting only the fill color with an HTML color name, HEX code or special keyword.
*
* @category Element Style
*/
export type FillColorGradient = {
/**
* It can be any HTML color name or HEX code, as well as special keywords such as:
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
* - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}.
*/
startColor: 'inherit' | 'none' | 'swimlane' | string;

/**
* It can be any HTML color name or HEX code, as well as special keywords such as:
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
* - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}.
*/
endColor: 'inherit' | 'none' | 'swimlane' | string;

/**
* Specifies how the colors transition within the gradient.
*
* Taking the example of `bottom-to-top`, this means that the start color is at the bottom of the paint pattern and the end color is at the top, with a gradient between them.
*
* @see {@link GradientDirection}
*/
direction: GradientDirection;
};

/**
* @category Element Style
*/
export type GradientDirection = 'left-to-right' | 'right-to-left' | 'bottom-to-top' | 'top-to-bottom';

/**
* @category Element Style
*/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 39 additions & 3 deletions test/e2e/style.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ class StyleImageSnapshotThresholds extends MultiBrowserImageSnapshotThresholds {
windows: 0.12 / 100, // 0.11000117996341485%
},
],
[
'fill.color.gradient',
{
linux: 0.11 / 100, // 0.10872082550010823%
macos: 0.11 / 100, // 0.10872082550010823%
windows: 0.11 / 100, // 0.10872082550010823%
},
],
]);
}

Expand All @@ -51,13 +59,21 @@ class StyleImageSnapshotThresholds extends MultiBrowserImageSnapshotThresholds {
},
],
[
'fill.color',
'fill.color.string',
{
linux: 0.09 / 100, // 0.08216175057789155%
macos: 0.09 / 100, // 0.08216175057789155%
windows: 0.09 / 100, // 0.08216175057789155%
},
],
[
'fill.color.gradient',
{
linux: 0.15 / 100, // 0.1477596574325335%
macos: 0.15 / 100, // 0.1477596574325335%
windows: 0.15 / 100, // 0.1477596574325335%
},
],
[
'fill.color.opacity.group',
{
Expand Down Expand Up @@ -112,15 +128,35 @@ describe('Style API', () => {
expect(image).toMatchImageSnapshot(config);
});

it(`Update 'fill.color'`, async () => {
it(`Update 'fill.color' as string`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram('01.most.bpmn.types.without.label', {
styleOptions: {
styleUpdate: { fill: { color: 'chartreuse' } },
},
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig('fill.color');
const config = imageSnapshotConfigurator.getConfig('fill.color.string');
expect(image).toMatchImageSnapshot(config);
});

it(`Update 'fill.color' as gradient`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram('01.most.bpmn.types.without.label', {
styleOptions: {
styleUpdate: {
fill: {
color: {
startColor: 'pink',
endColor: 'lime',
direction: 'top-to-bottom',
},
},
},
},
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig('fill.color.gradient');
expect(image).toMatchImageSnapshot(config);
});

Expand Down
16 changes: 14 additions & 2 deletions test/integration/helpers/model-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ limitations under the License.
*/

import type {
Fill,
FlowKind,
GlobalTaskKind,
MessageVisibleKind,
Expand Down Expand Up @@ -167,6 +166,18 @@ type ExpectedModelElement = {
extraCssClasses?: string[];
};

export interface ExpectedFill {
color?: string;
opacity?: Opacity;
}

export type ExpectedDirection = 'west' | 'east' | 'north' | 'south';

export interface ExpectedGradient {
color: string;
direction?: ExpectedDirection;
}

export interface ExpectedShapeModelElement extends ExpectedModelElement {
kind?: ShapeBpmnElementKind;
/** Generally needed when the BPMN shape doesn't exist yet (use an arbitrary shape until the final render is implemented) */
Expand All @@ -179,7 +190,8 @@ export interface ExpectedShapeModelElement extends ExpectedModelElement {
* - Vertical pool/lane --> true (the label is horizontal)
**/
isSwimLaneLabelHorizontal?: boolean;
fill?: Fill;
fill?: ExpectedFill;
gradient?: ExpectedGradient;
}

export interface ExpectedEventModelElement extends ExpectedShapeModelElement {
Expand Down
4 changes: 4 additions & 0 deletions test/integration/matchers/matcher-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface BpmnCellStyle extends StyleMap {
fillColor: string;
fillOpacity?: Opacity;
swimlaneFillColor?: string;
gradientColor?: string;
gradientDirection?: string;
fontColor: string;
fontFamily: string;
fontSize: number;
Expand Down Expand Up @@ -208,6 +210,8 @@ function toBpmnStyle(rawStyle: StyleMap, isEdge: boolean): BpmnCellStyle {
style.horizontal = rawStyle.horizontal;
style.swimlaneFillColor = rawStyle.swimlaneFillColor;
style.fillOpacity = rawStyle.fillOpacity;
style.gradientColor = rawStyle.gradientColor;
style.gradientDirection = rawStyle.gradientDirection;
}
return style;
}
Expand Down
24 changes: 16 additions & 8 deletions test/integration/matchers/toBeShape/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,26 @@ export function buildExpectedShapeCellStyle(expectedModel: ExpectedShapeModelEle
style.align = expectedModel.align ?? 'center';
style.strokeWidth = style.strokeWidth ?? expectedStrokeWidth(expectedModel.kind);

style.fillColor =
expectedModel.fill?.color ??
([ShapeBpmnElementKind.LANE, ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.TEXT_ANNOTATION, ShapeBpmnElementKind.GROUP].includes(expectedModel.kind)
? 'none'
: style.fillColor);
const fill = expectedModel.fill;
if (fill) {
style.fillColor = fill.color ?? style.fillColor;
style.fillOpacity = fill.opacity;
}
if (!fill?.color && [ShapeBpmnElementKind.LANE, ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.TEXT_ANNOTATION, ShapeBpmnElementKind.GROUP].includes(expectedModel.kind)) {
style.fillColor = 'none';
}

if (expectedModel.gradient) {
style.gradientColor = expectedModel.gradient.color;
style.gradientDirection = expectedModel.gradient.direction;
}

style.swimlaneFillColor = [ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.LANE].includes(expectedModel.kind) && style.fillColor !== 'none' ? style.fillColor : undefined;

style.fillOpacity = expectedModel.fill?.opacity;
'isSwimLaneLabelHorizontal' in expectedModel && (style.horizontal = Number(expectedModel.isSwimLaneLabelHorizontal));
expectedModel.isSwimLaneLabelHorizontal && (style.horizontal = Number(expectedModel.isSwimLaneLabelHorizontal));

// ignore marker order, which is only relevant when rendering the shape (it has its own order algorithm)
'markers' in expectedModel && (style.markers = expectedModel.markers.sort());
style.markers = expectedModel.markers?.sort();

return style;
}
Expand Down
Loading

0 comments on commit 7ecdd37

Please sign in to comment.