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 →