Skip to content

CSS Animation Playground — PHP source

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

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

<?php
/**
 * css-animation-playground — PHP 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 third-party dependencies, fully deterministic.
 *
 * 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.
 */

declare(strict_types=1);

/**
 * Matches a CSS cubic-bezier(...) string with four numeric args.
 * Case-insensitive (the /i flag), mirroring the TS /…/i literal.
 */
const BEZIER_RE =
    '/^\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$/i';

/**
 * Compute the polynomial coefficients (a, b, c) for one axis of the cubic,
 * where $c1 and $c2 are the control-point coordinates on that axis.
 *
 * @return array{0:float,1:float,2:float}
 */
function bezier_coeffs(float $c1, float $c2): array
{
    $c = 3.0 * $c1;
    $b = 3.0 * ($c2 - $c1) - $c;
    $a = 1.0 - $c - $b;
    return [$a, $b, $c];
}

/** Evaluate the axis polynomial: ((a·t + b)·t + c)·t (Horner form). */
function sample(float $t, array $coeffs): float
{
    [$a, $b, $c] = $coeffs;
    return (($a * $t + $b) * $t + $c) * $t;
}

/** Derivative of the axis polynomial: (3a·t + 2b)·t + c. */
function sample_derivative(float $t, array $coeffs): float
{
    [$a, $b, $c] = $coeffs;
    return (3.0 * $a * $t + 2.0 * $b) * $t + $c;
}

/** Coerce non-finite values to 0.0, mirroring the TS Number.isFinite guard. */
function fin(float $v): float
{
    return is_finite($v) ? $v : 0.0;
}

/**
 * Solve the cubic-bezier easing for the output y given progress x in [0,1].
 *
 * Newton-Raphson (clamped to [0,1]). Endpoints are exact: y(0)=0, y(1)=1.
 * Non-finite inputs are coerced to 0. Never throws.
 */
function cubic_bezier_y(float $x, float $x1, float $y1, float $x2, float $y2): float
{
    // Total function: coerce non-finite inputs to 0, clamp progress, guarantee endpoints.
    $px = fin($x);
    if ($px <= 0.0) {
        return 0.0;
    }
    if ($px >= 1.0) {
        return 1.0;
    }

    $xC = bezier_coeffs(fin($x1), fin($x2));
    $yC = bezier_coeffs(fin($y1), fin($y2));

    // px is a strong initial guess because x(t) is monotonic for valid curves.
    $t = $px;
    for ($i = 0; $i < 8; $i++) {
        $dx = sample($t, $xC) - $px;
        if (abs($dx) < 1e-6) {
            break;
        }
        $d = sample_derivative($t, $xC);
        if (abs($d) < 1e-7) {  // guard against division by ~0
            break;
        }
        $t -= $dx / $d;
    }
    if ($t < 0.0) {
        $t = 0.0;
    } elseif ($t > 1.0) {
        $t = 1.0;
    }
    return sample($t, $yC);
}

/**
 * Render a coord as JS String() would.
 *
 * JS Math.round rounds half toward +∞; PHP round() rounds half away from
 * zero, so floor(x + 0.5) is used to match JS exactly on negative halves.
 * PHP's float-to-string cast already omits the trailing ".0" on integers, so
 * it matches JS String() once -0.0 is normalized to "0".
 */
function fmt_coord(float $n): string
{
    $r = floor($n * 1e6 + 0.5) / 1e6;  // half-up, matching JS Math.round
    if ($r == 0.0) {  // normalize -0.0 → "0" (also clears the sign bit)
        $r = 0.0;
    }
    return (string) $r;
}

/** Format four control-point coords as a CSS cubic-bezier(...) string. */
function css_bezier(float $x1, float $y1, float $x2, float $y2): string
{
    return 'cubic-bezier('
        . fmt_coord($x1) . ',' . fmt_coord($y1) . ','
        . fmt_coord($x2) . ',' . fmt_coord($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.
 *
 * @param mixed $str
 * @return array{0:float,1:float,2:float,3:float}|null
 */
function parse_css_bezier($str): ?array
{
    if (!is_string($str)) {
        return null;
    }
    $ok = preg_match(BEZIER_RE, $str, $m);
    if ($ok !== 1) {
        return null;
    }
    $coords = [(float) $m[1], (float) $m[2], (float) $m[3], (float) $m[4]];
    foreach ($coords as $n) {
        if (!is_finite($n)) {
            return null;
        }
    }
    return $coords;
}

/** Named CSS easings expressed as their cubic-bezier control-point coords. */
const 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],
];

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 →