Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New color contrast algorithm #2

Open
rtsao opened this issue Apr 19, 2018 · 7 comments
Open

New color contrast algorithm #2

rtsao opened this issue Apr 19, 2018 · 7 comments

Comments

@rtsao
Copy link
Owner

rtsao commented Apr 19, 2018

It appears GitHub has adopted a new color contrast algorithm.

We'll need to change ours to match this again.

@zephraph
Copy link

zephraph commented Jul 1, 2019

I don't know exactly what their algorithm is, but from their message on twitter they were moving to match WCAG contrast ratio recommendations. There's a library wcag-contrast that gives contrast ratios between two colors. I wrote a simple method just to compare the contrast of black and white against a given color and pick the highest.

I don't know for 100% certainty that this is what GitHub is using, but it's likely close.

import { rgb } from "wcag-contrast";

const calcFontColor = (color: RGBColor) => {
  const blackContrastScore = rgb(color.values, [0, 0, 0]);
  const whiteContrastScore = rgb(color.values, [255, 255, 255]);

  return blackContrastScore >= whiteContrastScore ? "#000" : "#FFF";
};

@rtsao
Copy link
Owner Author

rtsao commented Jul 2, 2019

It seems there's a new URL used for label previews, for example:
https://github.com/rtsao/gh-label-svg/labels/preview/Label%20preview?color=71f2af

This seems the source of truth for client-side previewing of labels (when editing labels).

Digging around the GitHub client-side JS, I also found this:

function yc(e, t) {
  const n = (function(e) {
    const t = "string" == typeof e ? parseInt(e.replace("#", ""), 16) : e;
    return +(
      (299 * ((t >> 16) & 255) + 587 * ((t >> 8) & 255) + 114 * (255 & t)) /
      1e3 /
      255
    ).toFixed(2);
  })(t);
  null != n &&
    (n < 0.6
      ? (e.classList.remove("text-gray-dark"), e.classList.add("text-white"))
      : (e.classList.remove("text-white"), e.classList.add("text-gray-dark")));
}

I'm not sure if this is dead code now, but it seems different than what the old algorithm was (I took the old algorithm directly from the client-side JS bundle from GitHub).

It seems the new magic number is 153 (aka 0.6 * 255) instead of 150 and there's some new rounding.

gh-label-svg/index.js

Lines 70 to 77 in 3608c9b

// http://24ways.org/2010/calculating-color-contrast
// This is the same lightness algorithm as used on GitHub
function isDarkColor(color) {
const [r, g, b] = color.values;
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
// Note: the value 150 is hardcoded into GitHub
return yiq < 150;
}

At the very least, we could use the label preview endpoint to test whether our algorithm is the same.

@zephraph
Copy link

zephraph commented Jul 3, 2019

Oh, cool. That'd be an interesting test to write... So there are 16,777,215 colors in the RGB space. Granted, many of those are similar. I wrote a little script to take that list an narrow it down by eliminating a lot of the similar colors.

https://codesandbox.io/s/visual-4db6i?fontsize=14

So this is super naive, but it uses the delta-e 2000 algorithm to select a subset of colors that have enough of a visual different to be noticeable. That takes it down to 383,254. That's about a 98% decrease, not bad!

Definitely don't want to spam GitHub with nearly 400K requests though. Given that GitHub rate-limits to 5000 requests an hour, I decided just to set that as the limit. Again, I'm just taking the naive approach and taking 383,254 / 5000 which is about 77. So taking every 77th item from out list gets us down to 4978 colors. Not an exhaustive list by any means, but it's a start.

Here's a gist of that list of colors. The codesandbox link above will give you a visual preview.

I was able to write an automation script and collect a lot of those color values. I spaced it out with pauses, but I still ran into an abuse limit around 3000 colors in. Still, that gives us a bit to test with. You can see the fruits of my labor here. The output colors need to be cleaned up / formatted. I can do that later if you don't have a chance to get to it.

(This comment took a few hours, hah)

@zephraph
Copy link

zephraph commented Jul 3, 2019

The next steps are

  • Clean up the data from GitHub
  • Run tests on each of the functions to check for discrepancies.

@zephraph
Copy link

zephraph commented Jul 3, 2019

Here's the parsed out colors: https://gist.github.com/zephraph/0270e1a45ae17f9029aa845166d712ba

@zephraph
Copy link

zephraph commented Jul 3, 2019

Okay I did the tests over my lunch break.

Here are the results:

  • WCAG method I provided: 29/3901 invalid (99% success rate)
  • Old method (isDarkColor): 2903/3901 invalid (26% success rate)
  • yc from GitHub code: 1049/3901 invalid (73% success rate)

None of them are exact, but the method I provided seems to be the closest.

Feel free to double check my tests: https://gist.github.com/zephraph/174d872cc0beb36be44ec49406e50439

@rtsao
Copy link
Owner Author

rtsao commented Jul 3, 2019

Wow, thanks for investigating! That's really interesting. https://twitter.com/github/status/954467266434945024 mentions that labels now follow WCAG guidelines so it makes sense that's pretty close. Must some minor tweaks to handle some edge cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants