bread-theme: clamp spring_to's interpolated value to [from,to]
spring_ease's backOut overshoot (peaks ~1.065) drove a shrinking animation's interpolated value below its target, which could go negative for a size request and trip GTK's height >= -1 assertion. Previously only patched at breadbar's call sites; clamp inside spring_to itself so the safety is automatic for every caller.
This commit is contained in:
parent
410f97cb28
commit
cff77d473a
1 changed files with 77 additions and 3 deletions
|
|
@ -29,6 +29,28 @@ fn spring_ease(t: f64) -> f64 {
|
|||
1.0 + t1 * t1 * ((c + 1.0) * t1 + c)
|
||||
}
|
||||
|
||||
/// One frame's worth of `spring_to`'s interpolation math — split out from
|
||||
/// the tick callback below purely so it has a name a unit test can call
|
||||
/// directly instead of re-deriving the same arithmetic.
|
||||
///
|
||||
/// `spring_ease` is a backOut curve that legitimately overshoots past 1.0
|
||||
/// (peaks ~1.065) partway through the run — fine for a growing animation
|
||||
/// (`to` > `from`), but on a *shrink* (`from` > `to`, e.g. a drawer
|
||||
/// collapsing to 0) that overshoot drives the raw interpolated value BELOW
|
||||
/// `to`, which can go negative for a size request and trip GTK's
|
||||
/// `height >= -1` assertion. Clamping here, once, protects every caller
|
||||
/// automatically instead of relying on each call site to remember
|
||||
/// `h.max(0)` — this already bit breadbar once (see
|
||||
/// `breadbar::main::animate_drawer_height`'s own clamp, now
|
||||
/// redundant-but-harmless defense in depth on top of this).
|
||||
fn frame_value(from: i32, to: i32, t: f64) -> i32 {
|
||||
let eased = spring_ease(t);
|
||||
let value = from as f64 + (to - from) as f64 * eased;
|
||||
let lo = from.min(to) as f64;
|
||||
let hi = from.max(to) as f64;
|
||||
value.clamp(lo, hi).round() as i32
|
||||
}
|
||||
|
||||
/// Interpolates from `from` to `to` over `duration_ms`, calling `on_frame`
|
||||
/// with each intermediate value (and, on the final tick, the exact `to` —
|
||||
/// never an off-by-rounding near-miss) via `widget`'s frame clock.
|
||||
|
|
@ -60,9 +82,61 @@ pub fn spring_to(
|
|||
on_frame(to);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
let t = spring_ease(elapsed / duration_ms);
|
||||
let value = from as f64 + (to - from) as f64 * t;
|
||||
on_frame(value.round() as i32);
|
||||
on_frame(frame_value(from, to, elapsed / duration_ms));
|
||||
ControlFlow::Continue
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Samples [`frame_value`] — the exact function `spring_to`'s tick
|
||||
/// callback calls every frame — across the full `t` timeline at fine
|
||||
/// granularity, bypassing the real frame clock (which needs a running
|
||||
/// main loop the test environment doesn't have).
|
||||
fn sample_all_frames(from: i32, to: i32) -> Vec<i32> {
|
||||
let steps = 2000;
|
||||
(0..=steps)
|
||||
.map(|i| frame_value(from, to, i as f64 / steps as f64))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spring_ease_overshoots_past_one_for_a_backout_curve() {
|
||||
// Sanity check on the premise: without a clamp, a shrink would
|
||||
// legitimately go negative around this point in the curve.
|
||||
assert!(spring_ease(0.8) > 1.0, "expected overshoot past 1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrink_never_emits_a_value_outside_the_from_to_range() {
|
||||
// from > to (a drawer collapsing to 0) is exactly the case the
|
||||
// unclamped overshoot could drive negative.
|
||||
for value in sample_all_frames(480, 0) {
|
||||
assert!(
|
||||
(0..=480).contains(&value),
|
||||
"shrink emitted {value}, outside [0, 480]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grow_never_emits_a_value_outside_the_from_to_range() {
|
||||
for value in sample_all_frames(0, 480) {
|
||||
assert!(
|
||||
(0..=480).contains(&value),
|
||||
"grow emitted {value}, outside [0, 480]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_span_never_clamps_outside_the_single_point() {
|
||||
// from == to: lo == hi == that point for every t, including past
|
||||
// the overshoot peak.
|
||||
for value in sample_all_frames(120, 120) {
|
||||
assert_eq!(value, 120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue