#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub fn clamp01(value: f32) -> f32 {
if value.is_nan() {
0.0
} else {
value.clamp(0.0, 1.0)
}
}
pub fn lerp(a: f32, b: f32, amount: f32) -> f32 {
let amount = clamp01(amount);
if amount <= 0.0 {
a
} else if amount >= 1.0 {
b
} else {
a + (b - a) * amount
}
}
pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
if !edge0.is_finite() || !edge1.is_finite() || !x.is_finite() {
return 0.0;
}
let span = edge1 - edge0;
if span.abs() <= f32::EPSILON {
return if x < edge0 { 0.0 } else { 1.0 };
}
let t = clamp01((x - edge0) / span);
t * t * (3.0 - 2.0 * t)
}
pub fn pulse(time: f32, frequency: f32) -> f32 {
if !time.is_finite() || !frequency.is_finite() {
return 0.5;
}
let phase = time * frequency;
if !phase.is_finite() {
return 0.5;
}
let value = phase.sin() * 0.5 + 0.5;
if value.is_finite() {
value
} else {
0.5
}
}
pub fn ease_in_out_cubic(value: f32) -> f32 {
let value = clamp01(value);
if value < 0.5 {
4.0 * value * value * value
} else {
1.0 - (-2.0 * value + 2.0).powi(3) * 0.5
}
}
pub fn spring(time: f32, damping: f32, frequency: f32) -> f32 {
if !time.is_finite() || !damping.is_finite() || !frequency.is_finite() {
return 0.0;
}
let decay_phase = -damping * time;
let oscillation_phase = frequency * time;
if !decay_phase.is_finite() || !oscillation_phase.is_finite() {
return 0.0;
}
let value = 1.0 - decay_phase.exp() * oscillation_phase.cos();
if value.is_finite() {
value
} else {
0.0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Vec2 {
pub x: f32,
pub y: f32,
}
impl Vec2 {
pub const ZERO: Self = Self::new(0.0, 0.0);
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn dot(self, other: Self) -> f32 {
self.x * other.x + self.y * other.y
}
pub fn length(self) -> f32 {
self.dot(self).sqrt()
}
pub fn normalized(self) -> Self {
let length = self.length();
if !length.is_finite() || length <= f32::EPSILON {
Self::ZERO
} else {
Self::new(self.x / length, self.y / length)
}
}
}
impl std::ops::Add for Vec2 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl std::ops::Sub for Vec2 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl std::ops::Mul<f32> for Vec2 {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Self::new(self.x * rhs, self.y * rhs)
}
}
impl std::ops::Div<f32> for Vec2 {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
if rhs.is_finite() && rhs.abs() > f32::EPSILON {
Self::new(self.x / rhs, self.y / rhs)
} else {
Self::ZERO
}
}
}
pub fn rotate(value: Vec2, radians: f32) -> Vec2 {
let (sin, cos) = radians.sin_cos();
Vec2::new(value.x * cos - value.y * sin, value.x * sin + value.y * cos)
}
pub fn hash12(value: Vec2) -> f32 {
if !value.x.is_finite() || !value.y.is_finite() {
return 0.0;
}
let x = (value.x * 127.1 + value.y * 311.7).sin() * 43_758.547;
if x.is_finite() {
x.fract().abs()
} else {
0.0
}
}
pub fn fractal_brownian_motion(mut value: Vec2, octaves: usize) -> f32 {
const MAX_FBM_OCTAVES: usize = 16;
if !value.x.is_finite() || !value.y.is_finite() {
return 0.0;
}
let mut total = 0.0;
let mut amplitude = 0.5;
for _ in 0..octaves.clamp(1, MAX_FBM_OCTAVES) {
total += hash12(value) * amplitude;
value = rotate(Vec2::new(value.x * 2.03 + 17.1, value.y * 2.07 - 9.2), 0.47);
amplitude *= 0.5;
if amplitude <= f32::EPSILON || !value.x.is_finite() || !value.y.is_finite() {
break;
}
}
total
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn dot(self, other: Self) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
pub fn cross(self, other: Self) -> Self {
Self::new(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
}
pub fn length(self) -> f32 {
self.dot(self).sqrt()
}
pub fn normalized(self) -> Self {
let length = self.length();
if !length.is_finite() || length <= f32::EPSILON {
Self::ZERO
} else {
Self::new(self.x / length, self.y / length, self.z / length)
}
}
}
pub fn catmull_rom(a: f32, b: f32, c: f32, d: f32, t: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
0.5 * ((2.0 * b)
+ (-a + c) * t
+ (2.0 * a - 5.0 * b + 4.0 * c - d) * t2
+ (-a + 3.0 * b - 3.0 * c + d) * t3)
}
pub fn remap(value: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
if (in_max - in_min).abs() <= f32::EPSILON {
return out_min;
}
lerp(out_min, out_max, (value - in_min) / (in_max - in_min))
}
pub fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> Vec3 {
let hue = if hue.is_finite() {
hue.rem_euclid(1.0)
} else {
0.0
} * 6.0;
let value = if value.is_finite() {
value.max(0.0)
} else {
0.0
};
let chroma = value * clamp01(saturation);
let x = chroma * (1.0 - ((hue % 2.0) - 1.0).abs());
let m = value - chroma;
let (r, g, b) = if hue < 1.0 {
(chroma, x, 0.0)
} else if hue < 2.0 {
(x, chroma, 0.0)
} else if hue < 3.0 {
(0.0, chroma, x)
} else if hue < 4.0 {
(0.0, x, chroma)
} else if hue < 5.0 {
(x, 0.0, chroma)
} else {
(chroma, 0.0, x)
};
Vec3::new(r + m, g + m, b + m)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Oscillator {
pub speed: f32,
pub phase: f32,
pub amplitude: f32,
pub bias: f32,
}
impl Oscillator {
pub const fn new(speed: f32, phase: f32, amplitude: f32, bias: f32) -> Self {
Self {
speed,
phase,
amplitude,
bias,
}
}
pub fn sample(self, time: f32) -> f32 {
if !time.is_finite()
|| !self.speed.is_finite()
|| !self.phase.is_finite()
|| !self.amplitude.is_finite()
|| !self.bias.is_finite()
{
return 0.0;
}
let phase = time * self.speed + self.phase;
if !phase.is_finite() {
return self.bias;
}
let value = phase.sin() * self.amplitude + self.bias;
if value.is_finite() {
value
} else {
self.bias
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoothstep_is_bounded() {
assert_eq!(smoothstep(0.0, 1.0, -10.0), 0.0);
assert_eq!(smoothstep(0.0, 1.0, 10.0), 1.0);
}
#[test]
fn smoothstep_handles_degenerate_and_non_finite_edges() {
assert_eq!(smoothstep(1.0, 1.0, 0.5), 0.0);
assert_eq!(smoothstep(1.0, 1.0, 1.0), 1.0);
assert_eq!(smoothstep(f32::NAN, 1.0, 1.0), 0.0);
}
#[test]
fn hsv_to_rgb_wraps_negative_hue_and_sanitizes_inputs() {
let wrapped = hsv_to_rgb(-0.1, 1.0, 1.0);
let positive = hsv_to_rgb(0.9, 1.0, 1.0);
let sanitized = hsv_to_rgb(f32::NAN, f32::NAN, f32::NAN);
assert_eq!(wrapped, positive);
assert!(wrapped.x >= 0.0 && wrapped.y >= 0.0 && wrapped.z >= 0.0);
assert_eq!(sanitized, Vec3::ZERO);
}
#[test]
fn pulse_and_normalization_sanitize_non_finite_inputs() {
assert_eq!(pulse(f32::INFINITY, 1.0), 0.5);
assert_eq!(pulse(1.0, f32::NAN), 0.5);
assert_eq!(pulse(f32::MAX, 2.0), 0.5);
assert_eq!(Vec2::new(f32::NAN, 1.0).normalized(), Vec2::ZERO);
assert_eq!(Vec3::new(1.0, f32::INFINITY, 0.0).normalized(), Vec3::ZERO);
}
#[test]
fn spring_and_oscillator_sanitize_huge_public_values() {
assert_eq!(spring(f32::MAX, 2.0, 1.0), 0.0);
assert_eq!(spring(1.0, f32::NAN, 1.0), 0.0);
let oscillator = Oscillator::new(f32::MAX, f32::MAX, f32::MAX, 0.25);
assert_eq!(oscillator.sample(2.0), 0.25);
assert_eq!(Oscillator::new(1.0, 0.0, 1.0, f32::NAN).sample(1.0), 0.0);
}
#[test]
fn lerp_short_circuits_exact_endpoints() {
assert_eq!(lerp(1.0, f32::INFINITY, 0.0), 1.0);
assert_eq!(lerp(f32::NAN, 2.0, 1.0), 2.0);
assert_eq!(lerp(1.0, 2.0, f32::NAN), 1.0);
}
#[test]
fn vector_rotation_preserves_length() {
let source = Vec2::new(3.0, 4.0);
let rotated = rotate(source, 1.2);
assert!((source.length() - rotated.length()).abs() < 0.0001);
}
#[test]
fn fbm_is_positive_and_bounded_for_default_octaves() {
let value = fractal_brownian_motion(Vec2::new(0.25, 0.75), 5);
assert!((0.0..=1.0).contains(&value));
}
#[test]
fn fbm_sanitizes_non_finite_inputs_and_extreme_octaves() {
assert_eq!(hash12(Vec2::new(f32::NAN, 1.0)), 0.0);
assert_eq!(
fractal_brownian_motion(Vec2::new(f32::INFINITY, 0.0), 4),
0.0
);
let value = fractal_brownian_motion(Vec2::new(0.25, 0.75), usize::MAX);
assert!(value.is_finite());
assert!((0.0..=1.0).contains(&value));
}
#[test]
fn vector_cross_product_is_perpendicular() {
let x = Vec3::new(1.0, 0.0, 0.0);
let y = Vec3::new(0.0, 1.0, 0.0);
let z = x.cross(y);
assert_eq!(z, Vec3::new(0.0, 0.0, 1.0));
assert_eq!(z.dot(x), 0.0);
assert_eq!(z.dot(y), 0.0);
}
}