glyphon icon indicating copy to clipboard operation
glyphon copied to clipboard

corrected srgb blending

Open BillyDM opened this issue 5 months ago • 13 comments

I've experimented with using corrected srgb alpha blending, like what egui has done in their latest release https://github.com/emilk/egui/releases/tag/0.32.0.

To me, the text at small sizes looks significantly better and more readable, especially light text on a dark background.

However, I'm not sold on the implementation or the API, and it's probably not the best way to do it. I mostly just wanted to showcase what I've found in hopes it will improve glyphon.

Also, this breaks rendering when using "web colors", so that will need to be fixed before merging.

Before (srgb)

before_dark_on_light_srgb before_light_on_dark_srgb

Before (web colors)

before_dark_on_light_web_colors before_light_on_dark_web_colors

After (srgb)

after_dark_on_light_srgb after_light_on_dark_srgb

BillyDM avatar Jul 26 '25 17:07 BillyDM

Done some more experimenting, and I found let corrected_coverage = 1.0 - pow(1.0 - coverage, 0.7); makes light text one dark backgrounds even more readable.

BillyDM avatar Aug 15 '25 20:08 BillyDM

Here's what light text on a dark background looks like with let corrected_coverage = 1.0 - pow(1.0 - coverage, 0.7);:

Screenshot_20250817_113751

BillyDM avatar Aug 17 '25 16:08 BillyDM

And here's what dark text on a light background looks like with a subtle tweak of: let corrected_coverage = 1.0 - pow(1.0 - coverage, 2.1);. It's subtle, but I think it looks a bit sharper.

Screenshot_20250817_115304

BillyDM avatar Aug 17 '25 16:08 BillyDM

I've also been thinking of how to better integrate this.

For one, we can probably have the renderer automatically detect if it should use DarkOnLight mode or LightOnDark mode by measuring the value of the color. If the value is greater than 50%, then use LightOnDark mode. If it is less than 50%, use DarkOnLight mode.

And secondly, using the pow function in the fragment shader is less than ideal. It is probably possible to do this on the CPU first before uploading the texture. Though we would need to add an extra flag to the glyph cache key to differentiate between DarkOnLight mode and LightOnDark glyphs.

BillyDM avatar Aug 17 '25 17:08 BillyDM

Here's what light text on a dark background looks like with let corrected_coverage = 1.0 - pow(1.0 - coverage, 0.7);:

Actually looking back on this it looks a little too bold. let corrected_coverage = 1.0 - sqrt(1.0 - coverage) was fine.

BillyDM avatar Aug 17 '25 17:08 BillyDM

I noticed that let corrected_coverage = 1.0 - sqrt(1.0 - coverage); actually makes light text on dark backgrounds look a bit dimmer than its intended color, so scaling it up a little bit with let corrected_coverage = min(1.0, (1.0 - sqrt(1.0 - coverage)) * 1.11); makes the text look more like its intended color.

Screenshot_20250819_162741

BillyDM avatar Aug 19 '25 21:08 BillyDM

And playing around more with dark text on light backgrounds, I think the equation let corrected_coverage = min(1.0, sqrt(coverage) * 1.11); gives better results.

Screenshot_20250819_165233

BillyDM avatar Aug 19 '25 21:08 BillyDM

Oh actually, I just made a new discovery.

I discovered that the let corrected_coverage = 2.0 * coverage - coverage * coverage; for dark text on a light background was actually an approximation for the srgb gamma function let corrected_coverage = pow(coverage, 1.0 / 2.2);.

So changed dark on light to let corrected_coverage = pow(coverage, 1.0 / 2.2); and light on dark to let corrected_coverage = 1.0 - pow(1.0 - coverage, 1.0 / 2.2); gives these results, which definitely looks the smoothest. So these are probably the actually mathematical correct functions to use.

dark_on_light_actual_srgb light_on_dark_actual_srgb

BillyDM avatar Aug 19 '25 22:08 BillyDM

Using let corrected_coverage = pow(coverage, 1.4); for light text on a dark background also works quite well, and seems better at preserving the intended color.

Screenshot_20250819_180458

BillyDM avatar Aug 19 '25 23:08 BillyDM

Or wait, perhaps using sqrt would be better than pow for performance reasons.

BillyDM avatar Aug 19 '25 23:08 BillyDM

Apparently sqrt is actually quite cheap on GPUs.

BillyDM avatar Aug 19 '25 23:08 BillyDM

I've found that let corrected_coverage = 1.0 - sqrt(min(1.0, max(0.0, 1.0 - (coverage * 1.2)))); seems to work the best for preserving the intended color for light text on a dark background.

BillyDM avatar Sep 12 '25 23:09 BillyDM

This piecewise approximation seems to work very well for light text on a dark background too:

let part_1 = 0.67 * coverage;
let part_2 = coverage - 0.14;
let part_3 = min(1.0, 2.0 * (coverage - 0.42));
let corrected_coverage = select(select(part_1, part_2, coverage > 0.424242), part_3, coverage > 0.7);

BillyDM avatar Sep 14 '25 17:09 BillyDM