UNPKG

@eccenca/gui-elements

Version:

GUI elements based on other libraries, usable in React application, written in Typescript.

196 lines (168 loc) 6.91 kB
import Color from "color"; import { CLASSPREFIX as eccgui, COLORMINDISTANCE } from "../../configuration/constants"; import { colorCalculateDistance } from "./colorCalculateDistance"; import CssCustomProperties from "./CssCustomProperties"; type ColorOrFalse = Color | false; type ColorWeight = 100 | 300 | 500 | 700 | 900; type PaletteGroup = "identity" | "semantic" | "layout" | "extra"; interface getEnabledColorsProps { /** Specify the palette groups used to define the set of colors. */ includePaletteGroup?: PaletteGroup[]; /** Use only some weights of a color tint. */ includeColorWeight?: ColorWeight[]; /** Only keep colors in the stack with a minimal color distance to all other colors. */ minimalColorDistance?: number; /** Extend color stack by values generated by mixing tints of the same weight, e.g. `yellow100` with `purple100`. */ // includeMixedColors?: boolean; } const getEnabledColorsFromPaletteCache = new Map<string, Color[]>(); export function getEnabledColorsFromPalette({ includePaletteGroup = ["layout"], includeColorWeight = [100, 300, 500, 700, 900], // TODO (planned for later): includeMixedColors = false, minimalColorDistance = COLORMINDISTANCE, }: getEnabledColorsProps): Color[] { const configId = JSON.stringify({ includePaletteGroup, includeColorWeight, }); if (getEnabledColorsFromPaletteCache.has(configId)) { return getEnabledColorsFromPaletteCache.get(configId)!; } const colorsFromPalette = new CssCustomProperties({ selectorText: `:root`, filterName: (name: string) => { if (!name.includes(`--${eccgui}-color-palette-`)) { // only allow custom properties created for the palette return false; } // test for correct group and weight of the palette color const tint = name.substring(`--${eccgui}-color-palette-`.length).split("-"); const group = tint[0] as PaletteGroup; const weight = parseInt(tint[2], 10) as ColorWeight; return includePaletteGroup.includes(group) && includeColorWeight.includes(weight); }, removeDashPrefix: false, returnObject: true, }).customProperties(); const colorsFromPaletteValues = Object.values(colorsFromPalette) as string[]; const colorsFromPaletteWithEnoughDistance = minimalColorDistance > 0 ? colorsFromPaletteValues.reduce((enoughDistance: string[], color: string) => { if (enoughDistance.includes(color)) { return enoughDistance.filter((checkColor) => { const distance = colorCalculateDistance({ color1: color, color2: checkColor }); return checkColor === color || (distance && minimalColorDistance <= distance); }); } else { return enoughDistance; } }, colorsFromPaletteValues) : colorsFromPaletteValues; getEnabledColorsFromPaletteCache.set( configId, colorsFromPaletteWithEnoughDistance.map((color: string) => { return Color(color); }) ); return getEnabledColorsFromPaletteCache.get(configId)!; } function getColorcode(text: string): ColorOrFalse { try { return Color(text); } catch { return false; } } interface textToColorOptions { /** Stack of colors that are allowed to be returned. */ enabledColors: Color[] | "all" | getEnabledColorsProps; /** Return input text if it represents a valid color string, e.g. `#000` or `black`. */ returnValidColorsDirectly: boolean; } interface textToColorProps { text: string; options?: textToColorOptions; } /** * Map a text string to a color. * It always returns the same color for a text as long as the options stay the same. * It returns `false` in case there are no colors defined to chose from. */ export function textToColorHash({ text, options = { enabledColors: getEnabledColorsFromPalette({}), returnValidColorsDirectly: false, }, }: textToColorProps): string | false { let color = getColorcode(text); if (options.returnValidColorsDirectly && color) { // return color code for text because it was a valid color string return color.hex().toString(); } if (!color) { color = getColorcode(stringToHexColorHash(text)) as Color; } if (options.enabledColors === "all" && color) { // all colors are allowed as return value return color.hex().toString(); } let enabledColors = [] as Color[]; if (Array.isArray(options.enabledColors)) { enabledColors = options.enabledColors; } else { enabledColors = getEnabledColorsFromPalette(options.enabledColors as getEnabledColorsProps); } if (enabledColors.length === 0) { // eslint-disable-next-line no-console console.warn("textToColorHash functionaliy need enabledColors list with at least 1 color."); return false; } return nearestColorNeighbour(color, enabledColors as Color[]) .hex() .toString(); } function stringToIntegerHash(inputString: string): number { /* this function is idempotend, meaning it retrieves the same result for the same input no matter how many times it's called */ // Convert the string to a hash code let hashCode = 0; for (let i = 0; i < inputString.length; i++) { hashCode = (hashCode << 5) - hashCode + inputString.charCodeAt(i); hashCode &= hashCode; // Convert to 32bit integer } return hashCode; } function integerToHexColor(number: number): string { // Convert the hash code to a positive number (32unsigned) const hash = Math.abs(number + Math.pow(31, 2)); // Convert the number to a hex color (excluding white) const hexColor = "#" + (hash % 0xffffff).toString(16).padStart(6, "0"); return hexColor; } function stringToHexColorHash(inputString: string): string { const integerHash = stringToIntegerHash(inputString); return integerToHexColor(integerHash); } function nearestColorNeighbour(color: Color, enabledColors: Color[]): Color { const nearestNeighbour = enabledColors.reduce( (nearestColor, enabledColorsItem) => { const distance = colorCalculateDistance({ color1: color, color2: enabledColorsItem, }); return distance && distance < nearestColor.distance ? { distance, color: enabledColorsItem, } : nearestColor; }, { distance: Number.POSITIVE_INFINITY, color: enabledColors[0], } ); return nearestNeighbour.color; }