apca-introduction

The missing introduction to APCA  https://p.ce9e.org/apca-introduction/
git clone https://git.ce9e.org/apca-introduction.git

commit
27cb34cdedde170670622389adee9d1a52dce9da
parent
0df253af9185d8cd99f1f537614c106c83503c25
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2022-08-19 15:46
use J for lightness

L is usually used for absolute luminance

Diffstat

M analysis.md 34 +++++++++++++++++-----------------
M plots/contrast_comparison.png 0
M plots/contrast_comparison.py 20 ++++++++++----------
M plots/coverage.py 6 +++---
M plots/lightness_comparison.png 0
M plots/lightness_comparison.py 2 +-

6 files changed, 31 insertions, 31 deletions


diff --git a/analysis.md b/analysis.md

@@ -53,15 +53,15 @@ correct 100% of the time.
   53    53 ### A naive approach
   54    54 
   55    55 ```js
   56    -1 function sRGBtoL(srgb) {
   -1    56 function sRGBtoJ(srgb) {
   57    57   return (srgb[0] + srgb[1] + srgb[2]) / 3;
   58    58 }
   59    59 
   60    60 function contrast(fg, bg) {
   61    -1   var lfg = sRGBtoL(fg);
   62    -1   var lbg = sRGBtoL(bg);
   -1    61   var jfg = sRGBtoJ(fg);
   -1    62   var jbg = sRGBtoJ(bg);
   63    63 
   64    -1   return lbg - lfg;
   -1    64   return jbg - jfg;
   65    65 };
   66    66 ```
   67    67 
@@ -73,7 +73,7 @@ values.
   73    73 
   74    74 ### Historical context
   75    75 
   76    -1 Lightness (L) is a measure for the perceived amount of light. Luminance (Y) is
   -1    76 Lightness (J) is a measure for the perceived amount of light. Luminance (Y) is
   77    77 a measure for the physical amount of light. In order to understand perceived
   78    78 contrast, we first have to understand the relationship between luminance and
   79    79 lightness.
