Skip to content

CSS Animation Playground — JavaScript source

Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// css-animation-playground - JavaScript 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.
// No dependencies, fully deterministic, safe to run in the browser or in Node.
//
// Ported from src/lib/animation.ts - display source, part of CosmoDev's
// polyglot tool pages (dev.cosmolabs.org). Behavior is functionally equivalent
// to the canonical TypeScript implementation.
//
// 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.

/* The four control-point coordinates as [x1, y1, x2, y2].
 * @typedef {[number, number, number, number]} BezierCoords
 */

const BEZIER_RE =
  /^\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$/i;

/**
 * Polynomial coefficients for one axis (c1, c2 are the control-point coords on
 * that axis). The cubic is rewritten in power form so it can be evaluated with
 * nested multiplication.
 */
function bezierCoeffs(c1, c2) {
  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 (Horner form). */
function sample(t, coeffs) {
  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, coeffs) {
  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, x1, y1, x2, y2) {
  // Total function: coerce non-finite inputs to 0, clamp progress, guarantee endpoints.
  const fin = (v) => (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, y1, x2, y2) {
  // Round to the nearest 1e-6 and stringify. JS String() already renders
  // integer-valued floats without a trailing ".0" and renders -0 as "0", so
  // this matches the canonical TS output exactly.
  const f = (n) => 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) {
  if (typeof str !== "string") return null;
  const m = str.match(BEZIER_RE);
  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;
}

/** Named CSS easings expressed as their cubic-bezier control-point coords. */
export const EASING_PRESETS = {
  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 →