Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parametrized contrast #217

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions docs/contrast.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ It is commonly used in lighting design,
and also to examine small, sharp edged symbols and text
on larger, uniformly-colored backgrounds.

> WC = Y<sub>max</sub> - Y<sub>min</sub> / Y<sub>min</sub>
> WC = (Y<sub>max</sub> - Y<sub>min</sub>) / Y<sub>min</sub>

Any two colors can be used,
and this formula does not care which is the text color and which is the background.
Expand All @@ -85,7 +85,7 @@ Weber contrast is typically used with printed test charts
with positive polarity (black text, white background).
The formula thus can be expressed as:

> WC<sub>pp</sub> = Y<sub>background</sub> - Y<sub>text</sub> / Y<sub>text</sub>
> WC<sub>pp</sub> = (Y<sub>background</sub> - Y<sub>text</sub>) / Y<sub>text</sub>

Hwang and Peli used a modified Weber contrast for
tablet-based, negative polarity testing (white text on black background),
Expand All @@ -95,8 +95,20 @@ and wrote:

The formula for negative polarity is

> WC<sub>np</sub> = Y<sub>text</sub> - Y<sub>background</sub> / Y<sub>text</sub>
> WC<sub>np</sub> = (Y<sub>text</sub> - Y<sub>background</sub>) / Y<sub>text</sub>

In another paper, Hwang and Peli proposed to add
a fixed luminance to both colors to account for viewing flare:

> WC = (Y<sub>max</sub> - Y<sub>min</sub>) / (Y<sub>min</sub> + Y<sub>ambient</sub>)

The value of that offset depends on the viewing conditions.

```js
let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, {algorithm: "weber", offset: .05});
```

## Michelson Contrast

