Skip to content

Commit 3cc6e93

Browse files
committed
add functionality to create color hashes for text inputs based on the configured palette
1 parent feca271 commit 3cc6e93

3 files changed

Lines changed: 202 additions & 0 deletions

File tree

src/common/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { invisibleZeroWidthCharacters } from "./utils/characters";
22
import { colorCalculateDistance } from "./utils/colorCalculateDistance";
33
import decideContrastColorValue from "./utils/colorDecideContrastvalue";
4+
import { getEnabledColorsFromPalette, textToColorHash } from "./utils/colorHash";
45
import getColorConfiguration from "./utils/getColorConfiguration";
56
import { getScrollParent } from "./utils/getScrollParent";
67
import { getGlobalVar, setGlobalVar } from "./utils/globalVars";
@@ -15,4 +16,6 @@ export const utils = {
1516
getGlobalVar,
1617
setGlobalVar,
1718
getScrollParent,
19+
getEnabledColorsFromPalette,
20+
textToColorHash,
1821
};

src/common/utils/colorHash.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import Color from "color";
2+
3+
import { CLASSPREFIX as eccgui, COLORMINDISTANCE } from "../../configuration/constants";
4+
5+
import { colorCalculateDistance } from "./colorCalculateDistance";
6+
import CssCustomProperties from "./CssCustomProperties";
7+
8+
type ColorOrFalse = Color | false;
9+
10+
interface getEnabledColorsProps {
11+
/** Include "identity colors" from palette. */
12+
includeIdentityColors?: boolean;
13+
/** Include "semantic colors" (used for info, success, warning, danger) from palette. */
14+
includeSemanticColors?: boolean;
15+
/** Include "layout colors" from palette. */
16+
includeLayoutColors?: boolean;
17+
/** Include "extra colors" (e.g. gold, silver, bronze) from palette. */
18+
includeExtraColors?: boolean;
19+
/** Only keep colors in the stack with a minimal color distance to all other colors. */
20+
minimalColorDistance?: number;
21+
/** Extend color stack by values generated by mixing tints of the same weight, e.g. `yellow100` with `purple100`. */
22+
// includeMixedColors?: boolean;
23+
}
24+
25+
const getEnabledColorsFromPaletteCache = new Map<string, Color[]>();
26+
27+
export function getEnabledColorsFromPalette({
28+
includeIdentityColors = false,
29+
includeSemanticColors = false,
30+
includeLayoutColors = true,
31+
includeExtraColors = false,
32+
// TODO (planned for later): includeMixedColors = false,
33+
minimalColorDistance = COLORMINDISTANCE,
34+
}: getEnabledColorsProps): Color[] {
35+
const configId = JSON.stringify({
36+
includeIdentityColors,
37+
includeSemanticColors,
38+
includeLayoutColors,
39+
includeExtraColors,
40+
minimalColorDistance,
41+
});
42+
43+
if (getEnabledColorsFromPaletteCache.has(configId)) {
44+
return getEnabledColorsFromPaletteCache.get(configId)!;
45+
}
46+
47+
const colorsFromPalette = new CssCustomProperties({
48+
selectorText: `:root`,
49+
filterName: (name: string) => {
50+
return (
51+
(includeIdentityColors && name.includes(`--${eccgui}-color-palette-identity-`)) ||
52+
(includeSemanticColors && name.includes(`--${eccgui}-color-palette-semantic-`)) ||
53+
(includeLayoutColors && name.includes(`--${eccgui}-color-palette-layout-`)) ||
54+
(includeExtraColors && name.includes(`--${eccgui}-color-palette-extra-`))
55+
);
56+
},
57+
removeDashPrefix: false,
58+
returnObject: true,
59+
}).customProperties();
60+
61+
const colorsFromPaletteValues = Object.values(colorsFromPalette) as string[];
62+
63+
const colorsFromPaletteWithEnoughDistance =
64+
minimalColorDistance > 0
65+
? colorsFromPaletteValues.reduce((enoughDistance: string[], color: string) => {
66+
if (enoughDistance.includes(color)) {
67+
return enoughDistance.filter((checkColor) => {
68+
const distance = colorCalculateDistance({ color1: color, color2: checkColor });
69+
return checkColor === color || (distance && minimalColorDistance <= distance);
70+
});
71+
} else {
72+
return enoughDistance;
73+
}
74+
}, colorsFromPaletteValues)
75+
: colorsFromPaletteValues;
76+
77+
getEnabledColorsFromPaletteCache.set(
78+
configId,
79+
colorsFromPaletteWithEnoughDistance.map((color: string) => {
80+
return Color(color);
81+
})
82+
);
83+
84+
return getEnabledColorsFromPaletteCache.get(configId)!;
85+
}
86+
87+
function getColorcode(text: string): ColorOrFalse {
88+
try {
89+
return Color(text);
90+
} catch {
91+
return false;
92+
}
93+
}
94+
95+
interface textToColorOptions {
96+
/** Stack of colors that are allowed to be returned. */
97+
enabledColors: Color[] | "all" | getEnabledColorsProps;
98+
/** Return input text if it represents a valid color string, e.g. `#000` or `black`. */
99+
returnValidColorsDirectly: boolean;
100+
}
101+
102+
interface textToColorProps {
103+
text: string;
104+
options?: textToColorOptions;
105+
}
106+
107+
/**
108+
* Map a text string to a color.
109+
* It always returns the same color for a text as long as the options stay the same.
110+
* It returns `false` in case there are no colors defined to chose from.
111+
*/
112+
export function textToColorHash({
113+
text,
114+
options = {
115+
enabledColors: getEnabledColorsFromPalette({}),
116+
returnValidColorsDirectly: false,
117+
},
118+
}: textToColorProps): string | false {
119+
let color = getColorcode(text);
120+
121+
if (options.returnValidColorsDirectly && color) {
122+
// return color code for text because it was a valid color string
123+
return color.hex().toString();
124+
}
125+
126+
color = getColorcode(stringToHexColorHash(text)) as Color;
127+
128+
if (options.enabledColors === "all" && color) {
129+
// all colors are allowed as return value
130+
return color.hex().toString();
131+
}
132+
133+
let enabledColors = [] as Color[];
134+
135+
if (Array.isArray(options.enabledColors)) {
136+
enabledColors = options.enabledColors;
137+
} else {
138+
enabledColors = getEnabledColorsFromPalette(options.enabledColors as getEnabledColorsProps);
139+
}
140+
141+
if (enabledColors.length === 0) {
142+
// eslint-disable-next-line no-console
143+
console.warn("textToColorHash functionaliy need enabledColors list with at least 1 color.");
144+
return false;
145+
}
146+
147+
return nearestColorNeighbour(color, enabledColors as Color[])
148+
.hex()
149+
.toString();
150+
}
151+
152+
function stringToIntegerHash(inputString: string): number {
153+
/* this function is idempotend, meaning it retrieves the same result for the same input
154+
no matter how many times it's called */
155+
// Convert the string to a hash code
156+
let hashCode = 0;
157+
for (let i = 0; i < inputString.length; i++) {
158+
hashCode = (hashCode << 5) - hashCode + inputString.charCodeAt(i);
159+
hashCode &= hashCode; // Convert to 32bit integer
160+
}
161+
return hashCode;
162+
}
163+
164+
function integerToHexColor(number: number): string {
165+
// Convert the hash code to a positive number (32unsigned)
166+
const hash = Math.abs(number + Math.pow(31, 2));
167+
// Convert the number to a hex color (excluding white)
168+
const hexColor = "#" + (hash % 0xffffff).toString(16).padStart(6, "0");
169+
return hexColor;
170+
}
171+
172+
function stringToHexColorHash(inputString: string): string {
173+
const integerHash = stringToIntegerHash(inputString);
174+
return integerToHexColor(integerHash);
175+
}
176+
177+
function nearestColorNeighbour(color: Color, enabledColors: Color[]): Color {
178+
const nearestNeighbour = enabledColors.reduce(
179+
(nearestColor, enabledColorsItem) => {
180+
const distance = colorCalculateDistance({
181+
color1: color,
182+
color2: enabledColorsItem,
183+
});
184+
return distance && distance < nearestColor.distance
185+
? {
186+
distance,
187+
color: enabledColorsItem,
188+
}
189+
: nearestColor;
190+
},
191+
{
192+
distance: Number.POSITIVE_INFINITY,
193+
color: enabledColors[0],
194+
}
195+
);
196+
return nearestNeighbour.color;
197+
}

src/configuration/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
// basic vars
22
export const CLASSPREFIX = "eccgui";
3+
export const COLORMINDISTANCE = 10;
4+
export const COLORMINCONTRAST = 4;

0 commit comments

Comments
 (0)