Myndex / SAPC-APCA

APCA (Accessible Perceptual Contrast Algorithm) is a new method for predicting contrast for use in emerging web standards (WCAG 3) for determining readability contrast. APCA is derived form the SAPC (S-LUV Advanced Predictive Color) which is an accessibility-oriented color appearance model designed for self-illuminated displays.
https://git.apcacontrast.com/documentation/APCAeasyIntro
Other
435 stars 14 forks source link

Question: find desired relative luminance by contrast and bg color? #9

Closed OrKoN closed 3 years ago

OrKoN commented 3 years ago

Hi,

I am implementing APCA based on the Resources section of https://w3c.github.io/silver/guidelines/methods/Method-font-characteristic-contrast.html. One of the features I am working on is the possibility to suggest a text color with a good contrast. For this, I need to know the desired luminance of the color that'd satisfy the contrast. As far as I see, there is no way to compute it because, in some cases, the contrast percentage depends on the actual luminance of the color yet to be suggest (due to the part Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + abs(Ytxt - blkThrs) ^ blkClmp;). Am I right?

In other words, I want to understand if the following function is possible with APCA: getDesiredLuminance(bgColor, desiredContrast).

P.S. sorry if it's not the right place to discuss it. Any suggestions are welcome!

Myndex commented 3 years ago

Hi Alex @OrKoN thank you for writing in, and sorry for the delay in response, I just now saw the post.

It is definitely possible to find a contrast target relative to one color. You may want to add 'polarity' to your parameters, though strictly speaking any good contrast for readability will essentially determine polarity (i.e light text on dark vss dark text on light).

TUTORIAL

Let's assume a light background and dark text, as that is the example you posted.

  1. Take the contrast level you want as a target, and divide the CONTRAST LEVEL by phi (1.618). so for contrast 80, that's 0.8 / 1.618 = 0.49, 0.49 is the "Raw" unscaled contrast value for the APCA 80 level in this example.

  2. Then Get Y for the sRGB background, using the APCA methods. For example, #CCC is Y: 60.96 — we'll use 60Y for your BG in this example.

  3. Next, you need to apply the BG exponent to your BG's luminance. This is where polarity comes in. If you are looking for a light background with dark text, then apply the const normBGExp = 0.38; so for the #CCC BG it is Math.pow(0.60, 0.38) which is ~ 0.824 (for a dark BG with light text, apply const revBGExp = 0.5; ).

  4. Now, subtract the raw unscaled contrast value from the processed BG value, so 0.824 - 0.49 = 0.334

  5. 0.334 is the APCA text target value for the #CCC background with APCA 80 contrast. Now let's find luminance.

  6. Raise 0.334 to the power of the inverse of const normTXTExp = 0.43; Which is 1/0.43 or about 2.326

  7. So then Math.pow(0.334, 2.326) is 0.078 Y, making the maximum luminance of your text 0.078 Y if scaled 0.0-1.0, or 7.8 Y if you are working with Y scaled 1-100.

  8. For the sRGB value (as grey) apply the inverse sRGB exponent: const sRGBtrcEncode = 1.0 / sRGBtrc;

  9. This results in a specified text color of #505050 or darker.


I hope that clarifies the inverse procedure. That is going to be added as a feature to the APCA library soon, but thank you for asking this question will become part of the FAQ.

Other Things

You asked about this line of code:

(due to the part Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + abs(Ytxt - blkThrs) ^ blkClmp;). Am I right?

I'm not clear on your question, but, this is the soft clip, a dynamic response to deal with flare/noise etc. It only applies to luminances under 0.02. It too can be reversed — in the above example, it would not apply as your target luminance was 0.078, much higher than the threshold. If your target luminance is under 0.02 (for 0-1 scaling obv) then you might want to add that in.

I hope that answers your questions,

Thank you,

Andy

OrKoN commented 3 years ago

Hi Andy @Myndex,

