From 2da82350a9bb5b27c36eb6f779851c8665d050c2 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Wed, 21 Apr 2021 16:59:58 -0400 Subject: [PATCH] Support style hierarchy and composition in StyleProvider The OutputElementStyle's values map properties can now override the properties defined for the style identified by the parent key. A multi-level hierarchy of parent styles is supported. Style composition by using a comma-delimited list of parent keys is supported. The 'background-color', 'height', 'border-style', 'border-color' and 'border-width' properties are supported for time graph states. The 'symbol-type', 'color', 'height' and 'vertical-align' properties are supported for time graph annotations. The 'opacity' property and '-blend' modifier are supported for color styles. The '-factor' modifier is supported for number styles. Signed-off-by: Patrick Tasse --- .../data-providers/style-properties.ts | 13 ++ .../data-providers/style-provider.ts | 176 +++++++++++++++++- .../components/timegraph-output-component.tsx | 81 ++++---- 3 files changed, 227 insertions(+), 43 deletions(-) create mode 100644 packages/react-components/src/components/data-providers/style-properties.ts diff --git a/packages/react-components/src/components/data-providers/style-properties.ts b/packages/react-components/src/components/data-providers/style-properties.ts new file mode 100644 index 000000000..7bb68189d --- /dev/null +++ b/packages/react-components/src/components/data-providers/style-properties.ts @@ -0,0 +1,13 @@ +export const StyleProperties = { + BACKGROUND_COLOR: 'background-color', + BLEND: '-blend', + BORDER_STYLE: 'border-style', + BORDER_COLOR: 'border-color', + BORDER_WIDTH: 'border-width', + COLOR: 'color', + FACTOR: '-factor', + HEIGHT: 'height', + OPACITY: 'opacity', + SYMBOL_TYPE: 'symbol-type', + VERTICAL_ALIGN: 'vertical-align' +}; diff --git a/packages/react-components/src/components/data-providers/style-provider.ts b/packages/react-components/src/components/data-providers/style-provider.ts index 089f64c80..c431dd5bc 100644 --- a/packages/react-components/src/components/data-providers/style-provider.ts +++ b/packages/react-components/src/components/data-providers/style-provider.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client'; import { QueryHelper } from 'tsp-typescript-client/lib/models/query/query-helper'; -import { OutputStyleModel } from 'tsp-typescript-client/lib/models/styles'; +import { OutputStyleModel, OutputElementStyle } from 'tsp-typescript-client/lib/models/styles'; +import { StyleProperties } from './style-properties'; export class StyleProvider { private tspClient: TspClient; @@ -98,4 +99,177 @@ export class StyleProvider { const styles = this.tmpStyleObject[this.outputId]; return styles ? styles : {}; } + + /** + * Get the style property value for the specified element style. The style + * hierarchy is traversed until a value is found. + * + * @param elementStyle + * the style + * @param property + * the style property + * @return the style property value, or undefined + */ + public getStyle(elementStyle: OutputElementStyle, property: string): any | undefined { + let style: OutputElementStyle | undefined = elementStyle; + const styleQueue: string[] = []; + while (style !== undefined) { + const styleValues = style.values; + const value = styleValues[property]; + if (value) { + return value; + } + + // Get the next style + style = this.popNextStyle(style, styleQueue); + } + return undefined; + } + + /** + * Get the style property number value for the specified element style. The + * style hierarchy is traversed until a number value is found, and the + * returned number value will be multiplied by the first + * StyleProperties.FACTOR suffixed modifier style that was found + * along the way, if any. + * + * @param elementStyle + * the style + * @param property + * the style property + * @return the style property number value, or undefined + */ + public getNumberStyle(elementStyle: OutputElementStyle, property: string): number | undefined { + let factor = undefined; + let style: OutputElementStyle | undefined = elementStyle; + const styleQueue: string[] = []; + while (style) { + const styleValues = style.values; + if (factor === undefined) { + const factorValue = styleValues[property + StyleProperties.FACTOR]; + if (typeof factorValue === 'number') { + factor = factorValue as number; + } + } + const value = styleValues[property]; + if (typeof value === 'number') { + const numberValue = value as number; + return (factor === undefined) ? numberValue : factor * numberValue; + } + + // Get the next style + style = this.popNextStyle(style, styleQueue); + } + return factor; + } + + /** + * Get the style property color value for the specified element style. The + * style hierarchy is traversed until a color and opacity value is found, + * and the returned color value will be blended with the first + * StyleProperties.BLEND suffixed modifier style that was found + * along the way, if any. + * + * @param elementStyle + * the style + * @param property + * the style property + * @return the style property color value, or undefined + */ + public getColorStyle(elementStyle: OutputElementStyle, property: string): { color: number, alpha: number } | undefined { + let color: string | undefined = undefined; + let opacity: number | undefined = undefined; + let blend = undefined; + let style: OutputElementStyle | undefined = elementStyle; + const styleQueue: string[] = []; + while (style) { + const styleValues = style.values; + if (blend === undefined) { + const blendValue = styleValues[property + StyleProperties.BLEND]; + if (typeof blendValue === 'string') { + blend = this.rgbaStringToColor(blendValue as string); + } + } + if (opacity === undefined) { + const opacityValue = styleValues[StyleProperties.OPACITY]; + if (typeof opacityValue === 'number') { + opacity = opacityValue as number; + if (color) { + break; + } + } + } + if (color === undefined) { + const value = styleValues[property]; + if (typeof value === 'string') { + color = value as string; + if (opacity) { + break; + } + } + } + + // Get the next style + style = this.popNextStyle(style, styleQueue); + } + const alpha = (opacity === undefined) ? 1.0 : opacity; + const rgba = (color === undefined) ? (opacity === undefined ? undefined : this.rgbaToColor(0, 0, 0, alpha)) : this.rgbStringToColor(color, alpha); + return (rgba === undefined) ? undefined : (blend === undefined) ? rgba : this.blend(rgba, blend); + } + + private popNextStyle(style: OutputElementStyle, styleQueue: string[]): OutputElementStyle | undefined { + // Get the next style + let nextStyle = undefined; + const parentKey = style.parentKey; + if (parentKey) { + const split = parentKey.split(','); + split.forEach(styleKey => styleQueue.push(styleKey)); + } + while (nextStyle === undefined && styleQueue.length !== 0) { + const nextKey = styleQueue.pop(); + if (nextKey) { + nextStyle = this.styleModel?.styles[nextKey]; + } + } + + return nextStyle; + } + + private blend(color1: { color: number, alpha: number }, color2: { color: number, alpha: number }): { color: number, alpha: number } { + /** + * If a color component 'c' with alpha 'a' is blended with color + * component 'd' with alpha 'b', the blended color and alpha are: + * + *
+         * color = (a*(1-b)*c + b*d) / (a + b - a*b)
+         * alpha = (a + b - a*b)
+         * 
+ */ + const alpha = color1.alpha + color2.alpha - color1.alpha * color2.alpha; + const r = this.blendComponent(color1.alpha, (color1.color >> 16) & 0xff, color2.alpha, (color2.color >> 16) & 0xff, alpha); + const g = this.blendComponent(color1.alpha, (color1.color >> 8) & 0xff, color2.alpha, (color2.color >> 8) & 0xff, alpha); + const b = this.blendComponent(color1.alpha, (color1.color) & 0xff, color2.alpha, (color2.color) & 0xff, alpha); + return this.rgbaToColor(r, g, b, alpha); + } + + private blendComponent(alpha1: number, color1: number, alpha2: number, color2: number, alpha: number): number { + return Math.floor((alpha1 * (1.0 - alpha2) * color1 + alpha2 * color2) / alpha); + } + + private rgbStringToColor(rgbString: string, alpha: number): { color: number, alpha: number } { + const color = parseInt(rgbString.replace(/^#/, ''), 16); + return { color, alpha }; + } + + private rgbaStringToColor(rgbaString: string): { color: number, alpha: number } { + const int = parseInt(rgbaString.replace(/^#/, ''), 16); + const color = (int >> 8) & 0xffffff; + const alpha = (int & 0xff) / 255; + return { color, alpha }; + } + + private rgbaToColor(r: number, g: number, b: number, alpha: number): { color: number, alpha: number } { + const color = (r << 16) + (g << 8) + b; + return { color, alpha }; + } } diff --git a/packages/react-components/src/components/timegraph-output-component.tsx b/packages/react-components/src/components/timegraph-output-component.tsx index c746dbdbb..f742c96ce 100644 --- a/packages/react-components/src/components/timegraph-output-component.tsx +++ b/packages/react-components/src/components/timegraph-output-component.tsx @@ -14,6 +14,7 @@ import { TimeGraphEntry } from 'tsp-typescript-client/lib/models/timegraph'; import { signalManager, Signals } from '@trace-viewer/base/lib/signals/signal-manager'; import { AbstractOutputProps, AbstractOutputState } from './abstract-output-component'; import { AbstractTreeOutputComponent } from './abstract-tree-output-component'; +import { StyleProperties } from './data-providers/style-properties'; import { StyleProvider } from './data-providers/style-provider'; import { TspDataProvider } from './data-providers/tsp-data-provider'; import { ReactTimeGraphContainer } from './utils/timegraph-container-component'; @@ -340,30 +341,36 @@ export class TimegraphOutputComponent extends AbstractTreeOutputComponent