Use analytic springs and instrument resize barriers
Share a stable damped oscillator solver across scalar and rectangle animations while preserving retarget and explicit no-crossing presentation policies. Add deterministic traces, scrolling and zoom continuity coverage, and pause-aware advancement. Measure resize barrier and handoff timing behind resize-metrics, expose the feature through the release installer, and wait for the intended backend in isolated portal tests.
This commit is contained in:
1 parent
42f022fde5
commit
28e03a2624
18 files changed
+1502
-84
No files matched your search
@@ -0,0 +1,96 @@
|
||||
//! Exact physical spring motion. Presentation policy is deliberately separate.
|
||||
use super::SpringConfig;
|
||||
|
||||
/// Solve m y'' + c y' + k y = 0, with y = current - target.
|
||||
/// No settling tolerance or crossing clamp is applied here.
|
||||
pub(super) fn solve(current: f64, velocity: f64, target: f64, dt: f64, config: SpringConfig) -> (f64, f64) {
|
||||
if dt <= 0.0 || !dt.is_finite() {
|
||||
return (current, velocity);
|
||||
}
|
||||
|
||||
let config = config.normalized();
|
||||
let omega2 = config.stiffness / config.mass;
|
||||
let omega = omega2.sqrt();
|
||||
let a = config.damping / (2.0 * config.mass);
|
||||
let d = (a - omega) * (a + omega);
|
||||
let y = current - target;
|
||||
let z2 = d * dt * dt;
|
||||
|
||||
// cos/sinc and cosh/sinhc have the same power series in signed z².
|
||||
// This keeps the actual discriminant, including near-critical damping,
|
||||
// rather than replacing a neighborhood with the critical solution.
|
||||
if z2.abs() < 0.01 {
|
||||
let c = 1.0 + z2 * (0.5 + z2 * (1.0 / 24.0 + z2 * (1.0 / 720.0 + z2 / 40320.0)));
|
||||
let s = dt * (1.0 + z2 * (1.0 / 6.0 + z2 * (1.0 / 120.0 + z2 * (1.0 / 5040.0 + z2 / 362880.0))));
|
||||
let e = (-a * dt).exp();
|
||||
return (
|
||||
target + e * (y * c + (velocity + a * y) * s),
|
||||
e * (velocity * c - (a * velocity + omega2 * y) * s),
|
||||
);
|
||||
}
|
||||
|
||||
if d < 0.0 {
|
||||
let w = (-d).sqrt();
|
||||
let (sin, cos) = (w * dt).sin_cos();
|
||||
let e = (-a * dt).exp();
|
||||
let s = sin / w;
|
||||
(
|
||||
target + e * (y * cos + (velocity + a * y) * s),
|
||||
e * (velocity * cos - (a * velocity + omega2 * y) * s),
|
||||
)
|
||||
} else {
|
||||
// Avoid -a + sqrt(a²-omega²) cancellation and exp/cosh overflow.
|
||||
let q = d.sqrt();
|
||||
let fast = -a - q;
|
||||
let slow = -omega2 / (a + q);
|
||||
let slow_coefficient = (velocity - fast * y) / (2.0 * q);
|
||||
let fast_coefficient = (slow * y - velocity) / (2.0 * q);
|
||||
let slow_term = slow_coefficient * (slow * dt).exp();
|
||||
let fast_term = fast_coefficient * (fast * dt).exp();
|
||||
(target + slow_term + fast_term, slow * slow_term + fast * fast_term)
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the physical trajectory reaches equilibrium anywhere in (0, dt].
|
||||
/// Endpoint-only tests miss an even number of crossings after a long stall.
|
||||
pub(super) fn crosses(current: f64, velocity: f64, target: f64, dt: f64, config: SpringConfig) -> bool {
|
||||
if dt <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let config = config.normalized();
|
||||
let a = config.damping / (2.0 * config.mass);
|
||||
let omega = (config.stiffness / config.mass).sqrt();
|
||||
let d = (a - omega) * (a + omega);
|
||||
let y = current - target;
|
||||
let b = velocity + a * y;
|
||||
if y == 0.0 && velocity == 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let crossing = if d < 0.0 {
|
||||
let w = (-d).sqrt();
|
||||
let phase = (-y * w).atan2(b).rem_euclid(std::f64::consts::PI);
|
||||
let phase = if phase == 0.0 { std::f64::consts::PI } else { phase };
|
||||
phase / w
|
||||
} else if y == 0.0 || b == 0.0 {
|
||||
return false;
|
||||
} else {
|
||||
let t = -y / b;
|
||||
if t <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// atanh(q*t)/q = t * (1 + (q*t)²/3 + ...), including q = 0.
|
||||
let z2 = d * t * t;
|
||||
if z2 < 1e-8 {
|
||||
t * (1.0 + z2 * (1.0 / 3.0 + z2 / 5.0))
|
||||
} else if z2 < 1.0 {
|
||||
let q = d.sqrt();
|
||||
(q * t).atanh() / q
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
crossing <= dt
|
||||
}
|
||||
Reference in new issue
Block a user