Thanks for your response! I have some questions still. For example, I working on the case when the text and background colors are black (#000). The goal is to fix the text color. Let's say the target contrast is 68%.

1) "Raw" unscaled contrast will be 0.68 / 1.618 = ~0.42 2) Relative luminance Y for the background color is 0. 3) In this case, the current text color is not lighter than the BG color. Therefore, applying the revBGExp exponent (dark BG with light text) to the BG's luminance. Math.pow(0, 0.38) = 0. 4) It makes the APCA text target value to be ~0.42 (0.42 - 0). 5) The desired text luminance will be 0.42 ^ (1 / normTXTExp) = 0.42 ^(1 / 0.43) = ~0.13

Then I find a color that is closest to black that has the luminance of ~0.13 that gives me #676767 (it has luminance of 0.13310678168645892). But if I compute the contrast for this color it's below the threshold (68%) so something is not quite correct. On step 2, I think I need to apply the soft clip because the BG's Y is less than blkThrs. It gives me a different color but it's not above the threshold although its luminance is correct.

Also, I notice some differences between my implementation and the implementation at https://www.myndex.com/SAPC/. For example, for the colors #676767(text) and #000000 (BG), I get the contrast value -62.88 and the SAPC calculator gives me -56.12. My implementation is based on the Resources section at https://w3c.github.io/silver/guidelines/methods/Method-font-characteristic-contrast.html:

const normBgExp = 0.38;
const normFgExp = 0.43;
const revBgExp = 0.5;
const revFgExp = 0.43;
const blkThrs = 0.02;
const blkClmp = 1.75;

function luminanceAPCA([rSRGB, gSRGB, bSRGB]) {
  const r = Math.pow(rSRGB, 2.218);
  const g = Math.pow(gSRGB, 2.218);
  const b = Math.pow(bSRGB, 2.218);

  return 0.2126 * r + 0.7156 * g + 0.0722 * b;
}

// 0 - 1 values for r, g, b components
function contrastRatioAPCA(fgRGB, bgRGB) {
  let bgLuminance = luminanceAPCA(bgRGB);
  let fgLuminance = luminanceAPCA(fgRGB);

  if (bgLuminance > fgLuminance) {
    fgLuminance =
        (fgLuminance > blkThrs) ? fgLuminance : fgLuminance + Math.pow(Math.abs(fgLuminance - blkThrs), blkClmp);
    return (Math.pow(bgLuminance, normBgExp) - Math.pow(fgLuminance, normFgExp)) * 161.8;
  }

  bgLuminance =
      (bgLuminance > blkThrs) ? bgLuminance : bgLuminance + Math.pow(Math.abs(bgLuminance - blkThrs), blkClmp);
  return (Math.pow(bgLuminance, revBgExp) - Math.pow(fgLuminance, revFgExp)) * 161.8;
}

Also, I didn't understand what you meant by:

For the sRGB value (as grey) apply the inverse sRGB exponent: const sRGBtrcEncode = 1.0 / sRGBtrc;

Where is the value sRGBtrc come from?

Best regards, Alex

OrKoN commented 3 years ago

Never mind, I have realised where I was making a mistake. Thanks for your help!

Myndex commented 3 years ago

Hi @OrKoN Alex,

The code samples at https://w3c.github.io/silver/guidelines/methods/Method-font-characteristic-contrast.html: are OBSOLETE. Do not use them please.

The authoritative code is here and only here in this repository.

What I think you are seeing is the black clip exponent has changed, it is now 1.33

If you are building conformance tools, you want only the files in this repository with APCA in the name.

Let me know if I can help further.

Thank you!

Andy

OrKoN commented 3 years ago

Thanks! I have not realised that the w3c page is not up to date. I have updated the implementation according to the content of this repo. Perhaps you'd be interested to take a look and provide feedback: https://source.chromium.org/chromium/chromium/src/+/master:third_party/devtools-frontend/src/front_end/common/ColorUtils.js;l=123;drc=1d0105053a41da58fe3861c48973f9377aabd2b2 It'd be available in the next Canary version as an experiment (so it's expected to change and evolve).

OrKoN commented 3 years ago

I think my questions are completely answered now. So we can close the issue.