Skip to content

Color Palette Generator — C source

Generate harmonious color palettes - complementary, analogous, triadic, tetradic, and monochromatic - from any base color. Export to CSS, Tailwind, or JSON.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * palette-generator — harmonious palettes from a base color (C11, stdlib only).
 * CosmoDev polyglot port of src/lib/colorPalette.ts. All inputs are clamped;
 * nothing throws — unparseable hex falls back to black, like the TS reference.
 */

#include <math.h>
#include <stdio.h>
#include <string.h>

typedef struct { double h, s, l; } Hsl; /* h ∈ [0,360), s/l ∈ [0,100] */

static int hexval(char c) {
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

/* Parse #rgb / #rrggbb (leading # optional) to bytes; fallback black. */
static void hex_to_rgb(const char *hex, unsigned char out[3]) {
    while (*hex == '#' || *hex == ' ') hex++;
    char b[7] = {0};
    size_t n = strlen(hex);
    if (n == 3) { b[0]=b[1]=hex[0]; b[2]=b[3]=hex[1]; b[4]=b[5]=hex[2]; }
    else if (n == 6) memcpy(b, hex, 6);
    else { out[0]=out[1]=out[2]=0; return; }
    for (int i = 0; i < 3; i++) {
        int hi = hexval(b[i*2]), lo = hexval(b[i*2+1]);
        out[i] = (hi < 0 || lo < 0) ? 0 : (unsigned char)(hi * 16 + lo);
    }
}

static int clamp_byte(double n) { /* round half up, then clamp — TS Math.round */
    int v = (int)floor(n + 0.5);
    return v < 0 ? 0 : v > 255 ? 255 : v;
}

static void rgb_to_hex(double r, double g, double b, char out[8]) {
    snprintf(out, 8, "#%02x%02x%02x", clamp_byte(r), clamp_byte(g), clamp_byte(b));
}

/* hex → HSL. Achromatic colors give h = 0, s = 0. */
static Hsl hex_to_hsl(const char *hex) {
    unsigned char rgb[3]; hex_to_rgb(hex, rgb);
    double r = rgb[0]/255.0, g = rgb[1]/255.0, b = rgb[2]/255.0;
    double mx = fmax(r, fmax(g, b)), mn = fmin(r, fmin(g, b)), l = (mx+mn)/2, h = 0, s = 0;
    if (mx != mn) {
        double d = mx - mn;
        s = l > 0.5 ? d/(2-mx-mn) : d/(mx+mn);
        if (mx == r) h = (g-b)/d + (g < b ? 6 : 0);
        else if (mx == g) h = (b-r)/d + 2;
        else h = (r-g)/d + 4;
        h /= 6;
    }
    Hsl out = { h*360, s*100, l*100 };
    return out;
}

/* HSL → hex. Double fmod = true modulo, matching the TS ((h%360)+360)%360. */
static void hsl_to_hex(double h, double s, double l, char out[8]) {
    double H = fmod(fmod(h, 360) + 360, 360);
    double S = fmin(100, fmax(0, s))/100, L = fmin(100, fmax(0, l))/100;
    double c = (1 - fabs(2*L - 1)) * S, x = c * (1 - fabs(fmod(H/60, 2) - 1)), m = L - c/2;
    double w[6][3] = {{c,x,0},{x,c,0},{0,c,x},{0,x,c},{x,0,c},{c,0,x}}; /* per 60° sector */
    double *p = w[(int)(H / 60)];
    rgb_to_hex((p[0]+m)*255, (p[1]+m)*255, (p[2]+m)*255, out);
}

/* Harmony table: hue rotations (deg); count is 2/3/3/3/4. Unknown scheme
 * falls through to base-only. `count` steers only monochromatic. */
static const struct { const char *name; int n; double rot[4]; } SCHEMES[] = {
    { "complement",       2, { 0, 180, 0, 0 } },
    { "split-complement", 3, { 0, 150, 210, 0 } },
    { "analogous",        3, { -30, 0, 30, 0 } },
    { "triadic",          3, { 0, 120, 240, 0 } },
    { "tetradic",         4, { 0, 90, 180, 270 } },
};

int generate_palette(const char *base_hex, const char *scheme, int count,
                     char out[][8], int cap) {
    Hsl base = hex_to_hsl(base_hex);
    int n = 0;
    for (size_t k = 0; k < sizeof SCHEMES / sizeof *SCHEMES; k++) {
        if (strcmp(scheme, SCHEMES[k].name) != 0) continue;
        for (int i = 0; i < SCHEMES[k].n && n < cap; i++)
            hsl_to_hex(base.h + SCHEMES[k].rot[i], base.s, base.l, out[n++]);
        return n;
    }
    if (strcmp(scheme, "monochromatic") == 0) { /* hold h/s, spread l in [10,90] */
        int steps = count < 1 ? 1 : count;
        double lo = fmax(10, base.l - 32), hi = fmin(90, base.l + 32);
        for (int i = 0; i < steps && n < cap; i++) {
            double ll = steps == 1 ? base.l : lo + (hi - lo) * i / (steps - 1);
            hsl_to_hex(base.h, base.s, ll, out[n++]);
        }
        return n;
    }
    hsl_to_hex(base.h, base.s, base.l, out[0]); /* unknown scheme -> base only */
    return 1;
}

/* n shades / tints — RGB lerp toward black / white; i runs 1..n so the base
 * color itself is never returned, only intermediate steps. */
static void mix(const char *base_hex, int n, char out[][8], int toward_white) {
    unsigned char rgb[3]; hex_to_rgb(base_hex, rgb);
    int steps = n < 1 ? 1 : n;
    for (int i = 1; i <= steps; i++) {
        double f = (double)i / (steps + 1);
        if (toward_white)
            rgb_to_hex(rgb[0]+(255-rgb[0])*f, rgb[1]+(255-rgb[1])*f, rgb[2]+(255-rgb[2])*f, out[i-1]);
        else
            rgb_to_hex(rgb[0]*(1-f), rgb[1]*(1-f), rgb[2]*(1-f), out[i-1]);
    }
}

int shades(const char *base_hex, int n, char out[][8]) { mix(base_hex, n, out, 0); return n < 1 ? 1 : n; }
int tints(const char *base_hex, int n, char out[][8]) { mix(base_hex, n, out, 1); return n < 1 ? 1 : n; }

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →