Skip to main content

firefly_rust/
math.rs

1//! A few useful math (trigonometric) functions for [f32].
2//!
3//! Use [micromath] and [nalgebra] if you need more.
4//!
5//! [micromath]: https://github.com/tarcieri/micromath
6//! [nalgebra]: https://github.com/dimforge/nalgebra
7
8use core::f32::consts::{FRAC_1_PI, PI};
9
10const SIGN_MASK: u32 = 0b1000_0000_0000_0000_0000_0000_0000_0000;
11
12/// Approximates `tan(x)` in radians with a maximum error of `0.6`.
13#[must_use]
14pub fn tan(x: f32) -> f32 {
15    sin(x) / cos(x)
16}
17
18/// Approximates `sin(x)` in radians with a maximum error of `0.002`.
19#[must_use]
20pub fn sin(x: f32) -> f32 {
21    cos(x - PI / 2.0)
22}
23
24/// Approximates `cos(x)` in radians with a maximum error of `0.002`.
25#[must_use]
26pub fn cos(x: f32) -> f32 {
27    // https://github.com/tarcieri/micromath/blob/main/src/float/cos.rs
28    let mut x = x;
29    x *= FRAC_1_PI / 2.0;
30    x -= 0.25 + floor(x + 0.25);
31    x *= 16.0 * (abs(x) - 0.5);
32    x += 0.225 * x * (abs(x) - 1.0);
33    x
34}
35
36/// Returns the largest integer less than or equal to a number.
37#[must_use]
38pub fn floor(x: f32) -> f32 {
39    // https://github.com/tarcieri/micromath/blob/main/src/float/floor.rs
40    #[expect(clippy::cast_precision_loss)]
41    let mut res = (x as i32) as f32;
42    if x < res {
43        res -= 1.0;
44    }
45    res
46}
47
48/// Returns the absolute value of a number.
49#[must_use]
50pub fn abs(x: f32) -> f32 {
51    // https://github.com/tarcieri/micromath/blob/main/src/float/abs.rs
52    f32::from_bits(x.to_bits() & !SIGN_MASK)
53}
54
55/// Approximates the square root of a number with an average deviation of ~5%.
56#[must_use]
57pub fn sqrt(x: f32) -> f32 {
58    // https://github.com/tarcieri/micromath/blob/main/src/float/sqrt.rs
59    if x >= 0. {
60        f32::from_bits((x.to_bits() + 0x3f80_0000) >> 1)
61    } else {
62        f32::NAN
63    }
64}
65
66// Calculates the least nonnegative remainder of lhs (mod rhs).
67#[must_use]
68pub fn rem_euclid(lhs: f32, rhs: f32) -> f32 {
69    // https://github.com/tarcieri/micromath/blob/main/src/float/rem_euclid.rs
70    let r = lhs % rhs;
71    if r < 0.0 { r + abs(rhs) } else { r }
72}
73
74#[must_use]
75pub fn atan2(lhs: f32, rhs: f32) -> f32 {
76    // https://github.com/tarcieri/micromath/blob/main/src/float/atan2.rs
77    let n = atan2_norm(lhs, rhs);
78    PI / 2.0 * if n > 2.0 { n - 4.0 } else { n }
79}
80
81/// Approximates `atan2(y,x)` normalized to the `[0, 4)` range with a maximum
82/// error of `0.1620` degrees.
83#[must_use]
84#[expect(clippy::similar_names)]
85pub(crate) fn atan2_norm(lhs: f32, rhs: f32) -> f32 {
86    const SIGN_MASK: u32 = 0x8000_0000;
87    const B: f32 = 0.596_227;
88
89    let y = lhs;
90    let x = rhs;
91
92    // Extract sign bits from floating point values
93    let ux_s = SIGN_MASK & x.to_bits();
94    let uy_s = SIGN_MASK & y.to_bits();
95
96    // Determine quadrant offset
97    #[expect(clippy::cast_precision_loss)]
98    let q = ((!ux_s & uy_s) >> 29 | ux_s >> 30) as f32;
99
100    // Calculate arctangent in the first quadrant
101    let bxy_a = (B * x * y).abs();
102    let n = bxy_a + y * y;
103    let atan_1q = n / (x * x + bxy_a + n);
104
105    // Translate it to the proper quadrant
106    let uatan_2q = (ux_s ^ uy_s) | atan_1q.to_bits();
107    q + f32::from_bits(uatan_2q)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    #[expect(clippy::float_cmp)]
116    fn test_sqrt() {
117        assert_eq!(sqrt(4.), 2.);
118        assert_eq!(sqrt(9.), 3.125);
119    }
120}