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 {
72        r + abs(rhs)
73    } else {
74        r
75    }
76}
77
78#[must_use]
79pub fn atan2(lhs: f32, rhs: f32) -> f32 {
80    // https://github.com/tarcieri/micromath/blob/main/src/float/atan2.rs
81    let n = atan2_norm(lhs, rhs);
82    PI / 2.0 * if n > 2.0 { n - 4.0 } else { n }
83}
84
85/// Approximates `atan2(y,x)` normalized to the `[0, 4)` range with a maximum
86/// error of `0.1620` degrees.
87#[must_use]
88#[expect(clippy::similar_names)]
89pub(crate) fn atan2_norm(lhs: f32, rhs: f32) -> f32 {
90    const SIGN_MASK: u32 = 0x8000_0000;
91    const B: f32 = 0.596_227;
92
93    let y = lhs;
94    let x = rhs;
95
96    // Extract sign bits from floating point values
97    let ux_s = SIGN_MASK & x.to_bits();
98    let uy_s = SIGN_MASK & y.to_bits();
99
100    // Determine quadrant offset
101    #[expect(clippy::cast_precision_loss)]
102    let q = ((!ux_s & uy_s) >> 29 | ux_s >> 30) as f32;
103
104    // Calculate arctangent in the first quadrant
105    let bxy_a = (B * x * y).abs();
106    let n = bxy_a + y * y;
107    let atan_1q = n / (x * x + bxy_a + n);
108
109    // Translate it to the proper quadrant
110    let uatan_2q = (ux_s ^ uy_s) | atan_1q.to_bits();
111    q + f32::from_bits(uatan_2q)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    #[expect(clippy::float_cmp)]
120    fn test_sqrt() {
121        assert_eq!(sqrt(4.), 2.);
122        assert_eq!(sqrt(9.), 3.125);
123    }
124}