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 →