From 3411fc80abce30d02fbf55a08bbc0b0ed3874fe0 Mon Sep 17 00:00:00 2001 From: storybored379 Date: Sat, 3 Oct 2026 23:36:10 -0700 Subject: [PATCH] Keyframes: a Hold key no longer eases the motion into it segment_value treated any non-Linear in-side of the next key as Bezier, so a key set to Hold (KeyframeInterpolationType.HOLD, add_keyframe "interpolation": "hold", or the Keyframe Interpolation dialog, which set both sides) turned the linear segment into it into an ease-in with the default ease: 0 -> 100 over 2 s read 27.0 instead of 25 at 0.5 s. Hold only affects what follows the key, which the out side already handles. The Graph Editor and the Lottie exporter already treat a Hold in-side as linear, so exports didn't match the preview either. Co-Authored-By: Claude Opus 5.5 --- crates/keyframe/src/lib.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/keyframe/src/lib.rs b/crates/keyframe/src/lib.rs index bc381d01..559c6860 100644 --- a/crates/keyframe/src/lib.rs +++ b/crates/keyframe/src/lib.rs @@ -441,7 +441,9 @@ pub fn segment_value(keys: &[Keyframe], i: usize, t: Tick, spatial: bool) -> Val let dur = (t1 - t0).max(1e-12); let x = ((secs(t) - t0) / dur).clamp(0.0, 1.0); let lin_out = a.out_interp == Interp::Linear; - let lin_in = b.in_interp == Interp::Linear; + // A Hold in-side only matters for the segment it ends, which the out side above decides: + // it shapes the incoming segment like a linear side, as in the Graph Editor and Lottie export. + let lin_in = b.in_interp != Interp::Bezier; let is_spatial = spatial && matches!(a.value, Value::Vec2(_) | Value::Vec3(_)); if is_spatial { let p0 = v3(&a.value); @@ -584,6 +586,17 @@ mod tests { assert_eq!(evaluate(&held, s(1.999), false), Some(Value::Scalar(0.0))); } + #[test] + fn hold_key_keeps_its_incoming_segment_linear() { + // Hold on the second key (in and out, as KeyframeInterpolationType.HOLD sets it) holds + // what comes after it, not the linear motion into it. + let keys = vec![Keyframe::new(s(0.0), Value::Scalar(0.0)), Keyframe::new(s(2.0), Value::Scalar(100.0)).hold()]; + assert_eq!(evaluate(&keys, s(0.5), false), Some(Value::Scalar(25.0))); + let keys = vec![Keyframe::new(s(0.0), Value::Vec2([0.0, 0.0])), Keyframe::new(s(2.0), Value::Vec2([100.0, 0.0])).hold()]; + let p = evaluate(&keys, s(0.5), true).unwrap().as_vec2(); + assert!((p[0] - 25.0).abs() < 0.01, "{p:?}"); + } + #[test] fn easy_ease_is_symmetric_and_slow_at_ends() { let keys = vec![Keyframe::new(s(0.0), Value::Scalar(0.0)).eased(), Keyframe::new(s(1.0), Value::Scalar(100.0)).eased()];