rae 0.1.2

Renderer-neutral widget, layout, state, and GLSL shader primitives for agentic Rust desktop tools.
Documentation
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

pub fn clamp01(value: f32) -> f32 {
    value.clamp(0.0, 1.0)
}

pub fn lerp(a: f32, b: f32, amount: f32) -> f32 {
    a + (b - a) * clamp01(amount)
}

pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
    let t = clamp01((x - edge0) / (edge1 - edge0));
    t * t * (3.0 - 2.0 * t)
}

pub fn pulse(time: f32, frequency: f32) -> f32 {
    (time * frequency).sin() * 0.5 + 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 {
    1.0 - (-damping * time).exp() * (frequency * time).cos()
}

#[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 <= f32::EPSILON {
            Self::ZERO
        } else {
            Self::new(self.x / length, self.y / length)
        }
    }
}

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 {
    let x = (value.x * 127.1 + value.y * 311.7).sin() * 43_758.547;
    x.fract().abs()
}

pub fn fractal_brownian_motion(mut value: Vec2, octaves: usize) -> f32 {
    let mut total = 0.0;
    let mut amplitude = 0.5;
    for _ in 0..octaves.max(1) {
        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;
    }
    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 <= 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 = hue.fract() * 6.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 {
        (time * self.speed + self.phase).sin() * self.amplitude + 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 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 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);
    }
}