Skip to content

CSS Animation Playground — C source

Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.

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

/*
 * css-animation-playground — C port (CosmoDev polyglot showcase).
 *
 * CSS cubic-bezier easing utilities: given an animation progress x in [0,1],
 * solve the cubic-bezier easing curve for its output y, and round-trip
 * control-point coords to/from the "cubic-bezier(x1, y1, x2, y2)" CSS string.
 *
 * Language: C11 (ISO/IEC 9899:2011), standard library only.
 * Source:   CosmoDev polyglot showcase port of src/lib/animation.ts —
 *           display source, part of CosmoDev's tool pages.
 * License:  Display source, part of CosmoDev's pages (dev.cosmolabs.org).
 *
 * The Bézier runs from P0=(0,0) to P3=(1,1) with control points P1=(x1,y1),
 * P2=(x2,y2). Every function below is total: it never traps and always
 * returns a finite value. C has no Option type, so parse_css_bezier reports
 * failure with a return code and writes its results through an out-parameter.
 */

#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Polynomial coefficients for one axis of the cubic, in power form so the
 * polynomial can be evaluated with nested multiplication. */
typedef struct {
    double a;
    double b;
    double c;
} bezier_coeffs;

/* The four control-point coordinates [x1, y1, x2, y2]. */
typedef double bezier_coords[4];

/* Named CSS easings expressed as their cubic-bezier control-point coords. */
typedef struct {
    const char *name;
    bezier_coords coords;
} easing_preset;

const easing_preset EASING_PRESETS[] = {
    {"linear", {0.0, 0.0, 1.0, 1.0}},
    {"ease", {0.25, 0.1, 0.25, 1.0}},
    {"ease-in", {0.42, 0.0, 1.0, 1.0}},
    {"ease-out", {0.0, 0.0, 0.58, 1.0}},
    {"ease-in-out", {0.42, 0.0, 0.58, 1.0}},
};

/* Compute the polynomial coefficients for one axis (c1, c2 are the
 * control-point coordinates on that axis). */
static bezier_coeffs coeffs_of(double c1, double c2) {
    bezier_coeffs k;
    k.c = 3.0 * c1;
    k.b = 3.0 * (c2 - c1) - k.c;
    k.a = 1.0 - k.c - k.b;
    return k;
}

/* Evaluate the axis polynomial: ((a*t + b)*t + c)*t (Horner form). */
static double sample(double t, bezier_coeffs k) {
    return ((k.a * t + k.b) * t + k.c) * t;
}

/* Derivative of the axis polynomial: (3a*t + 2b)*t + c. */
static double sample_derivative(double t, bezier_coeffs k) {
    return (3.0 * k.a * t + 2.0 * k.b) * t + k.c;
}

/* Coerce non-finite values to 0.0, mirroring the TS Number.isFinite guard. */
static double fin(double v) {
    return isfinite(v) ? v : 0.0;
}

/* Given animation progress x in [0,1], solve the cubic-bezier easing for the
 * corresponding output y. Newton-Raphson (clamped to [0,1]). Endpoints are
 * exact: y(0)=0 and y(1)=1. */
double cubic_bezier_y(double x, double x1, double y1, double x2, double y2) {
    double px = fin(x);
    if (px <= 0.0) {
        return 0.0;
    }
    if (px >= 1.0) {
        return 1.0;
    }

    bezier_coeffs x_c = coeffs_of(fin(x1), fin(x2));
    bezier_coeffs y_c = coeffs_of(fin(y1), fin(y2));

    /* Newton-Raphson: find t such that x(t) = px, then read y(t). px is a
     * strong initial guess because x(t) is monotonic for valid curves. */
    double t = px;
    for (int i = 0; i < 8; i++) {
        double dx = sample(t, x_c) - px;
        if (fabs(dx) < 1e-6) {
            break;
        }
        double d = sample_derivative(t, x_c);
        if (fabs(d) < 1e-7) {
            break; /* guard against division by ~0 */
        }
        t -= dx / d;
    }
    if (t < 0.0) {
        t = 0.0;
    } else if (t > 1.0) {
        t = 1.0;
    }
    return sample(t, y_c);
}

/* Round a coord to the nearest 1e-6 and render it the way JS String() would:
 * integers without a trailing ".0", and -0.0 normalized to "0". JS Math.round
 * rounds half toward +infinity, so floor(x + 0.5) matches it exactly. */
