- commit
- 1cb50665affef87b2ef825e5aad0d2bfe6b749b6
- parent
- 2860a9a9db41c1b87941bdbd6f9fad198a357b81
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2022-07-16 09:57
add analysis
Diffstat
| M | README.md | 2 | ++ |
| A | analysis.md | 363 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 365 insertions, 0 deletions
diff --git a/README.md b/README.md
@@ -70,6 +70,8 @@ function contrast(fg, bg) {
70 70 combinations meet at least level A, 11% meet at least level AA, and 3% meet
71 71 level AAA. So APCA is stricter.
72 72
-1 73 Also see my [detailed analysis](analysis.md).
-1 74
73 75 ## Examples
74 76
75 77 [](https://xi.github.io/apca-introduction/examples/)
diff --git a/analysis.md b/analysis.md
@@ -0,0 +1,363 @@
-1 1 # Detailed analysis of APCA (2022-07-16)
-1 2
-1 3 ## Context: The Web Content Accessibility Guidelines (WCAG)
-1 4
-1 5 APCA was developed to address some issues related to contrast in the [Web
-1 6 Content Accessibility Guidelines] (WCAG). WCAG is an official W3C
-1 7 recommendation, a normative part of many laws all over the world, and generally
-1 8 a good read.
-1 9
-1 10 WCAG faces a difficult challenge though: There is no one-size-fits-all solution
-1 11 for accessibility. Different humans have different needs, and different
-1 12 situations require different kinds of support.
-1 13
-1 14 In the context of color contrast, vision impairments, ambient light, and screen
-1 15 settings can all have a pronounced impact on legibility. None of these are
-1 16 known beforehand by website authors, so the rules provided by WCAG need to work
-1 17 regardless of these factors.
-1 18
-1 19 Faced with the question whether it wanted to give precise instructions (that
-1 20 might not be ideal in every situation) or give nuanced but ultimately vague
-1 21 advise, WCAG went with the former. So today WCAG provides a list of detailed
-1 22 steps for evaluating a website. Many of these checks can be automated. It does
-1 23 not always result in perfect accessibility, but it gives lawmakers a solid
-1 24 baseline.
-1 25
-1 26 ## Components of contrast
-1 27
-1 28 When we speak about contrast, we actually mean a few different things:
-1 29
-1 30 - How is the contrast between two colors calculated?
-1 31 - Which thresholds are used to decide whether that contrast is sufficient?
-1 32 - How do other features like font size and font weight factor into that
-1 33 decision?
-1 34 - Which parts of the UI need to be checked?
-1 35
-1 36 In the following sections I will take a closer look at how WCAG 2.x and APCA
-1 37 answer each of these questions.
-1 38
-1 39 ## The contrast formula
-1 40
-1 41 There is no *true* contrast formula. Instead, these formulas are supposed to
-1 42 predict how most humans will a color combination, even if they cannot be
-1 43 correct 100% of the time..
-1 44
-1 45 ### A naive approach
-1 46
-1 47 ```js
-1 48 function sRGBtoY(srgb) {
-1 49 return (srgb[0] + srgb[1] + srgb[2]) / 3;
-1 50 }
-1 51
-1 52 function contrast(fg, bg) {
-1 53 var yfg = sRGBtoY(fg);
-1 54 var ybg = sRGBtoY(bg);
-1 55
-1 56 return ybg - yfg;
-1 57 };
-1 58 ```
-1 59
-1 60 This naive approach provides a baseline for the other formulas we will look at.
-1 61 It does not consider anything we know about human vision, but it already
-1 62 features the basic structure: We first transform each color to a value that
-1 63 represents lightness. Then we calculate a difference between the two lightness
-1 64 values.
-1 65
-1 66 ### WCAG 2.x
-1 67
-1 68 ```js
-1 69 function gamma(x) {
-1 70 if (x < 0.04045) {
-1 71 return x / 12.92;
-1 72 } else {
-1 73 return Math.pow((c + 0.055) / 1.055, 2.4);
-1 74 }
-1 75 }
-1 76
-1 77 function sRGBtoY(srgb) {
-1 78 var r = gamma(srgb[0] / 255);
-1 79 var g = gamma(srgb[1] / 255);
-1 80 var b = gamma(srgb[2] / 255);
-1 81
-1 82 return 0.2126 * r + 0.7152 * g + 0.0722 * b;
-1 83 }
-1 84
-1 85 function contrast(fg, bg) {
-1 86 var yfg = sRGBtoY(fg);
-1 87 var ybg = sRGBtoY(bg);
-1 88
-1 89 var c = (ybg + 0.05) / (yfg + 0.05);
-1 90 return (c < 1) ? 1 / c : c;
-1 91 };
-1 92 ```
-1 93
-1 94 In WCAG 2.x we see the same general structure, but the individual steps are
-1 95 more complicated:
-1 96
-1 97 Colors on the web are defined in the [sRGB color space]. The first part of this
-1 98 formula is the official formula to convert a sRGB color to luminance. Luminance
-1 99 is a measure for the amount of light emitted from the screen. Doubling sRGB
-1 100 values (e.g. from `#444` to `#888`) does not actually double the physical
-1 101 amount of light, so the first step is a non-linear "gamma decoding". Then the
-1 102 red, green, and blue channels are weighted to sum to the final luminance. The
-1 103 weights result from different sensitivities in the human eye: Yellow light has
-1 104 a much bigger response than the same amount of blue light.
-1 105
-1 106 Next the [Weber contrast] of those two luminances is calculated. Weber contrast
-1 107 has been called the ["gold standard" for text contrast]. It is usually defined
-1 108 as `(yfg - ybg) / ybg` which is the same as `yfg / ybg - 1`. In this case, 0.05
-1 109 is added to both values to account for ambient light. The shift by 1 is removed
-1 110 because it has no impact on the results (as long as the thresholds are adapted
-1 111 accordingly).
-1 112
-1 113 Finally, the polarity is removed so that the formula has the same results when
-1 114 the two colors are switched.
-1 115
-1 116 All in all this is a pretty solid contrast formula (at least from a theoretical
-1 117 perspective), as it just reuses parts from well established standards.
-1 118
-1 119 ### APCA
-1 120
-1 121 ```js
-1 122 function sRGBtoY(srgb) {
-1 123 var r = Math.pow(srgb[0] / 255, 2.4);
-1 124 var g = Math.pow(srgb[1] / 255, 2.4);
-1 125 var b = Math.pow(srgb[2] / 255, 2.4);
-1 126 var y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
-1 127
-1 128 if (y < 0.022) {
-1 129 y += Math.pow(0.022 - y, 1.414);
-1 130 }
-1 131 return y;
-1 132 }
-1 133
-1 134 function contrast(fg, bg) {
-1 135 var yfg = sRGBtoY(fg);
-1 136 var ybg = sRGBtoY(bg);
-1 137 var c = 1.14;
-1 138
-1 139 if (ybg > yfg) {
-1 140 c *= Math.pow(ybg, 0.56) - Math.pow(yfg, 0.57);
-1 141 } else {
-1 142 c *= Math.pow(ybg, 0.65) - Math.pow(yfg, 0.62);
-1 143 }
-1 144
-1 145 if (Math.abs(c) < 0.1) {
-1 146 return 0;
-1 147 } else if (c > 0) {
-1 148 c -= 0.027;
-1 149 } else {
-1 150 c += 0.027;
-1 151 }
-1 152
-1 153 return c * 100;
-1 154 };
-1 155 ```
-1 156
-1 157 Again we can see the same structure: We first convert colors to lightness, then
-1 158 calculate the difference between them. However, in order to be able to compare
-1 159 APCA to WCAG 2.x, I will make some modifications:
-1 160
-1 161 - The final steps do some scaling and shifting that only serves to get nice
-1 162 threshold values. Just like the shift by 1 in the WCAG formula, this can
-1 163 simply be ignored.
-1 164
-1 165 - I will also ignore the `< 0.1` condition because it only affects contrasts
-1 166 that are too low to be interesting anyway.
-1 167
-1 168 - The contrast is calculated as a difference, not as a ratio as in WCAG. I
-1 169 will look at the `exp()` of that difference. Since
-1 170 `exp(a - b) == exp(a) / exp(b)`, this allows us to convert the APCA formula
-1 171 from a difference to a ratio. Again I user the same trick: Since `exp()` is
-1 172 monotonous, it does not change the results other than moving the
-1 173 thresholds.
-1 174
-1 175 With those changes. All other differences between APCA and WCAG 2.x can be
-1 176 pushed into `sRGBtoY()`:
-1 177
-1 178 ```js
-1 179 function sRGBtoY_modified(srgb, exponent) {
-1 180 var r = Math.pow(srgb[0] / 255, 2.4);
-1 181 var g = Math.pow(srgb[1] / 255, 2.4);
-1 182 var b = Math.pow(srgb[2] / 255, 2.4);
-1 183 var y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b;
-1 184
-1 185 if (y < 0.022) {
-1 186 y += Math.pow(0.022 - y, 1.414);
-1 187 }
-1 188 return Math.exp(Math.pow(y, exponent)) / Math.exp(1);
-1 189 }
-1 190 ```
-1 191
-1 192 An interesting feature of APCA is that it uses four different exponents for
-1 193 light foreground (0.62), dark foreground (0.57), light background (0.56), and
-1 194 dark background (0.65). `sRGBtoY_modified()` takes that exponent as a second
-1 195 parameter.
-1 196
-1 197 Now that we have aligned the two formulas, what are the actual differences?
-1 198
-1 199 This conversion again uses sRGB coefficients. However, the non-linear part is
-1 200 very different. The author of APCA provides some motivation for these changes
-1 201 in the article [Regarding APCA Exponents]. The main argument seems to be that
-1 202 this more closely models real-world computer screens.
-1 203
-1 204 To get a better feeling for how these formulas compare, I plotted the results
-1 205 of `sRGBtoY()`. In order to reduce colors to a single dimension, I used gray
-1 206 `[x, x, x]`, red `[x, 0, 0]`, green `[0, x, 0]` and blue `[0, 0, x]` values.
-1 207
-1 208 
-1 209
-1 210 The four curves for APCA are very similar. Despite the very different formula,
-1 211 the WCAG 2.x curve also has a similar shape, although it is a lot lower. I
-1 212 added another curve that uses the WCAG 2.x formula, but with an ambient light
-1 213 value of 0.6 instead of 0.05. This one is very similar to the APCA curves. The
-1 214 second column shows the differences between the APCA curves and this modified
-1 215 WCAG 2.x.
-1 216
-1 217 The APCA contrast formula is certainly not as obvious a choice as the one from
-1 218 WCAG 2.x. I was not able to find much information on how it was derived. A
-1 219 closer analysis reveals that it is actually not that different from WCAG 2.x,
-1 220 but assumes much more ambient light.
-1 221
-1 222 ## Spatial frequency
-1 223
-1 224 Smaller text is generally harder to read than bigger text. In a more general
-1 225 sense, we can speak about the spatial frequency of features. This is usually
-1 226 measured in cycles per degree (cpd), since the visual field is measured as an
-1 227 angle.
-1 228
-1 229 If content is easy to read because of its spacial frequency, I do not need as
-1 230 much color contrast. On the other hand, if the spatial frequency is bad, more
-1 231 color contrast is needed. So we can defined a minimum required color contrast
-1 232 based on spatial frequency.
-1 233
-1 234 Interestingly, a lower spatial frequency is not always easier to read though.
-1 235 [Studies have shown] that the optimal spatial frequency is at about 5-7 cycles
-1 236 per degree. Below that, features get slightly harder to detect. (Perhaps that
-1 237 is the reasons for the "you don't see the forest among the trees" phenomenon.)
-1 238
-1 239 It is not obvious how to define spatial frequency in the context of the web.
-1 240 For text, font size and weight certainly play a role. But different fonts have
-1 241 wildly different interpretations of these values. Since fonts depend on user
-1 242 preference, we cannot know beforehand which fonts will be used. We also don't
-1 243 know the size of device pixels or how far the user is from the screen.
-1 244
-1 245 So how do WCAG 2.x and APCA tackle this topic?
-1 246
-1 247 ### WCAG 2.x
-1 248
-1 249 WCAG 2.x makes the distinction between regular and [large text]. Large text is
-1 250 defined as anything above 18 point or 14 point bold. The definition comes with
-1 251 a lot of notes that explain the limits of that approach though, e.g. that some
-1 252 fonts are extremely thin.
-1 253
-1 254 WCAG 2.x also comes with some rules that allow users to adapt spatial frequency
-1 255 to their needs: [1.4.4] requires that users can resize the text, [1.4.10]
-1 256 requires that they can zoom the whole page, and [1.4.12] requires that they can
-1 257 adjust text spacing.
-1 258
-1 259 So WCAG 2.x doesn't really attempt to model spatial frequency for web content.
-1 260 It elegantly works around the issue by handing control over to the users who
-1 261 have all the facts.
-1 262
-1 263 ### APCA
-1 264
-1 265 Conversely, APCA [does attempt to model spatial frequency]:
-1 266
-1 267 1. If the font has an x-height ratio of less than 0.52, increase the size by a
-1 268 factor of `0.52 / xHeightRatio`.
-1 269 2. Experimentally find a weight offset so the font has a similar weight to
-1 270 Arial or Helvetica.
-1 271 3. Consider additional font features and adapt the values accordingly.
-1 272 4. Use the lookup table provided at the link above to find a minimum contrast
-1 273 for the given combination of size and weight.
-1 274
-1 275 WCAG 3 is still an early draft and does not yet contain many guidelines. I
-1 276 assume that guidelines similar to 1.4.4, 1.4.10, and 1.4.12 will again be
-1 277 included. So the strategy of giving users control over spatial frequency will
-1 278 still work.
-1 279
-1 280 With the more sophisticated link between spatial frequency and color contrast,
-1 281 user intervention might be less relevant though. However, the model described
-1 282 above is complicated and leaves a lot of wiggle room, especially in steps 2 and
-1 283 3.
-1 284
-1 285 ## Non-text contrast
-1 286
-1 287 So far we have mainly looked at text. But other parts of a website also need to
-1 288 be distinguishable. The concept of spatial frequency was explicitly picked
-1 289 because it can cover those cases. What do WCAG 2.x and APCA have to say about
-1 290 this?
-1 291
-1 292 ### WCAG 2.x
-1 293
-1 294 [1.4.11] is specifically about this issue. It basically says that all non-text
-1 295 content that is not inactive, decorative, or controlled by the browser must
-1 296 meet contrast requirements. Spatial frequency is not considered in this case.
-1 297 It is also not always clear which parts of the UI are decorative and which are
-1 298 actually relevant.
-1 299
-1 300 ### APCA
-1 301
-1 302 As of today, APCA focusses mostly on text. Its sophisticated approach to
-1 303 spatial frequency has a lot of potential for non-text content. I could not yet
-1 304 find any discussion of that though.
-1 305
-1 306 ## Thresholds
-1 307
-1 308 ### WCAG 2.x
-1 309
-1 310 WCAG 2.x defines 3 thresholds: 3, 4.5, and 7.
-1 311
-1 312 - non-text content must have a contrast of at least 3
-1 313 - large text must have a contrast of at least 3 (AA) or 4.5 (AAA)
-1 314 - other text must have a contrast of at least 4.5 (AA) or 7 (AAA)
-1 315 - logos and inactive or decorative elements are exempted
-1 316
-1 317 How these values were derived is not completely clear:
-1 318
-1 319 > There was some user testing associated with the validation of the 2.0
-1 320 > formula. I could not quickly find a cite for that. My recollection is that
-1 321 > the hard data pointed to a ratio of 4.65:1 as a defensible break point. The
-1 322 > working group was close to rounding that up to 5:1, just to have round
-1 323 > numbers. I successfully lobbied for 4.5:1 mostly because (1) the empirical
-1 324 > data was not overwhelmingly compelling, and (2) 4.5:1 allowed the option for
-1 325 > white and black (simultaneously) on a middle gray.\
-1 326 > -- <https://github.com/w3c/wcag/issues/695#issuecomment-484187617>
-1 327
-1 328 ### APCA
-1 329
-1 330 APCA defines 6 thresholds: 15, 30, 45, 60, 75, 90.
-1 331
-1 332 The required threshold depends on the spatial frequency (see above). 45, 60,
-1 333 and 70 loosely correspond to 3, 4.5, and 7 in WCAG 2.x.
-1 334
-1 335 ## Conclusion
-1 336
-1 337 In this analysis I took a deeper look at the Accessible Perceptual Contrast
-1 338 Algorithm (APCA), a new algorithm to predict visual contrast. I compared it to
-1 339 an existing algorithm that has been part of WCAG 2.x, the current standard for
-1 340 accessibility testing for the web.
-1 341
-1 342 Though still in early development, APCA already makes two major contributions:
-1 343
-1 344 - a different color contrast formula that assume much more ambient light
-1 345 - a more sophisticated link between spatial frequency and minimum color
-1 346 contrast that allows for more nuanced thresholds
-1 347
-1 348 It is hard to evaluate APCA from a purely theoretical standpoint. Instead,
-1 349 thorough empirical validation is required. This has not yet started and will be
-1 350 a considerable effort. See <https://github.com/w3c/silver/issues/574>.
-1 351
-1 352 [Web Content Accessibility Guidelines]: https://www.w3.org/TR/WCAG21/
-1 353 [sRGB color space]: https://en.wikipedia.org/wiki/SRGB
-1 354 [Weber contrast]: https://en.wikipedia.org/wiki/Weber_contrast
-1 355 ["gold standard" for text contrast]: https://github.com/w3c/wcag/issues/695#issuecomment-483805436
-1 356 [Regarding APCA Exponents]: https://git.apcacontrast.com/documentation/regardingexponents
-1 357 [Studies have shown]: https://en.wikipedia.org/wiki/Contrast_(vision)#Contrast_sensitivity_and_visual_acuity
-1 358 [large text]: https://www.w3.org/TR/WCAG21/#dfn-large-scale
-1 359 [1.4.4]: https://www.w3.org/TR/WCAG21/#resize-text
-1 360 [1.4.10]: https://www.w3.org/TR/WCAG21/#reflow
-1 361 [1.4.12]: https://www.w3.org/TR/WCAG21/#text-spacing
-1 362 [does attempt to model spatial frequency]: https://git.apcacontrast.com/WEBTOOLS/APCA/
-1 363 [1.4.11]: https://www.w3.org/TR/WCAG21/#non-text-contrast