rae 0.1.13

Renderer-neutral Rust design catalog for desktop widgets, layouts, sanitized output, and GLSL shader primitives.
Documentation
#[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);
    }
}