CSS Animation Playground — Ruby source
Design and test CSS animations live - preview easing curves, durations, and keyframes, then copy the exact CSS.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# css-animation-playground — Ruby 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: Ruby 3.2, 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 raises and always
# returns a finite value.
module CssEasing
module_function
# Matches a CSS cubic-bezier(...) string with four numeric args,
# case-insensitive (the /i flag), mirroring the TS /…/i literal.
BEZIER_RE = /\A\s*cubic-bezier\(\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*,
\s*(-?\d*\.?\d+)\s*,\s*(-?\d*\.?\d+)\s*\)\s*\z/ix.freeze
# Named CSS easings expressed as their cubic-bezier control-point coords.
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]
}.freeze
# Polynomial coefficients [a, b, c] for one axis of the cubic, in power form
# so the polynomial can be evaluated with nested multiplication. c1 and c2
# are the control-point coordinates on that axis.
def bezier_coeffs(c1, c2)
c = 3.0 * c1
b = 3.0 * (c2 - c1) - c
a = 1.0 - c - b
[a, b, c]
end
# Evaluate the axis polynomial: ((a·t + b)·t + c)·t (Horner form).
def sample(t, coeffs)
a, b, c = coeffs
((a * t + b) * t + c) * t
end
# Derivative of the axis polynomial: (3a·t + 2b)·t + c.
def sample_derivative(t, coeffs)
a, b, c = coeffs
(3.0 * a * t + 2.0 * b) * t + c
end
# Coerce non-finite values to 0.0, mirroring the TS Number.isFinite guard.
def fin(v)
v.finite? ? v : 0.0
end
# 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 raises.
def cubic_bezier_y(x, x1, y1, x2, y2)
px = fin(x)
return 0.0 if px <= 0.0
return 1.0 if px >= 1.0
x_c = bezier_coeffs(fin(x1), fin(x2))
y_c = bezier_coeffs(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.
t = px
8.times do
dx = sample(t, x_c) - px
break if dx.abs < 1e-6
d = sample_derivative(t, x_c)
break if d.abs < 1e-7 # guard against division by ~0
t -= dx / d
end
t = 0.0 if t < 0.0
t = 1.0 if t > 1.0
sample(t, y_c)
end
# 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 (x + 0.5).floor matches it.
def fmt_coord(n)
r = (n * 1e6 + 0.5).floor / 1e6
r = 0.0 if r == 0.0 # normalize -0.0
if r % 1.0 == 0.0
r.to_i.to_s
else
# Float#to_s is the shortest round-tripping form, matching JS String()
# for these 1e-6-rounded values.
r.to_s
end
end
# Format four control-point coords as a CSS "cubic-bezier(...)" string.
def css_bezier(x1, y1, x2, y2)
format('cubic-bezier(%s,%s,%s,%s)', fmt_coord(x1), fmt_coord(y1), fmt_coord(x2), fmt_coord(y2))
end
# Parse a CSS "cubic-bezier(x1, y1, x2, y2)" string into its four coords.
# Returns nil for anything that isn't a valid cubic-bezier() (including
# named easings like "linear"). Never raises.
def parse_css_bezier(s)
return nil unless s.is_a?(String)
m = BEZIER_RE.match(s)
return nil if m.nil?
# The regex already validated the token grammar; Kernel#Float keeps the
# "never raises" contract total by mapping parse failure to nil.
coords = (1..4).map do |i|
begin
Float(m[i])
rescue ArgumentError, TypeError
nil
end
end
return nil if coords.any? { |c| c.nil? || !c.finite? }
coords
end
end
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 →