diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts
index 75a6cd6541..3501cef922 100644
--- a/src/api/chart-api.ts
+++ b/src/api/chart-api.ts
@@ -1,3 +1,4 @@
+///
import { ChartWidget, MouseEventParamsImpl, MouseEventParamsImplSupplier } from '../gui/chart-widget';
import { assert, ensure, ensureDefined } from '../helpers/assertions';
@@ -264,6 +265,16 @@ export class ChartApi implements IChartApiBase, Da
}
public applyOptions(options: DeepPartial>): void {
+ if (process.env.NODE_ENV === 'development') {
+ const colorSpace = options.layout?.colorSpace;
+ if (colorSpace !== undefined && colorSpace !== this.options().layout.colorSpace) {
+ throw new Error(`colorSpace option should not be changed once the chart has been created.`);
+ }
+ const colorParsers = options.layout?.colorParsers;
+ if (colorParsers !== undefined && colorParsers !== this.options().layout.colorParsers) {
+ throw new Error(`colorParsers option should not be changed once the chart has been created.`);
+ }
+ }
this._chartWidget.applyOptions(toInternalOptions(options));
}
diff --git a/src/api/options/layout-options-defaults.ts b/src/api/options/layout-options-defaults.ts
index 8729f70314..6c96af426a 100644
--- a/src/api/options/layout-options-defaults.ts
+++ b/src/api/options/layout-options-defaults.ts
@@ -16,4 +16,6 @@ export const layoutOptionsDefaults: LayoutOptions = {
separatorHoverColor: 'rgba(178, 181, 189, 0.2)',
},
attributionLogo: true,
+ colorSpace: 'srgb',
+ colorParsers: [],
};
diff --git a/src/gui/attribution-logo-widget.ts b/src/gui/attribution-logo-widget.ts
index 71b807dfbb..b54ee39b0a 100644
--- a/src/gui/attribution-logo-widget.ts
+++ b/src/gui/attribution-logo-widget.ts
@@ -1,5 +1,3 @@
-import { colorStringToGrayscale } from '../helpers/color';
-
import { IChartWidgetBase } from './chart-widget';
type LogoTheme = 'dark' | 'light';
@@ -44,7 +42,12 @@ export class AttributionLogoWidget {
}
private _themeToUse(): LogoTheme {
- return colorStringToGrayscale(this._chart.options()['layout'].textColor) > 160 ? 'dark' : 'light';
+ return this._chart
+ .model()
+ .colorParser()
+ .colorStringToGrayscale(this._chart.options()['layout'].textColor) > 160
+ ? 'dark'
+ : 'light';
}
private _shouldBeVisible(): boolean {
diff --git a/src/gui/pane-widget.ts b/src/gui/pane-widget.ts
index 853f7cd622..89744efd85 100644
--- a/src/gui/pane-widget.ts
+++ b/src/gui/pane-widget.ts
@@ -484,9 +484,13 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers {
this._rightPriceAxisWidget.paint(type);
}
+ const canvasOptions: CanvasRenderingContext2DSettings = {
+ colorSpace: this._chart.options().layout.colorSpace,
+ };
+
if (type !== InvalidationLevel.Cursor) {
this._canvasBinding.applySuggestedBitmapSize();
- const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding);
+ const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding, canvasOptions);
if (target !== null) {
target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) => {
this._drawBackground(scope);
@@ -501,7 +505,7 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers {
}
this._topCanvasBinding.applySuggestedBitmapSize();
- const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding);
+ const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding, canvasOptions);
if (topTarget !== null) {
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }: BitmapCoordinatesRenderingScope) => {
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
diff --git a/src/gui/price-axis-stub.ts b/src/gui/price-axis-stub.ts
index 9023b2890b..164b05fa49 100644
--- a/src/gui/price-axis-stub.ts
+++ b/src/gui/price-axis-stub.ts
@@ -101,7 +101,9 @@ export class PriceAxisStub implements IDestroyable {
this._invalidated = false;
this._canvasBinding.applySuggestedBitmapSize();
- const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding);
+ const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding, {
+ colorSpace: this._options.layout.colorSpace,
+ });
if (target !== null) {
target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) => {
this._drawBackground(scope);
diff --git a/src/gui/price-axis-widget.ts b/src/gui/price-axis-widget.ts
index a1c613228d..c6b0c7fba4 100644
--- a/src/gui/price-axis-widget.ts
+++ b/src/gui/price-axis-widget.ts
@@ -250,7 +250,11 @@ export class PriceAxisWidget implements IDestroyable {
let tickMarkMaxWidth = 0;
const rendererOptions = this.rendererOptions();
- const ctx = ensureNotNull(this._canvasBinding.canvasElement.getContext('2d'));
+ const ctx = ensureNotNull(
+ this._canvasBinding.canvasElement.getContext('2d', {
+ colorSpace: this._pane.chart().options().layout.colorSpace,
+ })
+ );
ctx.save();
const tickMarks = this._priceScale.marks();
@@ -345,11 +349,13 @@ export class PriceAxisWidget implements IDestroyable {
if (this._size === null) {
return;
}
-
+ const canvasOptions: CanvasRenderingContext2DSettings = {
+ colorSpace: this._pane.chart().options().layout.colorSpace,
+ };
if (type !== InvalidationLevel.Cursor) {
this._alignLabels();
this._canvasBinding.applySuggestedBitmapSize();
- const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding);
+ const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding, canvasOptions);
if (target !== null) {
target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) => {
this._drawBackground(scope);
@@ -363,7 +369,7 @@ export class PriceAxisWidget implements IDestroyable {
}
this._topCanvasBinding.applySuggestedBitmapSize();
- const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding);
+ const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding, canvasOptions);
if (topTarget !== null) {
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }: BitmapCoordinatesRenderingScope) => {
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
diff --git a/src/gui/time-axis-widget.ts b/src/gui/time-axis-widget.ts
index e0a4494bc2..1153716c8d 100644
--- a/src/gui/time-axis-widget.ts
+++ b/src/gui/time-axis-widget.ts
@@ -298,10 +298,12 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestr
if (type === InvalidationLevel.None) {
return;
}
-
+ const canvasOptions: CanvasRenderingContext2DSettings = {
+ colorSpace: this._options.colorSpace,
+ };
if (type !== InvalidationLevel.Cursor) {
this._canvasBinding.applySuggestedBitmapSize();
- const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding);
+ const target = tryCreateCanvasRenderingTarget2D(this._canvasBinding, canvasOptions);
if (target !== null) {
target.useBitmapCoordinateSpace((scope: BitmapCoordinatesRenderingScope) => {
this._drawBackground(scope);
@@ -324,7 +326,7 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestr
}
this._topCanvasBinding.applySuggestedBitmapSize();
- const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding);
+ const topTarget = tryCreateCanvasRenderingTarget2D(this._topCanvasBinding, canvasOptions);
if (topTarget !== null) {
topTarget.useBitmapCoordinateSpace(({ context: ctx, bitmapSize }: BitmapCoordinatesRenderingScope) => {
ctx.clearRect(0, 0, bitmapSize.width, bitmapSize.height);
diff --git a/src/helpers/color.ts b/src/helpers/color.ts
deleted file mode 100644
index 85620de092..0000000000
--- a/src/helpers/color.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import { Nominal } from './nominal';
-
-/**
- * Red component of the RGB color value
- * The valid values are integers in range [0, 255]
- */
-type RedComponent = Nominal;
-
-/**
- * Green component of the RGB color value
- * The valid values are integers in range [0, 255]
- */
-type GreenComponent = Nominal;
-
-/**
- * Blue component of the RGB color value
- * The valid values are integers in range [0, 255]
- */
-type BlueComponent = Nominal;
-
-/**
- * Alpha component of the RGBA color value
- * The valid values are integers in range [0, 1]
- */
-type AlphaComponent = Nominal;
-
-type Rgba = [RedComponent, GreenComponent, BlueComponent, AlphaComponent];
-
-/**
- * Note this object should be explicitly marked as public so that dts-bundle-generator does not mangle the property names.
- *
- * @public
- * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
- */
-const namedColorRgbHexStrings: Record = {
- // The order of properties in this Record is not important for the internal logic.
- // It's just GZIPped better when props follows this order.
- // Please add new colors to the end of the record.
-
- khaki: '#f0e68c',
- azure: '#f0ffff',
- aliceblue: '#f0f8ff',
- ghostwhite: '#f8f8ff',
- gold: '#ffd700',
- goldenrod: '#daa520',
- gainsboro: '#dcdcdc',
- gray: '#808080',
- green: '#008000',
- honeydew: '#f0fff0',
- floralwhite: '#fffaf0',
- lightblue: '#add8e6',
- lightcoral: '#f08080',
- lemonchiffon: '#fffacd',
- hotpink: '#ff69b4',
- lightyellow: '#ffffe0',
- greenyellow: '#adff2f',
- lightgoldenrodyellow: '#fafad2',
- limegreen: '#32cd32',
- linen: '#faf0e6',
- lightcyan: '#e0ffff',
- magenta: '#f0f',
- maroon: '#800000',
- olive: '#808000',
- orange: '#ffa500',
- oldlace: '#fdf5e6',
- mediumblue: '#0000cd',
- transparent: '#0000',
- lime: '#0f0',
- lightpink: '#ffb6c1',
- mistyrose: '#ffe4e1',
- moccasin: '#ffe4b5',
- midnightblue: '#191970',
- orchid: '#da70d6',
- mediumorchid: '#ba55d3',
- mediumturquoise: '#48d1cc',
- orangered: '#ff4500',
- royalblue: '#4169e1',
- powderblue: '#b0e0e6',
- red: '#f00',
- coral: '#ff7f50',
- turquoise: '#40e0d0',
- white: '#fff',
- whitesmoke: '#f5f5f5',
- wheat: '#f5deb3',
- teal: '#008080',
- steelblue: '#4682b4',
- bisque: '#ffe4c4',
- aquamarine: '#7fffd4',
- aqua: '#0ff',
- sienna: '#a0522d',
- silver: '#c0c0c0',
- springgreen: '#00ff7f',
- antiquewhite: '#faebd7',
- burlywood: '#deb887',
- brown: '#a52a2a',
- beige: '#f5f5dc',
- chocolate: '#d2691e',
- chartreuse: '#7fff00',
- cornflowerblue: '#6495ed',
- cornsilk: '#fff8dc',
- crimson: '#dc143c',
- cadetblue: '#5f9ea0',
- tomato: '#ff6347',
- fuchsia: '#f0f',
- blue: '#00f',
- salmon: '#fa8072',
- blanchedalmond: '#ffebcd',
- slateblue: '#6a5acd',
- slategray: '#708090',
- thistle: '#d8bfd8',
- tan: '#d2b48c',
- cyan: '#0ff',
- darkblue: '#00008b',
- darkcyan: '#008b8b',
- darkgoldenrod: '#b8860b',
- darkgray: '#a9a9a9',
- blueviolet: '#8a2be2',
- black: '#000',
- darkmagenta: '#8b008b',
- darkslateblue: '#483d8b',
- darkkhaki: '#bdb76b',
- darkorchid: '#9932cc',
- darkorange: '#ff8c00',
- darkgreen: '#006400',
- darkred: '#8b0000',
- dodgerblue: '#1e90ff',
- darkslategray: '#2f4f4f',
- dimgray: '#696969',
- deepskyblue: '#00bfff',
- firebrick: '#b22222',
- forestgreen: '#228b22',
- indigo: '#4b0082',
- ivory: '#fffff0',
- lavenderblush: '#fff0f5',
- feldspar: '#d19275',
- indianred: '#cd5c5c',
- lightgreen: '#90ee90',
- lightgrey: '#d3d3d3',
- lightskyblue: '#87cefa',
- lightslategray: '#789',
- lightslateblue: '#8470ff',
- snow: '#fffafa',
- lightseagreen: '#20b2aa',
- lightsalmon: '#ffa07a',
- darksalmon: '#e9967a',
- darkviolet: '#9400d3',
- mediumpurple: '#9370d8',
- mediumaquamarine: '#66cdaa',
- skyblue: '#87ceeb',
- lavender: '#e6e6fa',
- lightsteelblue: '#b0c4de',
- mediumvioletred: '#c71585',
- mintcream: '#f5fffa',
- navajowhite: '#ffdead',
- navy: '#000080',
- olivedrab: '#6b8e23',
- palevioletred: '#d87093',
- violetred: '#d02090',
- yellow: '#ff0',
- yellowgreen: '#9acd32',
- lawngreen: '#7cfc00',
- pink: '#ffc0cb',
- paleturquoise: '#afeeee',
- palegoldenrod: '#eee8aa',
- darkolivegreen: '#556b2f',
- darkseagreen: '#8fbc8f',
- darkturquoise: '#00ced1',
- peachpuff: '#ffdab9',
- deeppink: '#ff1493',
- violet: '#ee82ee',
- palegreen: '#98fb98',
- mediumseagreen: '#3cb371',
- peru: '#cd853f',
- saddlebrown: '#8b4513',
- sandybrown: '#f4a460',
- rosybrown: '#bc8f8f',
- purple: '#800080',
- seagreen: '#2e8b57',
- seashell: '#fff5ee',
- papayawhip: '#ffefd5',
- mediumslateblue: '#7b68ee',
- plum: '#dda0dd',
- mediumspringgreen: '#00fa9a',
-};
-
-function normalizeRgbComponent(component: number): T {
- if (component < 0) {
- return 0 as T;
- }
- if (component > 255) {
- return 255 as T;
- }
- // NaN values are treated as 0
- return (Math.round(component) || 0) as T;
-}
-
-function normalizeAlphaComponent(component: AlphaComponent): AlphaComponent {
- if (component <= 0 || component > 1) {
- return Math.min(Math.max(component, 0), 1) as AlphaComponent;
- }
- // limit the precision of all numbers to at most 4 digits in fractional part
- return Math.round(component * 10000) / 10000 as AlphaComponent;
-}
-
-/**
- * @example
- * #fb0
- * @example
- * #f0f
- * @example
- * #f0fa
- */
-const shortHexRe = /^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/i;
-
-/**
- * @example
- * #00ff00
- * @example
- * #336699
- * @example
- * #336699FA
- */
-const hexRe = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i;
-
-/**
- * @example
- * rgb(123, 234, 45)
- * @example
- * rgb(255,234,245)
- */
-const rgbRe = /^rgb\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*\)$/;
-
-/**
- * @example
- * rgba(123, 234, 45, 1)
- * @example
- * rgba(255,234,245,0.1)
- */
-
-const rgbaRe = /^rgba\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d*\.?\d+)\s*\)$/;
-function colorStringToRgba(colorString: string): Rgba {
- colorString = colorString.toLowerCase();
-
- // eslint-disable-next-line no-restricted-syntax
- if (colorString in namedColorRgbHexStrings) {
- colorString = namedColorRgbHexStrings[colorString];
- }
-
- {
- const matches = rgbaRe.exec(colorString) || rgbRe.exec(colorString);
- if (matches) {
- return [
- normalizeRgbComponent(parseInt(matches[1], 10)),
- normalizeRgbComponent(parseInt(matches[2], 10)),
- normalizeRgbComponent(parseInt(matches[3], 10)),
- normalizeAlphaComponent((matches.length < 5 ? 1 : parseFloat(matches[4])) as AlphaComponent),
- ];
- }
- }
-
- {
- const matches = hexRe.exec(colorString);
- if (matches) {
- return [
- normalizeRgbComponent(parseInt(matches[1], 16)),
- normalizeRgbComponent(parseInt(matches[2], 16)),
- normalizeRgbComponent(parseInt(matches[3], 16)),
- 1 as AlphaComponent,
- ];
- }
- }
-
- {
- const matches = shortHexRe.exec(colorString);
- if (matches) {
- return [
- normalizeRgbComponent(parseInt(matches[1], 16) * 0x11),
- normalizeRgbComponent(parseInt(matches[2], 16) * 0x11),
- normalizeRgbComponent(parseInt(matches[3], 16) * 0x11),
- 1 as AlphaComponent,
- ];
- }
- }
-
- throw new Error(`Cannot parse color: ${colorString}`);
-}
-
-function rgbaToGrayscale(rgbValue: Rgba): number {
- // Originally, the NTSC RGB to YUV formula
- // perfected by @eugene-korobko's black magic
- const redComponentGrayscaleWeight = 0.199;
- const greenComponentGrayscaleWeight = 0.687;
- const blueComponentGrayscaleWeight = 0.114;
-
- return (
- redComponentGrayscaleWeight * rgbValue[0] +
- greenComponentGrayscaleWeight * rgbValue[1] +
- blueComponentGrayscaleWeight * rgbValue[2]
- );
-}
-
-export function applyAlpha(color: string, alpha: number): string {
- // special case optimization
- if (color === 'transparent') {
- return color;
- }
-
- const originRgba = colorStringToRgba(color);
- const originAlpha = originRgba[3];
- return `rgba(${originRgba[0]}, ${originRgba[1]}, ${originRgba[2]}, ${alpha * originAlpha})`;
-}
-
-export interface ContrastColors {
- foreground: string;
- background: string;
-}
-
-export function generateContrastColors(backgroundColor: string): ContrastColors {
- const rgb = colorStringToRgba(backgroundColor);
-
- return {
- background: `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`,
- foreground: rgbaToGrayscale(rgb) > 160 ? 'black' : 'white',
- };
-}
-
-export function colorStringToGrayscale(backgroundColor: string): number {
- return rgbaToGrayscale(colorStringToRgba(backgroundColor));
-}
-
-export function gradientColorAtPercent(topColor: string, bottomColor: string, percent: number): string {
- const [topR, topG, topB, topA] = colorStringToRgba(topColor);
- const [bottomR, bottomG, bottomB, bottomA] = colorStringToRgba(bottomColor);
-
- const resultRgba: Rgba = [
- normalizeRgbComponent(topR + percent * (bottomR - topR) as RedComponent),
- normalizeRgbComponent(topG + percent * (bottomG - topG) as GreenComponent),
- normalizeRgbComponent(topB + percent * (bottomB - topB) as BlueComponent),
- normalizeAlphaComponent(topA + percent * (bottomA - topA) as AlphaComponent),
- ];
-
- return `rgba(${resultRgba[0]}, ${resultRgba[1]}, ${resultRgba[2]}, ${resultRgba[3]})`;
-}
diff --git a/src/model/chart-model.ts b/src/model/chart-model.ts
index 8a7d11fc78..cc50e5c214 100644
--- a/src/model/chart-model.ts
+++ b/src/model/chart-model.ts
@@ -1,7 +1,6 @@
///
import { assert, ensureNotNull } from '../helpers/assertions';
-import { gradientColorAtPercent } from '../helpers/color';
import { Delegate } from '../helpers/delegate';
import { IDestroyable } from '../helpers/idestroyable';
import { ISubscription } from '../helpers/isubscription';
@@ -10,6 +9,7 @@ import { DeepPartial, merge } from '../helpers/strict-type-checks';
import { PriceAxisViewRendererOptions } from '../renderers/iprice-axis-view-renderer';
import { PriceAxisRendererOptionsProvider } from '../renderers/price-axis-renderer-options-provider';
+import { ColorParser } from './colors';
import { Coordinate } from './coordinate';
import { Crosshair, CrosshairOptions } from './crosshair';
import { DefaultPriceScaleId, isDefaultPriceScale } from './default-price-scale';
@@ -433,6 +433,8 @@ export interface IChartModelBase {
swapPanes(first: number, second: number): void;
removePane(index: number): void;
changePanesHeight(paneIndex: number, height: number): void;
+
+ colorParser(): ColorParser;
}
function isPanePrimitive(source: IPriceDataSource | IPrimitiveHitTestSource): source is IPrimitiveHitTestSource | Pane {
@@ -463,10 +465,13 @@ export class ChartModel implements IDestroyable, IChartModelBase
private readonly _horzScaleBehavior: IHorzScaleBehavior;
+ private _colorParser: ColorParser;
+
public constructor(invalidateHandler: InvalidateHandler, options: ChartOptionsInternal, horzScaleBehavior: IHorzScaleBehavior) {
this._invalidateHandler = invalidateHandler;
this._options = options;
this._horzScaleBehavior = horzScaleBehavior;
+ this._colorParser = new ColorParser(this._options.layout.colorParsers);
this._rendererOptionsProvider = new PriceAxisRendererOptionsProvider(this);
@@ -1031,7 +1036,7 @@ export class ChartModel implements IDestroyable, IChartModelBase
}
}
- const result = gradientColorAtPercent(topColor, bottomColor, percent / 100);
+ const result = this._colorParser.gradientColorAtPercent(topColor, bottomColor, percent / 100);
this._gradientColorsCache.colors.set(percent, result);
return result;
}
@@ -1040,6 +1045,10 @@ export class ChartModel implements IDestroyable, IChartModelBase
return this._panes.indexOf(pane);
}
+ public colorParser(): ColorParser {
+ return this._colorParser;
+ }
+
private _getOrCreatePane(index: number): Pane {
assert(index >= 0, 'Index should be greater or equal to 0');
index = Math.min(this._panes.length, index);
diff --git a/src/model/colors.ts b/src/model/colors.ts
new file mode 100644
index 0000000000..315959994c
--- /dev/null
+++ b/src/model/colors.ts
@@ -0,0 +1,194 @@
+import { Nominal } from '../helpers/nominal';
+
+/**
+ * Red component of the RGB color value
+ * The valid values are integers in range [0, 255]
+ */
+type RedComponent = Nominal;
+
+/**
+ * Green component of the RGB color value
+ * The valid values are integers in range [0, 255]
+ */
+type GreenComponent = Nominal;
+
+/**
+ * Blue component of the RGB color value
+ * The valid values are integers in range [0, 255]
+ */
+type BlueComponent = Nominal;
+
+/**
+ * Alpha component of the RGBA color value
+ * The valid values are integers in range [0, 1]
+ */
+type AlphaComponent = Nominal;
+
+export type Rgba = [RedComponent, GreenComponent, BlueComponent, AlphaComponent];
+
+function normalizeRgbComponent<
+ T extends RedComponent | GreenComponent | BlueComponent
+>(component: number): T {
+ if (component < 0) {
+ return 0 as T;
+ }
+ if (component > 255) {
+ return 255 as T;
+ }
+ // NaN values are treated as 0
+ return (Math.round(component) || 0) as T;
+}
+
+function normalizeAlphaComponent(component: AlphaComponent): AlphaComponent {
+ if (component <= 0 || component > 1) {
+ return Math.min(Math.max(component, 0), 1) as AlphaComponent;
+ }
+ // limit the precision of all numbers to at most 4 digits in fractional part
+ return (Math.round(component * 10000) / 10000) as AlphaComponent;
+}
+
+function rgbaToGrayscale(rgbValue: Rgba): number {
+ // Originally, the NTSC RGB to YUV formula
+ // perfected by @eugene-korobko's black magic
+ const redComponentGrayscaleWeight = 0.199;
+ const greenComponentGrayscaleWeight = 0.687;
+ const blueComponentGrayscaleWeight = 0.114;
+
+ return (
+ redComponentGrayscaleWeight * rgbValue[0] +
+ greenComponentGrayscaleWeight * rgbValue[1] +
+ blueComponentGrayscaleWeight * rgbValue[2]
+ );
+}
+
+/**
+ * For colors which fall within the sRGB space, the browser can
+ * be used to convert the color string into a rgb /rgba string.
+ *
+ * For other colors, it will be returned as specified (i.e. for
+ * newer formats like display-p3)
+ *
+ * See: https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
+ */
+function getRgbStringViaBrowser(color: string): string {
+ const element = document.createElement('div');
+ element.style.display = 'none';
+ // We append to the body as it is the most reliable way to get a color reading
+ // appending to the chart container or similar element can result in the following
+ // getComputedStyle returning empty strings on each check.
+ document.body.appendChild(element);
+ element.style.color = color;
+ const computed = window.getComputedStyle(element).color;
+ document.body.removeChild(element);
+ return computed;
+}
+
+export interface ContrastColors {
+ foreground: string;
+ background: string;
+}
+
+export type CustomColorParser = (color: string) => Rgba | null;
+
+export class ColorParser {
+ private _rgbaCache: Map = new Map();
+ private _customParsers: CustomColorParser[];
+
+ public constructor(customParsers: CustomColorParser[], initialCache?: Map) {
+ this._customParsers = customParsers;
+ if (initialCache) {
+ this._rgbaCache = initialCache;
+ }
+ }
+
+ /**
+ * We fallback to RGBA here since supporting alpha transformations
+ * on wider color gamuts would currently be a lot of extra code
+ * for very little benefit due to actual usage.
+ */
+ public applyAlpha(color: string, alpha: number): string {
+ // special case optimization
+ if (color === 'transparent') {
+ return color;
+ }
+
+ const originRgba = this._parseColor(color);
+ const originAlpha = originRgba[3];
+ return `rgba(${originRgba[0]}, ${originRgba[1]}, ${originRgba[2]}, ${
+ alpha * originAlpha
+ })`;
+ }
+
+ public generateContrastColors(background: string): ContrastColors {
+ const rgba = this._parseColor(background);
+ return {
+ background: `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`, // no alpha
+ foreground: rgbaToGrayscale(rgba) > 160 ? 'black' : 'white',
+ };
+ }
+
+ public colorStringToGrayscale(background: string): number {
+ return rgbaToGrayscale(this._parseColor(background));
+ }
+
+ public gradientColorAtPercent(
+ topColor: string,
+ bottomColor: string,
+ percent: number
+ ): string {
+ const [topR, topG, topB, topA] = this._parseColor(topColor);
+ const [bottomR, bottomG, bottomB, bottomA] = this._parseColor(bottomColor);
+ const resultRgba: Rgba = [
+ normalizeRgbComponent(
+ (topR + percent * (bottomR - topR)) as RedComponent
+ ),
+ normalizeRgbComponent(
+ (topG + percent * (bottomG - topG)) as GreenComponent
+ ),
+ normalizeRgbComponent(
+ (topB + percent * (bottomB - topB)) as BlueComponent
+ ),
+ normalizeAlphaComponent(
+ (topA + percent * (bottomA - topA)) as AlphaComponent
+ ),
+ ];
+ return `rgba(${resultRgba[0]}, ${resultRgba[1]}, ${resultRgba[2]}, ${resultRgba[3]})`;
+ }
+
+ private _parseColor(color: string): Rgba {
+ const cached = this._rgbaCache.get(color);
+ if (cached) {
+ return cached;
+ }
+
+ const computed = getRgbStringViaBrowser(color);
+
+ const match = computed.match(
+ /^rgba?\s*\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*\.?\d+))?\)$/
+ );
+
+ if (!match) {
+ if (this._customParsers.length) {
+ for (const parser of this._customParsers) {
+ const result = parser(color);
+ if (result) {
+ this._rgbaCache.set(color, result);
+ return result;
+ }
+ }
+ }
+ throw new Error(`Failed to parse color: ${color}`);
+ }
+
+ const rgba: Rgba = [
+ parseInt(match[1], 10) as RedComponent,
+ parseInt(match[2], 10) as GreenComponent,
+ parseInt(match[3], 10) as BlueComponent,
+ (match[4] ? parseFloat(match[4]) : 1) as AlphaComponent,
+ ];
+
+ this._rgbaCache.set(color, rgba);
+
+ return rgba;
+ }
+}
diff --git a/src/model/layout-options.ts b/src/model/layout-options.ts
index c77a5eec5f..eb376932e3 100644
--- a/src/model/layout-options.ts
+++ b/src/model/layout-options.ts
@@ -1,3 +1,5 @@
+import { CustomColorParser } from './colors';
+
/**
* Represents a type of color.
*/
@@ -48,6 +50,8 @@ export interface VerticalGradientColor {
*/
export type Background = SolidColor | VerticalGradientColor;
+export type ColorSpace = 'display-p3' | 'srgb';
+
/**
* Represents panes customizations.
*/
@@ -124,4 +128,47 @@ export interface LayoutOptions {
* @defaultValue true
*/
attributionLogo: boolean;
+
+ /**
+ * Specifies the color space of the rendering context for the internal
+ * canvas elements.
+ *
+ * Note: this option should only be specified during the chart creation
+ * and not changed at a later stage by using `applyOptions`.
+ *
+ * @defaultValue `srgb`
+ *
+ * See [HTMLCanvasElement: getContext() method - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext#colorspace) for more info
+ */
+ colorSpace: ColorSpace;
+
+ /**
+ * Array of custom color parser functions to handle color formats outside of standard sRGB values.
+ * Each parser function takes a string input and should return either:
+ * - An {@link Rgba} array [r,g,b,a] for valid colors (with values 0-255 for rgb and 0-1 for a)
+ * - null if the parser cannot handle that color string, allowing the next parser to attempt it
+ *
+ * Parsers are tried in order until one returns a non-null result. This allows chaining multiple
+ * parsers to handle different color space formats.
+ *
+ * Note: this option should only be specified during the chart creation
+ * and not changed at a later stage by using `applyOptions`.
+ *
+ * The library already supports these color formats by default:
+ * - Hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA)
+ * - RGB/RGBA functions (rgb(), rgba())
+ * - HSL/HSLA functions (hsl(), hsla())
+ * - HWB function (hwb())
+ * - Named colors (red, blue, etc.)
+ * - 'transparent' keyword
+ *
+ * Custom parsers are only needed for other color spaces like:
+ * - Display P3: color(display-p3 r g b)
+ * - CIE Lab: lab(l a b)
+ * - LCH: lch(l c h)
+ * - Oklab: oklab(l a b)
+ * - Oklch: oklch(l c h)
+ * - ...
+ */
+ colorParsers: CustomColorParser[];
}
diff --git a/src/model/pane.ts b/src/model/pane.ts
index ad4e7fb8f6..466ca43766 100644
--- a/src/model/pane.ts
+++ b/src/model/pane.ts
@@ -471,7 +471,8 @@ export class Pane implements IDestroyable, IPrimitiveHitTestSource {
id,
actualOptions,
this._model.options()['layout'],
- this._model.options().localization
+ this._model.options().localization,
+ this._model.colorParser()
);
priceScale.setHeight(this.height());
return priceScale;
diff --git a/src/model/price-scale.ts b/src/model/price-scale.ts
index a65380d1c0..0439b8aa58 100644
--- a/src/model/price-scale.ts
+++ b/src/model/price-scale.ts
@@ -8,6 +8,7 @@ import { ISubscription } from '../helpers/isubscription';
import { DeepPartial, merge } from '../helpers/strict-type-checks';
import { BarCoordinates, BarPrice, BarPrices } from './bar';
+import { ColorParser } from './colors';
import { Coordinate } from './coordinate';
import { FirstValue, IPriceDataSource } from './iprice-data-source';
import { LayoutOptions } from './layout-options';
@@ -240,12 +241,14 @@ export class PriceScale {
private _formatter: IPriceFormatter = defaultPriceFormatter;
private _logFormula: LogFormula = logFormulaForPriceRange(null);
+ private _colorParser: ColorParser;
- public constructor(id: string, options: PriceScaleOptions, layoutOptions: LayoutOptions, localizationOptions: LocalizationOptionsBase) {
+ public constructor(id: string, options: PriceScaleOptions, layoutOptions: LayoutOptions, localizationOptions: LocalizationOptionsBase, colorParser: ColorParser) {
this._id = id;
this._options = options;
this._layoutOptions = layoutOptions;
this._localizationOptions = localizationOptions;
+ this._colorParser = colorParser;
this._markBuilder = new PriceTickMarkBuilder(this, 100, this._coordinateToLogical.bind(this), this._logicalToCoordinate.bind(this));
}
@@ -835,6 +838,10 @@ export class PriceScale {
this._cachedOrderedSources = null;
}
+ public colorParser(): ColorParser {
+ return this._colorParser;
+ }
+
/**
* @returns The {@link IPriceDataSource} that will be used as the "formatter source" (take minMove for formatter).
*/
diff --git a/src/views/pane/series-last-price-animation-pane-view.ts b/src/views/pane/series-last-price-animation-pane-view.ts
index 40ac9479cf..9173e48b5b 100644
--- a/src/views/pane/series-last-price-animation-pane-view.ts
+++ b/src/views/pane/series-last-price-animation-pane-view.ts
@@ -1,5 +1,4 @@
import { assert } from '../../helpers/assertions';
-import { applyAlpha } from '../../helpers/color';
import { ISeries } from '../../model/iseries';
import { Point } from '../../model/point';
@@ -90,37 +89,10 @@ interface AnimationData {
strokeColor: string;
}
-function color(seriesLineColor: string, stage: number, startAlpha: number, endAlpha: number): string {
- const alpha = startAlpha + (endAlpha - startAlpha) * stage;
- return applyAlpha(seriesLineColor, alpha);
-}
-
function radius(stage: number, startRadius: number, endRadius: number): number {
return startRadius + (endRadius - startRadius) * stage;
}
-function animationData(durationSinceStart: number, lineColor: string): AnimationData {
- const globalStage = (durationSinceStart % Constants.AnimationPeriod) / Constants.AnimationPeriod;
-
- let currentStageData: AnimationStageData | undefined;
-
- for (const stageData of animationStagesData) {
- if (globalStage >= stageData.start && globalStage <= stageData.end) {
- currentStageData = stageData;
- break;
- }
- }
-
- assert(currentStageData !== undefined, 'Last price animation internal logic error');
-
- const subStage = (globalStage - currentStageData.start) / (currentStageData.end - currentStageData.start);
- return {
- fillColor: color(lineColor, subStage, currentStageData.startFillAlpha, currentStageData.endFillAlpha),
- strokeColor: color(lineColor, subStage, currentStageData.startStrokeAlpha, currentStageData.endStrokeAlpha),
- radius: radius(subStage, currentStageData.startRadius, currentStageData.endRadius),
- };
-}
-
export class SeriesLastPriceAnimationPaneView implements IUpdatablePaneView {
private readonly _series: ISeries<'Area'> | ISeries<'Line'> | ISeries<'Baseline'>;
private readonly _renderer: SeriesLastPriceAnimationRenderer = new SeriesLastPriceAnimationRenderer();
@@ -215,7 +187,7 @@ export class SeriesLastPriceAnimationPaneView implements IUpdatablePaneView {
const seriesLineColor = lastValue.color;
const seriesLineWidth = this._series.options().lineWidth;
- const data = animationData(this._duration(), seriesLineColor);
+ const data = this._animationData(this._duration(), seriesLineColor);
this._renderer.setData({
seriesLineColor,
@@ -230,7 +202,7 @@ export class SeriesLastPriceAnimationPaneView implements IUpdatablePaneView {
private _updateRendererDataStage(): void {
const rendererData = this._renderer.data();
if (rendererData !== null) {
- const data = animationData(this._duration(), rendererData.seriesLineColor);
+ const data = this._animationData(this._duration(), rendererData.seriesLineColor);
rendererData.fillColor = data.fillColor;
rendererData.strokeColor = data.strokeColor;
rendererData.radius = data.radius;
@@ -240,4 +212,34 @@ export class SeriesLastPriceAnimationPaneView implements IUpdatablePaneView {
private _duration(): number {
return this.animationActive() ? performance.now() - this._startTime : Constants.AnimationPeriod - 1;
}
+
+ private _color(seriesLineColor: string, stage: number, startAlpha: number, endAlpha: number): string {
+ const alpha = startAlpha + (endAlpha - startAlpha) * stage;
+ return this._series
+ .model()
+ .colorParser()
+ .applyAlpha(seriesLineColor, alpha);
+ }
+
+ private _animationData(durationSinceStart: number, lineColor: string): AnimationData {
+ const globalStage = (durationSinceStart % Constants.AnimationPeriod) / Constants.AnimationPeriod;
+
+ let currentStageData: AnimationStageData | undefined;
+
+ for (const stageData of animationStagesData) {
+ if (globalStage >= stageData.start && globalStage <= stageData.end) {
+ currentStageData = stageData;
+ break;
+ }
+ }
+
+ assert(currentStageData !== undefined, 'Last price animation internal logic error');
+
+ const subStage = (globalStage - currentStageData.start) / (currentStageData.end - currentStageData.start);
+ return {
+ fillColor: this._color(lineColor, subStage, currentStageData.startFillAlpha, currentStageData.endFillAlpha),
+ strokeColor: this._color(lineColor, subStage, currentStageData.startStrokeAlpha, currentStageData.endStrokeAlpha),
+ radius: radius(subStage, currentStageData.startRadius, currentStageData.endRadius),
+ };
+ }
}
diff --git a/src/views/price-axis/crosshair-price-axis-view.ts b/src/views/price-axis/crosshair-price-axis-view.ts
index 44fc6321be..f5adbe335a 100644
--- a/src/views/price-axis/crosshair-price-axis-view.ts
+++ b/src/views/price-axis/crosshair-price-axis-view.ts
@@ -1,5 +1,3 @@
-import { generateContrastColors } from '../../helpers/color';
-
import { Crosshair, CrosshairMode, CrosshairPriceAndCoordinate } from '../../model/crosshair';
import { PriceScale } from '../../model/price-scale';
import { PriceAxisViewRendererCommonData, PriceAxisViewRendererData } from '../../renderers/iprice-axis-view-renderer';
@@ -40,7 +38,7 @@ export class CrosshairPriceAxisView extends PriceAxisView {
return;
}
- const colors = generateContrastColors(options.labelBackgroundColor);
+ const colors = this._priceScale.colorParser().generateContrastColors(options.labelBackgroundColor);
commonRendererData.background = colors.background;
axisRendererData.color = colors.foreground;
diff --git a/src/views/price-axis/custom-price-line-price-axis-view.ts b/src/views/price-axis/custom-price-line-price-axis-view.ts
index ee23e22491..63ef61423c 100644
--- a/src/views/price-axis/custom-price-line-price-axis-view.ts
+++ b/src/views/price-axis/custom-price-line-price-axis-view.ts
@@ -1,5 +1,3 @@
-import { generateContrastColors } from '../../helpers/color';
-
import { CustomPriceLine } from '../../model/custom-price-line';
import { ISeries } from '../../model/iseries';
import { SeriesType } from '../../model/series-options';
@@ -53,7 +51,10 @@ export class CustomPriceLinePriceAxisView extends PriceAxisView {
axisRendererData.text = this._formatPrice(options.price);
axisRendererData.visible = true;
- const colors = generateContrastColors(options.axisLabelColor || options.color);
+ const colors = this._series
+ .model()
+ .colorParser()
+ .generateContrastColors(options.axisLabelColor || options.color);
commonData.background = colors.background;
const textColor = options.axisLabelTextColor || colors.foreground;
diff --git a/src/views/price-axis/series-price-axis-view.ts b/src/views/price-axis/series-price-axis-view.ts
index 391c7dae19..4bf8ac4b75 100644
--- a/src/views/price-axis/series-price-axis-view.ts
+++ b/src/views/price-axis/series-price-axis-view.ts
@@ -1,5 +1,3 @@
-import { generateContrastColors } from '../../helpers/color';
-
import { ISeries, LastValueDataResultWithData } from '../../model/iseries';
import { PriceAxisLastValueMode, SeriesType } from '../../model/series-options';
import { PriceAxisViewRendererCommonData, PriceAxisViewRendererData } from '../../renderers/iprice-axis-view-renderer';
@@ -50,7 +48,10 @@ export class SeriesPriceAxisView extends PriceAxisView {
}
const lastValueColor = source.priceLineColor(lastValueData.color);
- const colors = generateContrastColors(lastValueColor);
+ const colors = this._source
+ .model()
+ .colorParser()
+ .generateContrastColors(lastValueColor);
commonRendererData.background = colors.background;
commonRendererData.coordinate = lastValueData.coordinate;
diff --git a/src/views/time-axis/crosshair-time-axis-view.ts b/src/views/time-axis/crosshair-time-axis-view.ts
index e38f85677d..0de8f5bf4e 100644
--- a/src/views/time-axis/crosshair-time-axis-view.ts
+++ b/src/views/time-axis/crosshair-time-axis-view.ts
@@ -1,5 +1,4 @@
import { ensureNotNull } from '../../helpers/assertions';
-import { generateContrastColors } from '../../helpers/color';
import { IChartModelBase } from '../../model/chart-model';
import { Crosshair, CrosshairMode, TimeAndCoordinateProvider } from '../../model/crosshair';
@@ -75,7 +74,7 @@ export class CrosshairTimeAxisView implements ITimeAxisView {
data.text = timeScale.formatDateTime(ensureNotNull(currentTime));
data.visible = true;
- const colors = generateContrastColors(options.labelBackgroundColor);
+ const colors = this._model.colorParser().generateContrastColors(options.labelBackgroundColor);
data.background = colors.background;
data.color = colors.foreground;
data.tickVisible = timeScale.options().ticksVisible;
diff --git a/tests/e2e/coverage/coverage-config.ts b/tests/e2e/coverage/coverage-config.ts
index 7c0a1b508c..cf78fcdbcd 100644
--- a/tests/e2e/coverage/coverage-config.ts
+++ b/tests/e2e/coverage/coverage-config.ts
@@ -1,2 +1,2 @@
-export const expectedCoverage = 91;
+export const expectedCoverage = 90;
export const threshold = 1;
diff --git a/tests/e2e/coverage/test-cases/chart/color-p3-support.js b/tests/e2e/coverage/test-cases/chart/color-p3-support.js
new file mode 100644
index 0000000000..238b73eda0
--- /dev/null
+++ b/tests/e2e/coverage/test-cases/chart/color-p3-support.js
@@ -0,0 +1,109 @@
+function displayP3ToRGBA(r, g, b, a = 1) {
+ const linearP3 = [r, g, b].map(val => {
+ const abs = Math.abs(val);
+ if (abs <= 0.04045) {
+ return val / 12.92;
+ }
+ return Math.sign(val) * Math.pow((abs + 0.055) / 1.055, 2.4);
+ });
+
+ const p3ToXYZ = [
+ [0.486570948648216, 0.265667687339784, 0.198217285240192],
+ [0.228974564336137, 0.691738605302214, 0.079286829361649],
+ [0.0, 0.045113381858903, 1.043944368900976],
+ ];
+
+ const xyz = p3ToXYZ.map(
+ row => row[0] * linearP3[0] + row[1] * linearP3[1] + row[2] * linearP3[2]
+ );
+
+ const xyzToSRGB = [
+ [3.2409699419045226, -1.5373831775700939, -0.4986107602930034],
+ [-0.9692436362808796, 1.8759675015077204, 0.0415550574071756],
+ [0.0556300796969936, -0.2039769588889765, 1.0569715142428784],
+ ];
+
+ const linearSRGB = xyzToSRGB.map(
+ row => row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]
+ );
+
+ const srgb = linearSRGB.map(val => {
+ const abs = Math.abs(val);
+ if (abs <= 0.0031308) {
+ return 12.92 * val;
+ }
+ return Math.sign(val) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
+ });
+
+ return [...srgb, a];
+}
+
+const displayP3Parser = color => {
+ const match =
+ /^color\(display-p3\s+([.\d]+)\s+([.\d]+)\s+([.\d]+)(?:\s*\/\s*([.\d]+))?\)$/.exec(
+ color
+ );
+ if (!match) {
+ return null;
+ }
+
+ const r = Number(match[1]);
+ const g = Number(match[2]);
+ const b = Number(match[3]);
+ const a = match[4] ? Number(match[4]) : 1;
+
+ if ([r, g, b, a].some(v => isNaN(v))) {
+ return null;
+ }
+
+ return displayP3ToRGBA(r, g, b, a);
+};
+
+function generateData() {
+ const res = [];
+ const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+ for (let i = 0; i < 500; ++i) {
+ res.push({
+ time: time.getTime() / 1000,
+ value: i,
+ });
+
+ time.setUTCDate(time.getUTCDate() + 1);
+ }
+ return res;
+}
+
+function interactionsToPerform() {
+ return [];
+}
+
+function beforeInteractions(container) {
+ const chart = (window.chart = LightweightCharts.createChart(container, {
+ layout: {
+ attributionLogo: false,
+ background: {
+ type: 'solid',
+ color: 'red',
+ },
+ colorSpace: 'display-p3',
+ colorParsers: [displayP3Parser],
+ },
+ }));
+
+ const mainSeries = chart.addAreaSeries({
+ priceFormat: {
+ minMove: 1,
+ precision: 0,
+ },
+ topColor: 'color(display-p3 1 0 0)',
+ bottomColor: 'color(display-p3 1 0 0)',
+ lineColor: 'color(display-p3 1 0 0)',
+ });
+
+ mainSeries.setData(generateData());
+ return Promise.resolve();
+}
+
+function afterInteractions() {
+ return Promise.resolve();
+}
diff --git a/tests/e2e/coverage/test-cases/chart/color-support.js b/tests/e2e/coverage/test-cases/chart/color-support.js
new file mode 100644
index 0000000000..3a60777390
--- /dev/null
+++ b/tests/e2e/coverage/test-cases/chart/color-support.js
@@ -0,0 +1,47 @@
+function generateData() {
+ const res = [];
+ const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+ for (let i = 0; i < 500; ++i) {
+ res.push({
+ time: time.getTime() / 1000,
+ value: i,
+ });
+
+ time.setUTCDate(time.getUTCDate() + 1);
+ }
+ return res;
+}
+
+function interactionsToPerform() {
+ return [];
+}
+
+function beforeInteractions(container) {
+ const chart = (window.chart = LightweightCharts.createChart(container, {
+ layout: {
+ attributionLogo: false,
+ background: {
+ type: 'solid',
+ color: 'rgba(50,100,150, 0.1)',
+ },
+ textColor: 'rgba(255,200,100)',
+ },
+ }));
+
+ const mainSeries = chart.addAreaSeries({
+ priceFormat: {
+ minMove: 1,
+ precision: 0,
+ },
+ topColor: 'hsl(180, 50%, 45%)',
+ bottomColor: 'hsla(160, 50%, 45%, 0%)',
+ lineColor: 'hwb(160 10% 20%)',
+ });
+
+ mainSeries.setData(generateData());
+ return Promise.resolve();
+}
+
+function afterInteractions() {
+ return Promise.resolve();
+}
diff --git a/tests/e2e/graphics/generate-golden-content.ts b/tests/e2e/graphics/generate-golden-content.ts
index 9d5aa492ff..6c9cc3fa71 100644
--- a/tests/e2e/graphics/generate-golden-content.ts
+++ b/tests/e2e/graphics/generate-golden-content.ts
@@ -24,12 +24,12 @@ rmRf(goldenOutputPath);
fs.mkdirSync(goldenOutputPath, { recursive: true });
for (const groupName of Object.keys(testCases)) {
- generateTestCases(testCases[groupName]);
+ generateTestCases(groupName, testCases[groupName]);
}
-function generateTestCases(groupTestCases: TestCase[]): void {
+function generateTestCases(groupName: string, groupTestCases: TestCase[]): void {
for (const testCase of groupTestCases) {
- const testCaseOutDir = path.join(goldenOutputPath, testCase.name);
+ const testCaseOutDir = path.join(goldenOutputPath, groupName, testCase.name);
rmRf(testCaseOutDir);
fs.mkdirSync(testCaseOutDir, { recursive: true });
path.join(testCaseOutDir, 'test-content.html');
diff --git a/tests/e2e/graphics/graphics-test-cases.ts b/tests/e2e/graphics/graphics-test-cases.ts
index 9b3090e184..3a4953b25c 100644
--- a/tests/e2e/graphics/graphics-test-cases.ts
+++ b/tests/e2e/graphics/graphics-test-cases.ts
@@ -68,10 +68,10 @@ void describe(`Graphics tests with devicePixelRatio=${devicePixelRatioStr} (${bu
const currentGroupOutDir = path.join(currentDprOutDir, groupName);
if (groupName.length === 0) {
- registerTestCases(testCases[groupName], screenshoter, currentGroupOutDir);
+ registerTestCases(testCases[groupName], screenshoter, currentGroupOutDir, groupName);
} else {
void describe(groupName, () => {
- registerTestCases(testCases[groupName], screenshoter, currentGroupOutDir);
+ registerTestCases(testCases[groupName], screenshoter, currentGroupOutDir, groupName);
});
}
}
@@ -82,7 +82,7 @@ void describe(`Graphics tests with devicePixelRatio=${devicePixelRatioStr} (${bu
});
});
-function registerTestCases(testCases: TestCase[], screenshoter: Screenshoter, outDir: string): void {
+function registerTestCases(testCases: TestCase[], screenshoter: Screenshoter, outDir: string, groupName: string): void {
const attempts: Record = {};
testCases.forEach((testCase: TestCase) => {
attempts[testCase.name] = 0;
@@ -108,7 +108,7 @@ function registerTestCases(testCases: TestCase[], screenshoter: Screenshoter, ou
if (goldenContentDir) {
try {
const content = fs.readFileSync(
- path.join(goldenContentDir, testCase.name, 'test-content.html'),
+ path.join(goldenContentDir, groupName, testCase.name, 'test-content.html'),
{ encoding: 'utf-8' }
);
return content.replace('PATH_TO_STANDALONE_MODULE', goldenStandalonePath);
diff --git a/tests/e2e/graphics/test-cases/color-p3-support.js b/tests/e2e/graphics/test-cases/color-p3-support.js
new file mode 100644
index 0000000000..951df7369f
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/color-p3-support.js
@@ -0,0 +1,132 @@
+/**
+ * TEST NOTE: PNG Screenshots don't support a wide color gamut
+ * therefore at the moment this test won't visually show a difference
+ * between the background and the series plot.
+ *
+ * On a screen which supports wider gamuts then you will be able to
+ * spot a color difference.
+ */
+
+/**
+ * Converts a Display P3 color to RGBA (sRGB)
+ * @param r - Red component in Display P3 [0-1]
+ * @param g - Green component in Display P3 [0-1]
+ * @param b - Blue component in Display P3 [0-1]
+ * @param a - Alpha component [0-1], defaults to 1
+ * @returns RGBA values in sRGB color space [0-1]
+ */
+function displayP3ToRGBA(r, g, b, a = 1) {
+ // First convert from gamma-corrected P3 to linear-light P3
+ const linearP3 = [r, g, b].map(val => {
+ // Transfer function is same as sRGB
+ const abs = Math.abs(val);
+ if (abs <= 0.04045) {
+ return val / 12.92;
+ }
+ return Math.sign(val) * Math.pow((abs + 0.055) / 1.055, 2.4);
+ });
+
+ // Convert from linear-light P3 to CIE XYZ D65
+ // Matrix from CSS Color Level 4 specification
+ const p3ToXYZ = [
+ [0.486570948648216, 0.265667687339784, 0.198217285240192],
+ [0.228974564336137, 0.691738605302214, 0.079286829361649],
+ [0.0, 0.045113381858903, 1.043944368900976],
+ ];
+
+ const xyz = p3ToXYZ.map(
+ row => row[0] * linearP3[0] + row[1] * linearP3[1] + row[2] * linearP3[2]
+ );
+
+ // Convert from CIE XYZ D65 to linear-light sRGB
+ // Matrix from CSS Color Level 4 specification
+ const xyzToSRGB = [
+ [3.2409699419045226, -1.5373831775700939, -0.4986107602930034],
+ [-0.9692436362808796, 1.8759675015077204, 0.0415550574071756],
+ [0.0556300796969936, -0.2039769588889765, 1.0569715142428784],
+ ];
+
+ const linearSRGB = xyzToSRGB.map(
+ row => row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]
+ );
+
+ // Convert from linear-light sRGB to gamma-corrected sRGB
+ const srgb = linearSRGB.map(val => {
+ const abs = Math.abs(val);
+ if (abs <= 0.0031308) {
+ return 12.92 * val;
+ }
+ return Math.sign(val) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
+ });
+
+ // Return RGBA values
+ return [...srgb, a];
+}
+
+/**
+ * Example parser for Display P3 colors in the format: color(display-p3 r g b[ / a])
+ */
+const displayP3Parser = color => {
+ // Match color(display-p3 r g b[ / a]) format
+ const match =
+ /^color\(display-p3\s+([.\d]+)\s+([.\d]+)\s+([.\d]+)(?:\s*\/\s*([.\d]+))?\)$/.exec(
+ color
+ );
+ if (!match) {
+ return null;
+ }
+
+ // Parse components
+ const r = Number(match[1]);
+ const g = Number(match[2]);
+ const b = Number(match[3]);
+ const a = match[4] ? Number(match[4]) : 1;
+
+ // Validate ranges
+ if ([r, g, b, a].some(v => isNaN(v))) {
+ return null;
+ }
+
+ // Convert to sRGB
+ return displayP3ToRGBA(r, g, b, a);
+};
+
+function generateData() {
+ const res = [];
+ const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+ for (let i = 0; i < 500; ++i) {
+ res.push({
+ time: time.getTime() / 1000,
+ value: i,
+ });
+
+ time.setUTCDate(time.getUTCDate() + 1);
+ }
+ return res;
+}
+
+function runTestCase(container) {
+ const chart = (window.chart = LightweightCharts.createChart(container, {
+ layout: {
+ attributionLogo: false,
+ background: {
+ type: 'solid',
+ color: 'red',
+ },
+ colorSpace: 'display-p3',
+ colorParsers: [displayP3Parser],
+ },
+ }));
+
+ const mainSeries = chart.addAreaSeries({
+ priceFormat: {
+ minMove: 1,
+ precision: 0,
+ },
+ topColor: 'color(display-p3 1 0 0)', /* Bright Red */
+ bottomColor: 'color(display-p3 1 0 0)', /* Bright Red */
+ lineColor: 'color(display-p3 1 0 0)', /* Bright Red */
+ });
+
+ mainSeries.setData(generateData());
+}
diff --git a/tests/e2e/graphics/test-cases/color-support.js b/tests/e2e/graphics/test-cases/color-support.js
new file mode 100644
index 0000000000..873721580e
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/color-support.js
@@ -0,0 +1,38 @@
+function generateData() {
+ const res = [];
+ const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+ for (let i = 0; i < 500; ++i) {
+ res.push({
+ time: time.getTime() / 1000,
+ value: i,
+ });
+
+ time.setUTCDate(time.getUTCDate() + 1);
+ }
+ return res;
+}
+
+function runTestCase(container) {
+ const chart = (window.chart = LightweightCharts.createChart(container, {
+ layout: {
+ attributionLogo: false,
+ background: {
+ type: 'solid',
+ color: 'rgba(50,100,150, 0.1)',
+ },
+ textColor: 'rgba(255,200,100)',
+ },
+ }));
+
+ const mainSeries = chart.addAreaSeries({
+ priceFormat: {
+ minMove: 1,
+ precision: 0,
+ },
+ topColor: 'hsl(180, 50%, 45%)',
+ bottomColor: 'hsla(160, 50%, 45%, 0%)',
+ lineColor: 'hwb(160 10% 20%)',
+ });
+
+ mainSeries.setData(generateData());
+}
diff --git a/tests/unittests/color.spec.ts b/tests/unittests/color.spec.ts
index ec8bac8a1e..bff906a91f 100644
--- a/tests/unittests/color.spec.ts
+++ b/tests/unittests/color.spec.ts
@@ -2,147 +2,87 @@
import { expect } from 'chai';
import { describe, it } from 'node:test';
-import { generateContrastColors, gradientColorAtPercent } from '../../src/helpers/color';
+import { ColorParser, Rgba } from '../../src/model/colors';
+
+type SimpleRgba = number[] & { length: 4 };
+
+function generateRgba(rgba: SimpleRgba): Rgba {
+ return rgba as Rgba;
+}
+
+/**
+ * The initialCache is used so that we can skip the requirement
+ * to try create a document element and use the browser for
+ * color parsing. The assumption is that the browser will do
+ * this correctly anyway.
+ */
+const initialCache: Map = new Map([
+ ['rgb(255, 255, 255)', generateRgba([255, 255, 255, 1])],
+ ['rgba(255, 255, 255, 0)', generateRgba([255, 255, 255, 0])],
+ ['rgba(255, 255, 255, 1)', generateRgba([255, 255, 255, 1])],
+ ['rgb(0, 0, 0)', generateRgba([0, 0, 0, 1])],
+ ['rgba(0, 0, 0, 0)', generateRgba([0, 0, 0, 0])],
+ ['rgba(0, 0, 0, 1)', generateRgba([0, 0, 0, 1])],
+
+ ['rgb(150, 150, 150)', generateRgba([150, 150, 150, 1])],
+ ['rgb(170, 170, 170)', generateRgba([170, 170, 170, 1])],
+ ['rgba(150, 150, 150, 0)', generateRgba([150, 150, 150, 0])],
+ ['rgba(170, 170, 170, 0)', generateRgba([170, 170, 170, 0])],
+ ['rgb(130, 140, 160)', generateRgba([130, 140, 160, 1])],
+ ['rgb(190, 180, 160)', generateRgba([190, 180, 160, 1])],
+
+ ['rgb(150, 150, 150)', generateRgba([150, 150, 150, 1])],
+ ['rgb(170, 170, 170)', generateRgba([170, 170, 170, 1])],
+
+ ['#ffffff', generateRgba([255, 255, 255, 1])],
+ ['#000000', generateRgba([0, 0, 0, 1])],
+ ['#fff', generateRgba([255, 255, 255, 1])],
+ ['#000', generateRgba([0, 0, 0, 1])],
+]);
+const colorParser = new ColorParser([], initialCache);
describe('generateContrastColors', () => {
it('should work', () => {
- expect(generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgba(255, 255, 255, 0)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgba(255, 255, 255, 1)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
-
- expect(generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgba(0, 0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgba(0, 0, 0, 1)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- });
-
- it('should correctly parse known named colors', () => {
- expect(generateContrastColors('aliceblue')).to.deep.equal({ foreground: 'black', background: 'rgb(240, 248, 255)' });
- expect(generateContrastColors('coral')).to.deep.equal({ foreground: 'white', background: 'rgb(255, 127, 80)' });
- expect(generateContrastColors('darkmagenta')).to.deep.equal({ foreground: 'white', background: 'rgb(139, 0, 139)' });
- expect(generateContrastColors('linen')).to.deep.equal({ foreground: 'black', background: 'rgb(250, 240, 230)' });
- expect(generateContrastColors('whitesmoke')).to.deep.equal({ foreground: 'black', background: 'rgb(245, 245, 245)' });
- expect(generateContrastColors('white')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('transparent')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- });
-
- it('should correctly parse short hex colors', () => {
- expect(generateContrastColors('#fff')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('#000')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('#fffa')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- });
-
- it('should correctly parse hex colors', () => {
- expect(generateContrastColors('#ffffff')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('#ff0110')).to.deep.equal({ foreground: 'white', background: 'rgb(255, 1, 16)' });
- expect(generateContrastColors('#f0f0f0aa')).to.deep.equal({ foreground: 'black', background: 'rgb(240, 240, 240)' });
- });
-
- it('should correctly parse RGB tuple string', () => {
- expect(generateContrastColors('rgb(10, 20, 30)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgb(0,0,0)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgb( 10 , 20 , 30 )')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
-
- // RGB tuple may contain values exceeding 255, that should be clamped to 255 after parsing
- expect(generateContrastColors('rgb(256, 256, 256)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgb(100500, 100500, 100500)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgb(0, 100500, 0)')).to.deep.equal({ foreground: 'black', background: 'rgb(0, 255, 0)' });
-
- // RGB tuple may contain negative values, that should be clamped to zero after parsing
- expect(generateContrastColors('rgb(-10, -20, -30)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgb(10, -20, 30)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 0, 30)' });
-
- // whitespace characters before 'rgb', after 'rgb' and after the closing parenthesis are prohibited
- expect(generateContrastColors.bind(null, ' rgb( 10, 20, 30 )')).to.throw();
- expect(generateContrastColors.bind(null, 'rgb ( 10, 20, 30 )')).to.throw();
- expect(generateContrastColors.bind(null, 'rgb( 10, 20, 30 ) ')).to.throw();
-
- // RGB tuple should not contain non-integer values
- expect(generateContrastColors.bind(null, 'rgb(10.0, 20, 30)')).to.throw();
- expect(generateContrastColors.bind(null, 'rgb(10, 20.0, 30)')).to.throw();
- expect(generateContrastColors.bind(null, 'rgb(10, 20, 30.0)')).to.throw();
-
- // not enough values in the tuple
- expect(generateContrastColors.bind(null, 'rgb(10, 20)')).to.throw();
-
- // too much values in the tuple
- expect(generateContrastColors.bind(null, 'rgb(10, 20, 30, 40)')).to.throw();
+ expect(colorParser.generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+ expect(colorParser.generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+ expect(colorParser.generateContrastColors('rgba(255, 255, 255, 0)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+ expect(colorParser.generateContrastColors('rgba(255, 255, 255, 1)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+
+ expect(colorParser.generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+ expect(colorParser.generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+ expect(colorParser.generateContrastColors('rgba(0, 0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+ expect(colorParser.generateContrastColors('rgba(0, 0, 0, 1)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
});
- it('should correctly parse RGBA tuple string', () => {
- expect(generateContrastColors('rgba(10, 20, 30, 0.40)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(0,0,0,1)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgba( 10 , 20 , 30 , 0.40 )')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, 0.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .000000000001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .10001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .10005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, .100000000005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
-
- // RGB components of a tuple may contain values exceeding 255, that should be clamped to 255 after parsing
- expect(generateContrastColors('rgba(256, 256, 256, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgba(100500, 100500, 100500, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
- expect(generateContrastColors('rgba(0, 100500, 0, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(0, 255, 0)' });
-
- // RGB components of a tuple may contain negative values, that should be clamped to zero after parsing
- expect(generateContrastColors('rgba(-10, -20, -30, 1.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
- expect(generateContrastColors('rgba(10, -20, 30, 1.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 0, 30)' });
-
- // Alpha component of a tuple may be a value exceeding 1.0, that should be clamped to 1.0 after parsing
- expect(generateContrastColors('rgba(10, 20, 30, 1.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, 1000.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, 1000000)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
-
- // Alpha component of a tuple may be a negative value, that should be clamped to zero after parsing
- expect(generateContrastColors('rgba(10, 20, 30, -0.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, -1.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, -1000.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, -1000000)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
- expect(generateContrastColors('rgba(10, 20, 30, -1000000.100000000005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
-
- // dangling dot is prohibited
- expect(generateContrastColors.bind(null, 'rgba(10, 20, 30, 1.)')).to.throw();
-
- // whitespace characters before 'rgba', after 'rgba' and after the closing parenthesis are prohibited
- expect(generateContrastColors.bind(null, ' rgba( 10, 20, 30 , 0.40 )')).to.throw();
- expect(generateContrastColors.bind(null, 'rgba ( 10, 20, 30 , 0.40 )')).to.throw();
- expect(generateContrastColors.bind(null, 'rgba( 10, 20, 30 , 0.40 ) ')).to.throw();
-
- // RGB components of tuple should not contain non-integer values
- expect(generateContrastColors.bind(null, 'rgba(10.0, 20, 30, 0)')).to.throw();
- expect(generateContrastColors.bind(null, 'rgba(10, 20.0, 30, 0)')).to.throw();
- expect(generateContrastColors.bind(null, 'rgba(10, 20, 30.0, 0)')).to.throw();
-
- // not enough values in the tuple
- expect(generateContrastColors.bind(null, 'rgba(10, 20, 30)')).to.throw();
-
- // too much values in the tuple
- expect(generateContrastColors.bind(null, 'rgba(10, 20, 30, 1.0, 1.0)')).to.throw();
+ it('correct contrast color', () => {
+ expect(colorParser.generateContrastColors('rgb(150, 150, 150)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(150, 150, 150)' });
+ expect(colorParser.generateContrastColors('rgb(170, 170, 170)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(170, 170, 170)' });
+ expect(colorParser.generateContrastColors('rgba(150, 150, 150, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(150, 150, 150)' });
+ expect(colorParser.generateContrastColors('rgba(170, 170, 170, 0)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(170, 170, 170)' });
+ expect(colorParser.generateContrastColors('rgb(130, 140, 160)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(130, 140, 160)' });
+ expect(colorParser.generateContrastColors('rgb(190, 180, 160)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(190, 180, 160)' });
});
});
describe('gradientColorAtPercent', () => {
it('0%', () => {
- expect(gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 0)).to.be.equal('rgba(255, 255, 255, 1)');
- expect(gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 0)).to.be.equal('rgba(255, 255, 255, 1)');
- expect(gradientColorAtPercent('#ffffff', '#000000', 0)).to.be.equal('rgba(255, 255, 255, 1)');
- expect(gradientColorAtPercent('#fff', '#000', 0)).to.be.equal('rgba(255, 255, 255, 1)');
+ expect(colorParser.gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 0)).to.be.equal('rgba(255, 255, 255, 1)');
+ expect(colorParser.gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 0)).to.be.equal('rgba(255, 255, 255, 1)');
+ expect(colorParser.gradientColorAtPercent('#ffffff', '#000000', 0)).to.be.equal('rgba(255, 255, 255, 1)');
+ expect(colorParser.gradientColorAtPercent('#fff', '#000', 0)).to.be.equal('rgba(255, 255, 255, 1)');
});
it('50%', () => {
- expect(gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
- expect(gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 0.5)).to.be.equal('rgba(128, 128, 128, 0.5)');
- expect(gradientColorAtPercent('#ffffff', '#000000', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
- expect(gradientColorAtPercent('#fff', '#000', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
+ expect(colorParser.gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
+ expect(colorParser.gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 0.5)).to.be.equal('rgba(128, 128, 128, 0.5)');
+ expect(colorParser.gradientColorAtPercent('#ffffff', '#000000', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
+ expect(colorParser.gradientColorAtPercent('#fff', '#000', 0.5)).to.be.equal('rgba(128, 128, 128, 1)');
});
it('100%', () => {
- expect(gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 1)).to.be.equal('rgba(0, 0, 0, 1)');
- expect(gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 1)).to.be.equal('rgba(0, 0, 0, 0)');
- expect(gradientColorAtPercent('#ffffff', '#000000', 1)).to.be.equal('rgba(0, 0, 0, 1)');
- expect(gradientColorAtPercent('#fff', '#000', 1)).to.be.equal('rgba(0, 0, 0, 1)');
+ expect(colorParser.gradientColorAtPercent('rgb(255, 255, 255)', 'rgb(0, 0, 0)', 1)).to.be.equal('rgba(0, 0, 0, 1)');
+ expect(colorParser.gradientColorAtPercent('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0)', 1)).to.be.equal('rgba(0, 0, 0, 0)');
+ expect(colorParser.gradientColorAtPercent('#ffffff', '#000000', 1)).to.be.equal('rgba(0, 0, 0, 1)');
+ expect(colorParser.gradientColorAtPercent('#fff', '#000', 1)).to.be.equal('rgba(0, 0, 0, 1)');
});
});