CSS Animation Playground — Java source
Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// css-animation-playground — Java 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.
//
// Language: Java 17 (LTS), standard library only (java.util.regex).
// Source: CosmoDev polyglot showcase port of src/lib/animation.ts —
// display source, part of CosmoDev's tool pages.
// License: Display source, part of CosmoDev's pages (dev.cosmolabs.org).
//
// 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.
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class CssEasing {
private CssEasing() {}
/** Matches a CSS cubic-bezier(...) string with four numeric args,
* case-insensitive — mirroring the TS /…/i literal. */
private static final Pattern BEZIER_RE = Pattern.compile(
"^\\s*cubic-bezier\\(\\s*(-?\\d*\\.?\\d+)\\s*,\\s*(-?\\d*\\.?\\d+)\\s*,"
+ "\\s*(-?\\d*\\.?\\d+)\\s*,\\s*(-?\\d*\\.?\\d+)\\s*\\)\\s*$",
Pattern.CASE_INSENSITIVE);
/** Named CSS easings expressed as their cubic-bezier control-point coords
* [x1, y1, x2, y2], in CSS declaration order. */
public static final Map<String, double[]> EASING_PRESETS;
static {
Map<String, double[]> presets = new LinkedHashMap<>(8);
presets.put("linear", new double[] {0.0, 0.0, 1.0, 1.0});
presets.put("ease", new double[] {0.25, 0.1, 0.25, 1.0});
presets.put("ease-in", new double[] {0.42, 0.0, 1.0, 1.0});
presets.put("ease-out", new double[] {0.0, 0.0, 0.58, 1.0});
presets.put("ease-in-out", new double[] {0.42, 0.0, 0.58, 1.0});
EASING_PRESETS = java.util.Collections.unmodifiableMap(presets);
}
/** Polynomial coefficients for one axis of the cubic, in power form so the
* polynomial can be evaluated with nested multiplication. */
private record Coeffs(double a, double b, double c) {}
/** Compute the polynomial coefficients for one axis (c1 and c2 are the
* control-point coordinates on that axis). */
private static Coeffs bezierCoeffs(double c1, double c2) {
double c = 3.0 * c1;
double b = 3.0 * (c2 - c1) - c;
double a = 1.0 - c - b;
return new Coeffs(a, b, c);
}
/** Evaluate the axis polynomial: ((a·t + b)·t + c)·t (Horner form). */
private static double sample(double t, Coeffs k) {
return ((k.a() * t + k.b()) * t + k.c()) * t;
}
/** Derivative of the axis polynomial: (3a·t + 2b)·t + c. */
private static double sampleDerivative(double t, Coeffs k) {
return (3.0 * k.a() * t + 2.0 * k.b()) * t + k.c();
}
/** Coerce non-finite values to 0.0, mirroring the TS Number.isFinite guard. */
private static double fin(double v) {
return Double.isFinite(v) ? v : 0.0;
}
/**
* Given animation progress x in [0,1], solve the cubic-bezier easing for
* the corresponding output y. Newton-Raphson (clamped to [0,1]).
* Endpoints are exact: y(0)=0 and y(1)=1. Never throws.
*/
public static double cubicBezierY(double x, double x1, double y1, double x2, double y2) {
double px = fin(x);
if (px <= 0.0) {
return 0.0;
}
if (px >= 1.0) {
return 1.0;
}
Coeffs xc = bezierCoeffs(fin(x1), fin(x2));
Coeffs 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.
double t = px;
for (int i = 0; i < 8; i++) {
double dx = sample(t, xc) - px;
if (Math.abs(dx) < 1e-6) {
break;
}
double d = sampleDerivative(t, xc);
if (Math.abs(d) < 1e-7) {
break; // guard against division by ~0
}
t -= dx / d;
}
if (t < 0.0) {
t = 0.0;
} else if (t > 1.0) {
t = 1.0;
}
return sample(t, yc);
}
/**
* Round a coord to the nearest 1e-6 and render it the way JS String()
* would: integers without a trailing ".0", and -0.0 normalized to "0".
* JS Math.round rounds half toward +∞, so Math.floor(x + 0.5) matches it.
*/
private static String fmtCoord(double n) {
double r = Math.floor(n * 1e6 + 0.5) / 1e6;
if (r == 0.0) {
r = 0.0; // normalize -0.0
}
if (r == Math.rint(r)) {
// Integer-valued: print without the trailing ".0" Double.toString adds.
return Long.toString((long) r);
}
// Double.toString is the shortest representation that round-trips,
// matching JS String() for these 1e-6-rounded values.
return Double.toString(r);
}
/** Format four control-point coords as a CSS "cubic-bezier(...)" string. */
public static String cssBezier(double x1, double y1, double x2, double y2) {
return "cubic-bezier(" + fmtCoord(x1) + "," + fmtCoord(y1)
+ "," + fmtCoord(x2) + "," + fmtCoord(y2) + ")";
}
/**
* Parse a CSS "cubic-bezier(x1, y1, x2, y2)" string into its four coords
* [x1, y1, x2, y2]. Returns Optional.empty() for anything that isn't a
* valid cubic-bezier() (including named easings like "linear").
* Never throws.
*/
public static Optional<double[]> parseCssBezier(String s) {
if (s == null) {
return Optional.empty();
}
Matcher m = BEZIER_RE.matcher(s);
if (!m.matches()) {
return Optional.empty();
}
double[] coords = new double[4];
for (int i = 0; i < 4; i++) {
try {
double v = Double.parseDouble(m.group(i + 1));
if (!Double.isFinite(v)) {
return Optional.empty();
}
coords[i] = v;
} catch (NumberFormatException e) {
return Optional.empty();
}
}
return Optional.of(coords);
}
/** Presets in declaration order, as an unmodifiable copy for callers that
* iterate the registry. */
public static Map<String, double[]> easingPresetsOrdered() {
return Collections.unmodifiableMap(new LinkedHashMap<>(EASING_PRESETS));
}
}
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 →