[gnome-software: 2/3] gs-feature-tile: Switch to WCAG contrast instead of Weber contrast
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software: 2/3] gs-feature-tile: Switch to WCAG contrast instead of Weber contrast
- Date: Mon, 13 Dec 2021 13:23:33 +0000 (UTC)
commit 469f4e2d6f17972b389363776086fe578a5b0f06
Author: Philip Withnall <pwithnall endlessos org>
Date: Fri Dec 10 12:55:39 2021 +0000
gs-feature-tile: Switch to WCAG contrast instead of Weber contrast
The Weber contrast was routinely always returning `-1.0` for dark
foreground colours (i.e. black text), which meant it was not so useful
for making decisions with.
Switching to the WCAG contrast ratio (as defined at
https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html)
seems to produce better results.
Signed-off-by: Philip Withnall <pwithnall endlessos org>
Fixes: #1440
src/gs-feature-tile.c | 128 ++++++++++++++++++++++++++++++--------------------
1 file changed, 76 insertions(+), 52 deletions(-)
---
diff --git a/src/gs-feature-tile.c b/src/gs-feature-tile.c
index f0b19c2a6..0baf10bf9 100644
--- a/src/gs-feature-tile.c
+++ b/src/gs-feature-tile.c
@@ -40,7 +40,7 @@ typedef struct
gfloat hue; /* [0.0, 1.0] */
gfloat saturation; /* [0.0, 1.0] */
gfloat brightness; /* [0.0, 1.0]; also known as lightness (HSL) or value (HSV) */
- gfloat contrast; /* [-1.0, ∞], may actually be `INF` */
+ gfloat contrast; /* (0.047, 21] */
} GsHSBC;
G_DEFINE_TYPE (GsFeatureTile, gs_feature_tile, GS_TYPE_APP_TILE)
@@ -61,13 +61,10 @@ gs_feature_tile_dispose (GObject *object)
static const gfloat min_valid_saturation = 0.5;
static const gfloat max_valid_saturation = 0.85;
-/* Subjectively chosen as the minimum absolute contrast ratio between the
- * foreground and background colours.
- *
- * Note that contrast is in the range [-1.0, ∞], so @min_abs_contrast always has
- * to be handled with positive and negative branches.
- */
-static const gfloat min_abs_contrast = 0.78;
+/* The minimum absolute contrast ratio between the foreground and background
+ * colours, from WCAG:
+ * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html */
+static const gfloat min_abs_contrast = 4.5;
/* Sort two candidate background colours for the feature tile, ranking them by
* suitability for being chosen as the background colour, with the most suitable
@@ -114,62 +111,89 @@ colors_sort_cb (gconstpointer a,
return ABS (hsbc_b->contrast) - ABS (hsbc_a->contrast);
}
-/* Calculate the weber contrast between @foreground and @background. This is
- * only valid if the area covered by @foreground is significantly smaller than
- * that covered by @background.
+/* Calculate the relative luminance of @colour. This is [0.0, 1.0], where 0.0 is
+ * the darkest black, and 1.0 is the lightest white.
*
- * See https://en.wikipedia.org/wiki/Contrast_(vision)#Weber_contrast
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef */
+static gfloat
+relative_luminance (const GsHSBC *colour)
+{
+ gfloat red, green, blue;
+ gfloat r, g, b;
+ gfloat luminance;
+
+ /* Convert to sRGB */
+ gtk_hsv_to_rgb (colour->hue, colour->saturation, colour->brightness,
+ &red, &green, &blue);
+
+ r = (red <= 0.03928) ? red / 12.92 : pow ((red + 0.055) / 1.055, 2.4);
+ g = (green <= 0.03928) ? green / 12.92 : pow ((green + 0.055) / 1.055, 2.4);
+ b = (blue <= 0.03928) ? blue / 12.92 : pow ((blue + 0.055) / 1.055, 2.4);
+
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ g_assert (luminance >= 0.0 && luminance <= 1.0);
+ return luminance;
+}
+
+/* Calculate the WCAG contrast ratio between the two colours. The returned ratio
+ * is in the range (0.047, 21].
*
- * The return value is in the range [-1.0, ∞], and may actually be `INF`.
- */
+ * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef */
static gfloat
-weber_contrast (const GsHSBC *foreground,
- const GsHSBC *background)
+wcag_contrast (const GsHSBC *foreground,
+ const GsHSBC *background)
{
- /* Note that this may divide by zero, and that’s fine. However, in
- * IEEE 754, dividing ±0.0 by ±0.0 results in NAN, so avoid that. */
- if (foreground->brightness == background->brightness)
- return 0.0;
+ const GsHSBC *lighter, *darker;
+ gfloat ratio;
- return (foreground->brightness - background->brightness) / background->brightness;
+ if (foreground->brightness >= background->brightness) {
+ lighter = foreground;
+ darker = background;
+ } else {
+ lighter = background;
+ darker = foreground;
+ }
+
+ ratio = (relative_luminance (lighter) + 0.05) / (relative_luminance (darker) + 0.05);
+ g_assert (ratio > 0.047 && ratio <= 21);
+ return ratio;
}
-/* Inverse of the Weber contrast function which finds a brightness (luminance)
- * level for the background which gives an absolute contrast of at least
- * @desired_abs_contrast against @foreground. The same validity restrictions
- * apply as for weber_contrast().
+/* Calculate a new brightness value for @background which improves its contrast
+ * (as calculated using wcag_contrast()) with @foreground to at least
+ * @desired_contrast.
*
* The return value is in the range [0.0, 1.0].
*/
static gfloat
-weber_contrast_find_brightness (const GsHSBC *foreground,
- gfloat desired_abs_contrast)
+wcag_contrast_find_brightness (const GsHSBC *foreground,
+ const GsHSBC *background,
+ gfloat desired_contrast)
{
- g_assert (desired_abs_contrast >= 0.0);
+ GsHSBC modified_background;
- /* There are two solutions to solving
- * |(I - I_B) / I_B| ≥ C
- * in the general case, although given that I (`foreground->brightness`)
- * and I_B (the return value) are only valid in the range [0.0, 1.0],
- * there are many cases where only one solution is valid.
- *
- * Solutions are:
- * I_B ≤ I / (1 + C)
- * I_B ≥ I / (1 - C)
- *
- * When given a choice, prefer the solution which gives a higher
- * brightness.
+ g_assert (desired_contrast > 0.047 && desired_contrast <= 21);
+
+ /* This is an optimisation problem of modifying @background until
+ * the WCAG contrast is at least @desired_contrast. There might be a
+ * closed-form solution to this but I can’t be bothered to work it out
+ * right now. An optimisation loop should work.
*
- * In the case I == 0.0, and value of I_B is valid (as per the second
- * solution), so arbitrarily choose 0.5 as a solution.
- */
- if (foreground->brightness == 0.0)
- return 0.5;
- else if (foreground->brightness <= 1.0 - desired_abs_contrast &&
- desired_abs_contrast < 1.0)
- return foreground->brightness / (1.0 - desired_abs_contrast);
- else
- return foreground->brightness / (1.0 + desired_abs_contrast);
+ * wcag_contrast() compares the lightest and darkest of the two colours,
+ * so ensure the background brightness is modified in the correct
+ * direction (increased or decreased) depending on whether the
+ * foreground colour is originally the brighter. This gives the largest
+ * search space for the background colour brightness, and ensures the
+ * optimisation works with dark and light themes. */
+ for (modified_background = *background;
+ modified_background.brightness >= 0.0 &&
+ modified_background.brightness <= 1.0 &&
+ wcag_contrast (foreground, &modified_background) < desired_contrast;
+ modified_background.brightness += ((foreground->brightness > 0.5) ? -0.1 : 0.1)) {
+ /* Nothing to do here */
+ }
+
+ return CLAMP (modified_background.brightness, 0.0, 1.0);
}
static void
@@ -313,7 +337,7 @@ gs_feature_tile_refresh (GsAppTile *self)
gtk_rgb_to_hsv (rgba->red, rgba->green, rgba->blue,
&hsbc.hue, &hsbc.saturation, &hsbc.brightness);
- hsbc.contrast = weber_contrast (&fg_hsbc, &hsbc);
+ hsbc.contrast = wcag_contrast (&fg_hsbc, &hsbc);
g_array_append_val (colors, hsbc);
g_debug (" • RGB: (%f, %f, %f), HSB: (%f, %f, %f), contrast: %f",
@@ -340,7 +364,7 @@ gs_feature_tile_refresh (GsAppTile *self)
if (chosen_hsbc->contrast >= -min_abs_contrast &&
chosen_hsbc->contrast <= min_abs_contrast)
- chosen_hsbc_modified.brightness = weber_contrast_find_brightness
(&fg_hsbc, min_abs_contrast);
+ chosen_hsbc_modified.brightness = wcag_contrast_find_brightness
(&fg_hsbc, &chosen_hsbc_modified, min_abs_contrast);
gtk_hsv_to_rgb (chosen_hsbc_modified.hue,
chosen_hsbc_modified.saturation,
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]