Expand All @@ -123,6 +135,22 @@ let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, "Michelson");
```

## Stevens Contrast

Stevens contrast is often considered to supersede the Weber contrast.
It is based on powers rather than logarithms.

> SC = Y<sub>max</sub><sup>exp</sup> - Y<sub>min</sub><sup>exp</sup>

The value of that exponent depends on the viewing conditions.
It is also possible to use an offset to model flare.

```js
let color1 = new Color("p3", [0.9, 0.8, 0.1]);
let color2 = new Color("slategrey");
let contrast = color1.contrast(color2, {algorithm: "stevens", exponent: 1 / 3, offset: .0025});
```

## Accessible Perceptual Contrast Algorithm (APCA)

APCA is part of a color appearance system to determine _readability contrast_
Expand Down Expand Up @@ -209,7 +237,7 @@ _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 = = (Y<sub>max</sub> + 0.05) / (Y<sub>min</sub> + 0.05)
> WCAG21 = (Y<sub>max</sub> + 0.05) / (Y<sub>min</sub> + 0.05)

Because of it's widespread use,
color.js provides this method, mainly to aid comparison.
Expand All @@ -235,6 +263,8 @@ let contrast = color1.contrast(color2, "WCAG21");

- Alex D. Hwang and Eli Peli (2016) _Positive and negative polarity contrast sensitivity measuring app_. IS&T Int Symp Electron Imaging 2016: 10.2352. [full article](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5481843/)

- Alex D. Hwang and Eli Peli (2016) _New Contrast Metric for Realistic Display Performance Measure._. Dig Tech Pap. 2016: 10.1002. [full article](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5489230/)

- Gordon E. Legge, David H. Parish, Andrew Luebker, and Lee H. Wurm (1990) _Psychophysics of reading. XI. Comparing color contrast and luminance contrast_. Journal of the Optical Society of America vol **7** issue 10 pp. 2002-2010 [https://doi.org/10.1364/JOSAA.7.002002](https://doi.org/10.1364/JOSAA.7.002002)

- Gordon E.Legge, Gary S.Rubin, Andrew Luebker (1987) _Psychophysics of reading—V. The role of contrast in normal vision_. Vision Research vol **27**, Issue 7, pp. 1165-1177 [abstract](https://www.sciencedirect.com/science/article/abs/pii/0042698987900289)
Expand Down
19 changes: 19 additions & 0 deletions src/contrast/Stevens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Stevens contrast
// Symmetric, does not matter which is foreground and which is background
// Viewing conditions can be modified by changing exponent and offset

import getColor from "../getColor.js";
import {getLuminance} from "../luminance.js";

export default function contrastStevens (color1, color2, {exponent = 1 / 3, offset = .0025} = {}) {
color1 = getColor(color1);
color2 = getColor(color2);

let Y1 = Math.max(getLuminance(color1), 0) + offset;
let Y2 = Math.max(getLuminance(color2), 0) + offset;

let J1 = Math.pow(Y1, exponent);
let J2 = Math.pow(Y2, exponent);

return Math.abs(J1 - J2);
};
8 changes: 4 additions & 4 deletions src/contrast/Weber.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Weber luminance contrast
// The difference between the two luminances divided by the lower luminance
// Symmetric, does not matter which is foreground and which is background
// No black level compensation for flare.
// Optional offset for flare contribution

import getColor from "../getColor.js";
import {getLuminance} from "../luminance.js";
Expand All @@ -12,12 +12,12 @@ import {getLuminance} from "../luminance.js";
// max clamp for the plain Weber
const max = 50000;

export default function contrastWeber (color1, color2) {
export default function contrastWeber (color1, color2, {offset = 0} = {}) {
color1 = getColor(color1);
color2 = getColor(color2);

let Y1 = Math.max(getLuminance(color1), 0);
let Y2 = Math.max(getLuminance(color2), 0);
let Y1 = Math.max(getLuminance(color1), 0) + offset;
let Y2 = Math.max(getLuminance(color2), 0) + offset;

if (Y2 > Y1) {
[Y1, Y2] = [Y2, Y1];
Expand Down
1 change: 1 addition & 0 deletions src/contrast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export {default as contrastWCAG21} from "./WCAG21.js";
export {default as contrastAPCA} from "./APCA.js";
export {default as contrastMichelson} from "./Michelson.js";
export {default as contrastWeber} from "./Weber.js";
export {default as contrastStevens} from "./Stevens.js";
export {default as contrastLstar} from "./Lstar.js";
export {default as contrastDeltaPhi} from "./deltaPhi.js";
108 changes: 108 additions & 0 deletions tests/contrast.html
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,28 @@ <h1>Weber contrast, sRGB</h1>
</table>
</section>

<section>
<h1>Weber contrast with offset</h1>
<table class="reftest" data-test="numbers" data-columns="4" data-colors="1,2">
<tr>
<td>white</td>
<td>white</td>
<td>
<script>contrastTest({algorithm: "Weber", offset: .05});</script>
</td>
<td>0</td>
</tr>
<tr>
<td>white</td>
<td>black</td>
<td>
<script>contrastTest({algorithm: "Weber", offset: .05});</script>
</td>
<td>20</td>
</tr>
</table>
</section>

<section>
<h1>Michelson contrast, sRGB</h1>
<p>Same test color pairs as <a href="https://github.com/Myndex/apca-w3/blob/master/test/test.html">apca-w3 test</a>. </p>
Expand Down Expand Up @@ -533,6 +555,92 @@ <h1>Michelson contrast, sRGB</h1>
</table>
</section>

<section>
<h1>Stevens contrast, sRGB</h1>
<table class="reftest" data-test="numbers" data-columns="4" data-colors="1,2">
<tr>
<td>white</td>
<td>black</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.8651117590220451</td>
</tr>
<tr>
<td>white</td>
<td>white</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0</td>
</tr>
<tr>
<td>#888</td>
<td>#fff</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.37196482640638906</td>
</tr>
<tr>
<td>#fff</td>
<td>#888</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.37196482640638906</td>
</tr>
<tr>
<td>#000</td>
<td>#aaa</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.6038246032876674</td>
</tr>
<tr>
<td>#aaa</td>
<td>#000</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.6038246032876674</td>
</tr>
<tr>
<td>#123</td>
<td>#def</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.6837707144018808</td>
</tr>
<tr>
<td>#def</td>
<td>#123</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.6837707144018808</td>
</tr>
<tr>
<td>#123</td>
<td>#444</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.13241705354214078</td>
</tr>
<tr>
<td>#444</td>
<td>#123</td>
<td>
<script>contrastTest("Stevens");</script>
</td>
<td>0.13241705354214078</td>
</tr>
</table>
</section>

<script src="index.js" type="module"></script>

</body>
Expand Down