@eccenca/gui-elements
Version:
GUI elements based on other libraries, usable in React application, written in Typescript.
196 lines (168 loc) • 6.91 kB
text/typescript
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;
}