Skip to main content

emath/
lib.rs

1//! Opinionated 2D math library for building GUIs.
2//!
3//! Includes vectors, positions, rectangles etc.
4//!
5//! Conventions (unless otherwise specified):
6//!
7//! * All angles are in radians
8//! * X+ is right and Y+ is down.
9//! * (0,0) is left top.
10//! * Dimension order is always `x y`
11//!
12//! ## Integrating with other math libraries.
13//! `emath` does not strive to become a general purpose or all-powerful math library.
14//!
15//! For that, use something else ([`glam`](https://docs.rs/glam), [`nalgebra`](https://docs.rs/nalgebra), …)
16//! and enable the `mint` feature flag in `emath` to enable implicit conversion to/from `emath`.
17//!
18//! ## Feature flags
19#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
20//!
21
22#![expect(clippy::float_cmp)]
23
24use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
25
26// ----------------------------------------------------------------------------
27
28pub mod align;
29pub mod easing;
30mod gui_rounding;
31mod history;
32mod numeric;
33mod ordered_float;
34mod pos2;
35mod range;
36mod rect;
37mod rect_align;
38mod rect_transform;
39mod rot2;
40pub mod smart_aim;
41mod ts_transform;
42mod vec2;
43mod vec2b;
44
45pub use self::{
46    align::{Align, Align2},
47    gui_rounding::{GUI_ROUNDING, GuiRounding},
48    history::History,
49    numeric::*,
50    ordered_float::*,
51    pos2::*,
52    range::Rangef,
53    rect::*,
54    rect_align::RectAlign,
55    rect_transform::*,
56    rot2::*,
57    ts_transform::*,
58    vec2::*,
59    vec2b::*,
60};
61
62// ----------------------------------------------------------------------------
63
64/// Helper trait to implement [`lerp`] and [`remap`].
65pub trait One {
66    const ONE: Self;
67}
68
69impl One for f32 {
70    const ONE: Self = 1.0;
71}
72
73impl One for f64 {
74    const ONE: Self = 1.0;
75}
76
77/// Helper trait to implement [`lerp`] and [`remap`].
78pub trait Real:
79    Copy
80    + PartialEq
81    + PartialOrd
82    + One
83    + Add<Self, Output = Self>
84    + Sub<Self, Output = Self>
85    + Mul<Self, Output = Self>
86    + Div<Self, Output = Self>
87{
88}
89
90impl Real for f32 {}
91
92impl Real for f64 {}
93
94// ----------------------------------------------------------------------------
95
96/// Linear interpolation.
97///
98/// ```
99/// # use emath::lerp;
100/// assert_eq!(lerp(1.0..=5.0, 0.0), 1.0);
101/// assert_eq!(lerp(1.0..=5.0, 0.5), 3.0);
102/// assert_eq!(lerp(1.0..=5.0, 1.0), 5.0);
103/// assert_eq!(lerp(1.0..=5.0, 2.0), 9.0);
104/// ```
105#[inline(always)]
106pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
107where
108    T: Real + Mul<R, Output = R>,
109    R: Copy + Add<R, Output = R>,
110{
111    let range = range.into();
112    (T::ONE - t) * *range.start() + t * *range.end()
113}
114
115/// This is a faster version of [`f32::midpoint`] which doesn't handle overflow.
116///
117/// ```
118/// # use emath::fast_midpoint;
119/// assert_eq!(fast_midpoint(1.0, 5.0), 3.0);
120/// ```
121#[inline(always)]
122pub fn fast_midpoint<R>(a: R, b: R) -> R
123where
124    R: Copy + Add<R, Output = R> + Div<R, Output = R> + One,
125{
126    let two = R::ONE + R::ONE;
127    (a + b) / two
128}
129
130/// Where in the range is this value? Returns 0-1 if within the range.
131///
132/// Returns <0 if before and >1 if after.
133///
134/// Returns `None` if the input range is zero-width.
135///
136/// ```
137/// # use emath::inverse_lerp;
138/// assert_eq!(inverse_lerp(1.0..=5.0, 1.0), Some(0.0));
139/// assert_eq!(inverse_lerp(1.0..=5.0, 3.0), Some(0.5));
140/// assert_eq!(inverse_lerp(1.0..=5.0, 5.0), Some(1.0));
141/// assert_eq!(inverse_lerp(1.0..=5.0, 9.0), Some(2.0));
142/// assert_eq!(inverse_lerp(1.0..=1.0, 3.0), None);
143/// ```
144#[inline]
145pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
146where
147    R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
148{
149    let min = *range.start();
150    let max = *range.end();
151    if min == max {
152        None
153    } else {
154        Some((value - min) / (max - min))
155    }
156}
157
158/// Linearly remap a value from one range to another,
159/// so that when `x == from.start()` returns `to.start()`
160/// and when `x == from.end()` returns `to.end()`.
161pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
162where
163    T: Real,
164{
165    let from = from.into();
166    let to = to.into();
167    debug_assert!(
168        from.start() != from.end(),
169        "from.start() and from.end() should not be equal"
170    );
171    let t = (x - *from.start()) / (*from.end() - *from.start());
172    lerp(to, t)
173}
174
175/// Like [`remap`], but also clamps the value so that the returned value is always in the `to` range.
176pub fn remap_clamp<T>(
177    x: T,
178    from: impl Into<RangeInclusive<T>>,
179    to: impl Into<RangeInclusive<T>>,
180) -> T
181where
182    T: Real,
183{
184    let from = from.into();
185    let to = to.into();
186    if from.end() < from.start() {
187        return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
188    }
189    if x <= *from.start() {
190        *to.start()
191    } else if *from.end() <= x {
192        *to.end()
193    } else {
194        debug_assert!(
195            from.start() != from.end(),
196            "from.start() and from.end() should not be equal"
197        );
198        let t = (x - *from.start()) / (*from.end() - *from.start());
199        // Ensure no numerical inaccuracies sneak in:
200        if T::ONE <= t { *to.end() } else { lerp(to, t) }
201    }
202}
203
204/// Round a value to the given number of decimal places.
205pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
206    // This is a stupid way of doing this, but stupid works.
207    format!("{value:.decimal_places$}").parse().unwrap_or(value)
208}
209
210pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
211    format_with_decimals_in_range(value, decimals..=6)
212}
213
214/// Use as few decimals as possible to show the value accurately, but within the given range.
215///
216/// Decimals are counted after the decimal point.
217pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
218    let min_decimals = *decimal_range.start();
219    let max_decimals = *decimal_range.end();
220    debug_assert!(
221        min_decimals <= max_decimals,
222        "min_decimals should be <= max_decimals, but got min_decimals: {min_decimals}, max_decimals: {max_decimals}"
223    );
224    debug_assert!(
225        max_decimals < 100,
226        "max_decimals should be < 100, but got {max_decimals}"
227    );
228    let max_decimals = max_decimals.min(16);
229    let min_decimals = min_decimals.min(max_decimals);
230
231    if min_decimals < max_decimals {
232        // Ugly/slow way of doing this. TODO(emilk): clean up precision.
233        for decimals in min_decimals..max_decimals {
234            let text = format!("{value:.decimals$}");
235            let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
236            if let Ok(parsed_value) = text.parse::<f32>()
237                && almost_equal(parsed_value, value as f32, epsilon)
238            {
239                // Enough precision to show the value accurately - good!
240                return text;
241            }
242        }
243        // The value has more precision than we expected.
244        // Probably the value was set not by the slider, but from outside.
245        // In any case: show the full value
246    }
247    format!("{value:.max_decimals$}")
248}
249
250/// Return true when arguments are the same within some rounding error.
251///
252/// For instance `almost_equal(x, x.to_degrees().to_radians(), f32::EPSILON)` should hold true for all x.
253/// The `epsilon`  can be `f32::EPSILON` to handle simple transforms (like degrees -> radians)
254/// but should be higher to handle more complex transformations.
255pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
256    if a == b {
257        true // handle infinites
258    } else {
259        let abs_max = a.abs().max(b.abs());
260        abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
261    }
262}
263
264#[expect(clippy::approx_constant)]
265#[test]
266fn test_format() {
267    assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
268    assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
269    assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
270    assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
271    assert_eq!(
272        format_with_minimum_decimals(std::f64::consts::PI, 2),
273        "3.14159"
274    );
275}
276
277#[test]
278fn test_almost_equal() {
279    for &x in &[
280        0.0_f32,
281        f32::MIN_POSITIVE,
282        1e-20,
283        1e-10,
284        f32::EPSILON,
285        0.1,
286        0.99,
287        1.0,
288        1.001,
289        1e10,
290        f32::MAX / 100.0,
291        // f32::MAX, // overflows in rad<->deg test
292        f32::INFINITY,
293    ] {
294        for &x in &[-x, x] {
295            for roundtrip in &[
296                |x: f32| x.to_degrees().to_radians(),
297                |x: f32| x.to_radians().to_degrees(),
298            ] {
299                let epsilon = f32::EPSILON;
300                assert!(
301                    almost_equal(x, roundtrip(x), epsilon),
302                    "{} vs {}",
303                    x,
304                    roundtrip(x)
305                );
306            }
307        }
308    }
309}
310
311#[test]
312fn test_remap() {
313    assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
314    assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
315    assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
316}
317
318// ----------------------------------------------------------------------------
319
320/// Extends `f32`, [`Vec2`] etc with `at_least` and `at_most` as aliases for `max` and `min`.
321pub trait NumExt {
322    /// More readable version of `self.max(lower_limit)`
323    #[must_use]
324    fn at_least(self, lower_limit: Self) -> Self;
325
326    /// More readable version of `self.min(upper_limit)`
327    #[must_use]
328    fn at_most(self, upper_limit: Self) -> Self;
329}
330
331macro_rules! impl_num_ext {
332    ($t: ty) => {
333        impl NumExt for $t {
334            #[inline(always)]
335            fn at_least(self, lower_limit: Self) -> Self {
336                self.max(lower_limit)
337            }
338
339            #[inline(always)]
340            fn at_most(self, upper_limit: Self) -> Self {
341                self.min(upper_limit)
342            }
343        }
344    };
345}
346
347impl_num_ext!(u8);
348impl_num_ext!(u16);
349impl_num_ext!(u32);
350impl_num_ext!(u64);
351impl_num_ext!(u128);
352impl_num_ext!(usize);
353impl_num_ext!(i8);
354impl_num_ext!(i16);
355impl_num_ext!(i32);
356impl_num_ext!(i64);
357impl_num_ext!(i128);
358impl_num_ext!(isize);
359impl_num_ext!(f32);
360impl_num_ext!(f64);
361impl_num_ext!(Vec2);
362impl_num_ext!(Pos2);
363
364// ----------------------------------------------------------------------------
365
366/// Wrap angle to `[-PI, PI]` range.
367pub fn normalized_angle(mut angle: f32) -> f32 {
368    use std::f32::consts::{PI, TAU};
369    angle %= TAU;
370    if angle > PI {
371        angle -= TAU;
372    } else if angle < -PI {
373        angle += TAU;
374    }
375    angle
376}
377
378#[test]
379fn test_normalized_angle() {
380    macro_rules! almost_eq {
381        ($left: expr, $right: expr) => {
382            let left = $left;
383            let right = $right;
384            assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
385        };
386    }
387
388    use std::f32::consts::TAU;
389    almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
390    almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
391    almost_eq!(normalized_angle(-TAU), 0.0);
392    almost_eq!(normalized_angle(0.0), 0.0);
393    almost_eq!(normalized_angle(TAU), 0.0);
394    almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
395}
396
397// ----------------------------------------------------------------------------
398
399/// Calculate a lerp-factor for exponential smoothing using a time step.
400///
401/// * `exponential_smooth_factor(0.90, 1.0, dt)`: reach 90% in 1.0 seconds
402/// * `exponential_smooth_factor(0.50, 0.2, dt)`: reach 50% in 0.2 seconds
403///
404/// Example:
405/// ```
406/// # use emath::{lerp, exponential_smooth_factor};
407/// # let (mut smoothed_value, target_value, dt) = (0.0_f32, 1.0_f32, 0.01_f32);
408/// let t = exponential_smooth_factor(0.90, 0.2, dt); // reach 90% in 0.2 seconds
409/// smoothed_value = lerp(smoothed_value..=target_value, t);
410/// ```
411pub fn exponential_smooth_factor(
412    reach_this_fraction: f32,
413    in_this_many_seconds: f32,
414    dt: f32,
415) -> f32 {
416    1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
417}
418
419/// If you have a value animating over time,
420/// how much towards its target do you need to move it this frame?
421///
422/// You only need to store the start time and target value in order to animate using this function.
423///
424/// ``` rs
425/// struct Animation {
426///     current_value: f32,
427///
428///     animation_time_span: (f64, f64),
429///     target_value: f32,
430/// }
431///
432/// impl Animation {
433///     fn update(&mut self, now: f64, dt: f32) {
434///         let t = interpolation_factor(self.animation_time_span, now, dt, ease_in_ease_out);
435///         self.current_value = emath::lerp(self.current_value..=self.target_value, t);
436///     }
437/// }
438/// ```
439pub fn interpolation_factor(
440    (start_time, end_time): (f64, f64),
441    current_time: f64,
442    dt: f32,
443    easing: impl Fn(f32) -> f32,
444) -> f32 {
445    let animation_duration = (end_time - start_time) as f32;
446    let prev_time = current_time - dt as f64;
447    let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
448    let end_t = easing((current_time - start_time) as f32 / animation_duration);
449    if end_t < 1.0 {
450        (end_t - prev_t) / (1.0 - prev_t)
451    } else {
452        1.0
453    }
454}
455
456/// Ease in, ease out.
457///
458/// `f(0) = 0, f'(0) = 0, f(1) = 1, f'(1) = 0`.
459#[inline]
460pub fn ease_in_ease_out(t: f32) -> f32 {
461    let t = t.clamp(0.0, 1.0);
462    (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
463}