mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge pull request #1887 from ki2pixel/expressions-cubic-bezier
Expressions: cubic-Bezier easing helper (bezier(...)) (#1884)
This commit is contained in:
@@ -163,6 +163,8 @@ pub fn language_menu() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)>
|
||||
("easeIn(t, tMin, tMax, value1, value2)", "easeIn(t, tMin, tMax, value1, value2)"),
|
||||
("easeOut(t, value1, value2)", "easeOut(t, value1, value2)"),
|
||||
("easeOut(t, tMin, tMax, value1, value2)", "easeOut(t, tMin, tMax, value1, value2)"),
|
||||
("bezier(t, value1, value2, [x1, y1, x2, y2])", "bezier(t, value1, value2, [x1, y1, x2, y2])"),
|
||||
("bezier(t, tMin, tMax, value1, value2, [x1, y1, x2, y2])", "bezier(t, tMin, tMax, value1, value2, [x1, y1, x2, y2])"),
|
||||
],
|
||||
),
|
||||
(
|
||||
|
||||
@@ -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,60 @@ 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; }
|
||||
function dby(u) { var v = 1 - u; return 3 * v * v * y1 + 6 * v * u * (y2 - y1) + 3 * u * u * (1 - y2); }
|
||||
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;
|
||||
var d = dbx(u);
|
||||
if (Math.abs(d) < 1e-12) break;
|
||||
// Stop when the *value* is accurate: near the endpoints dy/dx is steep, so a residual in x
|
||||
// alone can still mean a visible error in y (f * dy/dx estimates it).
|
||||
if (Math.abs(f * dby(u) / d) < 1e-9) return by(u);
|
||||
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 sixth / fourth argument. Read it by position, not from the end: when it is missing or
|
||||
// unusable, the interpolation falls back to linear without consuming the last value.
|
||||
function bezier() {
|
||||
var a = arguments, n = a.length;
|
||||
if (n >= 6) return __interp(__cubicBezier(a[5]), [a[0], a[1], a[2], a[3], a[4]]);
|
||||
if (n === 5) return __interp(__lin, [a[0], a[1], a[2], a[3], a[4]]);
|
||||
if (n === 4) return __interp(__cubicBezier(a[3]), [a[0], a[1], a[2]]);
|
||||
return __interp(__lin, [a[0], a[1], a[2]]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Random numbers and noise.
|
||||
|
||||
|
||||
@@ -314,8 +314,78 @@ fn interpolation_with_arrays() {
|
||||
assert_eq!(f.pos("ease(time, 0, 1, [0, 0], [100, 50])", 1.0), [100.0, 50.0, 0.0]);
|
||||
}
|
||||
|
||||
// ---- object model -----------------------------------------------------------------------------
|
||||
#[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);
|
||||
// A missing or unusable curve must not consume the last value: read the curve by position.
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100)", 0.0), 50.0);
|
||||
assert_close!(f.num("bezier(0.5, 0, 100)", 0.0), 50.0);
|
||||
assert_close!(f.num("bezier(0.5, 0, 100, [1, 0])", 0.0), 50.0);
|
||||
let v = f.ev(f.shape, "contents/#1/contents/fill/color", "bezier(0.5, 0, 1, [0, 0, 0, 1], [1, 1, 1, 1])", 0.0).unwrap();
|
||||
assert_eq!(v, Value::Color([0.5, 0.5, 0.5, 1.0]));
|
||||
// Exactly four values are a curve; extra ones mean the curve is malformed, so linear.
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100, [0.42, 0, 0.58, 1, 0])", 0.0), 50.0);
|
||||
assert_close!(f.num("bezier(0.5, 0, 1, 0, 100, [0.42, 0, 0.58, 1, 2, 3])", 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}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubic_bezier_is_accurate_near_the_endpoints() {
|
||||
/// The value the curve reaches at `x`, by bisection on its monotone x(u).
|
||||
fn reference(x1: f64, y1: f64, x2: f64, y2: f64, x: f64) -> f64 {
|
||||
let bx = |u: f64| 3.0 * (1.0 - u) * (1.0 - u) * u * x1 + 3.0 * (1.0 - u) * u * u * x2 + u * u * u;
|
||||
let by = |u: f64| 3.0 * (1.0 - u) * (1.0 - u) * u * y1 + 3.0 * (1.0 - u) * u * u * y2 + u * u * u;
|
||||
let (mut lo, mut hi) = (0.0f64, 1.0f64);
|
||||
for _ in 0..100 {
|
||||
let mid = 0.5 * (lo + hi);
|
||||
if bx(mid) < x {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
by(0.5 * (lo + hi))
|
||||
}
|
||||
// Steep curves: a residual in x is not a residual in y near the endpoints (dy/dx is in the
|
||||
// thousands), so the solver stops on the estimated y error instead.
|
||||
let f = fx();
|
||||
for (c, x) in [([0.0, 1000.0, 1.0, 1.0], 1e-6), ([0.0, 100.0, 1.0, 0.0], 1e-9), ([0.8, -500.0, 1.0, 1.0], 0.999999)] {
|
||||
let got = f.num(&format!("bezier({x}, 0, 1, [{}, {}, {}, {}])", c[0], c[1], c[2], c[3]), 0.0);
|
||||
let want = reference(c[0], c[1], c[2], c[3], x);
|
||||
assert!((got - want).abs() < 1e-6, "curve {c:?} at {x}: {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- object model -----------------------------------------------------------------------------
|
||||
#[test]
|
||||
fn comp_attributes() {
|
||||
let f = fx();
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -111,6 +111,7 @@ pub const COMPLETIONS: &[&str] = &[
|
||||
"ease",
|
||||
"easeIn",
|
||||
"easeOut",
|
||||
"bezier",
|
||||
"clamp",
|
||||
"random",
|
||||
"gaussRandom",
|
||||
|
||||
Reference in New Issue
Block a user