CSS Animation Playground — Kotlin source
Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// css-animation-playground — Kotlin 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: Kotlin 1.9 (JVM), standard library only.
// 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.
/** The four control-point coordinates [x1, y1, x2, y2]. */
public typealias BezierCoords = DoubleArray
/** Polynomial coefficients for one axis of the cubic, in power form so the
* polynomial can be evaluated with nested multiplication. */
private class Coeffs(val a: Double, val b: Double, val c: Double)
/** Named CSS easings expressed as their cubic-bezier control-point coords. */
public val EASING_PRESETS: Map<String, BezierCoords> = linkedMapOf(
"linear" to doubleArrayOf(0.0, 0.0, 1.0, 1.0),
"ease" to doubleArrayOf(0.25, 0.1, 0.25, 1.0),
"ease-in" to doubleArrayOf(0.42, 0.0, 1.0, 1.0),
"ease-out" to doubleArrayOf(0.0, 0.0, 0.58, 1.0),
"ease-in-out" to doubleArrayOf(0.42, 0.0, 0.58, 1.0),
)
/** Matches a CSS cubic-bezier(...) string with four numeric args,
* case-insensitive — mirroring the TS /…/i literal. */
private val BEZIER_RE = Regex(
"""^\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*$""",
RegexOption.IGNORE_CASE,
)
/** Compute the polynomial coefficients for one axis (c1 and c2 are the
* control-point coordinates on that axis). */
private fun bezierCoeffs(c1: Double, c2: Double): Coeffs {
val c = 3.0 * c1
val b = 3.0 * (c2 - c1) - c
val a = 1.0 - c - b
return Coeffs(a, b, c)
}
/** Evaluate the axis polynomial: ((a·t + b)·t + c)·t (Horner form). */
private fun sample(t: Double, k: Coeffs): Double = ((k.a * t + k.b) * t + k.c) * t
/** Derivative of the axis polynomial: (3a·t + 2b)·t + c. */
private fun sampleDerivative(t: Double, k: Coeffs): Double = (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 fun fin(v: Double): Double = if (v.isFinite()) v else 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 fun cubicBezierY(x: Double, x1: Double, y1: Double, x2: Double, y2: Double): Double {
val px = fin(x)
if (px <= 0.0) return 0.0
if (px >= 1.0) return 1.0
val xc = bezierCoeffs(fin(x1), fin(x2))
val 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.
var t = px
repeat(8) {
val dx = sample(t, xc) - px
if (kotlin.math.abs(dx) < 1e-6) return@repeat
val d = sampleDerivative(t, xc)
if (kotlin.math.abs(d) < 1e-7) return@repeat // 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 floor(x + 0.5) matches it.
*/
private fun fmtCoord(n: Double): String {
var r = kotlin.math.floor(n * 1e6 + 0.5) / 1e6
if (r == 0.0) r = 0.0 // normalize -0.0
return if (r % 1.0 == 0.0) {
r.toLong().toString()
} else {
// Kotlin/Java Double.toString is the shortest round-tripping form,
// matching JS String() for these 1e-6-rounded values.
r.toString()
}
}
/** Format four control-point coords as a CSS "cubic-bezier(...)" string. */
public fun cssBezier(x1: Double, y1: Double, x2: Double, y2: Double): String =
"cubic-bezier(${fmtCoord(x1)},${fmtCoord(y1)},${fmtCoord(x2)},${fmtCoord(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.
*/
public fun parseCssBezier(s: String?): BezierCoords? {
if (s == null) return null
val m = BEZIER_RE.matchEntire(s) ?: return null
val coords = DoubleArray(4)
for (i in 0 until 4) {
val v = m.groupValues[i + 1].toDoubleOrNull() ?: return null
if (!v.isFinite()) return null
coords[i] = v
}
return coords
}
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 →