Skip to content

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 →