@@ -86,11 +86,11 @@ Y2` has the same value. This is known as Weber contrast and has been called the
   86    86 ["gold standard" for text contrast].
   87    87 
   88    88 Fechner concluded that the relation between a physical measure `Y` and a
   89    -1 perceived measure `L` can be expressed as `L = a * log(Y) + b`. This is called
   -1    89 perceived measure `J` can be expressed as `J = a * log(Y) + b`. This is called
   90    90 the Weber-Fechner law.
   91    91 
   92    92 In 1961 Stevens published a different model that was found to more accurately
   93    -1 predict human vision. It has the form `L = a * pow(Y, alpha) + b`. The exponent
   -1    93 predict human vision. It has the form `J = a * pow(Y, alpha) + b`. The exponent
   94    94 `alpha` has a value of approximately 1/3.[^1]
   95    95 
   96    96 ### WCAG 2.x
@@ -236,7 +236,7 @@ function sRGBtoY(srgb) {
  236   236   return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  237   237 }
  238   238 
  239    -1 function YtoL(y) {
   -1   239 function YtoJ(y) {
  240   240   return (Math.log(y + 0.05) - Math.log(0.05)) / Math.log(21);
  241   241 }
  242   242 
@@ -244,10 +244,10 @@ function contrast(fg, bg) {
  244   244   var yfg = sRGBtoY(fg);
  245   245   var ybg = sRGBtoY(bg);
  246   246 
  247    -1   var lfg = YtoL(yfg);
  248    -1   var lbg = YtoL(ybg);
   -1   247   var jfg = YtoJ(yfg);
   -1   248   var jbg = YtoJ(ybg);
  249   249 
  250    -1   return lbg - lfg;
   -1   250   return jbg - jfg;
  251   251 };
  252   252 
  253   253 function normalize(c) {
@@ -270,7 +270,7 @@ function sRGBtoY(srgb) {
  270   270   return y;
  271   271 }
  272   272 
  273    -1 function YtoL(y) {
   -1   273 function YtoJ(y) {
  274   274     return Math.pow(y, 0.6);
  275   275 }
  276   276 
@@ -278,13 +278,13 @@ function contrast(fg, bg) {
  278   278   var yfg = sRGBtoY(fg);
  279   279   var ybg = sRGBtoY(bg);
  280   280 
  281    -1   var lfg = YtoL(yfg);
  282    -1   var lbg = YtoL(ybg);
   -1   281   var jfg = YtoJ(yfg);
   -1   282   var jbg = YtoJ(ybg);
  283   283 
  284   284   if (ybg > yfg) {
  285    -1     return Math.pow(lbg, 0.56 / 0.6) - Math.pow(lfg, 0.57 / 0.6);
   -1   285     return Math.pow(jbg, 0.56 / 0.6) - Math.pow(jfg, 0.57 / 0.6);
  286   286   } else {
  287    -1     return Math.pow(ybg, 0.65 / 0.6) - Math.pow(lfg, 0.62 / 0.6);
   -1   287     return Math.pow(jbg, 0.65 / 0.6) - Math.pow(jfg, 0.62 / 0.6);
  288   288   }
  289   289 };
  290   290 
@@ -313,7 +313,7 @@ The difference goes up to 20%.
  313   313 
  314   314 The other three plots compare APCA to a modified version of APCA where one of
  315   315 the steps has been replaced by the corresponding step from WCAG 2.x. This way
  316    -1 we can see that `sRGBtoY` contributes 4% to the difference, `YtoL` contributes
   -1   316 we can see that `sRGBtoY` contributes 4% to the difference, `YtoJ` contributes
  317   317 15%, and `contrast` contributes 3%.
  318   318 
  319   319 Since the conversion from luminance to lightness causes the biggest difference,

diff --git a/plots/contrast_comparison.png b/plots/contrast_comparison.png

Binary files differ.

diff --git a/plots/contrast_comparison.py b/plots/contrast_comparison.py

@@ -12,8 +12,8 @@ def wcag_l(y, flare=0.05):
   12    12 	return np.log(y / flare + 1) / np.log(1 / flare + 1)
   13    13 
   14    14 
   15    -1 def wcag_contrast(lfg, lbg):
   16    -1 	return lbg - lfg
   -1    15 def wcag_contrast(jfg, jbg):
   -1    16 	return jbg - jfg
   17    17 
   18    18 
   19    19 def apca_y(color):
@@ -28,10 +28,10 @@ def apca_l(y):
   28    28 	return y ** 0.6
   29    29 
   30    30 
   31    -1 def apca_contrast(lfg, lbg):
   32    -1 	_lfg = lfg ** (np.where(lbg > lfg, 0.57, 0.62) / 0.6)
   33    -1 	_lbg = lbg ** (np.where(lbg > lfg, 0.56, 0.65) / 0.6)
   34    -1 	return _lbg - _lfg
   -1    31 def apca_contrast(jfg, jbg):
   -1    32 	_jfg = jfg ** (np.where(jbg > jfg, 0.57, 0.62) / 0.6)
   -1    33 	_jbg = jbg ** (np.where(jbg > jfg, 0.56, 0.65) / 0.6)
   -1    34 	return _jbg - _jfg
   35    35 
   36    36 
   37    37 if __name__ == '__main__':
@@ -48,13 +48,13 @@ if __name__ == '__main__':
   48    48 
   49    49 	apca_yfg = apca_y(fg)
   50    50 	apca_ybg = apca_y(bg)
   51    -1 	apca_lfg = apca_l(apca_yfg)
   52    -1 	apca_lbg = apca_l(apca_ybg)
   53    -1 	apca = apca_contrast(apca_lfg, apca_lbg)
   -1    51 	apca_jfg = apca_l(apca_yfg)
   -1    52 	apca_jbg = apca_l(apca_ybg)
   -1    53 	apca = apca_contrast(apca_jfg, apca_jbg)
   54    54 
   55    55 	for a, b, y, l, contrast, title in [
   56    56 		(0, 0, wcag_y, apca_l, apca_contrast, 'sRGBtoY'),
   57    -1 		(0, 1, apca_y, wcag_l, apca_contrast, 'YtoL'),
   -1    57 		(0, 1, apca_y, wcag_l, apca_contrast, 'YtoJ'),
   58    58 		(1, 0, apca_y, apca_l, wcag_contrast, 'contrast'),
   59    59 		(1, 1, wcag_y, wcag_l, wcag_contrast, 'all'),
   60    60 	]:

diff --git a/plots/coverage.py b/plots/coverage.py

@@ -32,9 +32,9 @@ def apca_y(color):
   32    32 
   33    33 
   34    34 def apca_contrast(yfg, ybg):
   35    -1 	lfg = yfg ** np.where(ybg > yfg, 0.57, 0.62)
   36    -1 	lbg = ybg ** np.where(ybg > yfg, 0.56, 0.65)
   37    -1 	c = (lbg - lfg) * 1.14
   -1    35 	jfg = yfg ** np.where(ybg > yfg, 0.57, 0.62)
   -1    36 	jbg = ybg ** np.where(ybg > yfg, 0.56, 0.65)
   -1    37 	c = (jbg - jfg) * 1.14
   38    38 	c = np.where(np.abs(c) < 0.1, 0, np.where(c > 0, c - 0.027, c + 0.027))
   39    39 	return np.abs(c) * 100
   40    40 

diff --git a/plots/lightness_comparison.png b/plots/lightness_comparison.png

Binary files differ.

diff --git a/plots/lightness_comparison.py b/plots/lightness_comparison.py

@@ -8,7 +8,7 @@ def norm(x, f):
    8     8 
    9     9 if __name__ == '__main__':
   10    10 	plt.xlabel('luminance of screen (Y)')
   11    -1 	plt.ylabel('predicted perceived lightness (L)')
   -1    11 	plt.ylabel('predicted perceived lightness (J)')
   12    12 
   13    13 	x = np.linspace(0, 1) ** 2
   14    14 	legend = []