#ifndef ACCENT_SCORE_H #define ACCENT_SCORE_H #include 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 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