Add function for finding contrasting colors
This commit is contained in:
parent
5d73487327
commit
95d13656ec
16
package-lock.json
generated
16
package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
"@types/simplemde": "^1.11.11",
|
"@types/simplemde": "^1.11.11",
|
||||||
|
"@types/wcag-contrast": "^3.0.3",
|
||||||
"a11y.css": "^5.3.0",
|
"a11y.css": "^5.3.0",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
@ -54,7 +55,8 @@
|
|||||||
"stimulus": "^3.2.2",
|
"stimulus": "^3.2.2",
|
||||||
"tempstream": "^0.4.6",
|
"tempstream": "^0.4.6",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0",
|
||||||
|
"wcag-contrast": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.54.1",
|
"@playwright/test": "1.54.1",
|
||||||
@ -2044,6 +2046,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/wcag-contrast": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/wcag-contrast/-/wcag-contrast-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-oprevfwJSLfpQK4KaWsRKJuNoebV76+xhmbXiWJGy+FkS34LpCgCMNIwRXWTb8xmmSxUE2ycFOYE7uyRVRm3LA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/webidl-conversions": {
|
"node_modules/@types/webidl-conversions": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
@ -14611,6 +14619,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wcag-contrast/-/wcag-contrast-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wcag-contrast/-/wcag-contrast-3.0.0.tgz",
|
||||||
"integrity": "sha512-RWbpg/S7FOXDCwqC2oFhN/vh8dHzj0OS6dpyOSDHyQFSmqmR+lAUStV/ziTT1GzDqL9wol+nZQB4vCi5yEak+w==",
|
"integrity": "sha512-RWbpg/S7FOXDCwqC2oFhN/vh8dHzj0OS6dpyOSDHyQFSmqmR+lAUStV/ziTT1GzDqL9wol+nZQB4vCi5yEak+w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"relative-luminance": "^2.0.0"
|
"relative-luminance": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -16637,6 +16646,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
|
||||||
},
|
},
|
||||||
|
"@types/wcag-contrast": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/wcag-contrast/-/wcag-contrast-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-oprevfwJSLfpQK4KaWsRKJuNoebV76+xhmbXiWJGy+FkS34LpCgCMNIwRXWTb8xmmSxUE2ycFOYE7uyRVRm3LA=="
|
||||||
|
},
|
||||||
"@types/webidl-conversions": {
|
"@types/webidl-conversions": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
"@types/simplemde": "^1.11.11",
|
"@types/simplemde": "^1.11.11",
|
||||||
|
"@types/wcag-contrast": "^3.0.3",
|
||||||
"a11y.css": "^5.3.0",
|
"a11y.css": "^5.3.0",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
@ -137,7 +138,8 @@
|
|||||||
"stimulus": "^3.2.2",
|
"stimulus": "^3.2.2",
|
||||||
"tempstream": "^0.4.6",
|
"tempstream": "^0.4.6",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0",
|
||||||
|
"wcag-contrast": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.54.1",
|
"@playwright/test": "1.54.1",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { colord } from "colord";
|
import { colord } from "colord";
|
||||||
|
import { hex as measureHexContrast } from "wcag-contrast";
|
||||||
|
|
||||||
export const colors: Record<string, Record<string, string>> = {
|
export const colors: Record<string, Record<string, string>> = {
|
||||||
basic: {
|
basic: {
|
||||||
@ -68,3 +69,108 @@ export function shade(color: string, shade_step: number /* 0-9 */) {
|
|||||||
shifted = shifted.rotate(hue_step * steps);
|
shifted = shifted.rotate(hue_step * steps);
|
||||||
return shifted.toHex();
|
return shifted.toHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getColorVariables(): string[] {
|
||||||
|
return Object.entries(colors).flatMap(([group, values]) =>
|
||||||
|
Object.keys(values).map((name) => `--color-${group}-${name}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterCandidates(
|
||||||
|
candidates: { color: string; contrast: number; shadeStep: number }[],
|
||||||
|
minimumContrast: number,
|
||||||
|
min_brightness: number,
|
||||||
|
max_brightness: number
|
||||||
|
) {
|
||||||
|
return candidates
|
||||||
|
.filter(({ contrast }) => contrast >= minimumContrast)
|
||||||
|
.filter(({ color }) => {
|
||||||
|
const brightness = colord(color).brightness();
|
||||||
|
return brightness > min_brightness && brightness < max_brightness;
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.contrast - a.contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFgColorVariable(
|
||||||
|
bgColorName: string,
|
||||||
|
minimumContrast: number = 4.5
|
||||||
|
): string {
|
||||||
|
if (bgColorName.startsWith("--color-")) {
|
||||||
|
bgColorName = bgColorName.slice("--color-".length);
|
||||||
|
}
|
||||||
|
if (bgColorName.startsWith("--")) {
|
||||||
|
bgColorName = bgColorName.slice(2);
|
||||||
|
}
|
||||||
|
const [group, ...nameParts] = bgColorName.split("-");
|
||||||
|
const name = nameParts.join("-");
|
||||||
|
|
||||||
|
if (!group || !name || !colors[group]?.[name]) {
|
||||||
|
throw new Error(`Unknown color: ${bgColorName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer an explicitly defined text-on token.
|
||||||
|
const explicitTextName = `text-on-${name}`;
|
||||||
|
|
||||||
|
if (colors[group]?.[explicitTextName]) {
|
||||||
|
return `--color-${group}-${explicitTextName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = colors[group][name];
|
||||||
|
|
||||||
|
// Check both ends of the generated scale. This gives us the closest
|
||||||
|
// suitable shade rather than blindly choosing black or white.
|
||||||
|
const candidates = Array.from({ length: 10 }, (_, shadeStep) => {
|
||||||
|
shadeStep += 0;
|
||||||
|
const color = shade(background, shadeStep);
|
||||||
|
const contrast = measureHexContrast(
|
||||||
|
colord(color).toHex(),
|
||||||
|
colord(background).toHex()
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidate = {
|
||||||
|
shadeStep,
|
||||||
|
contrast,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
return candidate;
|
||||||
|
});
|
||||||
|
|
||||||
|
let suitable = filterCandidates(candidates, minimumContrast, 0.2, 0.8);
|
||||||
|
|
||||||
|
if (suitable.length === 0) {
|
||||||
|
suitable = filterCandidates(candidates, minimumContrast, 0.1, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shadeStep;
|
||||||
|
if (suitable.length > 0) {
|
||||||
|
shadeStep = suitable[0]!.shadeStep;
|
||||||
|
} else {
|
||||||
|
const darkest = shade(background, 0);
|
||||||
|
const lightest = shade(background, 9);
|
||||||
|
const contrast_d = measureHexContrast(colord(darkest).toHex(), background);
|
||||||
|
const contrast_l = measureHexContrast(colord(lightest).toHex(), background);
|
||||||
|
if (contrast_l < minimumContrast && contrast_d < minimumContrast) {
|
||||||
|
if (colord(background).brightness() > 0.5) {
|
||||||
|
return `--color-basic-black`;
|
||||||
|
} else {
|
||||||
|
return `--color-basic-white`;
|
||||||
|
}
|
||||||
|
} else if (contrast_l >= minimumContrast && contrast_d >= minimumContrast) {
|
||||||
|
// pick the smaller contrast so it's prettier than just near black/white
|
||||||
|
if (contrast_l > contrast_d) {
|
||||||
|
shadeStep = 0;
|
||||||
|
} else {
|
||||||
|
shadeStep = 9;
|
||||||
|
}
|
||||||
|
} else if (contrast_l > minimumContrast) {
|
||||||
|
shadeStep = 9;
|
||||||
|
} else if (contrast_d > minimumContrast) {
|
||||||
|
shadeStep = 0;
|
||||||
|
} else {
|
||||||
|
shadeStep = 9;
|
||||||
|
// fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `--color-${group}-${name}-${String(shadeStep).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user