summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/mui/colors
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui/colors')
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/constants.ts342
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts333
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.ts328
3 files changed, 1003 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/mui/colors/constants.ts b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
new file mode 100644
index 000000000..0013d6cca
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
@@ -0,0 +1,342 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export const amber = {
+ 50: "#fff8e1",
+ 100: "#ffecb3",
+ 200: "#ffe082",
+ 300: "#ffd54f",
+ 400: "#ffca28",
+ 500: "#ffc107",
+ 600: "#ffb300",
+ 700: "#ffa000",
+ 800: "#ff8f00",
+ 900: "#ff6f00",
+ A100: "#ffe57f",
+ A200: "#ffd740",
+ A400: "#ffc400",
+ A700: "#ffab00",
+};
+
+export const blueGrey = {
+ 50: "#eceff1",
+ 100: "#cfd8dc",
+ 200: "#b0bec5",
+ 300: "#90a4ae",
+ 400: "#78909c",
+ 500: "#607d8b",
+ 600: "#546e7a",
+ 700: "#455a64",
+ 800: "#37474f",
+ 900: "#263238",
+ A100: "#cfd8dc",
+ A200: "#b0bec5",
+ A400: "#78909c",
+ A700: "#455a64",
+};
+
+export const blue = {
+ 50: "#e3f2fd",
+ 100: "#bbdefb",
+ 200: "#90caf9",
+ 300: "#64b5f6",
+ 400: "#42a5f5",
+ 500: "#2196f3",
+ 600: "#1e88e5",
+ 700: "#1976d2",
+ 800: "#1565c0",
+ 900: "#0d47a1",
+ A100: "#82b1ff",
+ A200: "#448aff",
+ A400: "#2979ff",
+ A700: "#2962ff",
+};
+
+export const brown = {
+ 50: "#efebe9",
+ 100: "#d7ccc8",
+ 200: "#bcaaa4",
+ 300: "#a1887f",
+ 400: "#8d6e63",
+ 500: "#795548",
+ 600: "#6d4c41",
+ 700: "#5d4037",
+ 800: "#4e342e",
+ 900: "#3e2723",
+ A100: "#d7ccc8",
+ A200: "#bcaaa4",
+ A400: "#8d6e63",
+ A700: "#5d4037",
+};
+
+export const common = {
+ black: "#000",
+ white: "#fff",
+};
+
+export const cyan = {
+ 50: "#e0f7fa",
+ 100: "#b2ebf2",
+ 200: "#80deea",
+ 300: "#4dd0e1",
+ 400: "#26c6da",
+ 500: "#00bcd4",
+ 600: "#00acc1",
+ 700: "#0097a7",
+ 800: "#00838f",
+ 900: "#006064",
+ A100: "#84ffff",
+ A200: "#18ffff",
+ A400: "#00e5ff",
+ A700: "#00b8d4",
+};
+
+export const deepOrange = {
+ 50: "#fbe9e7",
+ 100: "#ffccbc",
+ 200: "#ffab91",
+ 300: "#ff8a65",
+ 400: "#ff7043",
+ 500: "#ff5722",
+ 600: "#f4511e",
+ 700: "#e64a19",
+ 800: "#d84315",
+ 900: "#bf360c",
+ A100: "#ff9e80",
+ A200: "#ff6e40",
+ A400: "#ff3d00",
+ A700: "#dd2c00",
+};
+
+export const deepPurple = {
+ 50: "#ede7f6",
+ 100: "#d1c4e9",
+ 200: "#b39ddb",
+ 300: "#9575cd",
+ 400: "#7e57c2",
+ 500: "#673ab7",
+ 600: "#5e35b1",
+ 700: "#512da8",
+ 800: "#4527a0",
+ 900: "#311b92",
+ A100: "#b388ff",
+ A200: "#7c4dff",
+ A400: "#651fff",
+ A700: "#6200ea",
+};
+
+export const green = {
+ 50: "#e8f5e9",
+ 100: "#c8e6c9",
+ 200: "#a5d6a7",
+ 300: "#81c784",
+ 400: "#66bb6a",
+ 500: "#4caf50",
+ 600: "#43a047",
+ 700: "#388e3c",
+ 800: "#2e7d32",
+ 900: "#1b5e20",
+ A100: "#b9f6ca",
+ A200: "#69f0ae",
+ A400: "#00e676",
+ A700: "#00c853",
+};
+
+export const grey = {
+ 50: "#fafafa",
+ 100: "#f5f5f5",
+ 200: "#eeeeee",
+ 300: "#e0e0e0",
+ 400: "#bdbdbd",
+ 500: "#9e9e9e",
+ 600: "#757575",
+ 700: "#616161",
+ 800: "#424242",
+ 900: "#212121",
+ A100: "#f5f5f5",
+ A200: "#eeeeee",
+ A400: "#bdbdbd",
+ A700: "#616161",
+};
+
+export const indigo = {
+ 50: "#e8eaf6",
+ 100: "#c5cae9",
+ 200: "#9fa8da",
+ 300: "#7986cb",
+ 400: "#5c6bc0",
+ 500: "#3f51b5",
+ 600: "#3949ab",
+ 700: "#303f9f",
+ 800: "#283593",
+ 900: "#1a237e",
+ A100: "#8c9eff",
+ A200: "#536dfe",
+ A400: "#3d5afe",
+ A700: "#304ffe",
+};
+
+export const lightBlue = {
+ 50: "#e1f5fe",
+ 100: "#b3e5fc",
+ 200: "#81d4fa",
+ 300: "#4fc3f7",
+ 400: "#29b6f6",
+ 500: "#03a9f4",
+ 600: "#039be5",
+ 700: "#0288d1",
+ 800: "#0277bd",
+ 900: "#01579b",
+ A100: "#80d8ff",
+ A200: "#40c4ff",
+ A400: "#00b0ff",
+ A700: "#0091ea",
+};
+
+export const lightGreen = {
+ 50: "#f1f8e9",
+ 100: "#dcedc8",
+ 200: "#c5e1a5",
+ 300: "#aed581",
+ 400: "#9ccc65",
+ 500: "#8bc34a",
+ 600: "#7cb342",
+ 700: "#689f38",
+ 800: "#558b2f",
+ 900: "#33691e",
+ A100: "#ccff90",
+ A200: "#b2ff59",
+ A400: "#76ff03",
+ A700: "#64dd17",
+};
+
+export const lime = {
+ 50: "#f9fbe7",
+ 100: "#f0f4c3",
+ 200: "#e6ee9c",
+ 300: "#dce775",
+ 400: "#d4e157",
+ 500: "#cddc39",
+ 600: "#c0ca33",
+ 700: "#afb42b",
+ 800: "#9e9d24",
+ 900: "#827717",
+ A100: "#f4ff81",
+ A200: "#eeff41",
+ A400: "#c6ff00",
+ A700: "#aeea00",
+};
+
+export const orange = {
+ 50: "#fff3e0",
+ 100: "#ffe0b2",
+ 200: "#ffcc80",
+ 300: "#ffb74d",
+ 400: "#ffa726",
+ 500: "#ff9800",
+ 600: "#fb8c00",
+ 700: "#f57c00",
+ 800: "#ef6c00",
+ 900: "#e65100",
+ A100: "#ffd180",
+ A200: "#ffab40",
+ A400: "#ff9100",
+ A700: "#ff6d00",
+};
+
+export const pink = {
+ 50: "#fce4ec",
+ 100: "#f8bbd0",
+ 200: "#f48fb1",
+ 300: "#f06292",
+ 400: "#ec407a",
+ 500: "#e91e63",
+ 600: "#d81b60",
+ 700: "#c2185b",
+ 800: "#ad1457",
+ 900: "#880e4f",
+ A100: "#ff80ab",
+ A200: "#ff4081",
+ A400: "#f50057",
+ A700: "#c51162",
+};
+
+export const purple = {
+ 50: "#f3e5f5",
+ 100: "#e1bee7",
+ 200: "#ce93d8",
+ 300: "#ba68c8",
+ 400: "#ab47bc",
+ 500: "#9c27b0",
+ 600: "#8e24aa",
+ 700: "#7b1fa2",
+ 800: "#6a1b9a",
+ 900: "#4a148c",
+ A100: "#ea80fc",
+ A200: "#e040fb",
+ A400: "#d500f9",
+ A700: "#aa00ff",
+};
+
+export const red = {
+ 50: "#ffebee",
+ 100: "#ffcdd2",
+ 200: "#ef9a9a",
+ 300: "#e57373",
+ 400: "#ef5350",
+ 500: "#f44336",
+ 600: "#e53935",
+ 700: "#d32f2f",
+ 800: "#c62828",
+ 900: "#b71c1c",
+ A100: "#ff8a80",
+ A200: "#ff5252",
+ A400: "#ff1744",
+ A700: "#d50000",
+};
+
+export const teal = {
+ 50: "#e0f2f1",
+ 100: "#b2dfdb",
+ 200: "#80cbc4",
+ 300: "#4db6ac",
+ 400: "#26a69a",
+ 500: "#009688",
+ 600: "#00897b",
+ 700: "#00796b",
+ 800: "#00695c",
+ 900: "#004d40",
+ A100: "#a7ffeb",
+ A200: "#64ffda",
+ A400: "#1de9b6",
+ A700: "#00bfa5",
+};
+
+export const yellow = {
+ 50: "#fffde7",
+ 100: "#fff9c4",
+ 200: "#fff59d",
+ 300: "#fff176",
+ 400: "#ffee58",
+ 500: "#ffeb3b",
+ 600: "#fdd835",
+ 700: "#fbc02d",
+ 800: "#f9a825",
+ 900: "#f57f17",
+ A100: "#ffff8d",
+ A200: "#ffff00",
+ A400: "#ffea00",
+ A700: "#ffd600",
+};
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
new file mode 100644
index 000000000..78e9d9cf7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
@@ -0,0 +1,333 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { expect } from "chai";
+import {
+ recomposeColor,
+ hexToRgb,
+ rgbToHex,
+ hslToRgb,
+ darken,
+ decomposeColor,
+ emphasize,
+ alpha,
+ getContrastRatio,
+ getLuminance,
+ lighten,
+} from "./manipulation.js";
+
+describe("utils/colorManipulator", () => {
+ describe("recomposeColor", () => {
+ it("converts a decomposed rgb color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "rgb",
+ values: [255, 255, 255],
+ }),
+ ).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("converts a decomposed rgba color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "rgba",
+ values: [255, 255, 255, 0.5],
+ }),
+ ).to.equal("rgba(255, 255, 255, 0.5)");
+ });
+
+ it("converts a decomposed hsl color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "hsl",
+ values: [100, 50, 25],
+ }),
+ ).to.equal("hsl(100, 50%, 25%)");
+ });
+
+ it("converts a decomposed hsla color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "hsla",
+ values: [100, 50, 25, 0.5],
+ }),
+ ).to.equal("hsla(100, 50%, 25%, 0.5)");
+ });
+ });
+
+ describe("hexToRgb", () => {
+ it("converts a short hex color to an rgb color` ", () => {
+ expect(hexToRgb("#9f3")).to.equal("rgb(153, 255, 51)");
+ });
+
+ it("converts a long hex color to an rgb color` ", () => {
+ expect(hexToRgb("#a94fd3")).to.equal("rgb(169, 79, 211)");
+ });
+
+ it("converts a long alpha hex color to an argb color` ", () => {
+ expect(hexToRgb("#111111f8")).to.equal("rgba(17, 17, 17, 0.973)");
+ });
+ });
+
+ describe("rgbToHex", () => {
+ it("converts an rgb color to a hex color` ", () => {
+ expect(rgbToHex("rgb(169, 79, 211)")).to.equal("#a94fd3");
+ });
+
+ it("converts an rgba color to a hex color` ", () => {
+ expect(rgbToHex("rgba(169, 79, 211, 1)")).to.equal("#a94fd3ff");
+ });
+
+ it("idempotent", () => {
+ expect(rgbToHex("#A94FD3")).to.equal("#A94FD3");
+ });
+ });
+
+ describe("hslToRgb", () => {
+ it("converts an hsl color to an rgb color` ", () => {
+ expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
+ });
+
+ it("converts an hsla color to an rgba color` ", () => {
+ expect(hslToRgb("hsla(281, 60%, 57%, 0.5)")).to.equal(
+ "rgba(169, 80, 211, 0.5)",
+ );
+ });
+
+ it("allow to convert values only", () => {
+ expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
+ });
+ });
+
+ describe("decomposeColor", () => {
+ it("converts an rgb color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("rgb(255, 255, 255)");
+ expect(type).to.equal("rgb");
+ expect(values).to.deep.equal([255, 255, 255]);
+ });
+
+ it("converts an rgba color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("rgba(255, 255, 255, 0.5)");
+ expect(type).to.equal("rgba");
+ expect(values).to.deep.equal([255, 255, 255, 0.5]);
+ });
+
+ it("converts an hsl color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("hsl(100, 50%, 25%)");
+ expect(type).to.equal("hsl");
+ expect(values).to.deep.equal([100, 50, 25]);
+ });
+
+ it("converts an hsla color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("hsla(100, 50%, 25%, 0.5)");
+ expect(type).to.equal("hsla");
+ expect(values).to.deep.equal([100, 50, 25, 0.5]);
+ });
+
+ it("converts rgba hex", () => {
+ const decomposed = decomposeColor("#111111f8");
+ expect(decomposed).to.deep.equal({
+ type: "rgba",
+ colorSpace: undefined,
+ values: [17, 17, 17, 0.973],
+ });
+ });
+ });
+
+ describe("getContrastRatio", () => {
+ it("returns a ratio for black : white", () => {
+ expect(getContrastRatio("#000", "#FFF")).to.equal(21);
+ });
+
+ it("returns a ratio for black : black", () => {
+ expect(getContrastRatio("#000", "#000")).to.equal(1);
+ });
+
+ it("returns a ratio for white : white", () => {
+ expect(getContrastRatio("#FFF", "#FFF")).to.equal(1);
+ });
+
+ it("returns a ratio for dark-grey : light-grey", () => {
+ expect(getContrastRatio("#707070", "#E5E5E5")).to.be.approximately(
+ 3.93,
+ 0.01,
+ );
+ });
+
+ it("returns a ratio for black : light-grey", () => {
+ expect(getContrastRatio("#000", "#888")).to.be.approximately(5.92, 0.01);
+ });
+ });
+
+ describe("getLuminance", () => {
+ it("returns a valid luminance for rgb white ", () => {
+ expect(getLuminance("rgba(255, 255, 255)")).to.equal(1);
+ expect(getLuminance("rgb(255, 255, 255)")).to.equal(1);
+ });
+
+ it("returns a valid luminance for rgb mid-grey", () => {
+ expect(getLuminance("rgba(127, 127, 127)")).to.equal(0.212);
+ expect(getLuminance("rgb(127, 127, 127)")).to.equal(0.212);
+ });
+
+ it("returns a valid luminance for an rgb color", () => {
+ expect(getLuminance("rgb(255, 127, 0)")).to.equal(0.364);
+ });
+
+ it("returns a valid luminance from an hsl color", () => {
+ expect(getLuminance("hsl(100, 100%, 50%)")).to.equal(0.735);
+ });
+
+ it("returns an equal luminance for the same color in different formats", () => {
+ const hsl = "hsl(100, 100%, 50%)";
+ const rgb = "rgb(85, 255, 0)";
+ expect(getLuminance(hsl)).to.equal(getLuminance(rgb));
+ });
+ });
+
+ describe("emphasize", () => {
+ it("lightens a dark rgb color with the coefficient provided", () => {
+ expect(emphasize("rgb(1, 2, 3)", 0.4)).to.equal(
+ lighten("rgb(1, 2, 3)", 0.4),
+ );
+ });
+
+ it("darkens a light rgb color with the coefficient provided", () => {
+ expect(emphasize("rgb(250, 240, 230)", 0.3)).to.equal(
+ darken("rgb(250, 240, 230)", 0.3),
+ );
+ });
+
+ it("lightens a dark rgb color with the coefficient 0.15 by default", () => {
+ expect(emphasize("rgb(1, 2, 3)")).to.equal(lighten("rgb(1, 2, 3)", 0.15));
+ });
+
+ it("darkens a light rgb color with the coefficient 0.15 by default", () => {
+ expect(emphasize("rgb(250, 240, 230)")).to.equal(
+ darken("rgb(250, 240, 230)", 0.15),
+ );
+ });
+ });
+
+ describe("alpha", () => {
+ it("converts an rgb color to an rgba color with the value provided", () => {
+ expect(alpha("rgb(1, 2, 3)", 0.4)).to.equal("rgba(1, 2, 3, 0.4)");
+ });
+
+ it("updates an rgba color with the alpha value provided", () => {
+ expect(alpha("rgba(255, 0, 0, 0.2)", 0.5)).to.equal(
+ "rgba(255, 0, 0, 0.5)",
+ );
+ });
+
+ it("converts an hsl color to an hsla color with the value provided", () => {
+ expect(alpha("hsl(0, 100%, 50%)", 0.1)).to.equal(
+ "hsla(0, 100%, 50%, 0.1)",
+ );
+ });
+
+ it("updates an hsla color with the alpha value provided", () => {
+ expect(alpha("hsla(0, 100%, 50%, 0.2)", 0.5)).to.equal(
+ "hsla(0, 100%, 50%, 0.5)",
+ );
+ });
+ });
+
+ describe("darken", () => {
+ it("doesn't modify rgb black", () => {
+ expect(darken("rgb(0, 0, 0)", 0.1)).to.equal("rgb(0, 0, 0)");
+ });
+
+ it("darkens rgb white to black when coefficient is 1", () => {
+ expect(darken("rgb(255, 255, 255)", 1)).to.equal("rgb(0, 0, 0)");
+ });
+
+ it("retains the alpha value in an rgba color", () => {
+ expect(darken("rgba(0, 0, 0, 0.5)", 0.1)).to.equal("rgba(0, 0, 0, 0.5)");
+ });
+
+ it("darkens rgb white by 10% when coefficient is 0.1", () => {
+ expect(darken("rgb(255, 255, 255)", 0.1)).to.equal("rgb(229, 229, 229)");
+ });
+
+ it("darkens rgb red by 50% when coefficient is 0.5", () => {
+ expect(darken("rgb(255, 0, 0)", 0.5)).to.equal("rgb(127, 0, 0)");
+ });
+
+ it("darkens rgb grey by 50% when coefficient is 0.5", () => {
+ expect(darken("rgb(127, 127, 127)", 0.5)).to.equal("rgb(63, 63, 63)");
+ });
+
+ it("doesn't modify rgb colors when coefficient is 0", () => {
+ expect(darken("rgb(255, 255, 255)", 0)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("darkens hsl red by 50% when coefficient is 0.5", () => {
+ expect(darken("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 25%)");
+ });
+
+ it("doesn't modify hsl colors when coefficient is 0", () => {
+ expect(darken("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
+ });
+
+ it("doesn't modify hsl colors when l is 0%", () => {
+ expect(darken("hsl(0, 50%, 0%)", 0.5)).to.equal("hsl(0, 50%, 0%)");
+ });
+ });
+
+ describe("lighten", () => {
+ it("doesn't modify rgb white", () => {
+ expect(lighten("rgb(255, 255, 255)", 0.1)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("lightens rgb black to white when coefficient is 1", () => {
+ expect(lighten("rgb(0, 0, 0)", 1)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("retains the alpha value in an rgba color", () => {
+ expect(lighten("rgba(255, 255, 255, 0.5)", 0.1)).to.equal(
+ "rgba(255, 255, 255, 0.5)",
+ );
+ });
+
+ it("lightens rgb black by 10% when coefficient is 0.1", () => {
+ expect(lighten("rgb(0, 0, 0)", 0.1)).to.equal("rgb(25, 25, 25)");
+ });
+
+ it("lightens rgb red by 50% when coefficient is 0.5", () => {
+ expect(lighten("rgb(255, 0, 0)", 0.5)).to.equal("rgb(255, 127, 127)");
+ });
+
+ it("lightens rgb grey by 50% when coefficient is 0.5", () => {
+ expect(lighten("rgb(127, 127, 127)", 0.5)).to.equal("rgb(191, 191, 191)");
+ });
+
+ it("doesn't modify rgb colors when coefficient is 0", () => {
+ expect(lighten("rgb(127, 127, 127)", 0)).to.equal("rgb(127, 127, 127)");
+ });
+
+ it("lightens hsl red by 50% when coefficient is 0.5", () => {
+ expect(lighten("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 75%)");
+ });
+
+ it("doesn't modify hsl colors when coefficient is 0", () => {
+ expect(lighten("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
+ });
+
+ it("doesn't modify hsl colors when `l` is 100%", () => {
+ expect(lighten("hsl(0, 50%, 100%)", 0.5)).to.equal("hsl(0, 50%, 100%)");
+ });
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
new file mode 100644
index 000000000..f9bf9eb2b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
@@ -0,0 +1,328 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha;
+export type ColorFormatWithAlpha = "rgb" | "hsl";
+export type ColorFormatWithoutAlpha = "rgba" | "hsla";
+export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha;
+export interface ColorObjectWithAlpha {
+ type: ColorFormatWithAlpha;
+ values: [number, number, number];
+ colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
+}
+export interface ColorObjectWithoutAlpha {
+ type: ColorFormatWithoutAlpha;
+ values: [number, number, number, number];
+ colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
+}
+
+/**
+ * Returns a number whose value is limited to the given range.
+ * @param {number} value The value to be clamped
+ * @param {number} min The lower boundary of the output range
+ * @param {number} max The upper boundary of the output range
+ * @returns {number} A number in the range [min, max]
+ */
+function clamp(value: number, min = 0, max = 1): number {
+ // if (process.env.NODE_ENV !== 'production') {
+ // if (value < min || value > max) {
+ // console.error(`MUI: The value provided ${value} is out of range [${min}, ${max}].`);
+ // }
+ // }
+
+ return Math.min(Math.max(min, value), max);
+}
+
+/**
+ * Converts a color from CSS hex format to CSS rgb format.
+ * @param {string} color - Hex color, i.e. #nnn or #nnnnnn
+ * @returns {string} A CSS rgb color string
+ */
+export function hexToRgb(color: string): string {
+ color = color.substr(1);
+
+ const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, "g");
+ let colors = color.match(re);
+
+ if (colors && colors[0].length === 1) {
+ colors = colors.map((n) => n + n) as RegExpMatchArray;
+ }
+
+ return colors
+ ? `rgb${colors.length === 4 ? "a" : ""}(${colors
+ .map((n, index) => {
+ return index < 3
+ ? parseInt(n, 16)
+ : Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
+ })
+ .join(", ")})`
+ : "";
+}
+
+function intToHex(int: number): string {
+ const hex = int.toString(16);
+ return hex.length === 1 ? `0${hex}` : hex;
+}
+
+/**
+ * Returns an object with the type and values of a color.
+ *
+ * Note: Does not support rgb % values.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @returns {object} - A MUI color object: {type: string, values: number[]}
+ */
+export function decomposeColor(color: string): ColorObject {
+ const colorSpace = undefined;
+ if (color.charAt(0) === "#") {
+ return decomposeColor(hexToRgb(color));
+ }
+
+ const marker = color.indexOf("(");
+ const type = color.substring(0, marker);
+ // if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') {
+ // }
+
+ const values = color.substring(marker + 1, color.length - 1).split(",");
+ if (type == "rgb" || type == "hsl") {
+ return {
+ type,
+ colorSpace,
+ values: [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ ],
+ };
+ }
+ if (type == "rgba" || type == "hsla") {
+ return {
+ type,
+ colorSpace,
+ values: [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ parseFloat(values[3]),
+ ],
+ };
+ }
+ throw new Error(
+ `Unsupported '${color}' color. The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()`,
+ );
+}
+
+/**
+ * Converts a color object with type and values to a string.
+ * @param {object} color - Decomposed color
+ * @param {string} color.type - One of: 'rgb', 'rgba', 'hsl', 'hsla'
+ * @param {array} color.values - [n,n,n] or [n,n,n,n]
+ * @returns {string} A CSS color string
+ */
+export function recomposeColor(color: ColorObject): string {
+ const { type, values: valuesNum } = color;
+
+ const valuesStr: string[] = [];
+ if (type.indexOf("rgb") !== -1) {
+ // Only convert the first 3 values to int (i.e. not alpha)
+ valuesNum
+ .map((n, i) => (i < 3 ? parseInt(String(n), 10) : n))
+ .forEach((n, i) => (valuesStr[i] = String(n)));
+ } else if (type.indexOf("hsl") !== -1) {
+ valuesStr[0] = String(valuesNum[0]);
+ valuesStr[1] = `${valuesNum[1]}%`;
+ valuesStr[2] = `${valuesNum[2]}%`;
+ if (type === "hsla") {
+ valuesStr[3] = String(valuesNum[3]);
+ }
+ }
+
+ return `${type}(${valuesStr.join(", ")})`;
+}
+
+/**
+ * Converts a color from CSS rgb format to CSS hex format.
+ * @param {string} color - RGB color, i.e. rgb(n, n, n)
+ * @returns {string} A CSS rgb color string, i.e. #nnnnnn
+ */
+export function rgbToHex(color: string): string {
+ // Idempotent
+ if (color.indexOf("#") === 0) {
+ return color;
+ }
+
+ const { values } = decomposeColor(color);
+ return `#${values
+ .map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : n))
+ .join("")}`;
+}
+
+/**
+ * Converts a color from hsl format to rgb format.
+ * @param {string} color - HSL color values
+ * @returns {string} rgb color values
+ */
+export function hslToRgb(color: string): string {
+ const colorObj = decomposeColor(color);
+ const { values } = colorObj;
+ const h = values[0];
+ const s = values[1] / 100;
+ const l = values[2] / 100;
+ const a = s * Math.min(l, 1 - l);
+ const f = (n: number, k = (n + h / 30) % 12): number =>
+ l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
+
+ if (colorObj.type === "hsla") {
+ return recomposeColor({
+ type: "rgba",
+ values: [
+ Math.round(f(0) * 255),
+ Math.round(f(8) * 255),
+ Math.round(f(4) * 255),
+ colorObj.values[3],
+ ],
+ });
+ }
+
+ return recomposeColor({
+ type: "rgb",
+ values: [
+ Math.round(f(0) * 255),
+ Math.round(f(8) * 255),
+ Math.round(f(4) * 255),
+ ],
+ });
+}
+/**
+ * The relative brightness of any point in a color space,
+ * normalized to 0 for darkest black and 1 for lightest white.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @returns {number} The relative brightness of the color in the range 0 - 1
+ */
+export function getLuminance(color: string): number {
+ const colorObj = decomposeColor(color);
+
+ const rgb2 =
+ colorObj.type === "hsl"
+ ? decomposeColor(hslToRgb(color)).values
+ : colorObj.values;
+ const rgb = rgb2.map((val) => {
+ val /= 255; // normalized
+ return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
+ }) as typeof rgb2;
+
+ // Truncate at 3 digits
+ return Number(
+ (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3),
+ );
+}
+
+/**
+ * Calculates the contrast ratio between two colors.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @returns {number} A contrast ratio value in the range 0 - 21.
+ */
+export function getContrastRatio(
+ foreground: string,
+ background: string,
+): number {
+ const lumA = getLuminance(foreground);
+ const lumB = getLuminance(background);
+ return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
+}
+
+/**
+ * Sets the absolute transparency of a color.
+ * Any existing alpha values are overwritten.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} value - value to set the alpha channel to in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function alpha(color: string, value: number): string {
+ const colorObj = decomposeColor(color);
+ value = clamp(value);
+
+ if (colorObj.type === "rgb" || colorObj.type === "hsl") {
+ colorObj.type += "a";
+ }
+ colorObj.values[3] = value;
+
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Darkens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function darken(color: string, coefficient: number): string {
+ const colorObj = decomposeColor(color);
+ coefficient = clamp(coefficient);
+
+ if (colorObj.type.indexOf("hsl") !== -1) {
+ colorObj.values[2] *= 1 - coefficient;
+ } else if (
+ colorObj.type.indexOf("rgb") !== -1 ||
+ colorObj.type.indexOf("color") !== -1
+ ) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] *= 1 - coefficient;
+ }
+ }
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Lightens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function lighten(color: string, coefficient: number): string {
+ const colorObj = decomposeColor(color);
+ coefficient = clamp(coefficient);
+
+ if (colorObj.type.indexOf("hsl") !== -1) {
+ colorObj.values[2] += (100 - colorObj.values[2]) * coefficient;
+ } else if (colorObj.type.indexOf("rgb") !== -1) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] += (255 - colorObj.values[i]) * coefficient;
+ }
+ } else if (colorObj.type.indexOf("color") !== -1) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] += (1 - colorObj.values[i]) * coefficient;
+ }
+ }
+
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Darken or lighten a color, depending on its luminance.
+ * Light colors are darkened, dark colors are lightened.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient=0.15 - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function emphasize(color: string, coefficient = 0.15): string {
+ return getLuminance(color) > 0.5
+ ? darken(color, coefficient)
+ : lighten(color, coefficient);
+}