#ifndef ACCENT_SCORE_H
#define ACCENT_SCORE_H
#include <stdint.h>
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} RGB;
// Compute a heuristic score for how suitable an accent colour is for
// non-text UI components/accents against a background colour.
//
// Candidates below the required contrast ratio are rejected.
// Remaining candidates are ranked by perceptual lightness separation
// and chroma.
//
// Returns a value in [0, 1], where higher values indicate a better fit.
float accent_score_custom(
RGB accent,
RGB background,
float contrast_requirement,
float lightness_delta_preference,
float chroma_preference,
float lightness_tolerance,
float chroma_tolerance,
float lightness_weight,
float chroma_weight
);
// accent_score_custom with sensible defaults:
// contrast requirement: 3.00
// preferred lightness delta: 0.30
// preferred chroma: 0.15
// lightness tolerance: 0.20
// chroma tolerance: 0.15
// lightness weight: 0.75
// chroma weight: 0.25
//
// Suggested heuristic interpretation for these default parameters:
//
// 0.00..0.40 - poor
// 0.40..0.60 - marginal
// 0.60..0.75 - viable
// 0.75..0.90 - good
// 0.90..=1.00 - excellent
//
// A typical policy is to prefer good or excellent candidates, accept
// viable candidates, and reject poor or marginal candidates.
float accent_score(RGB accent, RGB background);
#ifdef ACCENT_SCORE_IMPLEMENTATION
#include <math.h>
typedef struct {
float L; // 0..1
float C; // chroma
} OKLC;
static float srgb_linear(float x) {
x /= 255.0f;
if (x <= 0.04045f) {
return x / 12.92f;
} else {
return powf((x + 0.055f) / 1.055f, 2.4f);
}
}
static float relative_luminance(RGB c) {
float r = srgb_linear(c.r);
float g = srgb_linear(c.g);
float b = srgb_linear(c.b);
return 0.2126f * r + 0.7152f * g + 0.0722f * b;
}
static float contrast_ratio(RGB a, RGB b) {
float la = relative_luminance(a);
float lb = relative_luminance(b);
if (la < lb) {
float tmp = la;
la = lb;
lb = tmp;
}
return (la + 0.05f) / (lb + 0.05f);
}
static OKLC rgb_to_oklc(RGB c) {
float r = srgb_linear(c.r);
float g = srgb_linear(c.g);
float b = srgb_linear(c.b);
// Linear sRGB -> LMS
float l =
0.4122214708f * r +
0.5363325363f * g +
0.0514459929f * b;
float m =
0.2119034982f * r +
0.6806995451f * g +
0.1073969566f * b;
float s =
0.0883024619f * r +
0.2817188376f * g +
0.6299787005f * b;
l = cbrtf(l);
m = cbrtf(m);
s = cbrtf(s);
// LMS -> Oklab
float L =
0.2104542553f * l +
0.7936177850f * m -
0.0040720468f * s;
float A =
1.9779984951f * l -
2.4285922050f * m +
0.4505937099f * s;
float B =
0.0259040371f * l +
0.7827717662f * m -
0.8086757660f * s;
OKLC out;
out.L = L;
out.C = sqrtf(A * A + B * B);
return out;
}
static float clamp01(float x) {
if (x < 0.0f) return 0.0f;
if (x > 1.0f) return 1.0f;
return x;
}
float accent_score_custom(
RGB accent,
RGB background,
float contrast_requirement,
float lightness_delta_preference,
float chroma_preference,
float lightness_tolerance,
float chroma_tolerance,
float lightness_weight,
float chroma_weight
) {
if (
contrast_requirement < 1.0f ||
lightness_delta_preference < 0.0f ||
chroma_preference < 0.0f ||
lightness_tolerance <= 0.0f ||
chroma_tolerance <= 0.0f ||
lightness_weight < 0.0f ||
chroma_weight < 0.0f
) {
return 0.0f;
}
float weight_sum = lightness_weight + chroma_weight;
if (weight_sum <= 0.0f) {
return 0.0f;
}
OKLC a = rgb_to_oklc(accent);
OKLC b = rgb_to_oklc(background);
float contrast = contrast_ratio(accent, background);
if (contrast < contrast_requirement) {
return 0.0f;
}
float dL = fabsf(a.L - b.L);
// Score reaches 1 at the preferred ΔL and falls to 0 when
// the distance from that preference reaches lightness_tolerance.
float lightness_score = clamp01(
1.0f -
fabsf(dL - lightness_delta_preference) / lightness_tolerance
);
// Score reaches 1 at the preferred chroma and falls to 0 when
// the distance from that preference reaches chroma_tolerance.
float chroma_score = clamp01(
1.0f -
fabsf(a.C - chroma_preference) / chroma_tolerance
);
return (
lightness_weight * lightness_score +
chroma_weight * chroma_score
) / weight_sum;
}
float accent_score(RGB accent, RGB background) {
return accent_score_custom(
accent,
background,
3.0f, // minimum contrast ratio
0.30f, // preferred lightness delta
0.15f, // preferred chroma
0.20f, // lightness tolerance
0.15f, // chroma tolerance
0.75f, // lightness weight
0.25f // chroma weight
);
}
#endif // ACCENT_SCORE_IMPLEMENTATION
#endif // ACCENT_SCORE_H