UNPKG

apphouse

Version:

Component library for React that uses observable state management and theme-able components.

1,003 lines (919 loc) 30.3 kB
// /* eslint-disable no-octal */ // /* eslint-disable no-mixed-operators */ import { uuidv4 } from '@firebase/util'; import { isEmpty } from '../../utils/obj/isEmpty'; import { values } from '../../utils/obj/values'; import { ThemeColors } from '../../styles/defaults/themes.interface'; import { Color } from '../Color'; import { Palette } from '../Palette'; import { ensureAllLowerCase, StringUtils } from './string.utils'; import { makeFirstLetterUppercase } from './styles.utils'; import { ApphouseRGBColorFormat, colorsByName } from '../../utils/color/names'; import { ColorDefinition, HslaColor, RgbaColor } from './color.interface'; import { PaletteType } from '../palette.interface'; export const WHITE = 'rgba(255,255,255,1)'; export const BLACK = 'rgba(0,0,0,1)'; export const WHITE_ = 'rgba(255, 255, 255, 1)'; export const BLACK_ = 'rgba(0, 0, 0, 1)'; export function rgba2hex(color: string) { let a; let rgb = color .replace(/\s/g, '') .match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i); let alpha = ((rgb && rgb[4]) || '').trim(); //@ts-ignore let hex = rgb ? //@ts-ignore (rgb[1] | (1 << 8)).toString(16).slice(1) + //@ts-ignore (rgb[2] | (1 << 8)).toString(16).slice(1) + //@ts-ignore (rgb[3] | (1 << 8)).toString(16).slice(1) : color; if (alpha !== '') { a = alpha; } else { a = 1; } // multiply before convert to HEX //@ts-ignore a = ((a * 255) | (1 << 8)).toString(16).slice(1); hex = hex + a; return hex; } export const ensureHexColor = (color: string): string | undefined => { let str = color; if (!str) { return undefined; } if (Array.isArray(str)) { return `linear-gradient(90deg, ${str[0]} 0%, ${str[1]} 100%)`; } if (str.indexOf('!important') >= 0) { str = str.replace('!important', ''); } if (str.startsWith('#')) { if (str.length === 4) { const color = str.split('#'); const threeDigitColor = color.join(''); const updatedColor = `#${threeDigitColor}${threeDigitColor}`; return updatedColor; } return str; } else if (str.startsWith('rgba')) { return rgba2hex(str); } else if (str.startsWith('hsla') || str.startsWith('hsl')) { return hslStringToRgba(str); } else if (str.startsWith('rgb')) { return rgba2hex(str); } else if (str.startsWith('linear-gradient')) { return str; } else if (str.length === 8) { // potential hex color } else { const name = makeFirstLetterUppercase(str); const potentialColor = colorsByName[name]; if (potentialColor) { return potentialColor; } // other color } return undefined; }; export const fromHexStringToRgbaObject = ( color: string ): RgbaColor | undefined => { const rgbString = ColorUtils.toRgbaStringFromHex(color); return ColorUtils.toRgbaObjectFromRgbaString(rgbString); }; export const fromColorStringToRgbaObject = ( color: string ): RgbaColor | undefined => { if (!color) { return undefined; } if (color.startsWith('#')) { const rgbString = ColorUtils.toRgbaStringFromHex(color); return ColorUtils.toRgbaObjectFromRgbaString(rgbString); } else { return ColorUtils.toRgbaObjectFromRgbaString(color); } }; export const fromColorToRgbaString = (color: Color) => { return ColorUtils.toRgbaStringFromRgbaObject(color.color.rgb); }; /** * Convert palette into consumable objects with simple key value pairs * with only the rgba string * @param palette * @returns */ export const objectifyPaletteColorsFlat = ( palette: Palette ): Record<string, string> => { const obj: any = {}; Object.keys(palette.colors).forEach((key) => { // create keys const color = palette.colors[key]; obj[color.id] = fromColorToRgbaString(color); }); return obj; }; export const toColorsObjectFlat = (palettes: Record<string, Palette>) => { const colors: Record<string, Record<string, string>> = {}; values(palettes).forEach((palette) => { if (!colors[palette.id]) { colors[palette.id] = objectifyPaletteColorsFlat(palette); } }); return colors; }; export const toColorsObjectFull = (palettes: Record<string, Palette>) => { const colors: Record<string, PaletteType> = {}; Object.keys(palettes).forEach((paletteId) => { if (!colors[paletteId]) { if (!isEmpty(palettes[paletteId])) { colors[paletteId] = palettes[paletteId].objectify; } } }); return colors; }; export const toApphouseColors = (palette: Palette): ThemeColors => { const palettes: Record<string, Palette> = {}; palettes[palette.id] = palette; // TODO: ensure colors is of theme tokens type const themeColors: any = toColorsObjectFlat(palettes); let colors: any; const colorsForMode = themeColors[palette.mode]; if (colorsForMode) { colors = colorsForMode[palette.id]; } return colors as ThemeColors; }; export function getOpacity(str: string): number { if (str) { const tmp = str.split(',')[3]; if (tmp) { const val = parseFloat(tmp); if (!isNaN(val) && val < 1) { return val; } } } return 1; } export const getColorFromColorString = ( color: string, key: string ): Color | undefined => { if (typeof color === 'string') { // user may be attempting to add a raw color // let's normalize it let isHex = false; let rgb = ColorUtils.toRgbaObjectFromRgbaString(color); if (!rgb) { // let's try hex rgb = fromHexStringToRgbaObject(color); if (rgb) { isHex = true; } } if (rgb) { return new Color({ title: key, id: key, color: { hex: isHex ? ensureFullHex(color) || color : rgba2hex(color), rgb } }); } } return undefined; }; export function ensureFullHex(hex: string) { const color = ensureHexColor(hex); let updatedColor = color; if (color && color.startsWith('#')) { if (color.length === 4) { const hexColor = color.split('#')[1]; if (hexColor.length === 3) { updatedColor = hexColor[0] + hexColor[0] + hexColor[1] + hexColor[1] + hexColor[2] + hexColor[2]; } return `#${updatedColor}`; } } return updatedColor; } /** * * @param rgbaString in the format 'rgba(r,g,b,a)' * @returns */ export function rbgaStringToHex(rgbaString: string): string | undefined { const rgba = ColorUtils.toRgbaObjectFromRgbaString(rgbaString); if (rgba) { const { a } = rgba; if (a === 1) { return ColorUtils.toHexFromRgbaObject(rgba); } else { return rgbaString; } } return rgbaString; } export const hslStringToRgba = (hsl: string): string | undefined => { if (hsl.startsWith('hsla')) { // need to convert to const hslColor = hsl .replace('hsla(', '') .replace(')', '') .split(',') .map((v) => parseFloat(v)); // hsla color const h = hslColor[0]; const s = hslColor[1]; const l = hslColor[2]; const a = hslColor[3]; const rgba = hslToRgba({ h, s, l, a }); return rbgaStringToHex(`rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`); } else if (hsl.startsWith('hsl')) { const hslColor = hsl .replace('hsl(', '') .replace(')', '') .split(',') .map((v) => parseFloat(v)); const h = hslColor[0]; const s = hslColor[1]; const l = hslColor[2]; const a = 1; const rgba = hslToRgba({ h, s, l, a }); return rbgaStringToHex(`rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`); } return ''; }; export const hslToRgba = (hsl: HslaColor): RgbaColor => { let { h, s, l } = hsl; // IMPORTANT if s and l between 0,1 remove the next two lines: s /= 100; l /= 100; const k = (n: number) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return { r: Math.round(255 * f(0)), g: Math.round(255 * f(8)), b: Math.round(255 * f(4)), a: 1 }; }; // /**** FROM HERE TO BOTTOM. OK */ export const getNameForColor = (color: string): string => { if (!color) { return uuidv4(); } return color; }; export const getColorId = (color: string): string => { let name = color; return name; }; export const sortColorsByRgb = ( sortBy: 'r' | 'g' | 'b', colors: ApphouseRGBColorFormat[] ): ApphouseRGBColorFormat[] => { return colors.slice().sort((a, b) => { if (a[sortBy] < b[sortBy]) { return 1; } if (a[sortBy] > b[sortBy]) { return -1; } return 0; }); }; export function moveCursorToEndAndFocus(inputId: string) { const input: HTMLTextAreaElement = document?.getElementById( inputId ) as HTMLTextAreaElement; if (input) { const inputLength = input.value.length; input.setSelectionRange(inputLength, inputLength); input.focus(); } } export default class ColorUtils { /** * Get the complementary color for a given color * A complementary color is a direct opposite of a color on the color wheel * @param color a color in string format * @returns the complementary color in #hex format */ static getComplementaryColor = (color: string): string => { const rgb = ColorUtils.toRgbaObjectFromColorString(color); if (rgb) { const { r, g, b } = rgb; return ColorUtils.toHexFromRgbaObject({ r: 255 - r, g: 255 - g, b: 255 - b, a: 1 }); } return color; }; /** * Get the a significantly lighter/darker shade of a color * @param color a color in string format * @returns */ static getInverseColor = (color: string): string => { const shades = ColorUtils.getSurfaceColors(10, color); const inverse = shades[shades.length - 1]; // if (inverse === BLACK_) { // // went too far // inverse = shades[shades.length - 2]; // } else if (inverse === WHITE_) { // // went too far // inverse = shades[shades.length - 2]; // } return inverse; }; /** * A function to that creates shaded or tinted colors * based on a single color. The way it decides on tinted vs * shaded is based how close the surface color is to "white" or * "black". If it is closer to "white", it creates shades and if its * closer to "black" it creates tints * @param variants number of variants to create * @param color a color used for a background * @returns an array of colors */ static getSurfaceColors = (variants: number, color: string): string[] => { // we check what would its foreground color be based on color const foregroundColor = ColorUtils.getColorForeground(color); const rgba = ColorUtils.toRgbaObjectFromColorString(foregroundColor); if (rgba && rgba?.r === 0) { // this means that foreground color is black // which means the given color is more on the lighter side // we get color shades (the colors will come from lighter to darker) return ColorUtils.getColorShades(color, variants, 0.3); } // The foreground color is white. // The given color is more on the darker side // We get color tints (the colors will come from darker to lighter) return ColorUtils.getColorTints(color, variants, 0.4); }; /** * Pair background colors with their respective foreground colors * @param colors a list of colors to be paired * @returns a list of matching colors, Color[][] where the first color * in the pair is the background and the second color is the foreground */ static getPairedColors = (colors: Color[]): Color[][] => { const onColor: { [wantsToMatchWith: string]: Color[] } = ColorUtils.getOnColors(colors); const matchingPairs: Color[][] = []; Object.keys(onColor).forEach((backgroundColorId) => { const matchingColors = onColor[backgroundColorId as any]; const onColors = matchingColors.filter( (c) => c.id.toLocaleLowerCase() !== backgroundColorId.toLocaleLowerCase() ); const pairWith = colors.filter( (c) => c.id.toLocaleLowerCase() === backgroundColorId.toLocaleLowerCase() ); onColors.forEach((color) => { //Order is important here, the first color is the background and the second color is the foreground matchingPairs.push([pairWith[0], color]); }); }); return matchingPairs; }; /** * Get onColors based on a list of colors * @param colors a list of colors to extract the onColors from * @returns a hashed object containing a list of colors with the key being the color the onColor would pair with */ static getOnColors = ( colors: Color[] ): { [wantsToMatchWith: string]: Color[] } => { const onColor: { [wantsToMatchWith: string]: Color[] } = {}; colors.forEach((color) => { const lowercaseId = ensureAllLowerCase(color.id); if (lowercaseId.startsWith('on')) { // if color has "_" it supposedly means it is a variant/shade or tint from the main color. // let's match that with the appropriate color const id = lowercaseId.split('_')[0]; const wantsToMatchWith = id.replace('on', ''); if (!onColor[wantsToMatchWith]) { onColor[wantsToMatchWith] = [color]; } else { onColor[wantsToMatchWith] = [...onColor[wantsToMatchWith], color]; } } }); return onColor; }; /** * Convert rgba string to rgba object * @param color color in rgba() string format * @returns Rgba Object */ static toRgbaObjectFromRgbaString = ( color: string ): RgbaColor | undefined => { if (color.startsWith('rgba')) { const m = color.match(/(\d+){1}/g); if (m) { return { r: parseInt(m[0]), g: parseInt(m[1]), b: parseInt(m[2]), a: getOpacity(color) }; } return undefined; } if (color.startsWith('rgb')) { const m = color.match(/(\d+){1}/g); if (m) { return { r: parseInt(m[0]), g: parseInt(m[1]), b: parseInt(m[2]), a: 1 }; } } }; /** * Helper function to transform a hex color into RGB numbers into a comma * separated string. Example: #FFFFFF will return '255,255,255' * @param hex color string in hex * @returns string in the format 'number, number, number' */ static toRgbValues = (hex: string): string => { if (typeof hex !== 'string') { console.warn( 'attempted to convert a non-string hex color to an rgba string', hex ); return hex; } if (hex.startsWith('rgba')) { // already an rgb string, let's just split it const rgba = hex.replace('rgba(', '').replace(')', ''); // drop the alpha value return rgba.split(',').slice(0, 3).join(','); } if (hex.startsWith('rgb')) { // already an rgb string, let's just split it return hex.replace('rgb(', '').replace(')', ''); } let hexColor = hex.replace(/^#/, ''); if (hexColor.length === 3) { hexColor = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } const num = parseInt(hexColor, 16); return `${num >> 16}, ${(num >> 8) & 255}, ${num & 255}`; }; /** * Convert hex color to Rgba string * @param hex hex color * @returns rgba string color */ static toRgbaStringFromHex = (hex: string): string => { if (typeof hex !== 'string') { // throw new TypeError("Expected a string"); console.warn( 'attempted to convert a non-string hex color to an rgba string', hex ); return hex; } if (hex.startsWith('rgb')) { // already an rgba color return hex; } const hexColor = ensureFullHex(hex); if (!hexColor) { return BLACK; } const color = hexColor.split('#')[1]; const num = parseInt(color, 16); return `rgba(${num >> 16}, ${(num >> 8) & 255}, ${num & 255}, 1)`; }; /** * Get white or black foreground color based on a background color. * (it not always works when the color has opacity < 1) * @param backgroundColor the background color you want the foreground color for * @returns the foreground color with enough contrast from the original color * it will be either white or black */ static getColorForeground = (backgroundColor?: string): string => { if (!backgroundColor) { console.warn( 'Returned default #ffffff for color foreground, no background color found' ); return '#FFFFFF'; } // we need to find out which color is the brightest and which color is the lightest const rgb = ColorUtils.toRgbaObjectFromColorString(backgroundColor); if (rgb && rgb.r === 255 && rgb.g === 255 && rgb.b === 255) { return BLACK; } if (rgb && rgb.r === 0 && rgb.g === 0 && rgb.b === 0) { return WHITE; } const c2 = ColorUtils.getContrastRatio('#FFFFFF', backgroundColor); return c2 && c2 > 3 ? WHITE : BLACK; }; /** * Convert a color name from theme.dark.colorname to color name * Strips out the "colorname" from theme.dark.colorname * @param colorNameToken a string in the format of theme.colorname or theme.dark.colorname * @returns the last part of the string after the last dot */ static getColorTitleFromTokenString = (colorNameToken: string): string => { const colorName = colorNameToken.split('.'); return colorName[colorName.length - 1]; }; /** * Converts an RGBA oject to a rgba string * @param rgba RgbaColor * @returns string rgba color in the format rgba(r, g, b, a) */ static toRgbaStringFromRgbaObject = ( rgba?: RgbaColor ): string | undefined => { if (!rgba) { // console.warn("no rgba color"); return undefined; } try { const { r, g, b, a } = rgba; const hasRGBColors = r >= 0 && g >= 0 && b >= 0 ? true : false; if (!hasRGBColors) { console.warn('missing r,g,b values'); return undefined; } const parse = (v: number) => Math.round(v); if (hasRGBColors && (!a || a === 1)) { return `rgba(${parse(r)}, ${parse(g)}, ${parse(b)}, 1)`; } return `rgba(${parse(r)}, ${parse(g)}, ${parse(b)}, ${a.toFixed(1)})`; } catch (error) { return undefined; } }; /** * Convert string color to rgba object * @param color color string to be converted, it can be a hex or rgba color * @returns */ static toRgbaObjectFromColorString = ( color: string ): RgbaColor | undefined => { if (!color) { return undefined; } if (color.startsWith('#')) { const rgbString = ColorUtils.toRgbaStringFromHex(color); return ColorUtils.toRgbaObjectFromRgbaString(rgbString); } else if (color.startsWith('rgb')) { return ColorUtils.toRgbaObjectFromRgbaString(color); } else if (color.startsWith('hsl')) { return ColorUtils.toRgbaObjectFromHslaString(color); } }; static toHslaObjectFromHslaString = (hsla: string) => { const hslaArray = hsla.replace('hsla(', '').replace(')', '').split(','); const h = parseFloat(hslaArray[0]); const s = parseFloat(hslaArray[1]); const l = parseFloat(hslaArray[2]); const a = parseFloat(hslaArray[3]); return { h, s, l, a }; }; static toRgbaObjectFromHslaString = (hsla: string): RgbaColor | undefined => { if (!hsla) { return undefined; } if (!hsla.startsWith('hsl')) { console.warn('not a hsla color'); return undefined; } const hslaObj = ColorUtils.toHslaObjectFromHslaString(hsla); const rgba = hslToRgba(hslaObj); return rgba; }; static toHslaObjectFromHex = (hex: string): HslaColor => { const rgbaString = ColorUtils.toRgbaStringFromHex(hex); const rgb = ColorUtils.toRgbaObjectFromRgbaString(rgbaString); if (!rgb) { return { h: 0, s: 0, l: 0, a: 1 }; } const { r, g, b } = rgb; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const l = (max + min) / 2; let h; let s; if (max === min) { h = 0; s = 0; } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } // h && h /= 6; } return { h: h || 0, s, l, a: 1 }; }; static toHslaFromColorString = (colorStr: string): HslaColor => { if (colorStr.startsWith('#')) { // color is a hex color // should convert hex to hsla return ColorUtils.toHslaObjectFromHex(colorStr); } else if (colorStr.startsWith('rgb')) { // color is a hex color // should convert hex to hsla return ColorUtils.toHslaObjectFromHslaString(colorStr); } else if (colorStr.startsWith('hsl')) { // color is a hex color // should convert hex to hsla return ColorUtils.toHslaObjectFromHslaString(colorStr); } return { h: 0, s: 0, l: 0, a: 0 }; }; static toColorDefinitionFromColorString = ( colorStr: string ): ColorDefinition => { return { hex: ColorUtils.toHexFromColorString(colorStr) || '', rgb: ColorUtils.toRgbaObjectFromColorString(colorStr), hsl: ColorUtils.toHslaFromColorString(colorStr) }; }; static toHexFromColorString = (color: string) => { if (!color) { return undefined; } if (color.startsWith('#')) { return color; } if (color.startsWith('rgb')) { const rgba = ColorUtils.toRgbaObjectFromRgbaString(color); return rgba && ColorUtils.toHexFromRgbaObject(rgba); } else if (color.startsWith('hsl')) { const rgba = ColorUtils.toRgbaObjectFromHslaString(color); return rgba && ColorUtils.toHexFromRgbaObject(rgba); } }; static toHexFromRgbaObject = (rgbaObject: RgbaColor) => { const { r, g, b } = rgbaObject; return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); }; /** * Get color shades from a color * A shade is produced by "darkening" a hue or "adding black" * @param color the hex or rgb string color you want shades from * @param variations the number of shades * @param multiplier how close together you want the color shades to be (the lower the number the closer) * @returns an array of color strings with shaded variations of the original color */ static getColorShades = ( color: string, variations: number, multiplier: number = 2 ): string[] => { const colorShades: string[] = []; const colorObject = ColorUtils.toRgbaObjectFromColorString(color); if (!colorObject) { return colorShades; } else { const { r, g, b } = colorObject; // const step = (1 / variations) * multiplier; for (let i = 0; i < variations; i++) { const shade = ColorUtils.toRgbaStringFromRgbaObject({ r: r * (1 - step * i), g: g * (1 - step * i), b: b * (1 - step * i), a: 1 }); if (shade) { colorShades.push(shade); } } } return colorShades; }; /** * Get color tints from a color * A tint is produced by "lightening" a hue or "adding white" * @param color the hex or rgb string color you want shades from * @param variations the number of shades * @param multiplier how close together you want the color tints to be (the lower the number the closer) * @returns an array of color strings with shaded variations of the original color */ static getColorTints = ( color: string, variations: number, multiplier: number = 1 ): string[] => { const colorTints: string[] = []; const colorObject = ColorUtils.toRgbaObjectFromColorString(color); if (!colorObject) { return colorTints; } else { const { r, g, b } = colorObject; // const step = (1 / variations) * multiplier; for (let i = 0; i < variations; i++) { const tint = ColorUtils.toRgbaStringFromRgbaObject({ r: r + (255 - r) * (step * i), g: g + (255 - g) * (step * i), b: b + (255 - b) * (step * i), a: 1 }); if (tint) { colorTints.push(tint); } } } return colorTints; }; /** * Calculate contrast ratio number for colors * @param colors Array of 2 colors, the first color being the background and second the foreground * @returns the contrast ratio number */ static getContrastRatioForColors = (colors: Color[]): number | undefined => { // The latest accessibility guidelines (e.g., WCAG 2.0 1.4.3) require that text // (and images of text) provide adequate contrast for people who have visual impairments. // Contrast is measured using a formula that gives a ratio ranging from 1:1 // (no contrast, e.g., black text on a black background) to 21:1 (maximum contrast, // e.g., black text on a white background). Using this formula, the requirements are: // 3:1 - minimum contrast for "large scale" text (18 pt or 14 pt bold, or larger) under WCAG 2.0 1.4.3 (Level AA) // 4.5:1 - minimum contrast for regular sized text under WCAG 2.0 1.4.3 (Level AA) // 7:1 - "enhanced" contrast for regular sized text under WCAG 2.0 1.4.6 (Level AAA) // FORMULA // contrastRatio = (L1 + 0.05) / (L2 + 0.05), where: // L1 is the relative luminance of the lighter of the colors, and // L2 is the relative luminance of the darker of the colors. if (colors.length < 2) { return undefined; } const background = colors[0]; const foreground = colors[1]; if (!background || !foreground) { return undefined; } const backgroundColor = background.color?.hex; const foregroundColor = foreground.color?.hex; let darkestColor = backgroundColor; let lightestColor = foregroundColor; // we need to find out which color is the brightest and which color is the lightest const b = ColorUtils.getContrastRatio('#FFFFFF', backgroundColor); const f = ColorUtils.getContrastRatio('#FFFFFF', foregroundColor); if (f && b && lightestColor && darkestColor) { // foreground color is darker if (f > b) { darkestColor = foregroundColor; lightestColor = backgroundColor; } const contrastRatio = ColorUtils.getContrastRatio( lightestColor, darkestColor ); if (contrastRatio) { return parseFloat(contrastRatio.toFixed(1)); } } return undefined; }; // Calculating a Contrast Ratio // Contrast ratios can range from 1 to 21 (commonly written 1:1 to 21:1). // (L1 + 0.05) / (L2 + 0.05), whereby: // L1 is the relative luminance of the lighter of the colors, and // L2 is the relative luminance of the darker of the colors. /** * Function to calculate the contrast ratio between two colors. * @param lightestColor * @param darkestColor * @returns */ static getContrastRatio = ( lightestColor: string, darkestColor: string ): number | undefined => { const L1 = ColorUtils.getLuminance(lightestColor); const L2 = ColorUtils.getLuminance(darkestColor); let contrastRatio = 0; if (L1 !== undefined && L2 !== undefined && L1 >= 0 && L2 >= 0) { const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05); contrastRatio = parseFloat(ratio.toFixed(2)); } return contrastRatio; }; /** * Calculate brightness value by RGB or HEX color. * @param color (String) The color value in RGB or HEX (for example: #000000 || #000 || rgb(0,0,0) || rgba(0,0,0,0)) * @returns (Number) The brightness value (dark) 0 ... 255 (light) */ static getLuminance = (color: string): number | undefined => { /** * relative luminance * The relative brightness of any point in a colorspace, normalized to 0 for * darkest black and 1 for lightest white Note 1: For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as: if RsRGB <= 0.03928 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4 if GsRGB <= 0.03928 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4 if BsRGB <= 0.03928 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4 */ const rgba = ColorUtils.toRgbaObjectFromColorString(color); if (rgba) { const { r, g, b } = rgba; // const luminance = (r * 299 + g * 587 + b * 114) / 1000; const luminance = 0.2126 * ColorUtils.getsRGB(r) + 0.7152 * ColorUtils.getsRGB(g) + 0.0722 * ColorUtils.getsRGB(b); return luminance; } return undefined; }; /** * * @param c a number from 0 to 255 * @returns srgb equivalent */ static getsRGB = (c: number) => { c = c / 255; c = c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); return c; }; static getMatchingColorPair = ( color: Color, paletteColors: Color[] ): Color[] => { if (!color || !paletteColors || paletteColors.length === 0) { return []; } if (!color.id) { return []; } if (color.id.startsWith('on')) { const matchingColors: Color[] = []; // color is foreground, matching pair is background paletteColors.forEach((pColor) => { const matchingKey = color.id.replace('on', ''); const key = StringUtils.makeFirstLetterLowercase(matchingKey).split('_')[0]; if (pColor.id === key) { matchingColors.push(pColor); } }); return matchingColors; } else { // color is background, matching pair is foreground const matchingColors: Color[] = []; // color is foreground, matching pair is background paletteColors.forEach((pColor) => { const matchingKey = `on${StringUtils.makeFirstLetterUppercase( color.id )}`; if (pColor.id.split('_')[0] === matchingKey) { matchingColors.push(pColor); } }); return matchingColors; } }; }