use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct CASpring {
pub response: f32,
pub damping_ratio: f32,
}
impl Default for CASpring {
fn default() -> Self {
Self {
response: 0.35,
damping_ratio: 0.85,
}
}
}
impl CASpring {
pub fn new(response: f32, damping_ratio: f32) -> Self {
Self {
response: response.max(0.0001),
damping_ratio: damping_ratio.max(0.0),
}
}
#[inline]
pub fn from_perceptual(response: f32, damping_ratio: f32) -> Self {
Self::new(response, damping_ratio)
}
pub fn from_frequency(omega_0: f32, damping_ratio: f32) -> Self {
let safe_omega = omega_0.max(0.0001);
let response = 2.0 * std::f32::consts::PI / safe_omega;
Self::new(response, damping_ratio)
}
pub fn from_physical(mass: f32, stiffness: f32, damping: f32) -> Self {
let m = mass.max(0.0001);
let k = stiffness.max(0.0001);
let omega_0 = (k / m).sqrt();
let damping_ratio = damping / (2.0 * (k * m).sqrt());
Self::from_frequency(omega_0, damping_ratio)
}
#[inline]
pub fn omega_0(&self) -> f32 {
2.0 * std::f32::consts::PI / self.response
}
#[inline]
pub fn omega_d(&self) -> f32 {
let zeta = self.damping_ratio;
self.omega_0() * (1.0 - zeta * zeta).abs().sqrt()
}
pub fn settling_duration(&self) -> f32 {
let zeta = self.damping_ratio;
if zeta <= 0.0 {
return f32::INFINITY;
}
let omega0 = self.omega_0();
if zeta <= 1.0 {
6.907755 / (zeta * omega0)
} else {
let alpha = self.omega_d();
let rate = (zeta * omega0 - alpha).max(0.0001);
6.907755 / rate
}
}
pub fn animate_f32(&self, ctx: &egui::Context, id: egui::Id, target: f32) -> f32 {
let now = ctx.input(|i| i.time);
self.animate_f32_at(ctx, id, target, now, false)
}
pub fn animate_f32_at(
&self,
ctx: &egui::Context,
id: egui::Id,
target: f32,
now: f64,
frame_paced: bool,
) -> f32 {
let stored = ctx.data(|d| d.get_temp::<(f32, f32, f64)>(id));
let Some((prev_target, start_val, start_time)) = stored else {
ctx.data_mut(|d| d.insert_temp(id, (target, target, now)));
return target;
};
let request_next_frame = |ctx: &egui::Context| {
if frame_paced {
ctx.request_repaint_after(std::time::Duration::from_millis(16));
} else {
ctx.request_repaint();
}
};
if (prev_target - target).abs() > f32::EPSILON {
let elapsed = (now - start_time).max(0.0) as f32;
let cur = if (prev_target - start_val).abs() > f32::EPSILON
&& elapsed < self.settling_duration()
{
start_val + (prev_target - start_val) * self.position_at(elapsed)
} else {
prev_target
};
ctx.data_mut(|d| d.insert_temp(id, (target, cur, now)));
request_next_frame(ctx);
cur
} else {
let elapsed = (now - start_time).max(0.0) as f32;
if (target - start_val).abs() > f32::EPSILON && elapsed < self.settling_duration() {
request_next_frame(ctx);
start_val + (target - start_val) * self.position_at(elapsed)
} else {
if (target - start_val).abs() > f32::EPSILON {
ctx.data_mut(|d| d.insert_temp(id, (target, target, now)));
}
target
}
}
}
#[inline]
pub fn position_at(&self, t: f32) -> f32 {
self.evaluate(t)
}
pub fn overshoot_ratio(&self) -> f32 {
let zeta = self.damping_ratio;
if (0.0..1.0).contains(&zeta) {
(-std::f32::consts::PI * zeta / (1.0 - zeta * zeta).sqrt()).exp()
} else {
0.0
}
}
pub fn evaluate(&self, t: f32) -> f32 {
if t <= 0.0 {
return 0.0;
}
let omega0 = self.omega_0();
let zeta = self.damping_ratio;
if zeta < 1.0 {
let omega_d = self.omega_d();
let decay = (-zeta * omega0 * t).exp();
if decay == 0.0 {
1.0
} else {
let envelope =
decay * ((omega_d * t).cos() + (zeta * omega0 / omega_d) * (omega_d * t).sin());
1.0 - envelope
}
} else if (zeta - 1.0).abs() < 1e-5 {
let decay = (-omega0 * t).exp();
if decay == 0.0 {
1.0
} else {
1.0 - decay * (1.0 + omega0 * t)
}
} else {
let alpha = self.omega_d();
let rate_minus = (zeta * omega0 - alpha).max(0.0);
let rate_plus = zeta * omega0 + alpha;
let exp_minus = (-rate_minus * t).exp();
let exp_plus = (-rate_plus * t).exp();
let c = zeta * omega0 / alpha;
let term = 0.5 * ((1.0 + c) * exp_minus + (1.0 - c) * exp_plus);
1.0 - term
}
}
pub fn velocity(&self, t: f32) -> f32 {
if t <= 0.0 {
return 0.0;
}
let omega0 = self.omega_0();
let zeta = self.damping_ratio;
if zeta < 1.0 {
let omega_d = self.omega_d();
let decay = (-zeta * omega0 * t).exp();
if decay == 0.0 {
0.0
} else {
decay * (omega0 * omega0 / omega_d) * (omega_d * t).sin()
}
} else if (zeta - 1.0).abs() < 1e-5 {
let decay = (-omega0 * t).exp();
if decay == 0.0 {
0.0
} else {
omega0 * omega0 * t * decay
}
} else {
let alpha = self.omega_d();
let rate_minus = (zeta * omega0 - alpha).max(0.0);
let rate_plus = zeta * omega0 + alpha;
let exp_minus = (-rate_minus * t).exp();
let exp_plus = (-rate_plus * t).exp();
(omega0 * omega0 / (2.0 * alpha)) * (exp_minus - exp_plus)
}
}
}