apphouse
Version:
Component library for React that uses observable state management and theme-able components.
1,003 lines (919 loc) • 30.3 kB
text/typescript
// /* 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;
}
};
}