diff --git a/src/common/color/convert-color.ts b/src/common/color/convert-color.ts index 40d7e63311..5afac322e8 100644 --- a/src/common/color/convert-color.ts +++ b/src/common/color/convert-color.ts @@ -90,9 +90,9 @@ export const lab2rgb = ( x = Xn * lab_xyz(x); z = Zn * lab_xyz(z); - const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB - const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z); - const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z); + const r = Math.round(xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z)); // D65 -> sRGB + const g = Math.round(xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z)); + const b_ = Math.round(xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z)); return [r, g, b_]; }; diff --git a/src/common/color/convert-light-color.ts b/src/common/color/convert-light-color.ts index 5834151bf6..db4bf38f67 100644 --- a/src/common/color/convert-light-color.ts +++ b/src/common/color/convert-light-color.ts @@ -8,9 +8,9 @@ export const temperature2rgb = ( ): [number, number, number] => { const value = temperature / 100; return [ - temperatureRed(value), - temperatureGreen(value), - temperatureBlue(value), + Math.round(temperatureRed(value)), + Math.round(temperatureGreen(value)), + Math.round(temperatureBlue(value)), ]; }; @@ -59,10 +59,10 @@ const matchMaxScale = ( }; export const mired2kelvin = (miredTemperature: number) => - Math.floor(1000000 / miredTemperature); + miredTemperature === 0 ? 1000000 : Math.floor(1000000 / miredTemperature); -export const kelvin2mired = (kelvintTemperature: number) => - Math.floor(1000000 / kelvintTemperature); +export const kelvin2mired = (kelvinTemperature: number) => + kelvinTemperature === 0 ? 1000000 : Math.floor(1000000 / kelvinTemperature); export const rgbww2rgb = ( rgbww: [number, number, number, number, number], diff --git a/src/common/color/hex.ts b/src/common/color/hex.ts index a25b6d6442..af394ed85f 100644 --- a/src/common/color/hex.ts +++ b/src/common/color/hex.ts @@ -14,8 +14,8 @@ export const hexBlend = (c1: string, c2: string, blend = 50): string => { c1 = expandHex(c1); c2 = expandHex(c2); for (let i = 0; i <= 5; i += 2) { - const h1 = parseInt(c1.substr(i, 2), 16); - const h2 = parseInt(c2.substr(i, 2), 16); + const h1 = parseInt(c1.substring(i, i + 2), 16); + const h2 = parseInt(c2.substring(i, i + 2), 16); let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16); while (hex.length < 2) hex = "0" + hex; color += hex; diff --git a/src/common/color/lab.ts b/src/common/color/lab.ts index 4ba5558ad3..29dd50e61d 100644 --- a/src/common/color/lab.ts +++ b/src/common/color/lab.ts @@ -1,12 +1,13 @@ // From https://github.com/gka/chroma.js // Copyright (c) 2011-2019, Gregor Aisch -export const labDarken = ( - lab: [number, number, number], - amount = 1 -): [number, number, number] => [lab[0] - 18 * amount, lab[1], lab[2]]; +export type LabColor = [number, number, number]; -export const labBrighten = ( - lab: [number, number, number], - amount = 1 -): [number, number, number] => labDarken(lab, -amount); +export const labDarken = (lab: LabColor, amount = 1): LabColor => [ + lab[0] - 18 * amount, + lab[1], + lab[2], +]; + +export const labBrighten = (lab: LabColor, amount = 1): LabColor => + labDarken(lab, -amount); diff --git a/test/common/color/colors.test.ts b/test/common/color/colors.test.ts new file mode 100644 index 0000000000..44c4a4f71e --- /dev/null +++ b/test/common/color/colors.test.ts @@ -0,0 +1,35 @@ +import { describe, test, expect } from "vitest"; +import { + getColorByIndex, + getGraphColorByIndex, + COLORS, +} from "../../../src/common/color/colors"; +import { theme2hex } from "../../../src/common/color/convert-color"; + +describe("getColorByIndex", () => { + test("return the correct color for a given index", () => { + expect(getColorByIndex(0)).toBe(COLORS[0]); + expect(getColorByIndex(10)).toBe(COLORS[10]); + }); + + test("wrap around if the index is greater than the length of COLORS", () => { + expect(getColorByIndex(COLORS.length)).toBe(COLORS[0]); + expect(getColorByIndex(COLORS.length + 4)).toBe(COLORS[4]); + }); +}); + +describe("getGraphColorByIndex", () => { + test("return the correct theme color if it exists", () => { + const style = { + getPropertyValue: (prop) => (prop === "--graph-color-1" ? "#123456" : ""), + } as CSSStyleDeclaration; + expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#123456")); + }); + + test("return the default color if the theme color does not exist", () => { + const style = { + getPropertyValue: () => "", + } as unknown as CSSStyleDeclaration; + expect(getGraphColorByIndex(0, style)).toBe(theme2hex(COLORS[0])); + }); +}); diff --git a/test/common/color/compute-color.test.ts b/test/common/color/compute-color.test.ts new file mode 100644 index 0000000000..c704982f4d --- /dev/null +++ b/test/common/color/compute-color.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; +import { + computeCssColor, + THEME_COLORS, +} from "../../../src/common/color/compute-color"; + +describe("computeCssColor", () => { + it("should return CSS variable for theme colors", () => { + THEME_COLORS.forEach((color) => { + expect(computeCssColor(color)).toBe(`var(--${color}-color)`); + }); + }); + + it("should return the input color if it is not a theme color", () => { + const nonThemeColor = "non-theme-color"; + expect(computeCssColor(nonThemeColor)).toBe(nonThemeColor); + }); +}); diff --git a/test/common/color/convert-color.test.ts b/test/common/color/convert-color.test.ts new file mode 100644 index 0000000000..c04e995b2c --- /dev/null +++ b/test/common/color/convert-color.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { + hex2rgb, + rgb2hex, + rgb2lab, + lab2rgb, + lab2hex, + rgb2hsv, + hsv2rgb, + rgb2hs, + hs2rgb, + theme2hex, +} from "../../../src/common/color/convert-color"; + +describe("Color Conversion Tests", () => { + it("should convert hex to rgb", () => { + expect(hex2rgb("#ffffff")).toEqual([255, 255, 255]); + expect(hex2rgb("#000000")).toEqual([0, 0, 0]); + }); + + it("should convert rgb to hex", () => { + expect(rgb2hex([255, 255, 255])).toBe("#ffffff"); + expect(rgb2hex([0, 0, 0])).toBe("#000000"); + }); + + it("should convert rgb to lab and back", () => { + const rgb: [number, number, number] = [12, 206, 7]; + const lab = rgb2lab(rgb); + expect(lab2rgb(lab)).toEqual(rgb); + }); + + it("should convert lab to hex", () => { + const lab: [number, number, number] = [53.23288, 80.10933, 67.22006]; + expect(lab2hex(lab)).toBe("#ff0000"); + }); + + it("should convert rgb to hsv and back", () => { + const rgb: [number, number, number] = [255, 0, 0]; + const hsv = rgb2hsv(rgb); + expect(hsv2rgb(hsv)).toEqual(rgb); + }); + + it("should convert rgb to hs and back", () => { + const rgb: [number, number, number] = [255, 0, 0]; + const hs = rgb2hs(rgb); + expect(hs2rgb(hs)).toEqual([255, 0, 0]); + }); + + it("should convert theme color to hex", () => { + expect(theme2hex("red")).toBe("#ff0000"); + expect(theme2hex("#ff0000")).toBe("#ff0000"); + expect(theme2hex("unicorn")).toBe("unicorn"); + }); +}); diff --git a/test/common/color/convert-light-color.test.ts b/test/common/color/convert-light-color.test.ts new file mode 100644 index 0000000000..8585ba9893 --- /dev/null +++ b/test/common/color/convert-light-color.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { + temperature2rgb, + mired2kelvin, + kelvin2mired, + rgbww2rgb, + rgbw2rgb, +} from "../../../src/common/color/convert-light-color"; + +describe("temperature2rgb", () => { + it("should convert temperature to RGB", () => { + expect(temperature2rgb(6600)).toEqual([255, 255, 255]); + expect(temperature2rgb(6601)).toEqual([255, 252, 255]); + expect(temperature2rgb(1900)).toEqual([255, 132, 0]); + }); +}); + +describe("mired2kelvin", () => { + it("should convert mired to kelvin", () => { + expect(mired2kelvin(20)).toBe(50000); + expect(mired2kelvin(0)).toBe(1000000); + }); +}); + +describe("kelvin2mired", () => { + it("should convert kelvin to mired", () => { + expect(kelvin2mired(6500)).toBe(153); + expect(kelvin2mired(2700)).toBe(370); + expect(kelvin2mired(0)).toBe(1000000); + }); +}); + +describe("rgbww2rgb", () => { + it("should convert RGBWW to RGB", () => { + expect(rgbww2rgb([255, 0, 0, 255, 0])).toEqual([255, 128, 126]); + expect(rgbww2rgb([0, 255, 0, 0, 255])).toEqual([154, 255, 53]); + expect(rgbww2rgb([255, 0, 0, 255, 128], 1000)).toEqual([255, 75, 25]); + expect(rgbww2rgb([255, 0, 0, 255, 128], undefined, 5000)).toEqual([ + 255, 102, 81, + ]); + expect(rgbww2rgb([255, 0, 0, 255, 128], 3000, 4000)).toEqual([255, 98, 73]); + }); +}); + +describe("rgbw2rgb", () => { + it("should convert RGBW to RGB", () => { + expect(rgbw2rgb([255, 0, 0, 255])).toEqual([255, 128, 128]); + expect(rgbw2rgb([0, 255, 0, 0])).toEqual([0, 255, 0]); + }); +}); diff --git a/test/common/color/hex.test.ts b/test/common/color/hex.test.ts new file mode 100644 index 0000000000..08e01f5d03 --- /dev/null +++ b/test/common/color/hex.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { expandHex, hexBlend } from "../../../src/common/color/hex"; + +describe("expandHex", () => { + it("should expand a 3-digit hex code to 6 digits", () => { + expect(expandHex("#abc")).toBe("aabbcc"); + }); + + it("should return a 6-digit hex code unchanged", () => { + expect(expandHex("#abcdef")).toBe("abcdef"); + }); +}); + +describe("hexBlend", () => { + it("should blend two hex colors with default blend value", () => { + expect(hexBlend("#000000", "#ffffff")).toBe("#7f7f7f"); + }); + + it("should blend two hex colors with a specified blend value", () => { + expect(hexBlend("#ff0000", "#0000ff", 25)).toBe("#3f00bf"); + }); + + it("should return the first color if blend is 100", () => { + expect(hexBlend("#ff0000", "#0000ff", 100)).toBe("#ff0000"); + }); + + it("should return the second color if blend is 0", () => { + expect(hexBlend("#ff0000", "#0000ff", 0)).toBe("#0000ff"); + }); +}); diff --git a/test/common/color/lab.test.ts b/test/common/color/lab.test.ts new file mode 100644 index 0000000000..3824d5d5b0 --- /dev/null +++ b/test/common/color/lab.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { + labDarken, + labBrighten, + type LabColor, +} from "../../../src/common/color/lab"; + +describe("labDarken", () => { + it("should darken the color by the default amount", () => { + const lab: LabColor = [50, 20, 30]; + const result = labDarken(lab); + expect(result).toEqual([32, 20, 30]); + }); + + it("should darken the color by a specified amount", () => { + const lab: LabColor = [50, 20, 30]; + const result = labDarken(lab, 2); + expect(result).toEqual([14, 20, 30]); + }); +}); + +describe("labBrighten", () => { + it("should brighten the color by the default amount", () => { + const lab: LabColor = [50, 20, 30]; + const result = labBrighten(lab); + expect(result).toEqual([68, 20, 30]); + }); + + it("should brighten the color by a specified amount", () => { + const lab: LabColor = [50, 20, 30]; + const result = labBrighten(lab, 2); + expect(result).toEqual([86, 20, 30]); + }); +}); diff --git a/test/common/color/rgb.test.ts b/test/common/color/rgb.test.ts new file mode 100644 index 0000000000..fceeada00b --- /dev/null +++ b/test/common/color/rgb.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { + luminosity, + rgbContrast, + getRGBContrastRatio, +} from "../../../src/common/color/rgb"; + +describe("luminosity", () => { + it("calculates the correct luminosity for black", () => { + expect(luminosity([0, 0, 0])).toBe(0); + }); + + it("calculates the correct luminosity for white", () => { + expect(luminosity([255, 255, 255])).toBe(1); + }); + + it("calculates the correct luminosity for red", () => { + expect(luminosity([255, 0, 0])).toBe(0.2126); + }); +}); + +describe("rgbContrast", () => { + it("calculates the correct contrast ratio between black and white", () => { + expect(rgbContrast([0, 0, 0], [255, 255, 255])).toBe(21); + expect(rgbContrast([255, 255, 255], [0, 0, 0])).toBe(21); + }); + + it("calculates the correct contrast ratio between red and white", () => { + expect(rgbContrast([255, 0, 0], [255, 255, 255])).toBeCloseTo(4); + }); +}); + +describe("getRGBContrastRatio", () => { + it("calculates the correct rounded contrast ratio between black and white", () => { + expect(getRGBContrastRatio([0, 0, 0], [255, 255, 255])).toBe(21); + }); + + it("calculates the correct rounded contrast ratio between red and white", () => { + expect(getRGBContrastRatio([255, 0, 0], [255, 255, 255])).toBe(4); + }); +});