diff options
author | Sebastian <sebasjm@gmail.com> | 2022-03-09 14:00:02 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-03-09 14:00:02 -0300 |
commit | 1607c728bca19a003ca08b64b4d2afc73e4d1e2a (patch) | |
tree | a02fc28342cf343cf91b182955cc903915989478 /packages/taler-wallet-webextension/src/mui/colors | |
parent | 6bc244cc1e0e08c4d86161fdf2b6a52b3f9f452e (diff) | |
download | wallet-core-1607c728bca19a003ca08b64b4d2afc73e4d1e2a.tar.xz |
first banner implementation with mui
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui/colors')
3 files changed, 926 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..a6e58caa9 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/colors/constants.ts @@ -0,0 +1,348 @@ +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..77b3ec884 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts @@ -0,0 +1,305 @@ +import { expect } from 'chai'; +import { + recomposeColor, + hexToRgb, + rgbToHex, + hslToRgb, + darken, + decomposeColor, + emphasize, + alpha, + getContrastRatio, + getLuminance, + lighten, +} from './manipulation'; + +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..633c80c94 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts @@ -0,0 +1,273 @@ + +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: number = 0, max: number = 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); + } + + 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) => 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: number = 0.15): string { + return getLuminance(color) > 0.5 ? darken(color, coefficient) : lighten(color, coefficient); +} |