-
Notifications
You must be signed in to change notification settings - Fork 0
/
apca.ts
87 lines (73 loc) · 2.71 KB
/
apca.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { Color } from "./color.ts";
import { round } from "./mod.ts";
// Code derived from formulas in https://github.com/Myndex/apca-w3/blob/99be5162ddb000e4d49551756830d2a9fd92955e/images/APCAw3_0.1.17_APCA0.0.98G.svg.
const B_EXP = 1.414;
const B_THRESH = 0.022;
const P_IN = 0.0005;
const R_SCALE = 1.14;
const W_OFFSET = 0.027;
const P_OUT = 0.1;
/**
* This function calculates the contrast value between two colors based on WCAG
* contrast readability criteria. The first color is the foreground color and the
* second the background color (defaulting to pure white, "#fff" / [255, 255, 255]).
*
* **NOTE:** The returned value is signed, but only the absolute value matters, so you
* may want to wrap the result in `Math.abs`. The reason for this is that light on dark
* colors returns negative numbers, while dark on light returns positive numbers.
*
* ```ts
* import { apcaContrastValue } from "./apca.ts";
*
* apcaContrastValue([26, 26, 26], [255, 255, 255]) // black on white
* // 104.3
*
* apcaContrastValue([255, 255, 255], [26, 26, 26]) // inverted from above
* // -106.55
* ```
*
* See https://github.com/Myndex/SAPC-APCA/ for concrete details for APCA.
*/
export function apcaContrastValue(foreground: Color, background: Color = [255, 255, 255]): number {
return round(clampMinimumContrast(foreground, background) * 100, 1);
}
const clampMinimumContrast = (foreground: Color, background: Color): number => {
const C = clampNoiseThenScale(foreground, background);
if (Math.abs(C) < P_OUT) {
return 0.0;
} else if (C > 0) {
return C - W_OFFSET;
} else {
return C + W_OFFSET;
}
};
export function screenLuminance(color: Color): number {
const r = Math.pow(color[0] / 255, 2.4) * 0.2126729;
const g = Math.pow(color[1] / 255, 2.4) * 0.7151522;
const b = Math.pow(color[2] / 255, 2.4) * 0.0721750;
return r + g + b;
}
const clampBlackLevels = (color: Color): number => {
const luminance = screenLuminance(color);
if (luminance >= B_THRESH) {
return luminance;
}
return luminance + Math.pow(B_THRESH - luminance, B_EXP);
};
const clampNoiseThenScale = (foreground: Color, background: Color): number => {
const y_bg = clampBlackLevels(background);
const y_txt = clampBlackLevels(foreground);
if (Math.abs(y_bg - y_txt) < P_IN) {
return 0.0;
} else if (y_txt < y_bg) {
return normalPolarity(y_txt, y_bg) * R_SCALE;
} else {
return reversePolarity(y_txt, y_bg) * R_SCALE;
}
};
const normalPolarity = (foreground: number, background: number): number => {
return Math.pow(background, 0.56) - Math.pow(foreground, 0.57);
};
const reversePolarity = (foreground: number, background: number): number => {
return Math.pow(background, 0.65) - Math.pow(foreground, 0.62);
};