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, FRAC_PI_2, 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/// Approximates `atan(x)` approximation in radians with a maximum error of
79/// `0.002`.
80///
81/// Returns [`f32::NAN`] if the number is [`f32::NAN`].
82#[must_use]
83pub fn atan(x: f32) -> f32 {
84 // https://github.com/tarcieri/micromath/blob/main/src/float/atan.rs
85 FRAC_PI_2 * atan_norm(x)
86}
87
88/// Approximates `atan(x)` normalized to the `[−1,1]` range with a maximum
89/// error of `0.1620` degrees.
90#[must_use]
91pub fn atan_norm(x: f32) -> f32 {
92 // https://github.com/tarcieri/micromath/blob/main/src/float/atan.rs
93 const SIGN_MASK: u32 = 0x8000_0000;
94 const B: f32 = 0.596_227;
95
96 // Extract the sign bit
97 let ux_s = SIGN_MASK & x.to_bits();
98
99 // Calculate the arctangent in the first quadrant
100 let bx_a = abs(B * x);
101 let n = bx_a + x * x;
102 let atan_1q = n / (1.0 + bx_a + n);
103
104 // Restore the sign bit and convert to float
105 f32::from_bits(ux_s | atan_1q.to_bits())
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 #[expect(clippy::float_cmp)]
114 fn test_sqrt() {
115 assert_eq!(sqrt(4.), 2.);
116 assert_eq!(sqrt(9.), 3.125);
117 }
118}