Skip to content

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 →