mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Expressions: reject malformed bezier curves and bound the y error
- accept only four-element arrays for bezier(): extra values make the curve malformed, so it falls back to linear like the other invalid forms (#1884); - stop Newton on the estimated y error (f * dy/dx) instead of a residual in x alone: near the endpoints dy/dx is in the thousands, so a 1e-9 residual in x could still move the returned value; - tests: a five-element curve is linear, and three steep curves match a bisection reference within 1e-6.
This commit is contained in:
@@ -213,7 +213,7 @@ function easeOut() { return __interp(__easeOut, arguments); }
|
||||
// finite numbers is linear, never an error.
|
||||
function __cubicBezier(curve) {
|
||||
var x1, y1, x2, y2;
|
||||
if (Array.isArray(curve) && curve.length >= 4) {
|
||||
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' ||
|
||||
@@ -225,15 +225,18 @@ function __cubicBezier(curve) {
|
||||
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;
|
||||
if (Math.abs(f) < 1e-9) return by(u);
|
||||
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;
|
||||
|
||||
@@ -343,13 +343,42 @@ fn cubic_bezier_short_form_arrays_and_fallbacks() {
|
||||
// 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);
|
||||
// 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}");
|
||||
}
|
||||
|
||||
// ---- object model -----------------------------------------------------------------------------
|
||||
#[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();
|
||||
|
||||
Reference in New Issue
Block a user