diff --git a/packages/devextreme/js/__internal/m_color.ts b/packages/devextreme/js/__internal/m_color.ts index eed05fea3f1c..9710cd9beecc 100644 --- a/packages/devextreme/js/__internal/m_color.ts +++ b/packages/devextreme/js/__internal/m_color.ts @@ -154,11 +154,117 @@ 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]]; + +// 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 = [ +const standardColorTypes: { + re: RegExp; + process: (colorString: RegExpExecArray) => ColorParseResult; +}[] = [ { 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), @@ -168,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), @@ -179,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), @@ -189,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), @@ -200,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), @@ -211,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), @@ -221,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); @@ -238,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); @@ -256,92 +362,38 @@ 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 { + // eslint-disable-next-line no-bitwise 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,32 +428,7 @@ function toHsvFromRgb(r, g, b) { }; } -function hsvToRgb(h, s, v) { - 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; - - 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, 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 +438,25 @@ 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 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,155 +466,136 @@ 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) { - let colorTint = h; - if (colorPart === 'r') { - colorTint = h + 1 / 3; - } - if (colorPart === 'b') { - colorTint = h - 1 / 3; +function isIntegerBetweenMinAndMax(number: number | undefined, min = 0, max = 255): boolean { + if (typeof number !== 'number' + || number % 1 !== 0 + || number < min + || number > max + || isNaN(number)) { + return false; } - return colorTint; + return true; } -function modifyColorTint(colorTint) { - if (colorTint < 0) { - colorTint += 1; - } - if (colorTint > 1) { - colorTint -= 1; - } +export class Color { + baseColor: string | undefined; - return colorTint; -} + r!: number; -function hueToRgb(p, q, colorTint) { - colorTint = modifyColorTint(colorTint); - if (colorTint < 1 / 6) { - return p + (q - p) * 6 * colorTint; - } - if (colorTint < 1 / 2) { - return q; - } - if (colorTint < 2 / 3) { - return p + (q - p) * (2 / 3 - colorTint) * 6; - } - return p; -} + g!: number; -function hslToRgb(h, s, l) { - let r; - let g; - let b; + b!: number; - h = convertTo01Bounds(h, 360); - s = convertTo01Bounds(s, 100); - l = convertTo01Bounds(l, 100); + a!: number; - if (s === 0) { - r = g = b = l; - } 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)); - } + hsv!: { h: number; s: number; v: number }; - return [_round(r * 255), _round(g * 255), _round(b * 255)]; -} + hsl!: { h: number; s: number; l: number }; -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); -} + colorIsInvalid = false; -function isIntegerBetweenMinAndMax(number, min?, max?) { - min = min || 0; - max = max || 255; + 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; + } - if (typeof number !== 'number' - || number % 1 !== 0 - || number < min - || number > max - || isNaN(number)) { - return false; + const parts = color ?? []; + 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] }; + } 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); + } } - return true; -} - -Color.prototype = { - constructor: Color, - - highlight(step) { - step = step || 10; - return this.alter(step).toHex(); - }, + highlight(step?: number): string { + const stepValue = step ?? 10; + return this.alter(stepValue).toHex(); + } - darken(step) { - step = step || 10; - return this.alter(-step).toHex(); - }, + darken(step?: number): string { + const stepValue = step ?? 10; + return this.alter(-stepValue).toHex(); + } - alter(step) { + 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 +607,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 { 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); });