js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
132 lines (131 loc) • 5.41 kB
JavaScript
import { Color4, Vec3 } from '@js-draw/math';
/**
* Adjusts the current editor theme such that colors have appropriate contrast.
*
* As this method overrides CSS variables using the `.style` property,
* **assumes** all original theme variables are set using CSS and not the `.style` property.
*
* If the editor changes theme in response to the system theme, this method should be
* called whenever the system theme changes (e.g. by using [the `matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)
* method).
*
* @example
* ```ts,runnable
* import { Editor, adjustEditorThemeForContrast } from 'js-draw';
*
* const editor = new Editor(document.body);
* editor.addToolbar();
*
* const css = `
* :root .imageEditorContainer {
* --background-color-1: #ffff77;
* --foreground-color-1: #fff;
* --background-color-2: #ffff99;
* --foreground-color-2: #ffff88;
* --background-color-3: #ddffff;
* --foreground-color-3: #eeffff;
* --selection-background-color: #9f7;
* --selection-foreground-color: #98f;
* }
*
* @media screen and (prefers-color-scheme: dark) {
* :root .imageEditorContainer {
* --background-color-1: black;
* }
* }
* `;
* editor.addStyleSheet(css);
*
* adjustEditorThemeForContrast(editor);
*
* // Because adjustEditorThemeForContrast overrides the current theme, it should be called again
* // to allow the editor to switch between light/dark themes.
* window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
* adjustEditorThemeForContrast(editor);
* });
*
* window.matchMedia('print').addEventListener('change', () => {
* adjustEditorThemeForContrast(editor);
* });
* ```
*/
const adjustEditorThemeForContrast = (editor, options) => {
const editorElem = editor.getRootElement();
// Each set of entries in colorPairs should resolve to colors with sufficient
// contrast.
const colorPairs = [
['--background-color-1', '--foreground-color-1', true, true],
['--background-color-2', '--foreground-color-2', true, true],
['--background-color-3', '--foreground-color-3', true, true],
['--background-color-2', '--primary-action-foreground-color', false, true],
['--selection-background-color', '--selection-foreground-color', false, true],
];
if (!options?.dontClearOverrides) {
// Clear any overrides
for (const [backgroundVar, foregroundVar] of colorPairs) {
editorElem.style.setProperty(backgroundVar, null);
editorElem.style.setProperty(foregroundVar, null);
}
}
const styles = getComputedStyle(editorElem);
const updatedColors = Object.create(null);
const adjustVariablesForContrast = (var1, var2, minContrast,
// true if the variable can be updated
updateVar1, updateVar2) => {
// Fetch from updatedColors if available -- styles isn't updated dynamically.
let color1 = updatedColors[var1]
? updatedColors[var1]
: Color4.fromString(styles.getPropertyValue(var1));
let color2 = updatedColors[var2]
? updatedColors[var2]
: Color4.fromString(styles.getPropertyValue(var2));
// Ensure that color1 has the lesser luminance
if (color1.relativeLuminance() < color2.relativeLuminance()) {
const tmp = color1;
color1 = color2;
color2 = tmp;
const oldVar2 = var2;
var2 = var1;
var1 = oldVar2;
const oldUpdateVar1 = updateVar1;
updateVar1 = updateVar2;
updateVar2 = oldUpdateVar1;
}
let colorsUpdated = false;
let currentContrast = Color4.contrastRatio(color1, color2);
let iterations = 0;
// Step the brightness of color1 and color2 in different directions while necessary
while (currentContrast < minContrast && iterations < 8) {
const step = Vec3.of(0.1, 0.1, 0.1);
if (updateVar1) {
if (color2.eq(Color4.white) && !updateVar2) {
color2 = Color4.black;
}
color1 = Color4.fromRGBVector(color1.rgb.plus(step));
}
if (updateVar2) {
if (color2.eq(Color4.black) && !updateVar1) {
color2 = Color4.white;
}
color2 = Color4.fromRGBVector(color2.rgb.minus(step));
}
currentContrast = Color4.contrastRatio(color1, color2);
colorsUpdated = true;
iterations++;
}
// Update the CSS variables if necessary
if (colorsUpdated) {
editorElem.style.setProperty(var1, color1.toHexString());
editorElem.style.setProperty(var2, color2.toHexString());
updatedColors[var1] = color1;
updatedColors[var2] = color2;
}
};
// Also adjust the selection background
adjustVariablesForContrast('--selection-background-color', '--background-color-2', 1.29, true, false);
for (const [backgroundVar, foregroundVar, updateBackground, updateForeground] of colorPairs) {
const minContrast = 4.5;
adjustVariablesForContrast(backgroundVar, foregroundVar, minContrast, updateBackground, updateForeground);
}
};
export default adjustEditorThemeForContrast;