CSS Gradient Generator — C source
Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* =============================================================================
* css-gradient-generator.c — CosmoDev polyglot showcase port of the
* `css-gradient-generator` tool
* -----------------------------------------------------------------------------
* Language : C (C11, standard library only)
* Source: ported from src/lib/cssGradient.ts (the canonical, live TypeScript
* lib); mirrors src/tool-sources/css-gradient-generator/{python.py,rust.rs}
* License : display source — part of CosmoDev's polyglot tool pages
* (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
* Python ports and the other language ports.
* -----------------------------------------------------------------------------
* Pure CSS-gradient builder. Build linear / radial / conic CSS gradient
* strings from a small config: a gradient type, an angle, and a list of color
* stops. Deterministic and side-effect free; invalid input degrades
* gracefully (unknown colors → solid black, too few stops → black/white
* default ramp) rather than failing. Returned strings are heap-allocated —
* the caller owns them and must free().
* =============================================================================
*/
#include <ctype.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* CSS gradient kinds we know how to render. */
typedef enum { GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_CONIC } gradient_type;
/* One color anchor on the gradient ramp. Position is a percentage 0..100. */
typedef struct {
const char *color;
double position;
} gradient_stop;
/* Full input to build_gradient. `radial_shape` is only meaningful for
* GRADIENT_RADIAL; NULL falls back to "circle" (mirroring the TypeScript
* `?? 'circle'` — an explicit "" passes through unchanged). */
typedef struct {
gradient_type type;
double angle;
const gradient_stop *stops;
size_t stop_count; /* number of entries in `stops` */
const char *radial_shape; /* NULL → "circle" */
} gradient_config;
/* Outcome of parse_color: an ok flag plus a human message (NULL when ok).
* `error` is heap-allocated when set — free() it. */
typedef struct {
int ok;
char *error;
} color_result;
/* Named CSS colors this tool accepts. The full CSS spec defines ~148, but we
* intentionally accept only the common, unambiguous set so output stays
* predictable (mirrors the TypeScript allow-list). */
static const char *const NAMED_COLORS[] = {
"transparent", "black", "white", "red", "green", "blue", "yellow", "orange",
"purple", "pink", "gray", "grey", "brown", "cyan", "magenta", "none", "currentcolor",
};
/* Whitespace JS String.prototype.trim() strips. */
static const char *const WS = " \t\n\r\f\v";
/* --- tiny growable string buffer -------------------------------------------- */
typedef struct {
char *data; /* NUL-terminated once non-NULL */
size_t len;
size_t cap;
} strbuf;
static int strbuf_grow(strbuf *b, size_t extra)
{
if (b->data && b->len + extra + 1 <= b->cap) return 0;
size_t cap = b->cap ? b->cap * 2 : 64;
while (cap < b->len + extra + 1) cap *= 2;
char *p = realloc(b->data, cap);
if (!p) return -1;
b->data = p;
b->cap = cap;
return 0;
}
static int strbuf_addn(strbuf *b, const char *s, size_t n)
{
if (strbuf_grow(b, n)) return -1;
memcpy(b->data + b->len, s, n);
b->len += n;
b->data[b->len] = '\0';
return 0;
}
static int strbuf_add(strbuf *b, const char *s)
{
return strbuf_addn(b, s, strlen(s));
}
/* printf-style append (vs*printf makes this C11-portable). */
static int strbuf_printf(strbuf *b, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n < 0 || strbuf_grow(b, (size_t)n)) return -1;
va_start(ap, fmt);
vsnprintf(b->data + b->len, (size_t)n + 1, fmt, ap);
va_end(ap);
b->len += (size_t)n;
return 0;
}
/* sprintf into a fresh heap string (a portable strdup/printf combo). */
static char *dupf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n < 0) return NULL;
char *s = malloc((size_t)n + 1);
if (!s) return NULL;
va_start(ap, fmt);
vsnprintf(s, (size_t)n + 1, fmt, ap);
va_end(ap);
return s;
}
/* --- color validation -------------------------------------------------------- */
static int is_named_color(const char *c)
{
const size_t count = sizeof NAMED_COLORS / sizeof NAMED_COLORS[0];
for (size_t i = 0; i < count; i++)
if (strcmp(NAMED_COLORS[i], c) == 0) return 1;
return 0;
}
/* True if a byte is a lowercase ASCII hex digit (`0-9` or `a-f`). Only the
* lowercase form is accepted because parse_color lowercases its input before
* testing, exactly like the TS regex `[0-9a-f]`. */
static int is_hex_byte(unsigned char b)
{
return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f');
}
/* Validates a hex color by shape: `#` followed by exactly 3, 6, or 8 lowercase
* hex digits. This collapses the two TS regexes (`#[0-9a-f]{3}([0-9a-f]{3})?`
* and `#[0-9a-f]{8}`) into one structural check — C11 has no regex. */
static int is_hex_color(const char *c)
{
size_t len = strlen(c);
if (len != 4 && len != 7 && len != 9) return 0; /* '#' + {3,6,8} digits */
if (c[0] != '#') return 0;
for (size_t i = 1; i < len; i++)
if (!is_hex_byte((unsigned char)c[i])) return 0;
return 1;
}
/* Validates a functional color form `name(...)`: the string must start with one
* of `openers` (e.g. "rgba(", "rgb("), end with ')', and have a nonempty body
* containing no ')'. Mirrors the TS `^rgba?\([^)]+\)$` / `^hsla?\([^)]+\)$`.
* Longer openers must come first so "rgba(" is tried before "rgb(". */
static int is_functional_color(const char *c, const char *const *openers, size_t opener_count)
{
for (size_t i = 0; i < opener_count; i++) {
size_t n = strlen(openers[i]);
if (strncmp(c, openers[i], n) != 0) continue;
size_t len = strlen(c);
if (len < n + 1 || c[len - 1] != ')') return 0;
size_t body_len = len - n - 1; /* body = c[n .. len-1) */
if (body_len == 0) return 0;
if (memchr(c + n, ')', body_len) != NULL) return 0;
return 1;
}
return 0;
}
/* Length of `s` with leading/trailing whitespace removed; `*start_out`
* receives the advanced pointer. Never modifies the input. */
static size_t trimmed_span(const char *s, const char **start_out)
{
const char *start = s + strspn(s, WS);
size_t len = strlen(start);
while (len > 0 && strchr(WS, start[len - 1]) != NULL) len--;
*start_out = start;
return len;
}
/* Validate a CSS color string.
*
* Accepts named colors, #RGB / #RRGGBB / #RRGGBBAA hex, and rgb()/rgba()/
* hsl()/hsla() functional forms. The input is trimmed and lowercased (into
* scratch memory — the original is never modified) before testing.
*/
color_result parse_color(const char *color)
{
color_result res = { 0, NULL };
if (!color) color = "";
const char *trimmed = NULL;
size_t len = trimmed_span(color, &trimmed);
char *c = malloc(len + 1);
if (!c) return res; /* out of memory: report invalid — callers degrade to black */
for (size_t i = 0; i < len; i++)
c[i] = (char)tolower((unsigned char)trimmed[i]);
c[len] = '\0';
static const char *const RGB_OPENERS[] = { "rgba(", "rgb(" };
static const char *const HSL_OPENERS[] = { "hsla(", "hsl(" };
if (len == 0) {
res.error = dupf("empty color");
} else if (is_named_color(c)) {
res.ok = 1;
} else if (is_hex_color(c)) {
res.ok = 1;
} else if (is_functional_color(c, RGB_OPENERS, 2) || is_functional_color(c, HSL_OPENERS, 2)) {
res.ok = 1;
} else {
res.error = dupf("invalid color: %s", color);
}
free(c);
return res;
}
/* Coerce a possibly-invalid color to a safe value: valid → the trimmed
* original (casing preserved), invalid → solid black. Heap-allocated; the
* caller frees. Guarantees the gradient always has a usable color value. */
static char *normalize_color(const char *color)
{
color_result r = parse_color(color);
free(r.error);
if (!r.ok) return dupf("%s", "#000000");
const char *start = NULL;
size_t len = trimmed_span(color, &start);
char *out = malloc(len + 1);
if (!out) return NULL; /* caller treats NULL as OOM */
memcpy(out, start, len);
out[len] = '\0';
return out;
}
/* --- number formatting ------------------------------------------------------- */
/* Render a double the way JavaScript's template literal does — shortest
* round-tripping decimal, no trailing ".0" on whole numbers. C has no direct
* shortest formatting, so ask %g for increasing precision until the output
* parses back to the same double. %g already drops trailing zeros, so 90.0
* renders as "90", matching String(90). Heap-allocated; the caller frees. */
static char *format_number(double x)
{
char buf[64];
for (int prec = 1; prec <= 17; prec++) {
snprintf(buf, sizeof buf, "%.*g", prec, x);
if (strtod(buf, NULL) == x) break;
}
return dupf("%s", buf);
}
/* --- the builder ------------------------------------------------------------- */
/* Render a complete CSS gradient string (heap-allocated; caller frees).
*
* Stops are sorted ascending by position, stable: qsort() is not guaranteed
* stable and qsort_r() is not portable C11, so an index array is
* insertion-sorted instead — stable by construction (an index only moves past
* strictly-smaller positions), matching modern JavaScript's Array.sort.
* Fewer than two stops collapse to a black → white default ramp so the output
* is always renderable. Positions round via floor(x + 0.5), which agrees with
* Math.round on the non-negative 0..100 position domain.
*/
char *build_gradient(const gradient_config *config)
{
size_t n = config->stop_count;
/* Stable insertion sort over indices (see comment above). */
size_t *order = NULL;
if (n > 0) {
order = malloc(n * sizeof *order);
if (!order) return NULL;
for (size_t i = 0; i < n; i++) order[i] = i;
for (size_t i = 1; i < n; i++) {
size_t key = order[i];
double kp = config->stops[key].position;
size_t j = i;
while (j > 0 && config->stops[order[j - 1]].position > kp) {
order[j] = order[j - 1];
j--;
}
order[j] = key;
}
}
const gradient_stop defaults[] = {
{ "#000000", 0.0 },
{ "#ffffff", 100.0 },
};
char *angle = format_number(config->angle);
if (!angle) {
free(order);
return NULL;
}
strbuf out = { NULL, 0, 0 };
switch (config->type) {
case GRADIENT_LINEAR:
if (strbuf_printf(&out, "linear-gradient(%sdeg, ", angle)) goto oom;
break;
case GRADIENT_RADIAL: {
/* `?? 'circle'`: NULL → "circle"; non-NULL → verbatim (even if empty). */
const char *shape = config->radial_shape ? config->radial_shape : "circle";
if (strbuf_printf(&out, "radial-gradient(%s, ", shape)) goto oom;
break;
}
case GRADIENT_CONIC:
if (strbuf_printf(&out, "conic-gradient(from %sdeg, ", angle)) goto oom;
break;
default:
/* Unknown type — mirrors the TypeScript, which falls off the switch and
* implicitly returns undefined (an empty string here). */
free(angle);
free(order);
return dupf("%s", "");
}
{
const gradient_stop *ramp = config->stops;
size_t count = n;
const size_t *seq = order;
if (n < 2) {
ramp = defaults; /* already in position order */
count = 2;
seq = NULL;
}
for (size_t i = 0; i < count; i++) {
if (i > 0 && strbuf_add(&out, ", ")) goto oom;
const gradient_stop *s = &ramp[seq ? seq[i] : i];
char *col = normalize_color(s->color);
if (!col) goto oom;
int failed = strbuf_printf(&out, "%s %ld%%", col, (long)floor(s->position + 0.5));
free(col);
if (failed) goto oom;
}
}
if (strbuf_add(&out, ")")) goto oom;
free(angle);
free(order);
return out.data ? out.data : dupf("%s", ""); /* empty stops still yield a head */
oom:
free(angle);
free(order);
free(out.data);
return NULL;
}
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 →