#[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);
}
}