makepad/platform/script/compute/std/ease.splash
Admin f3afcc4fff tween, script-compute: one easing family -- ease(@name, u) in the shared stdlib reads Motion's ease table, expo is the classic curve, the stdlib's own ease_* functions are gone
Documents had two curves under one name: a Tween's @ease_out_expo (2^(10p-10) rescaled to run 0..1) and the plates' and the stroke kernel's classic out_expo (1 - 2^(-10u), 1 at u = 1), plus the shared stdlib's ease_in_quad ... ease_in_out_expo (the rescaled one again) and a document function ease(@out_expo, u) with its own table.
- libs/tween: the document family's expo (Easing::ExpoClassic, was ExpoExact) is the classic curve: in 2^(10p-10) (0 at p = 0), out and in_out from it as before.
- mod.shared (register_shared_std, the `vm` feature, now with makepad-tween): ease(name, u) = named::Ease::from_id(name).apply(u): @linear, @hold, @ease_in/out/in_out, @ease_{in,out,in_out}_{quad,cubic,quart,quint,sine,expo,circ,back,elastic,bounce}; f64, u clamped, an unknown name lists the names. ease_id(name) gives std.ease.apply_d's number for a kernel. Every document VM gets them (Motion documents, their Scene3D scenes, the kinetic kits, the render graph).
- std/shared.splash: the ease_* functions are deleted (no caller in shaders; documents call ease(@name, u)).
- std/ease.splash: in_expo_d is the classic curve (the kernels' track_d stays Motion's bits); anim_track's reference follows.
Visible change: any @ease_*_expo key or tween ends without the rescale (at most 2^-10 of its travel away mid-move); Motion's pinned fixtures re-pinned in commercial.
Checked: cargo test --release -p makepad-tween (all), -p makepad-script-compute --features vm (all), -p makepad-render-kernels (all, anim_track bit-exact against the classic expo); cargo check --release -p makepad-script-compute --features vm --target wasm32-unknown-unknown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 11:12:19 +02:00

185 lines
7.6 KiB
Text

