Skip to main content

kvas_engine/
math.rs

1//! Small scalar helpers for gameplay scripts: interpolation, smoothing and angles.
2//!
3//! All functions are `f32`, allocation-free and safe for any finite input.
4//!
5//! ```
6//! use kvas_engine::math::*;
7//! assert_eq!(lerp(0.0, 10.0, 0.25), 2.5);
8//! assert_eq!(remap(5.0, 0.0, 10.0, 100.0, 200.0), 150.0);
9//! assert_eq!(approach(0.0, 10.0, 3.0), 3.0);
10//! assert!((wrap_degrees(190.0) + 170.0).abs() < 1e-4);
11//! ```
12
13/// Linear interpolation: `a` at `t = 0`, `b` at `t = 1` (not clamped).
14pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
15    a + (b - a) * t
16}
17
18/// Where `v` lies between `a` and `b` (0 at `a`, 1 at `b`); 0 when `a == b`.
19pub fn inverse_lerp(a: f32, b: f32, v: f32) -> f32 {
20    if (b - a).abs() < f32::EPSILON {
21        0.0
22    } else {
23        (v - a) / (b - a)
24    }
25}
26
27/// Maps `v` from the range `in_a..in_b` to `out_a..out_b` (not clamped).
28pub fn remap(v: f32, in_a: f32, in_b: f32, out_a: f32, out_b: f32) -> f32 {
29    lerp(out_a, out_b, inverse_lerp(in_a, in_b, v))
30}
31
32/// Like [`remap`] but the result never leaves `out_a..out_b`.
33pub fn remap_clamped(v: f32, in_a: f32, in_b: f32, out_a: f32, out_b: f32) -> f32 {
34    lerp(out_a, out_b, inverse_lerp(in_a, in_b, v).clamp(0.0, 1.0))
35}
36
37/// Hermite smoothstep between two edges (0 below `e0`, 1 above `e1`).
38pub fn smoothstep(e0: f32, e1: f32, v: f32) -> f32 {
39    let t = inverse_lerp(e0, e1, v).clamp(0.0, 1.0);
40    t * t * (3.0 - 2.0 * t)
41}
42
43/// Moves `current` towards `target` by at most `max_delta` (never overshoots).
44pub fn approach(current: f32, target: f32, max_delta: f32) -> f32 {
45    let d = target - current;
46    if d.abs() <= max_delta {
47        target
48    } else {
49        current + d.signum() * max_delta
50    }
51}
52
53/// Frame-rate independent exponential smoothing. Larger `lambda` is snappier;
54/// call every frame with the frame's `dt`: `x = damp(x, target, 8.0, ctx.dt())`.
55pub fn damp(current: f32, target: f32, lambda: f32, dt: f32) -> f32 {
56    lerp(current, target, 1.0 - (-lambda.max(0.0) * dt.max(0.0)).exp())
57}
58
59/// Wraps an angle in degrees into `-180..180`.
60pub fn wrap_degrees(angle: f32) -> f32 {
61    let a = angle.rem_euclid(360.0);
62    if a > 180.0 {
63        a - 360.0
64    } else {
65        a
66    }
67}
68
69/// Shortest signed difference `to - from` in degrees (`-180..180`).
70pub fn delta_degrees(from: f32, to: f32) -> f32 {
71    wrap_degrees(to - from)
72}
73
74/// Rotates an angle in degrees towards `target` along the short way by at most `max_delta`.
75pub fn approach_degrees(current: f32, target: f32, max_delta: f32) -> f32 {
76    current + approach(0.0, delta_degrees(current, target), max_delta)
77}
78
79/// Critically damped spring towards `target` (like a camera "smooth damp"): returns the new value and the
80/// new velocity. Keep the velocity between calls. `smooth_time` is roughly the seconds to arrive.
81///
82/// ```
83/// use kvas_engine::math::smooth_damp;
84/// let (mut x, mut v) = (0.0, 0.0);
85/// for _ in 0..120 { (x, v) = smooth_damp(x, 10.0, v, 0.3, 1.0 / 60.0); }
86/// assert!((x - 10.0).abs() < 0.05);
87/// ```
88pub fn smooth_damp(current: f32, target: f32, velocity: f32, smooth_time: f32, dt: f32) -> (f32, f32) {
89    let omega = 2.0 / smooth_time.max(1e-4);
90    let x = omega * dt;
91    let exp = 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x);
92    let change = current - target;
93    let temp = (velocity + omega * change) * dt;
94    let new_velocity = (velocity - omega * temp) * exp;
95    let out = target + (change + temp) * exp;
96    // Never overshoot the target.
97    if (target - current > 0.0) == (out > target) {
98        return (target, 0.0);
99    }
100    (out, new_velocity)
101}
102
103/// Smooth S curve `3t² - 2t³` of a value clamped to 0..1.
104pub fn ease_smooth(t: f32) -> f32 {
105    let t = t.clamp(0.0, 1.0);
106    t * t * (3.0 - 2.0 * t)
107}
108
109/// Even smoother S curve `6t⁵ - 15t⁴ + 10t³` with zero acceleration at both ends.
110pub fn ease_smoother(t: f32) -> f32 {
111    let t = t.clamp(0.0, 1.0);
112    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn helpers_behave() {
121        assert_eq!(inverse_lerp(2.0, 2.0, 5.0), 0.0);
122        assert_eq!(remap_clamped(20.0, 0.0, 10.0, 0.0, 1.0), 1.0);
123        assert_eq!(smoothstep(0.0, 1.0, 0.5), 0.5);
124        assert_eq!(approach(9.0, 10.0, 3.0), 10.0);
125        assert!(damp(0.0, 1.0, 10.0, 0.016) > 0.1);
126        assert!((approach_degrees(350.0, 10.0, 5.0) - 355.0).abs() < 1e-4);
127    }
128}