So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)
Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what
I need to reverse my contrast formula
means?
It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.
Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.
For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.
However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).
So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.
(The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)
Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.
RGB contrast % from 4.5 % from 0x747474
111111 1.11 305.41% 582.35%
191919 1.19 278.15% 364.00%
222222 1.32 240.91% 241.18%
2a2a2a 1.46 208.22% 176.19%
333333 1.66 171.08% 127.45%
3b3b3b 1.87 140.64% 96.61%
444444 2.16 108.33% 70.59%
4c4c4c 2.45 83.67% 52.63%
555555 2.82 59.57% 36.47%
5d5d5d 3.19 41.07% 24.73%
666666 3.66 22.95% 13.73%
6e6e6e 4.12 9.22% 5.45%
As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.
This would be a fascinating math problem that I currently don't have time to play with.
Update Jan 18, 2019
I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.
Test case
#ee0add
for the light color (magenta-ish)
#445566
for the dark color (dark gray)
- contrast ratio 2.09
When computing the "relative luminance" of a color, it has a conditional statement.
if X <= 0.03928 then
X = X/12.92
else
X = ((X+0.055)/1.055) ^ 2.4
Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).
The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)
The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)
The "contrast ratio" is
(0.236 + .05) / (0.0868 + .05) = 2.09
(You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)
If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:
4.5 = (0.236 + .05) / (XX + .05)
So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)
The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)
If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.
Original luminance for #445566
r = 0x44 = 68
g = 0x55 = 85
b = 0x66 = 102
r1 = 68 / 255 = 0.26666666666666666666666666666667
g1 = 85 / 255 = 0.33333333333333333333333333333333
b1 = 102 / 255 = 0.4
r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868
l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
= 0.0868525191131797135799815832377
Working backwards, take 15.6% of the r2, g2, and b2 values
r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804
Now undo that mess with ^^2.4
and the other stuff
- to undo
X^^2.4
you have to do the inverse, X^^(1/2.4)
or X^^(0.4167)
- then multiply by 1.055
- then subtract 0.055
- then multiply by 255
pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565
multiply by 1.055
0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341
subtract 0.055
0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341
multiply by 255
0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28
So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.
https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028
I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.