#include "colour.h"
#include <ctype.h>
#include <math.h>
#include <stddef.h>
#include <string.h>
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;
}