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: C++17 (ISO/IEC 14882:2017), 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 throws and always
// returns a finite value.
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
/// The four control-point coordinates [x1, y1, x2, y2].
using BezierCoords = std::array<double, 4>;
/// Named CSS easings expressed as their cubic-bezier control-point coords.
/// A static array of pairs keeps the registry dependency-free.
inline constexpr std::array<std::pair<const char *, BezierCoords>, 5> 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}},
}};
/// Polynomial coefficients for one axis of the cubic, in power form so the
/// polynomial can be evaluated with nested multiplication.
struct Coeffs {
double a;
double b;
double c;
};
/// Compute the polynomial coefficients for one axis (c1, c2 are the
/// control-point coordinates on that axis).
constexpr Coeffs bezier_coeffs(double c1, double c2) {
const double c = 3.0 * c1;
const double b = 3.0 * (c2 - c1) - c;
const double a = 1.0 - c - b;
return {a, b, c};
}
/// Evaluate the axis polynomial: ((a*t + b)*t + c)*t (Horner form).
constexpr double sample(double t, Coeffs k) {
return ((k.a * t + k.b) * t + k.c) * t;
}
/// Derivative of the axis polynomial: (3a*t + 2b)*t + c.
constexpr double sample_derivative(double t, 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.
inline double fin(double v) {
return std::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. Never throws.
inline double cubic_bezier_y(double x, double x1, double y1, double x2, double y2) {
const double px = fin(x);
if (px <= 0.0) {
return 0.0;
}
if (px >= 1.0) {
return 1.0;
}
const Coeffs x_c = bezier_coeffs(fin(x1), fin(x2));
const Coeffs y_c = bezier_coeffs(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++) {
const double dx = sample(t, x_c) - px;
if (std::fabs(dx) < 1e-6) {
break;
}
const double d = sample_derivative(t, x_c);
if (std::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 std::floor(x + 0.5) matches it exactly.
inline std::string fmt_coord(double n) {
double r = std::floor(n * 1e6 + 0.5) / 1e6;
if (r == 0.0) {
r = 0.0; // normalize -0.0
}
std::ostringstream out;
out << std::fixed << std::setprecision(6) << r;
std::string s = out.str();
// Trim trailing zeros; drop the '.' too when no fraction remains.
const auto dot = s.find('.');
if (dot != std::string::npos) {
s.erase(s.find_last_not_of('0') + 1, std::string::npos);
if (s.back() == '.') {
s.pop_back();
}
}
return s;
}
/// Format four control-point coords as a CSS "cubic-bezier(...)" string.
inline std::string css_bezier(double x1, double y1, double x2, double y2) {
return "cubic-bezier(" + fmt_coord(x1) + "," + fmt_coord(y1) + "," + fmt_coord(x2) + "," +
fmt_coord(y2) + ")";
}
/// 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.
inline bool is_valid_number_token(std::string_view s) {
std::size_t i = 0;
if (i < s.size() && s[i] == '-') {
i++;
}
bool seen_dot = false;
bool seen_digit = false;
for (; i < s.size(); i++) {
const char ch = s[i];
if (ch >= '0' && ch <= '9') {
seen_digit = true;
} else if (ch == '.' && !seen_dot) {
seen_dot = true;
} else {
return false;
}
}
return seen_digit;
}
/// Case-insensitive ASCII prefix strip.
inline std::optional<std::string_view> strip_prefix_ci(std::string_view s, std::string_view prefix) {
if (s.size() < prefix.size()) {
return std::nullopt;
}
const std::string_view head = s.substr(0, prefix.size());
if (std::equal(head.begin(), head.end(), prefix.begin(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
})) {
return s.substr(prefix.size());
}
return std::nullopt;
}
/// Parse a CSS "cubic-bezier(x1, y1, x2, y2)" string into its four coords.
/// Returns std::nullopt for anything that isn't a valid cubic-bezier()
/// (including named easings like "linear"). Never throws.
///
/// <regex> is heavy for four numbers, 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.
inline std::optional<BezierCoords> parse_css_bezier(std::string_view s) {
using std::isspace;
// Strip leading whitespace, then the case-insensitive "cubic-bezier(".
// (No whitespace is allowed between the name and the opening paren.)
std::size_t begin = 0;
while (begin < s.size() && isspace(static_cast<unsigned char>(s[begin]))) {
begin++;
}
auto rest = strip_prefix_ci(s.substr(begin), "cubic-bezier(");
if (!rest) {
return std::nullopt;
}
// Drop trailing whitespace; the last non-ws char must be the closing paren.
std::size_t end = rest->size();
while (end > 0 && isspace(static_cast<unsigned char>((*rest)[end - 1]))) {
end--;
}
if (end == 0 || (*rest)[end - 1] != ')') {
return std::nullopt;
}
const std::string_view inner = rest->substr(0, end - 1);
// Exactly four comma-separated numbers, whitespace permitted around each.
BezierCoords coords{};
std::size_t pos = 0;
for (int slot = 0; slot < 4; slot++) {
const std::size_t comma = inner.find(',', pos);
const bool has_comma = comma != std::string_view::npos;
if (slot < 3 && !has_comma) {
return std::nullopt; // fewer than four comma-separated fields
}
const std::size_t stop = has_comma ? comma : inner.size();
// Trim the field, validate its token, and convert it.
std::size_t lo = pos;
std::size_t hi = stop;
while (lo < hi && isspace(static_cast<unsigned char>(inner[lo]))) {
lo++;
}
while (hi > lo && isspace(static_cast<unsigned char>(inner[hi - 1]))) {
hi--;
}
const std::string_view token = inner.substr(lo, hi - lo);
if (!is_valid_number_token(token)) {
return std::nullopt;
}
const std::string number(token);
char *endp = nullptr;
const double v = std::strtod(number.c_str(), &endp);
if (endp == nullptr || *endp != '\0' || !std::isfinite(v)) {
return std::nullopt;
}
coords[static_cast<std::size_t>(slot)] = v;
pos = has_comma ? comma + 1 : inner.size();
}
if (pos != inner.size()) {
return std::nullopt; // a fifth field means this wasn't a 4-arg call
}
return coords;
}
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 →