#[cfg(feature = "std")]
extern crate std;
#[cfg(not(any(feature = "std", feature = "libm")))]
compile_error!(
"Either the 'std' or 'libm' feature must be enabled for gem's color space module. \
Add `features = [\"std\"]` (if you have std available) or `features = [\"libm\"]` \
(for no_std environments) to your dependency."
);
#[inline]
pub(super) fn sqrt(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::sqrt(x);
#[cfg(not(feature = "std"))]
return libm::sqrtf(x);
}
#[inline]
pub(super) fn round(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::round(x);
#[cfg(not(feature = "std"))]
return libm::roundf(x);
}
#[inline]
pub(super) fn pow24(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::powf(x, 2.4_f32);
#[cfg(not(feature = "std"))]
return libm::powf(x, 2.4_f32);
}
#[inline]
pub(super) fn pow_inv24(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::powf(x, 1.0_f32 / 2.4_f32);
#[cfg(not(feature = "std"))]
return libm::powf(x, 1.0_f32 / 2.4_f32);
}
#[cfg(any(feature = "std", feature = "libm"))]
#[inline]
pub(super) fn atan2(y: f32, x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::atan2(y, x);
#[cfg(not(feature = "std"))]
return libm::atan2f(y, x);
}
#[cfg(any(feature = "std", feature = "libm"))]
#[inline]
pub(super) fn sin(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::sin(x);
#[cfg(not(feature = "std"))]
return libm::sinf(x);
}
#[cfg(any(feature = "std", feature = "libm"))]
#[inline]
pub(super) fn cos(x: f32) -> f32 {
#[cfg(feature = "std")]
return f32::cos(x);
#[cfg(not(feature = "std"))]
return libm::cosf(x);
}
#[inline]
pub(super) const fn clamp(x: f32, min: f32, max: f32) -> f32 {
if x < min {
min
} else if x > max {
max
} else {
x
}
}
#[inline]
pub(super) const fn abs(x: f32) -> f32 {
f32::abs(x)
}
#[inline]
pub(super) fn signum(x: f32) -> f32 {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
#[inline]
pub(super) fn rem_euclid(x: f32, rhs: f32) -> f32 {
let r = x % rhs;
if r < 0.0 { r + abs(rhs) } else { r }
}
#[inline]
pub(super) fn srgb_to_linear_channel(c: f32) -> f32 {
if c <= 0.04045 {
c / 12.92
} else {
pow24((c + 0.055) / 1.055)
}
}
#[inline]
#[allow(clippy::suboptimal_flops)]
pub(super) fn linear_to_srgb_channel(c: f32) -> f32 {
if c <= 0.003_130_8 {
c * 12.92
} else {
1.055 * pow_inv24(c) - 0.055
}
}
#[inline]
#[allow(clippy::suboptimal_flops)]
pub(super) fn channel_to_u8(x: f32) -> u8 {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "value is clamped to [0, 1] then scaled to [0, 255]; cast is always safe"
)]
{
round(clamp(x, 0.0, 1.0) * 255.0) as u8
}
}
#[inline]
#[allow(clippy::suboptimal_flops)]
pub(super) fn cbrt(x: f32) -> f32 {
if x == 0.0 {
return 0.0;
}
let sign = signum(x);
let x = abs(x);
let mut y = f32::from_bits(x.to_bits() / 3 + 0x2A51_1CD3);
y = (2.0 * y + x / (y * y)) / 3.0;
y = (2.0 * y + x / (y * y)) / 3.0;
y = (2.0 * y + x / (y * y)) / 3.0;
y = (2.0 * y + x / (y * y)) / 3.0;
sign * y
}
#[inline]
#[allow(clippy::suboptimal_flops)]
pub(super) fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
#[inline]
pub(super) fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
let diff = b - a;
let diff = if diff > 0.5 {
diff - 1.0
} else if diff < -0.5 {
diff + 1.0
} else {
diff
};
#[allow(clippy::suboptimal_flops)]
rem_euclid(a + t * diff, 1.0)
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::float_cmp_const)]
mod tests {
use super::*;
#[test]
fn sqrt_four() {
assert!((sqrt(4.0) - 2.0).abs() < 1e-5);
}
#[test]
fn round_half() {
assert_eq!(round(1.5), 2.0);
assert_eq!(round(1.4), 1.0);
}
#[test]
fn clamp_range() {
assert_eq!(clamp(2.0, 0.0, 1.0), 1.0);
assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0);
assert_eq!(clamp(0.5, 0.0, 1.0), 0.5);
}
#[test]
fn abs_values() {
assert_eq!(abs(-3.0), 3.0);
assert_eq!(abs(3.0), 3.0);
assert_eq!(abs(0.0), 0.0);
}
#[test]
fn signum_values() {
assert_eq!(signum(-5.0), -1.0);
assert_eq!(signum(5.0), 1.0);
assert_eq!(signum(0.0), 0.0);
}
#[test]
fn rem_euclid_positive() {
assert!((rem_euclid(7.0, 3.0) - 1.0).abs() < 1e-6);
}
#[test]
fn rem_euclid_negative() {
assert!((rem_euclid(-1.0, 6.0) - 5.0).abs() < 1e-6);
}
#[test]
fn cbrt_positive() {
assert!((cbrt(27.0) - 3.0).abs() < 1e-4);
}
#[test]
fn cbrt_negative() {
assert!((cbrt(-8.0) - (-2.0)).abs() < 1e-4);
}
#[test]
fn cbrt_zero() {
assert_eq!(cbrt(0.0), 0.0);
}
#[test]
fn srgb_linear_channel_zero_one() {
assert!((srgb_to_linear_channel(0.0)).abs() < 1e-6);
assert!((srgb_to_linear_channel(1.0) - 1.0).abs() < 1e-4);
}
#[test]
fn linear_srgb_channel_zero_one() {
assert!((linear_to_srgb_channel(0.0)).abs() < 1e-6);
assert!((linear_to_srgb_channel(1.0) - 1.0).abs() < 1e-4);
}
#[test]
fn gamma_roundtrip() {
for &v in &[0.0_f32, 0.1, 0.5, 0.9, 1.0] {
let lin = srgb_to_linear_channel(v);
let back = linear_to_srgb_channel(lin);
assert!((back - v).abs() < 1e-4, "v={v}, back={back}");
}
}
#[test]
fn lerp_hue_no_wrap() {
assert!((lerp_hue(0.1, 0.3, 0.5) - 0.2).abs() < 1e-6);
}
#[test]
fn lerp_hue_wraps_through_zero() {
let h = lerp_hue(0.9, 0.1, 0.5);
assert!(!(0.05..=0.95).contains(&h), "h={h}");
}
}