// Easing curves with the documents' exact formulas (libs/tween named.rs, the curves of
// its Easing; what Motion's `@ease_*` names mean): u in
// 0..1, evaluated in f64 and rounded once. The polynomial, circ, back and
// bounce curves give Motion's bits; sine, expo, elastic and spring use the
// portable transcendental kernels (equal within an ulp of the host's).
fn unit(u) { clamp(f64(u), f64(0.0), f64(1.0)) }
fn in_quad_d(u) { u * u }
fn in_cubic_d(u) { u * u * u }
fn in_quart_d(u) { u * u * u * u }
fn in_quint_d(u) { u * u * u * u * u }
fn in_sine_d(u) { 1.0 - cos(u * f64(PI) * 0.5) }
fn in_expo_d(u) {
if u <= 0.0 { f64(0.0) } else { pow(f64(2.0), 10.0 * u - 10.0) }
}
fn in_circ_d(u) { 1.0 - sqrt(max(1.0 - u * u, f64(0.0))) }
fn in_back_d(u) {
let c1 = f64(1.70158)
let c3 = c1 + 1.0
c3 * u * u * u - c1 * u * u
}
fn in_elastic_d(u) {
if u <= 0.0 { f64(0.0) } else { -pow(f64(2.0), 10.0 * u - 10.0) * sin((u * 10.0 - 10.75) * ((2.0 * f64(PI)) / 3.0)) }
}
fn bounce_out_d(u) {
let n1 = f64(7.5625)
let d1 = f64(2.75)
if u < 1.0 / d1 { n1 * u * u }
elif u < 2.0 / d1 { let v = u - 1.5 / d1
n1 * v * v + 0.75 }
elif u < 2.5 / d1 { let v = u - 2.25 / d1
n1 * v * v + 0.9375 }
else { let v = u - 2.625 / d1
n1 * v * v + 0.984375 }
}
fn in_bounce_d(u) { 1.0 - bounce_out_d(1.0 - u) }
// Motion's `apply` around a curve: 0 at or below 0, 1 at or above 1.
fn ends(u, v) { if !(u > 0.0) { 0.0 } elif u >= 1.0 { 1.0 } else { float(v) } }
fn in_quad(u) { ends(u, in_quad_d(unit(u))) }
fn out_quad(u) { ends(u, 1.0 - in_quad_d(1.0 - unit(u))) }
fn in_out_quad(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_quad_d(2.0 * d) } else { 1.0 - 0.5 * in_quad_d(2.0 - 2.0 * d) }) }
fn in_cubic(u) { ends(u, in_cubic_d(unit(u))) }
fn out_cubic(u) { ends(u, 1.0 - in_cubic_d(1.0 - unit(u))) }
fn in_out_cubic(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_cubic_d(2.0 * d) } else { 1.0 - 0.5 * in_cubic_d(2.0 - 2.0 * d) }) }
fn in_quart(u) { ends(u, in_quart_d(unit(u))) }
fn out_quart(u) { ends(u, 1.0 - in_quart_d(1.0 - unit(u))) }
fn in_out_quart(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_quart_d(2.0 * d) } else { 1.0 - 0.5 * in_quart_d(2.0 - 2.0 * d) }) }
fn in_quint(u) { ends(u, in_quint_d(unit(u))) }
fn out_quint(u) { ends(u, 1.0 - in_quint_d(1.0 - unit(u))) }
fn in_out_quint(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_quint_d(2.0 * d) } else { 1.0 - 0.5 * in_quint_d(2.0 - 2.0 * d) }) }
fn in_sine(u) { ends(u, in_sine_d(unit(u))) }
fn out_sine(u) { ends(u, 1.0 - in_sine_d(1.0 - unit(u))) }
fn in_out_sine(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_sine_d(2.0 * d) } else { 1.0 - 0.5 * in_sine_d(2.0 - 2.0 * d) }) }
fn in_expo(u) { ends(u, in_expo_d(unit(u))) }
fn out_expo(u) { ends(u, 1.0 - in_expo_d(1.0 - unit(u))) }
fn in_out_expo(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_expo_d(2.0 * d) } else { 1.0 - 0.5 * in_expo_d(2.0 - 2.0 * d) }) }
fn in_circ(u) { ends(u, in_circ_d(unit(u))) }
fn out_circ(u) { ends(u, 1.0 - in_circ_d(1.0 - unit(u))) }
fn in_out_circ(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_circ_d(2.0 * d) } else { 1.0 - 0.5 * in_circ_d(2.0 - 2.0 * d) }) }
fn in_back(u) { ends(u, in_back_d(unit(u))) }
fn out_back(u) { ends(u, 1.0 - in_back_d(1.0 - unit(u))) }
fn in_out_back(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_back_d(2.0 * d) } else { 1.0 - 0.5 * in_back_d(2.0 - 2.0 * d) }) }
fn in_elastic(u) { ends(u, in_elastic_d(unit(u))) }
fn out_elastic(u) { ends(u, 1.0 - in_elastic_d(1.0 - unit(u))) }
fn in_out_elastic(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_elastic_d(2.0 * d) } else { 1.0 - 0.5 * in_elastic_d(2.0 - 2.0 * d) }) }
fn in_bounce(u) { ends(u, in_bounce_d(unit(u))) }
fn out_bounce(u) { ends(u, 1.0 - in_bounce_d(1.0 - unit(u))) }
fn in_out_bounce(u) { let d = unit(u)
ends(u, if d < 0.5 { 0.5 * in_bounce_d(2.0 * d) } else { 1.0 - 0.5 * in_bounce_d(2.0 - 2.0 * d) }) }
// A damped spring's step response at time t (stiffness k, damping d),
// in f64 (`spring` rounds it to f32).
fn spring_d(k, d, t) {
let kk = max(f64(k), f64(0.000001))
let dd = max(f64(d), f64(0.0))
let tt = f64(t)
let w0 = sqrt(kk)
let zeta = dd / (2.0 * w0)
if zeta < 1.0 - 0.000000001 {
let wd = w0 * sqrt(1.0 - zeta * zeta)
1.0 - exp(-zeta * w0 * tt) * (cos(wd * tt) + zeta * w0 / wd * sin(wd * tt))
} elif zeta <= 1.0 + 0.000000001 {
1.0 - exp(-w0 * tt) * (1.0 + w0 * tt)
} else {
let s = sqrt(zeta * zeta - 1.0)
let r1 = -w0 * (zeta - s)
let r2 = -w0 * (zeta + s)
1.0 + r2 / (r1 - r2) * exp(r1 * tt) + (-r1 / (r1 - r2)) * exp(r2 * tt)
}
}
fn spring(k, d, t) { float(spring_d(k, d, t)) }
// CSS cubic-bezier(x1, y1, x2, y2) at u, as Motion solves it: Newton from
// u (8 steps, to 1e-12 in x), falling back to bisection (64 steps).
fn bezier_d(x1, y1, x2, y2, u) {
let cx = 3.0 * clamp(f64(x1), f64(0.0), f64(1.0))
let bx = 3.0 * (clamp(f64(x2), f64(0.0), f64(1.0)) - clamp(f64(x1), f64(0.0), f64(1.0))) - cx
let ax = 1.0 - cx - bx
let cy = 3.0 * f64(y1)
let by = 3.0 * (f64(y2) - f64(y1)) - cy
let ay = 1.0 - cy - by
let uu = f64(u)
let s = uu
let solved = false
let stop = false
for it in 0..8 {
if !stop {
let err = ((ax * s + bx) * s + cx) * s - uu
if abs(err) < 0.000000000001 {
solved = true
stop = true
} else {
let dx = (3.0 * ax * s + 2.0 * bx) * s + cx
if abs(dx) < 0.000000001 {
stop = true
} else {
s = s - err / dx
if !(s >= 0.0 && s <= 1.0) { stop = true }
}
}
}
}
if !solved || !(s >= 0.0 && s <= 1.0) {
let lo = f64(0.0)
let hi = f64(1.0)
s = uu
stop = false
for it in 0..64 {
if !stop {
let x = ((ax * s + bx) * s + cx) * s
if abs(x - uu) < 0.000000000001 {
stop = true
} else {
if x < uu { lo = s } else { hi = s }
s = 0.5 * (lo + hi)
}
}
}
}
((ay * s + by) * s + cy) * s
}
// The ease-in curve `c` (0 quad, 1 cubic, 2 quart, 3 quint, 4 sine, 5 expo,
// 6 circ, 7 back, 8 elastic, 9 bounce) at u, in f64.
fn in_curve_d(c, u) {
if c == 0 { in_quad_d(u) } elif c == 1 { in_cubic_d(u) } elif c == 2 { in_quart_d(u) } elif c == 3 { in_quint_d(u) }
elif c == 4 { in_sine_d(u) } elif c == 5 { in_expo_d(u) } elif c == 6 { in_circ_d(u) } elif c == 7 { in_back_d(u) }
elif c == 8 { in_elastic_d(u) } else { in_bounce_d(u) }
}
// Motion's Ease::apply by id, in f64: the eased fraction at u of a segment
// `seconds` long (0 at or below u = 0, 1 at or above 1). Ids: 0 linear,
// 1 hold, 2 + 3 * curve + dir (curve as in_curve_d, dir 0 in, 1 out,
// 2 in_out; Motion's plain ease_in/out/in_out are the quad ones), 32 a
// spring (p0 = k, p1 = d) over the segment's seconds, 33 bezier(p0, p1, p2, p3).
fn apply_d(id, u, seconds, p0, p1, p2, p3) {
let k = int(id)
let uu = f64(u)
if !(uu > 0.0) { f64(0.0) }
elif uu >= 1.0 { f64(1.0) }
elif k == 0 { uu }
elif k == 1 { f64(0.0) }
elif k == 32 { spring_d(p0, p1, uu * max(f64(seconds), f64(0.0))) }
elif k == 33 { bezier_d(p0, p1, p2, p3, uu) }
else {
let c = (k - 2) / 3
let dir = (k - 2) % 3
if dir == 0 { in_curve_d(c, uu) }
elif dir == 1 { 1.0 - in_curve_d(c, 1.0 - uu) }
elif uu < 0.5 { 0.5 * in_curve_d(c, 2.0 * uu) }
else { 1.0 - 0.5 * in_curve_d(c, 2.0 - 2.0 * uu) }
}
}