mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Expressions: cubic-Bezier easing helper (bezier(...)) (#1884)
Add bezier(t, tMin, tMax, v1, v2, [x1, y1, x2, y2]) and the short bezier(t, v1, v2, curve) form: a CSS cubic-bezier easing curve solved with Newton-Raphson and a bisection fallback. Scalars and arrays work like linear/ease; x1/x2 clamp to 0..1, y1/y2 may overshoot, t clamps to tMin..tMax, and a curve that is not four finite numbers falls back to linear instead of throwing. Tests cover known curves (ease is [1/3, 0, 2/3, 1]), clamps, arrays, the short form, invalid curves and overshoot, plus two fuzz seeds. Closes #1884.
This commit is contained in:
@@ -17,7 +17,8 @@
|
||||
//! `sourceRectAtTime`), property methods (`valueAtTime`, `velocityAtTime`, `speedAtTime`, `key`,
|
||||
//! `nearestKey`, `numKeys`, `wiggle`, `temporalWiggle`, `smooth`, `loopIn/Out[Duration]`) and the
|
||||
//! global helpers (`linear`/`ease`/`easeIn`/`easeOut`, `random`/`gaussRandom`/`seedRandom`/
|
||||
//! `noise`, vector maths, `clamp`, `lookAt`, time and colour conversions, `posterizeTime`).
|
||||
//! `noise`, vector maths, `clamp`, `lookAt`, time and colour conversions, `posterizeTime`),
|
||||
//! plus `bezier`, a CSS-style cubic-Bezier easing helper that After Effects does not have.
|
||||
//! * **Determinism**: `wiggle` and `random` are seeded by layer index, property uid (and time /
|
||||
//! `seedRandom`), so frames render identically on every thread and platform.
|
||||
//! * **Runtime**: one boa `Context` per thread with compiled scripts cached by text. Scripts read
|
||||
|
||||
@@ -207,6 +207,56 @@ function ease() { return __interp(__ease, arguments); }
|
||||
function easeIn() { return __interp(__easeIn, arguments); }
|
||||
function easeOut() { return __interp(__easeOut, arguments); }
|
||||
|
||||
// Cubic-Bezier easing (the CSS cubic-bezier() shape): [x1, y1, x2, y2], with x1 / x2 in 0..1
|
||||
// and y1 / y2 free (overshoot included). Solves x(u) = x by Newton–Raphson, then by bisection
|
||||
// when Newton leaves 0..1 or the derivative vanishes, and returns y(u). A curve that is not four
|
||||
// finite numbers is linear, never an error.
|
||||
function __cubicBezier(curve) {
|
||||
var x1, y1, x2, y2;
|
||||
if (Array.isArray(curve) && curve.length >= 4) {
|
||||
x1 = curve[0]; y1 = curve[1]; x2 = curve[2]; y2 = curve[3];
|
||||
}
|
||||
if (typeof x1 !== 'number' || typeof y1 !== 'number' || typeof x2 !== 'number' || typeof y2 !== 'number' ||
|
||||
!isFinite(x1) || !isFinite(y1) || !isFinite(x2) || !isFinite(y2)) {
|
||||
return __lin;
|
||||
}
|
||||
x1 = Math.max(0, Math.min(1, x1));
|
||||
x2 = Math.max(0, Math.min(1, x2));
|
||||
function bx(u) { var v = 1 - u; return 3 * v * v * u * x1 + 3 * v * u * u * x2 + u * u * u; }
|
||||
function dbx(u) { var v = 1 - u; return 3 * v * v * x1 + 6 * v * u * (x2 - x1) + 3 * u * u * (1 - x2); }
|
||||
function by(u) { var v = 1 - u; return 3 * v * v * u * y1 + 3 * v * u * u * y2 + u * u * u; }
|
||||
return function (x) {
|
||||
if (x <= 0) return 0;
|
||||
if (x >= 1) return 1;
|
||||
var u = x;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var f = bx(u) - x;
|
||||
if (Math.abs(f) < 1e-9) return by(u);
|
||||
var d = dbx(u);
|
||||
if (Math.abs(d) < 1e-12) break;
|
||||
var n = u - f / d;
|
||||
if (!(n >= 0 && n <= 1)) break;
|
||||
u = n;
|
||||
}
|
||||
var lo = 0, hi = 1;
|
||||
for (var j = 0; j < 60; j++) {
|
||||
var mid = (lo + hi) / 2;
|
||||
if (bx(mid) < x) lo = mid; else hi = mid;
|
||||
}
|
||||
return by((lo + hi) / 2);
|
||||
};
|
||||
}
|
||||
|
||||
// bezier(t, tMin, tMax, value1, value2, curve), or bezier(t, value1, value2, curve): the curve is
|
||||
// the last argument; everything before it follows linear / ease.
|
||||
function bezier() {
|
||||
var curve = arguments[arguments.length - 1];
|
||||
var shape = __cubicBezier(curve);
|
||||
var args = [];
|
||||
for (var i = 0; i < arguments.length - 1; i++) args.push(arguments[i]);
|
||||
return __interp(shape, args);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Random numbers and noise.
|
||||
|
||||
|
||||
@@ -313,6 +313,41 @@ fn interpolation_with_arrays() {
|
||||
assert_eq!(f.pos("ease(time, 0, 1, [0, 0], [100, 50])", 1.0), [100.0, 50.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubic_bezier_curves() {
|
||||
let f = fx();
|
||||
// ease(t, …) is cubic-bezier(1/3, 0, 2/3, 1): x(u) = u and y(u) = 3u² - 2u³.
|
||||
assert_close!(f.num("bezier(0.25, 0, 1, 0, 100, [0.3333333333333333, 0, 0.6666666666666666, 1])", 0.0), f.num("ease(0.25, 0, 1, 0, 100)", 0.0));
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100, [0.3333333333333333, 0, 0.6666666666666666, 1])", 0.0), 50.0);
|
||||
// Symmetric ease-in-out: slower at both ends, faster in the middle.
|
||||
let early = f.num("bezier(0.25, 0, 1, 0, 100, [0.42, 0, 0.58, 1])", 0.0);
|
||||
let late = f.num("bezier(0.75, 0, 1, 0, 100, [0.42, 0, 0.58, 1])", 0.0);
|
||||
assert!(early < 25.0 && late > 75.0, "ease-in-out: {early}, {late}");
|
||||
// Control points on the diagonal are linear.
|
||||
assert_close!(f.num("bezier(0.3, 0, 1, 0, 100, [0, 0, 1, 1])", 0.0), 30.0);
|
||||
// t is clamped to tMin..tMax; tMin == tMax behaves like the other helpers.
|
||||
assert_close!(f.num("bezier(-1, 0, 1, 0, 100, [0.42, 0, 0.58, 1])", 0.0), 0.0);
|
||||
assert_close!(f.num("bezier(2, 0, 1, 0, 100, [0.42, 0, 0.58, 1])", 0.0), 100.0);
|
||||
assert_close!(f.num("bezier(5, 2, 2, 0, 100, [0.42, 0, 0.58, 1])", 0.0), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubic_bezier_short_form_arrays_and_fallbacks() {
|
||||
let f = fx();
|
||||
// Short form: no tMin / tMax.
|
||||
assert_close!(f.num("bezier(0.25, 10, 20, [0, 0, 1, 1])", 0.0), 12.5);
|
||||
assert_eq!(f.pos("bezier(time, 0, 1, [0, 0], [100, 50], [0.42, 0, 0.58, 1])", 1.0), [100.0, 50.0, 0.0]);
|
||||
// y outside 0..1 overshoots, as CSS allows.
|
||||
let over = f.num("bezier(0.5, 0, 1, 0, 100, [0.5, 2, 0.5, 2])", 0.0);
|
||||
assert!(over > 100.0, "overshoot: {over}");
|
||||
// A curve that is not four finite numbers falls back to linear instead of throwing.
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100, [1, 0])", 0.0), 50.0);
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100, 'ease')", 0.0), 50.0);
|
||||
// x control points outside 0..1 are clamped, as CSS requires.
|
||||
let v = f.num("bezier(0.5, 0, 1, 0, 100, [-1, 0, 2, 1])", 0.0);
|
||||
assert!(v.is_finite(), "clamped curve: {v}");
|
||||
}
|
||||
|
||||
// ---- object model -----------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -93,6 +93,8 @@ const SEEDS: &[&str] = &[
|
||||
"linear(time, 0, 1, 0, 100)",
|
||||
"ease(time, 0, 2, [0, 0], [100, 50])",
|
||||
"easeIn(time, 0, 1, 0, 360) + easeOut(time, 1, 2, 0, 10)",
|
||||
"bezier(time, 0, 1, 0, 100, [0.42, 0, 0.58, 1])",
|
||||
"bezier(time, [0, 0], [100, 50], [0.25, 0.1, 0.25, 1])",
|
||||
"clamp(value * 2, 0, 100)",
|
||||
"length([0, 0], [3, 4]) + dot([1, 0], [0, 1]) + cross([1, 0, 0], [0, 1, 0])[2]",
|
||||
"normalize([3, 4])[0] * degreesToRadians(180)",
|
||||
|
||||
Reference in New Issue
Block a user