Keyframes: a straight auto-Bezier segment interpolates its chord exactly (#650 follow-up)

Auto-Bezier end keys now carry handles along the path, so a straight two-key move went through
the arc-length table and landed a rounding error off its keys' midpoint (80.00000000000001).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
Claude committed 2026-10-10 22:52:36 +00:00
1 parent 9d689b1a61
commit d6efb795e4
1 file changed
+28 -1
+28 -1
View File
@@ -224,6 +224,20 @@ pub fn ease_progress(out_speed_n: f64, out_inf: f64, in_speed_n: f64, in_inf: f6
bez(0.0, out_speed_n * i0, 1.0 - in_speed_n * i1, 1.0, u)
}
/// Whether control point `c` lies on the segment `a`–`b` (within rounding), so a cubic with
/// such handles traces the straight chord.
fn on_chord(a: [f64; 3], c: [f64; 3], b: [f64; 3]) -> bool {
let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let dd = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
if dd <= 1e-24 {
return false;
}
let t = (v[0] * d[0] + v[1] * d[1] + v[2] * d[2]) / dd;
let off = [v[0] - d[0] * t, v[1] - d[1] * t, v[2] - d[2] * t];
(0.0..=1.0).contains(&t) && off[0] * off[0] + off[1] * off[1] + off[2] * off[2] <= dd * 1e-18
}
/// Spatial cubic segment with an arc-length table.
struct SpatialSeg {
p: [[f64; 3]; 4],
@@ -529,7 +543,13 @@ pub fn segment_value(keys: &[Keyframe], i: usize, t: Tick, spatial: bool) -> Val
let ei = if lin_in { Ease { speed: len / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i + 1, 0, true, false) };
ease_progress(eo.speed * dur / len, eo.influence, ei.speed * dur / len, ei.influence, x)
};
let p = seg.at_length(f * len);
// A straight path (both handles on the chord, as auto-Bezier keys on a line have) is the
// chord itself: interpolate it exactly instead of through the arc-length table.
let p = if on_chord(p0, seg.p[1], p3) && on_chord(p0, seg.p[2], p3) {
[p0[0] + (p3[0] - p0[0]) * f, p0[1] + (p3[1] - p0[1]) * f, p0[2] + (p3[2] - p0[2]) * f]
} else {
seg.at_length(f * len)
};
return a.value.with_components(&p);
}
if lin_out && lin_in {
@@ -819,6 +839,13 @@ mod tests {
assert!((m[0] - m[1]).abs() > 1.0, "{m:?}");
}
#[test]
fn straight_auto_bezier_path_is_exact() {
// Auto-Bezier ends now have handles along the line; the chord is still hit exactly.
let keys = vec![Keyframe::new(s(0.0), Value::Vec2([40.0, 50.0])), Keyframe::new(s(2.0), Value::Vec2([120.0, 50.0]))];
assert_eq!(segment_value(&keys, 0, s(1.0), true), Value::Vec2([80.0, 50.0]));
}
#[test]
fn spatial_auto_bezier_tangents_follow_after_effects() {
// Measured on After Effects 26.3 with original paths (docs/fidelity/README.md): each