diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui/colors')
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); +} |