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# 12 (.NET 8), standard library only (System.Text.RegularExpressions).
// 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.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace CosmoDev.Animation;
/// The four control-point coordinates (X1, Y1, X2, Y2).
public readonly record struct BezierCoords(double X1, double Y1, double X2, double Y2);
public static class CssEasing
{
/// Matches a CSS cubic-bezier(...) string with four numeric args,
/// case-insensitive — mirroring the TS /…/i literal.
private static readonly Regex BezierRegex = new(
@"^\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
/// Named CSS easings expressed as their cubic-bezier control-point coords.
public static readonly IReadOnlyDictionary<string, BezierCoords> EasingPresets =
new Dictionary<string, BezierCoords> {
["linear"] = new(0.0, 0.0, 1.0, 1.0),
["ease"] = new(0.25, 0.1, 0.25, 1.0),
["ease-in"] = new(0.42, 0.0, 1.0, 1.0),
["ease-out"] = new(0.0, 0.0, 0.58, 1.0),
["ease-in-out"] = new(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.
private readonly record struct Coeffs(double A, double B, double C);
/// Compute the polynomial coefficients for one axis (<paramref name="c1"/>
/// and <paramref name="c2"/> are the control-point coordinates on that axis).
private static Coeffs BezierCoeffs(double c1, double c2)
{
double c = 3.0 * c1;
double b = 3.0 * (c2 - c1) - c;
double a = 1.0 - c - b;
return new Coeffs(a, b, c);
}
/// Evaluate the axis polynomial: ((a·t + b)·t + c)·t (Horner form).
private static double Sample(double t, in Coeffs k) => ((k.A * t + k.B) * t + k.C) * t;
/// Derivative of the axis polynomial: (3a·t + 2b)·t + c.
private static double SampleDerivative(double t, in Coeffs k) => (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.
private static double Fin(double v) => double.IsFinite(v) ? v : 0.0;
/// Given animation progress <paramref name="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.
public static double CubicBezierY(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;
}
Coeffs xc = BezierCoeffs(Fin(x1), Fin(x2));
Coeffs yc = BezierCoeffs(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, in xc) - px;
if (Math.Abs(dx) < 1e-6)
{
break;
}
double d = SampleDerivative(t, in xc);
if (Math.Abs(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, in yc);
}
/// 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 +∞, so Math.Floor(x + 0.5) matches it.
private static string FmtCoord(double n)
{
double r = Math.Floor(n * 1e6 + 0.5) / 1e6;
if (r == 0.0)
{
r = 0.0; // normalize -0.0
}
// .NET's invariant "R"-free default double formatting is shortest
// round-trippable, matching JS String() for these 1e-6-rounded values.
return r % 1.0 == 0.0
? ((long)r).ToString(CultureInfo.InvariantCulture)
: r.ToString(CultureInfo.InvariantCulture);
}
/// Format four control-point coords as a CSS "cubic-bezier(...)" string.
public static string CssBezier(double x1, double y1, double x2, double y2) =>
string.Create(
CultureInfo.InvariantCulture,
$"cubic-bezier({FmtCoord(x1)},{FmtCoord(y1)},{FmtCoord(x2)},{FmtCoord(y2)})");
/// Parse a CSS "cubic-bezier(x1, y1, x2, y2)" string into its four coords.
/// Returns null for anything that isn't a valid cubic-bezier() (including
/// named easings like "linear"). Never throws.
public static BezierCoords? ParseCssBezier(string? s)
{
if (s is null)
{
return null;
}
Match m = BezierRegex.Match(s);
if (!m.Success)
{
return null;
}
double[] coords = new double[4];
for (int i = 0; i < 4; i++)
{
if (!double.TryParse(m.Groups[i + 1].Value, NumberStyles.Float,
CultureInfo.InvariantCulture, out double v) || !double.IsFinite(v))
{
return null;
}
coords[i] = v;
}
return new BezierCoords(coords[0], coords[1], coords[2], coords[3]);
}
}
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 →