Re: [csswg-drafts] [css-color-6] color-contrast() should distinguish foreground and background (#7359)

# Quick Syntax Thoughts
I am going to _briefly_ list some ideas relating to syntax, scope, and automation first. The remainder of this post is the supporting discussion.

Suggested parameter order:

```js
    color-contrast(<target contrast> <fg/bg ID (bg default)> <color to test (default where invoked)> / <array of colors (default grey per contrast)>);
```
|    Parameter    |      Default      |         Optional Values        |     Example    |
|:---------------:|:-----------------:|:------------------------------:|:--------------:|
| target contrast |         NA        |  contrast ID and target value  | lc60 or wc4.5  |
|     fg/bg ID    |   bg or invoked   |            fg or bg            |       fg       |
|  color to test  |   where invoked   | any CSS color or color keyword |  currentColor  |
| array of colors | grey per contrast |  comma sep. list of CSS colors |   #234, #abc   |
| Return fallback | grey/black/white  | returns grey at contrast or max of black/white as fallback|

"Where invoked" means that if `color-contrast()` is invoked in the `background-color:` property, the _**color to test**_ defaults to `currentColor`, as we are returning the background color. Conversely, if we invoke as:

```js
    p { color: color-contrast(lc60); }
    
    --myBackgroundColor: color-contrast(wc7 fg #e6e0dd / #123, #abc); }
```
In the first, simplest version, the algo is APCA (IDed by lc prefix) the color tested is the calculated background color, and the color returned is a grey that is Lc60 against the calculated bg color.

In the second example, because a variable is used, there is no invoked assumed use case, and either fg or bg should be specified (bg is default). The use of a slash as the divider between the first parameters and the color array/list permits unambiguous elimination of a specified color to test, which then defaults to the calculated background when invoked from the appropriate property type:

```js
    p { color: color-contrast(lc60 / #123, #abc, blue, yellow); }
```
Here the color to test is the default of the calculated background color in the p element, returning the text color from the list, because it was invoked from the `color:` property.

If none of the colors calculate to Lc60, then a fall back is returned.

The fall back is probably either the best of white or black, OR more preferably, an achromatic grey when that can satisfy the specified target contrast.

-----
## _le deeper dive_

-----
## _Spatial Freq., Polarity, Adaptation..._
Comments relating to the underlying reasons for the need (or not) to define foreground (typically high spatial frequency stimuli) vs background. (And not to mention vs the larger proximal field and other rabbit holes to explore.)


_@LeaVerou said:_
> _...provide a fixed background color and multiple candidates for the foreground color, we think the reverse should be possible as well... **We do not think there are enough use cases that warrant the complexity of providing multiple candidates for both**._

INDEED.

In terms of good guidance for minimum text contrast, there is not enough contrast range in an sRGB display to have both Lc -60 Lc and +60 even with a contrast color at the dead center of the range. The max is approximately Lc 53 or -54 ish (i.e. essentially half of the 106 or -108 range). The implication is that for fluent text, the polarity needed will be in one direction--if the fixed color is known.

BUT: if the fixed color is something like `currentColor` or `var(--someCalculatedColor)` then it is not possible to definitively know which polarity will result in the best contrast. And of course, this is the useful need for the `color-contrast()` function, ya?

## _Let's talk polarity_
The contrast models that are polarity sensitive are also spatial frequency sensitive. There is good reason for this: polarity sensitivity directly relates to high spatial frequency stimuli. So really, when we are talking about foreground, background and which is lighter or darker (polarity) we are talking about a stimuli (foreground) that is high enough in spatial frequency (i.e. small and thin) that it is not subject to "contrast constancy".

- For low spatial frequencies¹, i.e. large patches of color, very large bold text, big solid icons or buttons, polarity of a simple pair of colors is significantly less important, with the larger proximal field as more important as a factor driving field adaptation and resulting contrast constancy.
- With high spatial frequencies (i.e. body text) the proximal field and adaptation is still important, but the local adaptation of the stimuli and immediately adjacent background are weighted higher and contrast constancy effects do not come into play until much higher contrast levels.

### Putting Things on top of other things
At least for APCA, it is not "what is over" or "under". It is specifically:

**Is the _text_ lighter or darker than the _background_.**

This is the property that is most important: which is lighter in the final render, the text/line or the BG.

### Positively negative
APCA math naturally returns a positive number for dark text on a light BG, and returns a negative number for light text on a darker BG. The APCA guidelines also permit the use of an identifying string instead of a signed number. So, if using an _absolute_ Lc value, the polarity should be notated as:

- ` Lc + ` = dark text on white = BoW = light mode = positive or normal polarity.

- ` Lc - ` = light text on black = WoB = dark mode = negative or reverse polarity.


## Context of Use
The next question is, does color-contrast() need to have the color it is returning specified for use? Maybe, but possibly not, here are use cases:

In theory, when applied directly to certain CSS color properties, the "use" of the color to be returned is known:

```CSS
section { background-color: color-contrast(currentColor, Lc60, <array of colors>); }

p { color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
div { border-color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
button { outline-color: color-contrast(#e6e0dd, Lc60, <array of colors>); }
```

In the first example, since it is returning a background color, the first color is assumed to be either text, icon, border, or outline.

In the next three examples, the first color is assumed to be the background, as text, border, or outline are the "high spatial frequency foreground".

But a significant problem arises with the use of variables, wherein the returned color is not implicitly known:

```CSS
:root {
    --sectionBG: color-contrast(fg: currentColor, Lc60, <array of colors>);

    --pTextColor: color-contrast(bg: #e6e0dd, Lc60, <array of colors>); 
    --divBorderColor: color-contrast(bg: #e6e0dd, Lc60, <array of colors>); 
    --buttonOutline: color-contrast(bg: #124433, Lc60, <array of colors>);
}
```

A related syntax issue: arguably, the most important value is the amount of contrast and the algorithm used. Here is a use case where *only* the contrast value is used:

```CSS
p { color: color-contrast(Lc60); }
```

Perhaps this is too much for a CSS function to handle? But here, as `color-contrast()` is in the `color:` property, then we know we are to return the text color, and therefore want a text color that is a grey that is Lc60 relative to the current background-color (default).

### Algorithm ID
Let's next consider algorithms. It should not be needed to specify the algorithm separately from the contrast value, as the contrast value can easily contain an identifier for a given algorithm.

| Algorithm |  ID | Example Use |
|:---------:|:---:|:-----------:|
|    ∆L*    | lab |    lab60    |
|    APCA   |  lc |     lc60    |
|   WCAG2   |  wc |    wc4.5    |
| BridgePCA |  bc |    bc4.5    |
| Michelson |  %  |     60%     |
|    RMS    | rms |    rms60    |


### Defaults and Keywords
While we have the keyword `currentColor` which could be put to good use with `color-contrast()` there is use for new keywords `currentBackgroundColor`, `currentBorderColor`, and possibly `currentOutlineColor`.

As mentioned above, "default" behavior seems ideal to match per the property from where invoked.

| _Property Invoked From_ |                    _Default Test Color_                   |           _Determined From_           |
|:-----------------------:|:---------------------------------------------------------:|:-------------------------------------:|
|       background-color: |                        currentColor                       |        current/calculated color       |
|                  color: |                   currentBackgroundColor                  |      calculated background color      |
|           border-color: |                   currentBackgroundColor                  |      calculated background color      |
|             --variable: |                  NO DEFAULT/MUST SPECIFY                  |            Specified by ID            |
|       box-shadow:       | currentBorderColor OR currentBackgroundColor if no border | calculated border or background color |
|       text-shadow:      |                        currentColor                       |        current/calculated color       |


## _Two to come, pair too_
More complete appearance models have multiple color inputs. For the sake of simplicity, and also for "familiar use case", the base APCA works with only a pair: the high spatial frequency foreground, and the low spatial background (which guidelines further indicate should have ~1em padding around text, if the larger background is significantly different). 

SAPC and SACAM have additional inputs, and it's not out of the question for APCA to have either a proximal field (larger encompassing bg) or a "peripheral anchoring" input.

This might be defined as "foreground(text)", "local background", "larger background".

I mention this as in an automated context, the larger proximal may gain importance. Particular in the use case of text -> button -> background. I mention this as there may in the future be a need to consider three-way colors for a more complete automated solution.

### _THAT SAID_
In the current pair-wise APCA, the assumption is a proximal field and ambient that is at a common and "worst case" level, which in practical terms means a proximal field of between about `#dddddd` and `#ffffff`. As you can see in the following graph, a bright proximal field pushes "light mode" and "dark mode" closest together. 

![Mid Grey L* vs Adaptation proximal field chart from contrast matching experiments](https://user-images.githubusercontent.com/42009457/183515687-d14eef1e-e607-446d-be57-d126a0961f9c.png)

Also, "light mode" (dark text on a light BG) is most influenced by changes in the proximal field, at least as far as center contrast in concerned. But also, dark proximal fields "improve" light mode contrasts, but has minimal effect on dark mode contrasts.

As such, assuming a _bright_ proximal field is both reasonable and useful.

_Footnotes:_
1. And for clarity here: a big bold text or big solid buttons are a combination of low and high spatial frequencies: the large color patch area could be called "low" but the sharp edge is "high". Think in terms of signal theory and a square wave. For purposes of the contrast discussion though, let's just refer to this as low SF.

Thank you for reading,

Andy


-- 
GitHub Notification of comment by Myndex
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/7359#issuecomment-1208744612 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 9 August 2022 00:06:48 UTC