Contrast

Contrast

The color used for text and icons needs to be sufficiently different from the background color to be easily seen.

Insufficient contrast affects reading speed, and measuring contrast is also important to ensure accessibility, including for those with low vision (Legge 1987):

People with low vision usually require large characters to read, so high contrast is particularly important for them.

For people with normal vision, even when reading low-contrast text, increasing the character size makes it possible to maintain same maximum reading speed as when reading high-contrast text (Fujita et. al.).

Luminance and chromatic contrast

Contrast can be produced by sufficient difference in lightness (luminance contrast), and also by sufficient difference in hue or chroma (chromatic contrast).

Studies (Legge 1990) of both people with normal color vision, and people with low vision, have shown that sufficient luminance contrast is the primary factor affecting reading speed, and that adding chromatic contrast does not further increase reading speed. This is true for both those with low vision and those with normal vision.

Indeed, Zuffi et. al. (2009) found that for more muted color combinations, the presence of chromatic contrast (measured as Δab) impaired reading performance, compared to luminance contrast (measured as ΔL) alone.

Color.js provides several methods to estimate luminance contrast. Most methods report a contrast of 0 for a color on itself (WCAG 2.1 gives 1), and around 100 for the highest contrast black-on-white (WCAG 2.1 gives 21, Weber gives a high number which we cap at 50,000). Methods which distinguish polarity will report a negative number for reverse polarity; APCA gives -110 for white on black.

The strengths, drawbacks and uses of these methods have been examined by the NASA (Color Usage Research Lab).

Each contrast algorithm can be used via either its dedicated function name, or the general contrast() function:

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrastWCAG21(color2);
let sameContrast = color1.contrast(color2, "WCAG21");
// Case insensitive:
let sameContrast2 = color1.contrast(color2, "wcag21");

Warning: You need to provide a contrast algorithm to contrast(). Not providing one produces a TypeError:

Color.contrast("red", "white");

Weber Contrast

Weber contrast, also often simply called Luminance Contrast, is the relationship between the luminance Ymax of a brighter area of interest and that of an adjacent darker area, Ymin. It is commonly used in lighting design, and also to examine small, sharp edged symbols and text on larger, uniformly-colored backgrounds.

WC = (Ymax - Ymin) / Ymin

Any two colors can be used, and this formula does not care which is the text color and which is the background. The lighter of the two will be detected automatically.

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "weber");

Modified Weber Contrast

Weber contrast is typically used with printed test charts with positive polarity (black text, white background). The formula thus can be expressed as:

WCpp = (Ybackground - Ytext) / Ytext

Hwang and Peli used a modified Weber contrast for tablet-based, negative polarity testing (white text on black background), and wrote:

We found that our app generates more accurate and a wider range of contrast stimuli than the paper chart (especially at the critical high CS, low contrast range), and found a clear difference between NP and PP CS measures (CSNP>CSPP) despite the symmetry afforded by the modified Weber contrast definition. Our app provides a convenient way to measure CS in both lighted and dark environments.

The formula for negative polarity is

WCnp = (Ytext - Ybackground) / Ytext

Michelson Contrast

Michelson Contrast, also called Modulation or Peak-to-Peak Constrast, measures the relation between the spread and the sum of the two luminances. This definition is derived from signal processing theory, where it is used to determine the quality of a signal relative to its noise level.

MC = (Ymax - Ymin) / (Ymax + Ymin)

It is particularly useful for higher spatial frequency symbols such as complex text characters. Michelson Contrast was used, for example, by Ohnishi to study the relationship between contrast, character size and reading speed:

Critical Print Size (CPS) was found to increase as the contrast decreased. The relationship between contrast and CPS was linear in log–log coordinates, that is, log–CPS increased as the log-contrast of the characters decreased.

This formula does not care which is the text color and which is the background. The lighter of the two will be detected automatically.

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "Michelson");

Accessible Perceptual Contrast Algorithm (APCA)

