CSS Animation Playground — TypeScript source
Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// CSS cubic-bezier easing utilities. The Bézier runs from P0=(0,0) to
// P3=(1,1) with control points P1=(x1,y1), P2=(x2,y2). Every function here
// is total - it never throws and always returns a finite value.
export type BezierCoords = [number, number, number, number];
/** Polynomial coefficients for one axis (c1, c2 are the control-point coords). */
function bezierCoeffs(c1: number, c2: number) {
const c = 3 * c1;
const b = 3 * (c2 - c1) - c;
const a = 1 - c - b;
return { a, b, c };
}
/** Evaluate the axis polynomial: ((a·t + b)·t + c)·t */
function sample(t: number, coeffs: { a: number; b: number; c: number }): number {
const { a, b, c } = coeffs;
return ((a * t + b) * t + c) * t;
}
/** Derivative of the axis polynomial: (3a·t + 2b)·t + c */
function sampleDerivative(t: number, coeffs: { a: number; b: number; c: number }): number {
const { a, b, c } = coeffs;
return (3 * a * t + 2 * b) * t + c;
}
/**
* Given an animation progress `x` in [0,1], solve the cubic-bezier easing for
* the corresponding output `y`. Uses Newton-Raphson (clamped). Endpoints are
* exact: y(0)=0 and y(1)=1. Never throws.
*/
export function cubicBezierY(
x: number,
x1: number,
y1: number,
x2: number,
y2: number,
): number {
// Total function: coerce non-finite inputs to 0, clamp progress, guarantee endpoints.
const fin = (v: number) => (Number.isFinite(v) ? v : 0);
const px = fin(x);
if (px <= 0) return 0;
if (px >= 1) return 1;
const xC = bezierCoeffs(fin(x1), fin(x2));
const 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.
let t = px;
for (let i = 0; i < 8; i++) {
const dx = sample(t, xC) - px;
if (Math.abs(dx) < 1e-6) break;
const d = sampleDerivative(t, xC);
if (Math.abs(d) < 1e-7) break; // guard against division by ~0
t -= dx / d;
}
if (t < 0) t = 0;
else if (t > 1) t = 1;
return sample(t, yC);
}
/** Format four control-point coords as a CSS `cubic-bezier(...)` string. */
export function cssBezier(x1: number, y1: number, x2: number, y2: number): string {
const f = (n: number): string => String(Math.round(n * 1e6) / 1e6);
return `cubic-bezier(${f(x1)},${f(y1)},${f(x2)},${f(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.
*/
export function parseCssBezier(str: string): BezierCoords | null {
if (typeof str !== 'string') return null;
const m = str.match(
/^\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$/i,
);
if (!m) return null;
const coords = [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), parseFloat(m[4])];
if (coords.some((n) => !Number.isFinite(n))) return null;
return coords as BezierCoords;
}
/** Named CSS easings expressed as their cubic-bezier control-point coords. */
export const EASING_PRESETS: Record<string, BezierCoords> = {
linear: [0, 0, 1, 1],
ease: [0.25, 0.1, 0.25, 1],
'ease-in': [0.42, 0, 1, 1],
'ease-out': [0, 0, 0.58, 1],
'ease-in-out': [0.42, 0, 0.58, 1],
};
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 →