#include "colour.h" #include #include #include #include static int hex_digit(char value) { if (value >= '0' && value <= '9') { return value - '0'; } value = (char)tolower((unsigned char)value); if (value >= 'a' && value <= 'f') { return value - 'a' + 10; } return -1; } bool colour_parse_hex(const char *text, RGB *out) { int digits[6]; size_t index; if (text == NULL || out == NULL || strlen(text) != 7 || text[0] != '#') { return false; } for (index = 0; index < 6; ++index) { digits[index] = hex_digit(text[index + 1]); if (digits[index] < 0) { return false; } } out->r = (uint8_t)((digits[0] << 4) | digits[1]); out->g = (uint8_t)((digits[2] << 4) | digits[3]); out->b = (uint8_t)((digits[4] << 4) | digits[5]); return true; } double colour_srgb_to_linear(double value) { if (value <= 0.04045) { return value / 12.92; } return pow((value + 0.055) / 1.055, 2.4); } double colour_linear_to_srgb(double value) { if (value <= 0.0031308) { return value * 12.92; } return 1.055 * pow(value, 1.0 / 2.4) - 0.055; } static uint8_t linear_channel_to_u8(double value) { double encoded; if (value < 0.0) { value = 0.0; } else if (value > 1.0) { value = 1.0; } encoded = colour_linear_to_srgb(value); return (uint8_t)lround(encoded * 255.0); } RGB colour_from_linear(double red, double green, double blue) { RGB result; result.r = linear_channel_to_u8(red); result.g = linear_channel_to_u8(green); result.b = linear_channel_to_u8(blue); return result; } Oklab colour_to_oklab(RGB colour) { const double red = colour_srgb_to_linear((double)colour.r / 255.0); const double green = colour_srgb_to_linear((double)colour.g / 255.0); const double blue = colour_srgb_to_linear((double)colour.b / 255.0); const double l = cbrt(0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue); const double m = cbrt(0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue); const double s = cbrt(0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue); Oklab result; result.l = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s; result.a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s; result.b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s; return result; } double colour_oklab_distance(Oklab left, Oklab right) { const double dl = left.l - right.l; const double da = left.a - right.a; const double db = left.b - right.b; return sqrt(dl * dl + da * da + db * db); } static bool oklab_linear_rgb(Oklab lab, double *red, double *green, double *blue) { double l = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b; double m = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b; double s = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b; l = l * l * l; m = m * m * m; s = s * s * s; *red = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; *green = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; *blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; return *red >= 0.0 && *red <= 1.0 && *green >= 0.0 && *green <= 1.0 && *blue >= 0.0 && *blue <= 1.0; } RGB colour_tweak_accent(RGB colour, RGB background) { Oklab lab = colour_to_oklab(colour); const Oklab background_lab = colour_to_oklab(background); const double chroma = sqrt(lab.a * lab.a + lab.b * lab.b); double target_lightness = background_lab.l + (lab.l >= background_lab.l ? 0.30 : -0.30); double target_chroma = 0.15; double adjusted_chroma; double red = 0.0; double green = 0.0; double blue = 0.0; uint32_t attempt; if (target_lightness < 0.0) target_lightness = 0.0; if (target_lightness > 1.0) target_lightness = 1.0; lab.l += (target_lightness - lab.l) * 0.25; adjusted_chroma = chroma + (target_chroma - chroma) * 0.25; if (chroma > 0.000001) { lab.a *= adjusted_chroma / chroma; lab.b *= adjusted_chroma / chroma; } /* Keep hue and lightness while reducing chroma into the sRGB gamut. */ for (attempt = 0; attempt < 32; ++attempt) { if (oklab_linear_rgb(lab, &red, &green, &blue)) { return colour_from_linear(red, green, blue); } lab.a *= 0.90; lab.b *= 0.90; } (void)oklab_linear_rgb(lab, &red, &green, &blue); return colour_from_linear(red, green, blue); } static void accumulator_add(ColourAccumulator *acc, double red, double green, double blue, double alpha) { acc->red += colour_srgb_to_linear(red) * alpha; acc->green += colour_srgb_to_linear(green) * alpha; acc->blue += colour_srgb_to_linear(blue) * alpha; acc->alpha += alpha; } void colour_accumulator_add_u8(ColourAccumulator *acc, uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) { const double opacity = (double)alpha / 255.0; accumulator_add(acc, (double)red / 255.0, (double)green / 255.0, (double)blue / 255.0, opacity); } void colour_accumulator_add_u16(ColourAccumulator *acc, uint16_t red, uint16_t green, uint16_t blue, uint16_t alpha) { const double opacity = (double)alpha / 65535.0; accumulator_add(acc, (double)red / 65535.0, (double)green / 65535.0, (double)blue / 65535.0, opacity); } bool colour_accumulator_average(const ColourAccumulator *acc, RGB *out) { if (acc == NULL || out == NULL || acc->alpha <= 0.0) { return false; } *out = colour_from_linear(acc->red / acc->alpha, acc->green / acc->alpha, acc->blue / acc->alpha); return true; }