APCA is part of a color appearance system to determine readability contrast for ensuring both spot readability and fluent readability (fast reading speed) and takes into account spatial frequency (small, thin, or complex letter forms). APCA is being evaluated for use in version 3 of the W3C Web Content Accessibility Guidelines (WCAG).

The first stage of this system is the measurement of Lightness Contrast Lc. Unlike systems using luminance, which is not perceptually uniform, APCA calculates a measure of lightness. The formula for Lc starts with gamma-encoded sRGB values and involves multiple steps:

and so is too complex to be given here. Color.js implements (APCA version 0.0.98G-4g). Colors are converted to sRGB internally as a first stage, so any colors can be used with this function.

The APCA formula requires the text color and the foreground color to be distinguished. Swapping them gives a somewhat different, and incorrect, result.

let text = new Color("p3", [0.9, 0.8, 0.1]);
let background = new Color("slategrey");
let contrast = background.contrast(text, "APCA");
let wrongContrast = text.contrast(background, "APCA");

Lightness difference

Instead of being based on luminance, which is not perceptually uniform (and thus, the visual difference corresponding to a given luminance difference is greater for a dark pair of colors than a light pair), lightness difference uses the CIE Lightness L*, which is (approximately) perceptually uniform.

LstarC = Lmax - Lmin

This formula does not care which is the text color and which is the background. The lighter of the two will be detected automatically.

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "Lstar");

Zuffi et. al. found that a lightness difference of 30 units in CIE L* was sufficient for legible contrast.

Google Material Design uses a color model called Hue, Chroma, Tone (HCT) (O'Leary) where Chroma and Hue are calculated from the CAM16 color appearance model but Tone is the same as CIE Lightness:

The HCT color system makes meeting accessibility standards much easier. Instead of using the unintuitive measure of a contrast ratio, the system converts those same requirements to a simple difference in tone, HCT’s measure of lightness. Contrast is guaranteed simply by picking colors whose tone values are far enough apart—no complex calculations required.

So, color.js Lstar will give you the HCT Tone difference.

Delta Phi Star

Delta Phi Star (ΔΦ*) computes the difference between two CIE Lightnesses (D65-adapted), which have been raised to the power φ. The difference is raised to the power 1/φ, then scaled to give a convenient 0 to 101 range. Finally, low contrasts less than 7.5 are clipped to zero.

let phi = Math.pow(5, 0.5) * 0.5 + 0.5;
let difference = (Lbg ** phi) - (Ltxt ** phi);
let DeltaPhiStar = difference * Math.SQRT2 - 40;
if DeltaPhiStar < 7.5 DeltaPhiStar = 0;

Delta Phi Star was created by Andrew Somers as a "general" simplifed, polarity-insensitive perceptual contrast algorithm.

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "DeltaPhi");

Simple contrast

Simple Contrast values are used in photography, to specify the difference between bright and dark parts of the picture. This definition is not useful for real-world luminances, because of their much higher dynamic range, and the logarithmic response characteristics of the human eye (Schorsch).

SC = Ymax / Ymin

Because it is not very useful, and also trivial to compute, color.js does not provide a function for simple contrast.

WCAG 2.1

Widely used in accessibility checkers, the WCAG 2.1 algorithm is defined for sRGB only, and corresponds to Simple Contrast with a fixed 0.05 relative luminance increase to both colors to account for viewing flare.

This value is much higher than that in the sRGB standard, which puts white at 80 cd/m2 and black at 0.2cd/m2, a relative luminance boost of 0.0025.

WCAG21 = (Ymax + 0.05) / (Ymin + 0.05)

Because of it's widespread use, color.js provides this method, mainly to aid comparison. Note though that it has been criticized for numerous false positive and false negative results, particularly in dark mode. In a study of the legibility of white and black text against 8,000 random coloured backgrounds (Waller), WCAG 2.1 performed poorly compared to APCA.

This formula does not care which is the text color and which is the background. The lighter of the two will be detected automatically.

let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "WCAG21");

References