static void fmt_coord(double n, char *buf, size_t cap) {
    double r = floor(n * 1e6 + 0.5) / 1e6;
    if (r == 0.0) {
        r = 0.0; /* normalize -0.0 */
    }
    snprintf(buf, cap, "%.6f", r);
    /* Trim trailing zeros; drop the '.' too when no fraction remains. */
    char *dot = strchr(buf, '.');
    if (dot != NULL) {
        char *end = buf + strlen(buf);
        while (end > dot + 1 && end[-1] == '0') {
            *--end = '\0';
        }
        if (end[-1] == '.') {
            *--end = '\0';
        }
    }
}

/* Format four control-point coords as a CSS "cubic-bezier(...)" string into
 * the caller-provided buffer (at least 64 bytes hold any result). */
void css_bezier(double x1, double y1, double x2, double y2, char *out, size_t cap) {
    char a[32], b[32], c[32], d[32];
    fmt_coord(x1, a, sizeof a);
    fmt_coord(y1, b, sizeof b);
    fmt_coord(x2, c, sizeof c);
    fmt_coord(y2, d, sizeof d);
    snprintf(out, cap, "cubic-bezier(%s,%s,%s,%s)", a, b, c, d);
}

/* Case-insensitive ASCII prefix test (strncasecmp is POSIX, not ISO C11). */
static bool starts_with_ci(const char *s, const char *prefix) {
    size_t n = strlen(prefix);
    if (strlen(s) < n) {
        return false;
    }
    for (size_t i = 0; i < n; i++) {
        if (tolower((unsigned char)s[i]) != tolower((unsigned char)prefix[i])) {
            return false;
        }
    }
    return true;
}

/* Validate that the token matches the number grammar "-?\d*\.?\d+" used by the
 * TS regex: an optional leading '-', then digits with at most one '.' and at
 * least one digit overall. Rejects exponents, '+', trailing dots, etc. */
static bool is_valid_number_token(const char *s, size_t len) {
    size_t i = 0;
    if (i < len && s[i] == '-') {
        i++;
    }
    bool seen_dot = false;
    bool seen_digit = false;
    for (; i < len; i++) {
        if (s[i] >= '0' && s[i] <= '9') {
            seen_digit = true;
        } else if (s[i] == '.' && !seen_dot) {
            seen_dot = true;
        } else {
            return false;
        }
    }
    return seen_digit;
}

/* Parse a CSS "cubic-bezier(x1, y1, x2, y2)" string into its four coords,
 * written through out. Returns false for anything that isn't a valid
 * cubic-bezier() (including named easings like "linear"). Never traps.
 *
 * The C11 standard library has no regex, so this hand-rolls the same grammar
 * the TS regex encodes: "cubic-bezier(" followed by four comma-separated
 * numbers, then ")", with optional whitespace wherever a CSS parser would
 * allow it and none between the name and the opening paren. */
bool parse_css_bezier(const char *s, double out[4]) {
    /* Strip leading whitespace, then the case-insensitive "cubic-bezier(". */
    while (isspace((unsigned char)*s)) {
        s++;
    }
    if (!starts_with_ci(s, "cubic-bezier(")) {
        return false;
    }
    s += strlen("cubic-bezier(");

    /* Drop trailing whitespace; the last non-ws char must be the closing paren. */
    size_t len = strlen(s);
    while (len > 0 && isspace((unsigned char)s[len - 1])) {
        len--;
    }
    if (len == 0 || s[len - 1] != ')') {
        return false;
    }
    size_t inner_len = len - 1;

    /* Walk exactly four comma-separated numbers, whitespace allowed around each. */
    size_t pos = 0;
    for (int slot = 0; slot < 4; slot++) {
        /* Find the next ',' (or the end of the inner text for the fourth coord). */
        size_t start = pos;
        while (pos < inner_len && s[pos] != ',') {
            pos++;
        }
        if (slot < 3 && pos >= inner_len) {
            return false; /* fewer than four comma-separated fields */
        }
        /* Trim the field, validate its token, and convert it. */
        size_t end = pos;
        while (start < end && isspace((unsigned char)s[start])) {
            start++;
        }
        while (end > start && isspace((unsigned char)s[end - 1])) {
            end--;
        }
        if (!is_valid_number_token(s + start, end - start)) {
            return false;
        }
        char token[64];
        size_t tlen = end - start;
        if (tlen >= sizeof token) {
            return false;
        }
        memcpy(token, s + start, tlen);
        token[tlen] = '\0';

        char *endp = NULL;
        double v = strtod(token, &endp);
        if (endp == NULL || *endp != '\0' || !isfinite(v)) {
            return false;
        }
        out[slot] = v;
        pos++; /* skip the ',' just consumed */
    }
    /* The fourth field scanned to inner_len, then pos++ stepped one past it;
     * anything else means a fifth comma-separated field was present. */
    if (pos != inner_len + 1) {
        return false;
    }
    return true;
}

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 →