mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 11:17:12 -04:00
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:
1 file changed
+28
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user