From 8c3f6f303491493cfe6e1a3d3ac7d0e9474fb01c Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Mon, 15 Jun 2026 17:21:45 +0300 Subject: [PATCH 1/5] Color: class refactor and add types --- packages/devextreme/js/__internal/m_color.ts | 318 +++++++++--------- .../js/__internal/ui/color_box/color_view.ts | 8 +- .../devextreme/js/__internal/viz/palette.ts | 4 +- 3 files changed, 166 insertions(+), 164 deletions(-) diff --git a/packages/devextreme/js/__internal/m_color.ts b/packages/devextreme/js/__internal/m_color.ts index eed05fea3f1c..dbe900bf593a 100644 --- a/packages/devextreme/js/__internal/m_color.ts +++ b/packages/devextreme/js/__internal/m_color.ts @@ -154,8 +154,13 @@ const standardColorNames = { yellowgreen: '9acd32', }; +type ColorParseResult = (number | number[] | null)[]; + // array of color definition objects -const standardColorTypes = [ +const standardColorTypes: { + re: RegExp; + process: (colorString: RegExpExecArray) => ColorParseResult; +}[] = [ { re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, process(colorString) { @@ -259,89 +264,37 @@ const standardColorTypes = [ // eslint-disable-next-line @typescript-eslint/naming-convention const _round = Math.round; -export interface ColorInstance { - baseColor: string | undefined; - r: number; - g: number; - b: number; - a: number; - hsv: { h: number; s: number; v: number }; - hsl: { h: number; s: number; l: number }; - colorIsInvalid: boolean; - highlight: (step?: number) => string; - darken: (step?: number) => string; - alter: (step: number) => ColorInstance; - blend: (blendColor: ColorInstance | string, opacity: number) => ColorInstance; - toHex: () => string; - getPureColor: () => ColorInstance; - isValidHex: (hex: string) => boolean; - isValidRGB: (r: number | undefined, g: number | undefined, b: number | undefined) => boolean; - isValidAlpha: (a: number) => boolean; - fromHSL: (hsl: { h: number; s: number; l: number }) => ColorInstance; -} - -function Color(value?) { - this.baseColor = value; - let color; - if (value) { - color = String(value).toLowerCase().replace(/ /g, ''); - color = standardColorNames[color] ? `#${standardColorNames[color]}` : color; - color = parseColor(color); - } - if (!color) { - this.colorIsInvalid = true; - } - - color = color || {}; - this.r = normalize(color[0]); - this.g = normalize(color[1]); - this.b = normalize(color[2]); - this.a = normalize(color[3], 1, 1); - if (color[4]) { - this.hsv = { h: color[4][0], s: color[4][1], v: color[4][2] }; - } else { - this.hsv = toHsvFromRgb(this.r, this.g, this.b); - } - if (color[5]) { - this.hsl = { h: color[5][0], s: color[5][1], l: color[5][2] }; - } else { - this.hsl = toHslFromRgb(this.r, this.g, this.b); - } -} - -function parseColor(color) { +function parseColor(color: string): ColorParseResult | null { if (color === 'transparent') { return [0, 0, 0, 0]; } - let i = 0; - const ii = standardColorTypes.length; - let str; - for (; i < ii; ++i) { - str = standardColorTypes[i].re.exec(color); - if (str) { - return standardColorTypes[i].process(str); + for (const colorType of standardColorTypes) { + const match = colorType.re.exec(color); + if (match) { + return colorType.process(match); } } return null; } -function normalize(colorComponent, def?, max?) { - def = def || 0; - max = max || 255; - return colorComponent < 0 || isNaN(colorComponent) ? def : colorComponent > max ? max : colorComponent; +function normalize(colorComponent: number | undefined, def = 0, max = 255): number { + if (colorComponent === undefined || Number.isNaN(colorComponent) || colorComponent < 0) { + return def; + } + return colorComponent > max ? max : colorComponent; } -function toHexFromRgb(r, g, b) { +function toHexFromRgb(r: number, g: number, b: number): string { return `#${(0X01000000 | (r << 16) | (g << 8) | b).toString(16).slice(1)}`; } -function toHsvFromRgb(r, g, b) { +function toHsvFromRgb(r: number, g: number, b: number): { h: number; s: number; v: number } { const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; - let H; - let S; + let H = 0; + let S = 0; let V = max; S = max === 0 ? 0 : 1 - min / max; @@ -376,16 +329,16 @@ function toHsvFromRgb(r, g, b) { }; } -function hsvToRgb(h, s, v) { +function hsvToRgb(h: number, s: number, v: number): [number, number, number] { const index = Math.floor((h % 360) / 60); const vMin = ((100 - s) * v) / 100; const a = (v - vMin) * ((h % 60) / 60); const vInc = vMin + a; const vDec = v - a; - let r; - let g; - let b; + let r = 0; + let g = 0; + let b = 0; switch (index) { case 0: r = v; g = vInc; b = vMin; break; @@ -401,7 +354,7 @@ function hsvToRgb(h, s, v) { return [Math.round(r * 2.55), Math.round(g * 2.55), Math.round(b * 2.55)]; } -function calculateHue(r, g, b, delta) { +function calculateHue(r: number, g: number, b: number, delta: number): number { const max = Math.max(r, g, b); switch (max) { case r: @@ -411,24 +364,33 @@ function calculateHue(r, g, b, delta) { case b: return (r - g) / delta + 4; default: - return undefined; // should never happen + return 0; // unreachable: max is always one of r, g, b + } +} + +function convertTo01Bounds(n: number, max: number): number { + const bounded = Math.min(max, Math.max(0, n)); + if (Math.abs(bounded - max) < 0.000001) { + return 1; } + return (bounded % max) / max; } -function toHslFromRgb(r, g, b) { - r = convertTo01Bounds(r, 255); - g = convertTo01Bounds(g, 255); - b = convertTo01Bounds(b, 255); +function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; l: number } { + const r01 = convertTo01Bounds(r, 255); + const g01 = convertTo01Bounds(g, 255); + const b01 = convertTo01Bounds(b, 255); - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); + const max = Math.max(r01, g01, b01); + const min = Math.min(r01, g01, b01); const maxMinSum = max + min; - let h; - let s; + let h = 0; + let s = 0; const l = maxMinSum / 2; if (max === min) { - h = s = 0; + h = 0; + s = 0; } else { const delta = max - min; @@ -438,14 +400,14 @@ function toHslFromRgb(r, g, b) { s = delta / maxMinSum; } - h = calculateHue(r, g, b, delta); + h = calculateHue(r01, g01, b01, delta); h /= 6; } return { h: _round(h * 360), s: _round(s * 100), l: _round(l * 100) }; } -function makeColorTint(colorPart, h) { +function makeColorTint(colorPart: string, h: number): number { let colorTint = h; if (colorPart === 'r') { colorTint = h + 1 / 3; @@ -457,65 +419,57 @@ function makeColorTint(colorPart, h) { return colorTint; } -function modifyColorTint(colorTint) { - if (colorTint < 0) { - colorTint += 1; +function modifyColorTint(colorTint: number): number { + let tint = colorTint; + if (tint < 0) { + tint += 1; } - if (colorTint > 1) { - colorTint -= 1; + if (tint > 1) { + tint -= 1; } - return colorTint; + return tint; } -function hueToRgb(p, q, colorTint) { - colorTint = modifyColorTint(colorTint); - if (colorTint < 1 / 6) { - return p + (q - p) * 6 * colorTint; +function hueToRgb(p: number, q: number, colorTint: number): number { + const tint = modifyColorTint(colorTint); + if (tint < 1 / 6) { + return p + (q - p) * 6 * tint; } - if (colorTint < 1 / 2) { + if (tint < 1 / 2) { return q; } - if (colorTint < 2 / 3) { - return p + (q - p) * (2 / 3 - colorTint) * 6; + if (tint < 2 / 3) { + return p + (q - p) * (2 / 3 - tint) * 6; } return p; } -function hslToRgb(h, s, l) { - let r; - let g; - let b; +function hslToRgb(h: number, s: number, l: number): [number, number, number] { + let r = 0; + let g = 0; + let b = 0; - h = convertTo01Bounds(h, 360); - s = convertTo01Bounds(s, 100); - l = convertTo01Bounds(l, 100); + const h01 = convertTo01Bounds(h, 360); + const s01 = convertTo01Bounds(s, 100); + const l01 = convertTo01Bounds(l, 100); - if (s === 0) { - r = g = b = l; + if (s01 === 0) { + r = l01; + g = l01; + b = l01; } else { - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - r = hueToRgb(p, q, makeColorTint('r', h)); - g = hueToRgb(p, q, makeColorTint('g', h)); - b = hueToRgb(p, q, makeColorTint('b', h)); + const q = l01 < 0.5 ? l01 * (1 + s01) : l01 + s01 - l01 * s01; + const p = 2 * l01 - q; + r = hueToRgb(p, q, makeColorTint('r', h01)); + g = hueToRgb(p, q, makeColorTint('g', h01)); + b = hueToRgb(p, q, makeColorTint('b', h01)); } return [_round(r * 255), _round(g * 255), _round(b * 255)]; } -function convertTo01Bounds(n, max) { - n = Math.min(max, Math.max(0, parseFloat(n))); - if (Math.abs(n - max) < 0.000001) { - return 1; - } - return (n % max) / parseFloat(max); -} - -function isIntegerBetweenMinAndMax(number, min?, max?) { - min = min || 0; - max = max || 255; - +function isIntegerBetweenMinAndMax(number: number | undefined, min = 0, max = 255): boolean { if (typeof number !== 'number' || number % 1 !== 0 || number < min @@ -527,66 +481,117 @@ function isIntegerBetweenMinAndMax(number, min?, max?) { return true; } -Color.prototype = { - constructor: Color, +export class Color { + baseColor: string | undefined; - highlight(step) { - step = step || 10; - return this.alter(step).toHex(); - }, + r!: number; - darken(step) { - step = step || 10; - return this.alter(-step).toHex(); - }, + g!: number; + + b!: number; + + a!: number; + + hsv!: { h: number; s: number; v: number }; + + hsl!: { h: number; s: number; l: number }; + + colorIsInvalid = false; - alter(step) { + constructor(value?: string) { + this.baseColor = value; + let color: ColorParseResult | null = null; + if (value) { + let colorStr = String(value).toLowerCase().replace(/ /g, ''); + colorStr = standardColorNames[colorStr] ? `#${standardColorNames[colorStr]}` : colorStr; + color = parseColor(colorStr); + } + if (!color) { + this.colorIsInvalid = true; + } + + const parts = color ?? []; + this.r = normalize(parts[0] as number | undefined); + this.g = normalize(parts[1] as number | undefined); + this.b = normalize(parts[2] as number | undefined); + this.a = normalize(parts[3] as number | undefined, 1, 1); + const hsvData = parts[4] as number[] | null | undefined; + if (hsvData) { + this.hsv = { h: hsvData[0], s: hsvData[1], v: hsvData[2] }; + } else { + this.hsv = toHsvFromRgb(this.r, this.g, this.b); + } + const hslData = parts[5] as number[] | null | undefined; + if (hslData) { + this.hsl = { h: hslData[0], s: hslData[1], l: hslData[2] }; + } else { + this.hsl = toHslFromRgb(this.r, this.g, this.b); + } + } + + highlight(step?: number): string { + const stepValue = step ?? 10; + return this.alter(stepValue).toHex(); + } + + darken(step?: number): string { + const stepValue = step ?? 10; + return this.alter(-stepValue).toHex(); + } + + alter(step: number): Color { const result = new Color(); result.r = normalize(this.r + step); result.g = normalize(this.g + step); result.b = normalize(this.b + step); return result; - }, + } - blend(blendColor, opacity) { + blend(blendColor: Color | string, opacity: number): Color { const other = blendColor instanceof Color ? blendColor : new Color(blendColor); const result = new Color(); result.r = normalize(_round(this.r * (1 - opacity) + other.r * opacity)); result.g = normalize(_round(this.g * (1 - opacity) + other.g * opacity)); result.b = normalize(_round(this.b * (1 - opacity) + other.b * opacity)); return result; - }, + } - toHex() { + toHex(): string { return toHexFromRgb(this.r, this.g, this.b); - }, + } - getPureColor() { + getPureColor(): Color { const rgb = hsvToRgb(this.hsv.h, 100, 100); return new Color(`rgb(${rgb.join(',')})`); - }, + } - isValidHex(hex) { + static isValidHex(hex: string): boolean { return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); - }, + } - isValidRGB(r, g, b) { - if (!isIntegerBetweenMinAndMax(r) || !isIntegerBetweenMinAndMax(g) || !isIntegerBetweenMinAndMax(b)) { + static isValidRGB( + r: number | undefined, + g: number | undefined, + b: number | undefined, + ): boolean { + if ( + !isIntegerBetweenMinAndMax(r) + || !isIntegerBetweenMinAndMax(g) + || !isIntegerBetweenMinAndMax(b) + ) { return false; } return true; - }, + } - isValidAlpha(a) { + static isValidAlpha(a: number): boolean { if (isNaN(a) || a < 0 || a > 1 || typeof a !== 'number') { return false; } return true; - }, - - colorIsInvalid: false, + } - fromHSL(hsl) { + static fromHSL(hsl: { h: number; s: number; l: number }): Color { const color = new Color(); const rgb = hslToRgb(hsl.h, hsl.s, hsl.l); @@ -598,12 +603,9 @@ Color.prototype = { color.b = rgb[2]; return color; - }, -}; - -interface ColorConstructor { - prototype: ColorInstance; - new(value?: string): ColorInstance; + } } -export default Color as unknown as ColorConstructor; +export type ColorInstance = Color; + +export default Color; diff --git a/packages/devextreme/js/__internal/ui/color_box/color_view.ts b/packages/devextreme/js/__internal/ui/color_box/color_view.ts index 298eacecf8ee..32f110ad0de6 100644 --- a/packages/devextreme/js/__internal/ui/color_box/color_view.ts +++ b/packages/devextreme/js/__internal/ui/color_box/color_view.ts @@ -833,7 +833,7 @@ class ColorView extends Editor { step: 0.1, onValueChanged: (args) => { let { value } = args; - value = this._currentColor.isValidAlpha(value) ? value : this._currentColor.a; + value = Color.isValidAlpha(value) ? value : this._currentColor.a; if (args.event) { this._saveValueChangeEvent(args.event); } @@ -952,7 +952,7 @@ class ColorView extends Editor { if (this._alphaChannelInput) { const { value: alphaValue } = this._alphaChannelInput.option(); const isValidAlpha = alphaValue !== undefined - && this._currentColor.isValidAlpha(alphaValue); + && Color.isValidAlpha(alphaValue); const valueToAdd = isValidAlpha ? alphaValue : this._currentColor.a; @@ -972,7 +972,7 @@ class ColorView extends Editor { } _validateHex(hex: string): string { - return this._currentColor.isValidHex(hex) ? hex : this._currentColor.toHex(); + return Color.isValidHex(hex) ? hex : this._currentColor.toHex(); } _validateRgb(): number[] { @@ -980,7 +980,7 @@ class ColorView extends Editor { let { value: g } = this._rgbInputs[1].option(); let { value: b } = this._rgbInputs[2].option(); - const isInvalidRgb = !this._currentColor.isValidRGB(r, g, b); + const isInvalidRgb = !Color.isValidRGB(r, g, b); if (isInvalidRgb) { r = this._currentColor.r; g = this._currentColor.g; diff --git a/packages/devextreme/js/__internal/viz/palette.ts b/packages/devextreme/js/__internal/viz/palette.ts index 66e173209a70..1e7511b1f9d0 100644 --- a/packages/devextreme/js/__internal/viz/palette.ts +++ b/packages/devextreme/js/__internal/viz/palette.ts @@ -17,9 +17,9 @@ /* eslint-disable no-else-return */ /* eslint-disable @typescript-eslint/prefer-optional-chain */ -import _Color from '@js/color'; import { extend } from '@js/core/utils/extend'; import { isString as _isString } from '@js/core/utils/type'; +import _Color from '@ts/m_color'; import { normalizeEnum } from '@ts/viz/core/utils'; const _floor = Math.floor; @@ -280,7 +280,7 @@ function getExtrapolateColorsStrategy(palette, parameters) { } hsl.l = l * 100; - return _Color.prototype.fromHSL(hsl).toHex(); + return _Color.fromHSL(hsl).toHex(); } return { From 1914fe08fd431e0ad2767637a5fe339b24321074 Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Mon, 15 Jun 2026 17:34:17 +0300 Subject: [PATCH 2/5] Color: eslint fixes[C --- packages/devextreme/js/__internal/m_color.ts | 226 ++++++++++--------- 1 file changed, 115 insertions(+), 111 deletions(-) diff --git a/packages/devextreme/js/__internal/m_color.ts b/packages/devextreme/js/__internal/m_color.ts index dbe900bf593a..9710cd9beecc 100644 --- a/packages/devextreme/js/__internal/m_color.ts +++ b/packages/devextreme/js/__internal/m_color.ts @@ -154,7 +154,108 @@ const standardColorNames = { yellowgreen: '9acd32', }; -type ColorParseResult = (number | number[] | null)[]; +type ColorParseResult = | [number, number, number] + | [number, number, number, number] + | [number, number, number, number, [number, number, number]] + | [number, number, number, number, null, [number, number, number]]; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const _round = Math.round; + +function makeColorTint(colorPart: string, h: number): number { + let colorTint = h; + if (colorPart === 'r') { + colorTint = h + 1 / 3; + } + if (colorPart === 'b') { + colorTint = h - 1 / 3; + } + + return colorTint; +} + +function modifyColorTint(colorTint: number): number { + let tint = colorTint; + if (tint < 0) { + tint += 1; + } + if (tint > 1) { + tint -= 1; + } + + return tint; +} + +function hueToRgb(p: number, q: number, colorTint: number): number { + const tint = modifyColorTint(colorTint); + if (tint < 1 / 6) { + return p + (q - p) * 6 * tint; + } + if (tint < 1 / 2) { + return q; + } + if (tint < 2 / 3) { + return p + (q - p) * (2 / 3 - tint) * 6; + } + return p; +} + +function convertTo01Bounds(n: number, max: number): number { + const bounded = Math.min(max, Math.max(0, n)); + if (Math.abs(bounded - max) < 0.000001) { + return 1; + } + return (bounded % max) / max; +} + +function hsvToRgb(h: number, s: number, v: number): [number, number, number] { + const index = Math.floor((h % 360) / 60); + const vMin = ((100 - s) * v) / 100; + const a = (v - vMin) * ((h % 60) / 60); + const vInc = vMin + a; + const vDec = v - a; + + let r = 0; + let g = 0; + let b = 0; + + switch (index) { + case 0: r = v; g = vInc; b = vMin; break; + case 1: r = vDec; g = v; b = vMin; break; + case 2: r = vMin; g = v; b = vInc; break; + case 3: r = vMin; g = vDec; b = v; break; + case 4: r = vInc; g = vMin; b = v; break; + case 5: r = v; g = vMin; b = vDec; break; + default: + break; + } + + return [Math.round(r * 2.55), Math.round(g * 2.55), Math.round(b * 2.55)]; +} + +function hslToRgb(h: number, s: number, l: number): [number, number, number] { + let r = 0; + let g = 0; + let b = 0; + + const h01 = convertTo01Bounds(h, 360); + const s01 = convertTo01Bounds(s, 100); + const l01 = convertTo01Bounds(l, 100); + + if (s01 === 0) { + r = l01; + g = l01; + b = l01; + } else { + const q = l01 < 0.5 ? l01 * (1 + s01) : l01 + s01 - l01 * s01; + const p = 2 * l01 - q; + r = hueToRgb(p, q, makeColorTint('r', h01)); + g = hueToRgb(p, q, makeColorTint('g', h01)); + b = hueToRgb(p, q, makeColorTint('b', h01)); + } + + return [_round(r * 255), _round(g * 255), _round(b * 255)]; +} // array of color definition objects const standardColorTypes: { @@ -163,7 +264,7 @@ const standardColorTypes: { }[] = [ { re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString) { + process(colorString): [number, number, number] { return [ parseInt(colorString[1], 10), parseInt(colorString[2], 10), @@ -173,7 +274,7 @@ const standardColorTypes: { }, { re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*\.*\d+)\)$/, - process(colorString) { + process(colorString): [number, number, number, number] { return [ parseInt(colorString[1], 10), parseInt(colorString[2], 10), @@ -184,7 +285,7 @@ const standardColorTypes: { }, { re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, - process(colorString) { + process(colorString): [number, number, number] { return [ parseInt(colorString[1], 16), parseInt(colorString[2], 16), @@ -194,7 +295,7 @@ const standardColorTypes: { }, { re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, - process(colorString) { + process(colorString):[number, number, number, number] { return [ parseInt(colorString[1], 16), parseInt(colorString[2], 16), @@ -205,7 +306,7 @@ const standardColorTypes: { }, { re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, - process(colorString) { + process(colorString): [number, number, number, number] { return [ parseInt(colorString[1] + colorString[1], 16), parseInt(colorString[2] + colorString[2], 16), @@ -216,7 +317,7 @@ const standardColorTypes: { }, { re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, - process(colorString) { + process(colorString): [number, number, number] { return [ parseInt(colorString[1] + colorString[1], 16), parseInt(colorString[2] + colorString[2], 16), @@ -226,7 +327,7 @@ const standardColorTypes: { }, { re: /^hsv\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString) { + process(colorString): [number, number, number, number, [number, number, number]] { const h = parseInt(colorString[1], 10); const s = parseInt(colorString[2], 10); const v = parseInt(colorString[3], 10); @@ -243,7 +344,7 @@ const standardColorTypes: { }, { re: /^hsl\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString) { + process(colorString): [number, number, number, number, null, [number, number, number]] { const h = parseInt(colorString[1], 10); const s = parseInt(colorString[2], 10); const l = parseInt(colorString[3], 10); @@ -261,9 +362,6 @@ const standardColorTypes: { }, ]; -// eslint-disable-next-line @typescript-eslint/naming-convention -const _round = Math.round; - function parseColor(color: string): ColorParseResult | null { if (color === 'transparent') { return [0, 0, 0, 0]; @@ -286,6 +384,7 @@ function normalize(colorComponent: number | undefined, def = 0, max = 255): numb } function toHexFromRgb(r: number, g: number, b: number): string { + // eslint-disable-next-line no-bitwise return `#${(0X01000000 | (r << 16) | (g << 8) | b).toString(16).slice(1)}`; } @@ -329,31 +428,6 @@ function toHsvFromRgb(r: number, g: number, b: number): { h: number; s: number; }; } -function hsvToRgb(h: number, s: number, v: number): [number, number, number] { - const index = Math.floor((h % 360) / 60); - const vMin = ((100 - s) * v) / 100; - const a = (v - vMin) * ((h % 60) / 60); - const vInc = vMin + a; - const vDec = v - a; - - let r = 0; - let g = 0; - let b = 0; - - switch (index) { - case 0: r = v; g = vInc; b = vMin; break; - case 1: r = vDec; g = v; b = vMin; break; - case 2: r = vMin; g = v; b = vInc; break; - case 3: r = vMin; g = vDec; b = v; break; - case 4: r = vInc; g = vMin; b = v; break; - case 5: r = v; g = vMin; b = vDec; break; - default: - break; - } - - return [Math.round(r * 2.55), Math.round(g * 2.55), Math.round(b * 2.55)]; -} - function calculateHue(r: number, g: number, b: number, delta: number): number { const max = Math.max(r, g, b); switch (max) { @@ -368,14 +442,6 @@ function calculateHue(r: number, g: number, b: number, delta: number): number { } } -function convertTo01Bounds(n: number, max: number): number { - const bounded = Math.min(max, Math.max(0, n)); - if (Math.abs(bounded - max) < 0.000001) { - return 1; - } - return (bounded % max) / max; -} - function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; l: number } { const r01 = convertTo01Bounds(r, 255); const g01 = convertTo01Bounds(g, 255); @@ -407,68 +473,6 @@ function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; return { h: _round(h * 360), s: _round(s * 100), l: _round(l * 100) }; } -function makeColorTint(colorPart: string, h: number): number { - let colorTint = h; - if (colorPart === 'r') { - colorTint = h + 1 / 3; - } - if (colorPart === 'b') { - colorTint = h - 1 / 3; - } - - return colorTint; -} - -function modifyColorTint(colorTint: number): number { - let tint = colorTint; - if (tint < 0) { - tint += 1; - } - if (tint > 1) { - tint -= 1; - } - - return tint; -} - -function hueToRgb(p: number, q: number, colorTint: number): number { - const tint = modifyColorTint(colorTint); - if (tint < 1 / 6) { - return p + (q - p) * 6 * tint; - } - if (tint < 1 / 2) { - return q; - } - if (tint < 2 / 3) { - return p + (q - p) * (2 / 3 - tint) * 6; - } - return p; -} - -function hslToRgb(h: number, s: number, l: number): [number, number, number] { - let r = 0; - let g = 0; - let b = 0; - - const h01 = convertTo01Bounds(h, 360); - const s01 = convertTo01Bounds(s, 100); - const l01 = convertTo01Bounds(l, 100); - - if (s01 === 0) { - r = l01; - g = l01; - b = l01; - } else { - const q = l01 < 0.5 ? l01 * (1 + s01) : l01 + s01 - l01 * s01; - const p = 2 * l01 - q; - r = hueToRgb(p, q, makeColorTint('r', h01)); - g = hueToRgb(p, q, makeColorTint('g', h01)); - b = hueToRgb(p, q, makeColorTint('b', h01)); - } - - return [_round(r * 255), _round(g * 255), _round(b * 255)]; -} - function isIntegerBetweenMinAndMax(number: number | undefined, min = 0, max = 255): boolean { if (typeof number !== 'number' || number % 1 !== 0 @@ -511,10 +515,10 @@ export class Color { } const parts = color ?? []; - this.r = normalize(parts[0] as number | undefined); - this.g = normalize(parts[1] as number | undefined); - this.b = normalize(parts[2] as number | undefined); - this.a = normalize(parts[3] as number | undefined, 1, 1); + this.r = normalize(parts[0]); + this.g = normalize(parts[1]); + this.b = normalize(parts[2]); + this.a = normalize(parts[3], 1, 1); const hsvData = parts[4] as number[] | null | undefined; if (hsvData) { this.hsv = { h: hsvData[0], s: hsvData[1], v: hsvData[2] }; From ab888e506c205bb6e36f75a01d0c29073305ab71 Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Mon, 15 Jun 2026 18:20:51 +0300 Subject: [PATCH 3/5] Color: fix tests --- .../testing/tests/DevExpress/color.tests.js | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress/color.tests.js b/packages/devextreme/testing/tests/DevExpress/color.tests.js index 48b258fd1c07..52f75ff1ba14 100644 --- a/packages/devextreme/testing/tests/DevExpress/color.tests.js +++ b/packages/devextreme/testing/tests/DevExpress/color.tests.js @@ -245,37 +245,34 @@ QUnit.test('blend - paired', function(assert) { QUnit.module('Color validation'); QUnit.test('is valid hex', function(assert) { - const color = new Color(); - assert.equal(color.isValidHex('#ff0000'), true); - assert.equal(color.isValidHex('#0000FF'), true); - assert.equal(color.isValidHex('#606060'), true); - assert.equal(color.isValidHex('#FFF'), true); - assert.equal(color.isValidHex('646400'), false); - assert.equal(color.isValidHex('#0000ZZ'), false); - assert.equal(color.isValidHex('100'), false); - assert.equal(color.isValidHex('#FFX'), false); + assert.equal(Color.isValidHex('#ff0000'), true); + assert.equal(Color.isValidHex('#0000FF'), true); + assert.equal(Color.isValidHex('#606060'), true); + assert.equal(Color.isValidHex('#FFF'), true); + assert.equal(Color.isValidHex('646400'), false); + assert.equal(Color.isValidHex('#0000ZZ'), false); + assert.equal(Color.isValidHex('100'), false); + assert.equal(Color.isValidHex('#FFX'), false); }); QUnit.test('is valid RGB', function(assert) { - const color = new Color(); - assert.equal(color.isValidRGB(0, 0, 0), true); - assert.equal(color.isValidRGB(250, 100, 255), true); - assert.equal(color.isValidRGB(-250, 100, 255), false); - assert.equal(color.isValidRGB(250, 400, 255), false); - assert.equal(color.isValidRGB(250, 'sdsd', 255), false); - assert.equal(color.isValidRGB(250, null, 100), false); - assert.equal(color.isValidRGB(250, 100), false); - assert.equal(color.isValidRGB(250, 100, NaN), false); - assert.equal(color.isValidRGB(250, 100, 123.5), false); + assert.equal(Color.isValidRGB(0, 0, 0), true); + assert.equal(Color.isValidRGB(250, 100, 255), true); + assert.equal(Color.isValidRGB(-250, 100, 255), false); + assert.equal(Color.isValidRGB(250, 400, 255), false); + assert.equal(Color.isValidRGB(250, 'sdsd', 255), false); + assert.equal(Color.isValidRGB(250, null, 100), false); + assert.equal(Color.isValidRGB(250, 100), false); + assert.equal(Color.isValidRGB(250, 100, NaN), false); + assert.equal(Color.isValidRGB(250, 100, 123.5), false); }); QUnit.test('is valid alpha', function(assert) { - const color = new Color(); - assert.equal(color.isValidAlpha(0), true); - assert.equal(color.isValidAlpha(0.8), true); - assert.equal(color.isValidAlpha(1), true); - assert.equal(color.isValidAlpha(-0.5), false); - assert.equal(color.isValidAlpha(), false); - assert.equal(color.isValidAlpha(100), false); - assert.equal(color.isValidAlpha('sdss'), false); + assert.equal(Color.isValidAlpha(0), true); + assert.equal(Color.isValidAlpha(0.8), true); + assert.equal(Color.isValidAlpha(1), true); + assert.equal(Color.isValidAlpha(-0.5), false); + assert.equal(Color.isValidAlpha(), false); + assert.equal(Color.isValidAlpha(100), false); + assert.equal(Color.isValidAlpha('sdss'), false); }); From 9b62ba501e961eb45cdac843dbe6dba6b15d780a Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Tue, 16 Jun 2026 17:36:29 +0300 Subject: [PATCH 4/5] Color: fixed comment from PR and typeguards --- packages/devextreme/js/__internal/m_color.ts | 40 ++++++++-------- .../devextreme/js/__internal/viz/palette.ts | 46 +++++++++---------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/packages/devextreme/js/__internal/m_color.ts b/packages/devextreme/js/__internal/m_color.ts index 9710cd9beecc..9718b00d3360 100644 --- a/packages/devextreme/js/__internal/m_color.ts +++ b/packages/devextreme/js/__internal/m_color.ts @@ -159,9 +159,6 @@ type ColorParseResult = | [number, number, number] | [number, number, number, number, [number, number, number]] | [number, number, number, number, null, [number, number, number]]; -// eslint-disable-next-line @typescript-eslint/naming-convention -const _round = Math.round; - function makeColorTint(colorPart: string, h: number): number { let colorTint = h; if (colorPart === 'r') { @@ -254,7 +251,7 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] { b = hueToRgb(p, q, makeColorTint('b', h01)); } - return [_round(r * 255), _round(g * 255), _round(b * 255)]; + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } // array of color definition objects @@ -295,7 +292,7 @@ const standardColorTypes: { }, { re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, - process(colorString):[number, number, number, number] { + process(colorString): [number, number, number, number] { return [ parseInt(colorString[1], 16), parseInt(colorString[2], 16), @@ -470,15 +467,15 @@ function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; h /= 6; } - return { h: _round(h * 360), s: _round(s * 100), l: _round(l * 100) }; + return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; } -function isIntegerBetweenMinAndMax(number: number | undefined, min = 0, max = 255): boolean { - if (typeof number !== 'number' - || number % 1 !== 0 - || number < min - || number > max - || isNaN(number)) { +function isIntegerBetweenMinAndMax(value: unknown, min = 0, max = 255): value is number { + if (typeof value !== 'number' + || value % 1 !== 0 + || value < min + || value > max + || isNaN(value)) { return false; } @@ -554,9 +551,9 @@ export class Color { blend(blendColor: Color | string, opacity: number): Color { const other = blendColor instanceof Color ? blendColor : new Color(blendColor); const result = new Color(); - result.r = normalize(_round(this.r * (1 - opacity) + other.r * opacity)); - result.g = normalize(_round(this.g * (1 - opacity) + other.g * opacity)); - result.b = normalize(_round(this.b * (1 - opacity) + other.b * opacity)); + result.r = normalize(Math.round(this.r * (1 - opacity) + other.r * opacity)); + result.g = normalize(Math.round(this.g * (1 - opacity) + other.g * opacity)); + result.b = normalize(Math.round(this.b * (1 - opacity) + other.b * opacity)); return result; } @@ -574,9 +571,9 @@ export class Color { } static isValidRGB( - r: number | undefined, - g: number | undefined, - b: number | undefined, + r: unknown, + g: unknown, + b: unknown, ): boolean { if ( !isIntegerBetweenMinAndMax(r) @@ -588,8 +585,8 @@ export class Color { return true; } - static isValidAlpha(a: number): boolean { - if (isNaN(a) || a < 0 || a > 1 || typeof a !== 'number') { + static isValidAlpha(a: unknown): a is number { + if (typeof a !== 'number' || isNaN(a) || a < 0 || a > 1) { return false; } return true; @@ -605,6 +602,9 @@ export class Color { color.g = rgb[1]; // eslint-disable-next-line prefer-destructuring color.b = rgb[2]; + color.hsl = { ...hsl }; + color.hsv = toHsvFromRgb(color.r, color.g, color.b); + color.colorIsInvalid = false; return color; } diff --git a/packages/devextreme/js/__internal/viz/palette.ts b/packages/devextreme/js/__internal/viz/palette.ts index 1e7511b1f9d0..5d722ffcfbe0 100644 --- a/packages/devextreme/js/__internal/viz/palette.ts +++ b/packages/devextreme/js/__internal/viz/palette.ts @@ -18,14 +18,10 @@ /* eslint-disable @typescript-eslint/prefer-optional-chain */ import { extend } from '@js/core/utils/extend'; -import { isString as _isString } from '@js/core/utils/type'; -import _Color from '@ts/m_color'; +import { isString } from '@js/core/utils/type'; +import Color from '@ts/m_color'; import { normalizeEnum } from '@ts/viz/core/utils'; -const _floor = Math.floor; -const _ceil = Math.ceil; -const _isArray = Array.isArray; - const HIGHLIGHTING_STEP = 50; const DEFAULT_PALETTE = 'material'; @@ -155,10 +151,10 @@ export function getPalette(palette, parameters) { let result; const type = parameters.type; - if (_isArray(palette)) { + if (Array.isArray(palette)) { return palette.slice(0); } else { - if (_isString(palette)) { + if (isString(palette)) { result = palettes[normalizeEnum(palette)]; } if (!result) { @@ -173,16 +169,16 @@ export function registerPalette(name, palette) { const item = {}; let paletteName; - if (_isArray(palette)) { + if (Array.isArray(palette)) { // @ts-expect-error item.simpleSet = palette.slice(0); } else if (palette) { // @ts-expect-error - item.simpleSet = _isArray(palette.simpleSet) ? palette.simpleSet.slice(0) : undefined; + item.simpleSet = Array.isArray(palette.simpleSet) ? palette.simpleSet.slice(0) : undefined; // @ts-expect-error - item.indicatingSet = _isArray(palette.indicatingSet) ? palette.indicatingSet.slice(0) : undefined; + item.indicatingSet = Array.isArray(palette.indicatingSet) ? palette.indicatingSet.slice(0) : undefined; // @ts-expect-error - item.gradientSet = _isArray(palette.gradientSet) ? palette.gradientSet.slice(0) : undefined; + item.gradientSet = Array.isArray(palette.gradientSet) ? palette.gradientSet.slice(0) : undefined; // @ts-expect-error item.accentColor = palette.accentColor; } @@ -257,7 +253,7 @@ function getAlternateColorsStrategy(palette, parameters) { function getExtrapolateColorsStrategy(palette, parameters) { function convertColor(color, cycleIndex, cycleCount) { - const hsl = new _Color(color).hsl; + const hsl = new Color(color).hsl; let l = hsl.l / 100; const diapason = cycleCount - 1 / cycleCount; let minL = l - diapason * 0.5; @@ -280,17 +276,17 @@ function getExtrapolateColorsStrategy(palette, parameters) { } hsl.l = l * 100; - return _Color.fromHSL(hsl).toHex(); + return Color.fromHSL(hsl).toHex(); } return { getColor(index, count) { const paletteCount = palette.length; - const cycles = _floor((count - 1) / paletteCount + 1); + const cycles = Math.floor((count - 1) / paletteCount + 1); const color = palette[index % paletteCount]; if (cycles > 1) { - return convertColor(color, _floor(index / paletteCount), cycles); + return convertColor(color, Math.floor(index / paletteCount), cycles); } return color; @@ -360,9 +356,9 @@ function getColorMixer(palette, parameters) { } else { const c2 = getColorAndDistance(paletteWithEmptyColors, i, paletteLength); // @ts-expect-error - const color2 = new _Color(c2[0]); + const color2 = new Color(c2[0]); - color1 = new _Color(color1); + color1 = new Color(color1); // @ts-expect-error for (let j = 0; j < c2[1]; j++, i++) { // @ts-expect-error @@ -487,10 +483,10 @@ function getAlteredPalette(originalPalette, step) { } function getNewColor(currentColor, step) { - let newColor = new _Color(currentColor).alter(step); + let newColor = new Color(currentColor).alter(step); const lightness = getLightness(newColor); if (lightness > 200 || lightness < 55) { - newColor = new _Color(currentColor).alter(-step / 2); + newColor = new Color(currentColor).alter(-step / 2); } return newColor.toHex(); } @@ -518,15 +514,15 @@ function createDiscreteColors(source, count) { function addColor(pos) { const k = sourceCount * pos; - const kl = _floor(k); - const kr = _ceil(k); + const kl = Math.floor(k); + const kr = Math.ceil(k); // @ts-expect-error gradient.push(colors[kl].blend(colors[kr], k - kl).toHex()); } for (i = 0; i <= sourceCount; ++i) { // @ts-expect-error - colors.push(new _Color(source[i])); + colors.push(new Color(source[i])); } if (colorCount > 0) { for (i = 0; i <= colorCount; ++i) { @@ -541,8 +537,8 @@ function createDiscreteColors(source, count) { export function getGradientPalette(source, themeDefaultPalette) { // TODO: Looks like some new set is going to be added const palette = getPalette(source, { type: 'gradientSet', themeDefault: themeDefaultPalette }); - const color1 = new _Color(palette[0]); - const color2 = new _Color(palette[1]); + const color1 = new Color(palette[0]); + const color2 = new Color(palette[1]); return { getColor(ratio) { From f7cc2885c63241519597247b589d70c5bfd0ac39 Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Tue, 16 Jun 2026 18:20:59 +0300 Subject: [PATCH 5/5] Color: separate folder form utils --- packages/devextreme/js/__internal/m_color.ts | 336 +----------------- .../js/__internal/ui/color_box/color_box.ts | 2 +- .../js/__internal/ui/color_box/color_view.ts | 2 +- .../js/__internal/utils/color.helpers.ts | 327 +++++++++++++++++ .../devextreme/js/__internal/viz/palette.ts | 2 +- 5 files changed, 338 insertions(+), 331 deletions(-) create mode 100644 packages/devextreme/js/__internal/utils/color.helpers.ts diff --git a/packages/devextreme/js/__internal/m_color.ts b/packages/devextreme/js/__internal/m_color.ts index 9718b00d3360..31f1f2c5932e 100644 --- a/packages/devextreme/js/__internal/m_color.ts +++ b/packages/devextreme/js/__internal/m_color.ts @@ -1,5 +1,13 @@ /* eslint-disable spellcheck/spell-checker */ +import type { ColorParseResult } from './utils/color.helpers'; +import { + hslToRgb, + hsvToRgb, + isIntegerBetweenMinAndMax, + normalize, parseColor, toHexFromRgb, toHslFromRgb, toHsvFromRgb, +} from './utils/color.helpers'; + const standardColorNames = { aliceblue: 'f0f8ff', antiquewhite: 'faebd7', @@ -154,334 +162,6 @@ const standardColorNames = { yellowgreen: '9acd32', }; -type ColorParseResult = | [number, number, number] - | [number, number, number, number] - | [number, number, number, number, [number, number, number]] - | [number, number, number, number, null, [number, number, number]]; - -function makeColorTint(colorPart: string, h: number): number { - let colorTint = h; - if (colorPart === 'r') { - colorTint = h + 1 / 3; - } - if (colorPart === 'b') { - colorTint = h - 1 / 3; - } - - return colorTint; -} - -function modifyColorTint(colorTint: number): number { - let tint = colorTint; - if (tint < 0) { - tint += 1; - } - if (tint > 1) { - tint -= 1; - } - - return tint; -} - -function hueToRgb(p: number, q: number, colorTint: number): number { - const tint = modifyColorTint(colorTint); - if (tint < 1 / 6) { - return p + (q - p) * 6 * tint; - } - if (tint < 1 / 2) { - return q; - } - if (tint < 2 / 3) { - return p + (q - p) * (2 / 3 - tint) * 6; - } - return p; -} - -function convertTo01Bounds(n: number, max: number): number { - const bounded = Math.min(max, Math.max(0, n)); - if (Math.abs(bounded - max) < 0.000001) { - return 1; - } - return (bounded % max) / max; -} - -function hsvToRgb(h: number, s: number, v: number): [number, number, number] { - const index = Math.floor((h % 360) / 60); - const vMin = ((100 - s) * v) / 100; - const a = (v - vMin) * ((h % 60) / 60); - const vInc = vMin + a; - const vDec = v - a; - - let r = 0; - let g = 0; - let b = 0; - - switch (index) { - case 0: r = v; g = vInc; b = vMin; break; - case 1: r = vDec; g = v; b = vMin; break; - case 2: r = vMin; g = v; b = vInc; break; - case 3: r = vMin; g = vDec; b = v; break; - case 4: r = vInc; g = vMin; b = v; break; - case 5: r = v; g = vMin; b = vDec; break; - default: - break; - } - - return [Math.round(r * 2.55), Math.round(g * 2.55), Math.round(b * 2.55)]; -} - -function hslToRgb(h: number, s: number, l: number): [number, number, number] { - let r = 0; - let g = 0; - let b = 0; - - const h01 = convertTo01Bounds(h, 360); - const s01 = convertTo01Bounds(s, 100); - const l01 = convertTo01Bounds(l, 100); - - if (s01 === 0) { - r = l01; - g = l01; - b = l01; - } else { - const q = l01 < 0.5 ? l01 * (1 + s01) : l01 + s01 - l01 * s01; - const p = 2 * l01 - q; - r = hueToRgb(p, q, makeColorTint('r', h01)); - g = hueToRgb(p, q, makeColorTint('g', h01)); - b = hueToRgb(p, q, makeColorTint('b', h01)); - } - - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; -} - -// array of color definition objects -const standardColorTypes: { - re: RegExp; - process: (colorString: RegExpExecArray) => ColorParseResult; -}[] = [ - { - re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString): [number, number, number] { - return [ - parseInt(colorString[1], 10), - parseInt(colorString[2], 10), - parseInt(colorString[3], 10), - ]; - }, - }, - { - re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*\.*\d+)\)$/, - process(colorString): [number, number, number, number] { - return [ - parseInt(colorString[1], 10), - parseInt(colorString[2], 10), - parseInt(colorString[3], 10), - parseFloat(colorString[4]), - ]; - }, - }, - { - re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, - process(colorString): [number, number, number] { - return [ - parseInt(colorString[1], 16), - parseInt(colorString[2], 16), - parseInt(colorString[3], 16), - ]; - }, - }, - { - re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, - process(colorString): [number, number, number, number] { - return [ - parseInt(colorString[1], 16), - parseInt(colorString[2], 16), - parseInt(colorString[3], 16), - Number((parseInt(colorString[4], 16) / 255).toFixed(2)), - ]; - }, - }, - { - re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, - process(colorString): [number, number, number, number] { - return [ - parseInt(colorString[1] + colorString[1], 16), - parseInt(colorString[2] + colorString[2], 16), - parseInt(colorString[3] + colorString[3], 16), - Number((parseInt(colorString[4] + colorString[4], 16) / 255).toFixed(2)), - ]; - }, - }, - { - re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, - process(colorString): [number, number, number] { - return [ - parseInt(colorString[1] + colorString[1], 16), - parseInt(colorString[2] + colorString[2], 16), - parseInt(colorString[3] + colorString[3], 16), - ]; - }, - }, - { - re: /^hsv\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString): [number, number, number, number, [number, number, number]] { - const h = parseInt(colorString[1], 10); - const s = parseInt(colorString[2], 10); - const v = parseInt(colorString[3], 10); - const rgb = hsvToRgb(h, s, v); - - return [ - rgb[0], - rgb[1], - rgb[2], - 1, - [h, s, v], - ]; - }, - }, - { - re: /^hsl\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, - process(colorString): [number, number, number, number, null, [number, number, number]] { - const h = parseInt(colorString[1], 10); - const s = parseInt(colorString[2], 10); - const l = parseInt(colorString[3], 10); - const rgb = hslToRgb(h, s, l); - - return [ - rgb[0], - rgb[1], - rgb[2], - 1, - null, - [h, s, l], - ]; - }, - }, -]; - -function parseColor(color: string): ColorParseResult | null { - if (color === 'transparent') { - return [0, 0, 0, 0]; - } - - for (const colorType of standardColorTypes) { - const match = colorType.re.exec(color); - if (match) { - return colorType.process(match); - } - } - return null; -} - -function normalize(colorComponent: number | undefined, def = 0, max = 255): number { - if (colorComponent === undefined || Number.isNaN(colorComponent) || colorComponent < 0) { - return def; - } - return colorComponent > max ? max : colorComponent; -} - -function toHexFromRgb(r: number, g: number, b: number): string { - // eslint-disable-next-line no-bitwise - return `#${(0X01000000 | (r << 16) | (g << 8) | b).toString(16).slice(1)}`; -} - -function toHsvFromRgb(r: number, g: number, b: number): { h: number; s: number; v: number } { - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const delta = max - min; - let H = 0; - let S = 0; - let V = max; - S = max === 0 ? 0 : 1 - min / max; - - if (max === min) { - H = 0; - } else { - switch (max) { - case r: - H = 60 * ((g - b) / delta); - if (g < b) { - H += 360; - } - break; - case g: - H = 60 * ((b - r) / delta) + 120; - break; - case b: - H = 60 * ((r - g) / delta) + 240; - break; - default: - break; - } - } - - S *= 100; - V *= 100 / 255; - - return { - h: Math.round(H), - s: Math.round(S), - v: Math.round(V), - }; -} - -function calculateHue(r: number, g: number, b: number, delta: number): number { - const max = Math.max(r, g, b); - switch (max) { - case r: - return (g - b) / delta + (g < b ? 6 : 0); - case g: - return (b - r) / delta + 2; - case b: - return (r - g) / delta + 4; - default: - return 0; // unreachable: max is always one of r, g, b - } -} - -function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; l: number } { - const r01 = convertTo01Bounds(r, 255); - const g01 = convertTo01Bounds(g, 255); - const b01 = convertTo01Bounds(b, 255); - - const max = Math.max(r01, g01, b01); - const min = Math.min(r01, g01, b01); - const maxMinSum = max + min; - let h = 0; - let s = 0; - const l = maxMinSum / 2; - - if (max === min) { - h = 0; - s = 0; - } else { - const delta = max - min; - - if (l > 0.5) { - s = delta / (2 - maxMinSum); - } else { - s = delta / maxMinSum; - } - - h = calculateHue(r01, g01, b01, delta); - h /= 6; - } - - return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; -} - -function isIntegerBetweenMinAndMax(value: unknown, min = 0, max = 255): value is number { - if (typeof value !== 'number' - || value % 1 !== 0 - || value < min - || value > max - || isNaN(value)) { - return false; - } - - return true; -} - export class Color { baseColor: string | undefined; diff --git a/packages/devextreme/js/__internal/ui/color_box/color_box.ts b/packages/devextreme/js/__internal/ui/color_box/color_box.ts index 8475a143abe6..387960372154 100644 --- a/packages/devextreme/js/__internal/ui/color_box/color_box.ts +++ b/packages/devextreme/js/__internal/ui/color_box/color_box.ts @@ -4,7 +4,7 @@ import $ from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; import type { Properties } from '@js/ui/color_box'; import type { OptionChanged } from '@ts/core/widget/types'; -import Color from '@ts/m_color'; +import { Color } from '@ts/m_color'; import DropDownEditor from '@ts/ui/drop_down_editor/drop_down_editor'; import type { ValueChangedEvent } from '@ts/ui/editor/editor'; import type { PopupProperties } from '@ts/ui/popup/popup'; diff --git a/packages/devextreme/js/__internal/ui/color_box/color_view.ts b/packages/devextreme/js/__internal/ui/color_box/color_view.ts index 32f110ad0de6..9a495c052fed 100644 --- a/packages/devextreme/js/__internal/ui/color_box/color_view.ts +++ b/packages/devextreme/js/__internal/ui/color_box/color_view.ts @@ -14,7 +14,7 @@ import type { SupportedKeyHandler, SupportedKeys } from '@ts/core/widget/widget' import eventsEngine from '@ts/events/core/m_events_engine'; import { name as clickEventName } from '@ts/events/m_click'; import { isCommandKeyPressed } from '@ts/events/utils/index'; -import Color, { type ColorInstance } from '@ts/m_color'; +import { Color, type ColorInstance } from '@ts/m_color'; import Draggable from '@ts/m_draggable'; import type { EditorProperties, ValueChangedEvent } from '@ts/ui/editor/editor'; import Editor from '@ts/ui/editor/editor'; diff --git a/packages/devextreme/js/__internal/utils/color.helpers.ts b/packages/devextreme/js/__internal/utils/color.helpers.ts new file mode 100644 index 000000000000..c4872379b1ef --- /dev/null +++ b/packages/devextreme/js/__internal/utils/color.helpers.ts @@ -0,0 +1,327 @@ +export function makeColorTint(colorPart: string, h: number): number { + let colorTint = h; + if (colorPart === 'r') { + colorTint = h + 1 / 3; + } + if (colorPart === 'b') { + colorTint = h - 1 / 3; + } + + return colorTint; +} + +export function modifyColorTint(colorTint: number): number { + let tint = colorTint; + if (tint < 0) { + tint += 1; + } + if (tint > 1) { + tint -= 1; + } + + return tint; +} + +export function hueToRgb(p: number, q: number, colorTint: number): number { + const tint = modifyColorTint(colorTint); + if (tint < 1 / 6) { + return p + (q - p) * 6 * tint; + } + if (tint < 1 / 2) { + return q; + } + if (tint < 2 / 3) { + return p + (q - p) * (2 / 3 - tint) * 6; + } + return p; +} + +export function convertTo01Bounds(n: number, max: number): number { + const bounded = Math.min(max, Math.max(0, n)); + if (Math.abs(bounded - max) < 0.000001) { + return 1; + } + return (bounded % max) / max; +} + +export function hsvToRgb(h: number, s: number, v: number): [number, number, number] { + const index = Math.floor((h % 360) / 60); + const vMin = ((100 - s) * v) / 100; + const a = (v - vMin) * ((h % 60) / 60); + const vInc = vMin + a; + const vDec = v - a; + + let r = 0; + let g = 0; + let b = 0; + + switch (index) { + case 0: r = v; g = vInc; b = vMin; break; + case 1: r = vDec; g = v; b = vMin; break; + case 2: r = vMin; g = v; b = vInc; break; + case 3: r = vMin; g = vDec; b = v; break; + case 4: r = vInc; g = vMin; b = v; break; + case 5: r = v; g = vMin; b = vDec; break; + default: + break; + } + + return [Math.round(r * 2.55), Math.round(g * 2.55), Math.round(b * 2.55)]; +} + +export function hslToRgb(h: number, s: number, l: number): [number, number, number] { + let r = 0; + let g = 0; + let b = 0; + + const h01 = convertTo01Bounds(h, 360); + const s01 = convertTo01Bounds(s, 100); + const l01 = convertTo01Bounds(l, 100); + + if (s01 === 0) { + r = l01; + g = l01; + b = l01; + } else { + const q = l01 < 0.5 ? l01 * (1 + s01) : l01 + s01 - l01 * s01; + const p = 2 * l01 - q; + r = hueToRgb(p, q, makeColorTint('r', h01)); + g = hueToRgb(p, q, makeColorTint('g', h01)); + b = hueToRgb(p, q, makeColorTint('b', h01)); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +} + +export type ColorParseResult = | [number, number, number] + | [number, number, number, number] + | [number, number, number, number, [number, number, number]] + | [number, number, number, number, null, [number, number, number]]; + +// array of color definition objects +const standardColorTypes: { + re: RegExp; + process: (colorString: RegExpExecArray) => ColorParseResult; +}[] = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + process(colorString): [number, number, number] { + return [ + parseInt(colorString[1], 10), + parseInt(colorString[2], 10), + parseInt(colorString[3], 10), + ]; + }, + }, + { + re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*\.*\d+)\)$/, + process(colorString): [number, number, number, number] { + return [ + parseInt(colorString[1], 10), + parseInt(colorString[2], 10), + parseInt(colorString[3], 10), + parseFloat(colorString[4]), + ]; + }, + }, + { + re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, + process(colorString): [number, number, number] { + return [ + parseInt(colorString[1], 16), + parseInt(colorString[2], 16), + parseInt(colorString[3], 16), + ]; + }, + }, + { + re: /^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/, + process(colorString): [number, number, number, number] { + return [ + parseInt(colorString[1], 16), + parseInt(colorString[2], 16), + parseInt(colorString[3], 16), + Number((parseInt(colorString[4], 16) / 255).toFixed(2)), + ]; + }, + }, + { + re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, + process(colorString): [number, number, number, number] { + return [ + parseInt(colorString[1] + colorString[1], 16), + parseInt(colorString[2] + colorString[2], 16), + parseInt(colorString[3] + colorString[3], 16), + Number((parseInt(colorString[4] + colorString[4], 16) / 255).toFixed(2)), + ]; + }, + }, + { + re: /^#([a-f0-9]{1})([a-f0-9]{1})([a-f0-9]{1})$/, + process(colorString): [number, number, number] { + return [ + parseInt(colorString[1] + colorString[1], 16), + parseInt(colorString[2] + colorString[2], 16), + parseInt(colorString[3] + colorString[3], 16), + ]; + }, + }, + { + re: /^hsv\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + process(colorString): [number, number, number, number, [number, number, number]] { + const h = parseInt(colorString[1], 10); + const s = parseInt(colorString[2], 10); + const v = parseInt(colorString[3], 10); + const rgb = hsvToRgb(h, s, v); + + return [ + rgb[0], + rgb[1], + rgb[2], + 1, + [h, s, v], + ]; + }, + }, + { + re: /^hsl\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + process(colorString): [number, number, number, number, null, [number, number, number]] { + const h = parseInt(colorString[1], 10); + const s = parseInt(colorString[2], 10); + const l = parseInt(colorString[3], 10); + const rgb = hslToRgb(h, s, l); + + return [ + rgb[0], + rgb[1], + rgb[2], + 1, + null, + [h, s, l], + ]; + }, + }, +]; + +export function parseColor(color: string): ColorParseResult | null { + if (color === 'transparent') { + return [0, 0, 0, 0]; + } + + for (const colorType of standardColorTypes) { + const match = colorType.re.exec(color); + if (match) { + return colorType.process(match); + } + } + return null; +} + +export function normalize(colorComponent: number | undefined, def = 0, max = 255): number { + if (colorComponent === undefined || Number.isNaN(colorComponent) || colorComponent < 0) { + return def; + } + return colorComponent > max ? max : colorComponent; +} + +export function toHexFromRgb(r: number, g: number, b: number): string { + // eslint-disable-next-line no-bitwise + return `#${(0X01000000 | (r << 16) | (g << 8) | b).toString(16).slice(1)}`; +} + +export function toHsvFromRgb(r: number, g: number, b: number): { h: number; s: number; v: number } { + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + let H = 0; + let S = 0; + let V = max; + S = max === 0 ? 0 : 1 - min / max; + + if (max === min) { + H = 0; + } else { + switch (max) { + case r: + H = 60 * ((g - b) / delta); + if (g < b) { + H += 360; + } + break; + case g: + H = 60 * ((b - r) / delta) + 120; + break; + case b: + H = 60 * ((r - g) / delta) + 240; + break; + default: + break; + } + } + + S *= 100; + V *= 100 / 255; + + return { + h: Math.round(H), + s: Math.round(S), + v: Math.round(V), + }; +} + +function calculateHue(r: number, g: number, b: number, delta: number): number { + const max = Math.max(r, g, b); + switch (max) { + case r: + return (g - b) / delta + (g < b ? 6 : 0); + case g: + return (b - r) / delta + 2; + case b: + return (r - g) / delta + 4; + default: + return 0; // unreachable: max is always one of r, g, b + } +} + +export function toHslFromRgb(r: number, g: number, b: number): { h: number; s: number; l: number } { + const r01 = convertTo01Bounds(r, 255); + const g01 = convertTo01Bounds(g, 255); + const b01 = convertTo01Bounds(b, 255); + + const max = Math.max(r01, g01, b01); + const min = Math.min(r01, g01, b01); + const maxMinSum = max + min; + let h = 0; + let s = 0; + const l = maxMinSum / 2; + + if (max === min) { + h = 0; + s = 0; + } else { + const delta = max - min; + + if (l > 0.5) { + s = delta / (2 - maxMinSum); + } else { + s = delta / maxMinSum; + } + + h = calculateHue(r01, g01, b01, delta); + h /= 6; + } + + return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; +} + +export function isIntegerBetweenMinAndMax(value: unknown, min = 0, max = 255): value is number { + if (typeof value !== 'number' + || value % 1 !== 0 + || value < min + || value > max + || isNaN(value)) { + return false; + } + + return true; +} diff --git a/packages/devextreme/js/__internal/viz/palette.ts b/packages/devextreme/js/__internal/viz/palette.ts index 5d722ffcfbe0..e3a291b152aa 100644 --- a/packages/devextreme/js/__internal/viz/palette.ts +++ b/packages/devextreme/js/__internal/viz/palette.ts @@ -19,7 +19,7 @@ import { extend } from '@js/core/utils/extend'; import { isString } from '@js/core/utils/type'; -import Color from '@ts/m_color'; +import { Color } from '@ts/m_color'; import { normalizeEnum } from '@ts/viz/core/utils'; const HIGHLIGHTING_STEP = 50;