#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
#![allow(clippy::float_cmp)]
use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
pub mod align;
pub mod easing;
mod gui_rounding;
mod history;
mod numeric;
mod ordered_float;
mod pos2;
mod range;
mod rect;
mod rect_align;
mod rect_transform;
mod rot2;
pub mod smart_aim;
mod ts_transform;
mod vec2;
mod vec2b;
pub use self::{
align::{Align, Align2},
gui_rounding::{GUI_ROUNDING, GuiRounding},
history::History,
numeric::*,
ordered_float::*,
pos2::*,
range::Rangef,
rect::*,
rect_align::RectAlign,
rect_transform::*,
rot2::*,
ts_transform::*,
vec2::*,
vec2b::*,
};
pub trait One {
const ONE: Self;
}
impl One for f32 {
const ONE: Self = 1.0;
}
impl One for f64 {
const ONE: Self = 1.0;
}
pub trait Real:
Copy
+ PartialEq
+ PartialOrd
+ One
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Mul<Self, Output = Self>
+ Div<Self, Output = Self>
{
}
impl Real for f32 {}
impl Real for f64 {}
#[inline(always)]
pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
where
T: Real + Mul<R, Output = R>,
R: Copy + Add<R, Output = R>,
{
let range = range.into();
(T::ONE - t) * *range.start() + t * *range.end()
}
#[inline(always)]
pub fn fast_midpoint<R>(a: R, b: R) -> R
where
R: Copy + Add<R, Output = R> + Div<R, Output = R> + One,
{
let two = R::ONE + R::ONE;
(a + b) / two
}
#[inline]
pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
where
R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
{
let min = *range.start();
let max = *range.end();
if min == max {
None
} else {
Some((value - min) / (max - min))
}
}
pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
where
T: Real,
{
let from = from.into();
let to = to.into();
debug_assert!(
from.start() != from.end(),
"from.start() and from.end() should not be equal"
);
let t = (x - *from.start()) / (*from.end() - *from.start());
lerp(to, t)
}
pub fn remap_clamp<T>(
x: T,
from: impl Into<RangeInclusive<T>>,
to: impl Into<RangeInclusive<T>>,
) -> T
where
T: Real,
{
let from = from.into();
let to = to.into();
if from.end() < from.start() {
return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
}
if x <= *from.start() {
*to.start()
} else if *from.end() <= x {
*to.end()
} else {
debug_assert!(
from.start() != from.end(),
"from.start() and from.end() should not be equal"
);
let t = (x - *from.start()) / (*from.end() - *from.start());
if T::ONE <= t { *to.end() } else { lerp(to, t) }
}
}
pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
format!("{value:.decimal_places$}").parse().unwrap_or(value)
}
pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
format_with_decimals_in_range(value, decimals..=6)
}
pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
let min_decimals = *decimal_range.start();
let max_decimals = *decimal_range.end();
debug_assert!(
min_decimals <= max_decimals,
"min_decimals should be <= max_decimals, but got min_decimals: {min_decimals}, max_decimals: {max_decimals}"
);
debug_assert!(
max_decimals < 100,
"max_decimals should be < 100, but got {max_decimals}"
);
let max_decimals = max_decimals.min(16);
let min_decimals = min_decimals.min(max_decimals);
if min_decimals < max_decimals {
for decimals in min_decimals..max_decimals {
let text = format!("{value:.decimals$}");
let epsilon = 16.0 * f32::EPSILON; if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
return text;
}
}
}
format!("{value:.max_decimals$}")
}
pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
if a == b {
true } else {
let abs_max = a.abs().max(b.abs());
abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
}
}
#[expect(clippy::approx_constant)]
#[test]
fn test_format() {
assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
assert_eq!(
format_with_minimum_decimals(std::f64::consts::PI, 2),
"3.14159"
);
}
#[test]
fn test_almost_equal() {
for &x in &[
0.0_f32,
f32::MIN_POSITIVE,
1e-20,
1e-10,
f32::EPSILON,
0.1,
0.99,
1.0,
1.001,
1e10,
f32::MAX / 100.0,
f32::INFINITY,
] {
for &x in &[-x, x] {
for roundtrip in &[
|x: f32| x.to_degrees().to_radians(),
|x: f32| x.to_radians().to_degrees(),
] {
let epsilon = f32::EPSILON;
assert!(
almost_equal(x, roundtrip(x), epsilon),
"{} vs {}",
x,
roundtrip(x)
);
}
}
}
}
#[test]
fn test_remap() {
assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
}
pub trait NumExt {
#[must_use]
fn at_least(self, lower_limit: Self) -> Self;
#[must_use]
fn at_most(self, upper_limit: Self) -> Self;
}
macro_rules! impl_num_ext {
($t: ty) => {
impl NumExt for $t {
#[inline(always)]
fn at_least(self, lower_limit: Self) -> Self {
self.max(lower_limit)
}
#[inline(always)]
fn at_most(self, upper_limit: Self) -> Self {
self.min(upper_limit)
}
}
};
}
impl_num_ext!(u8);
impl_num_ext!(u16);
impl_num_ext!(u32);
impl_num_ext!(u64);
impl_num_ext!(u128);
impl_num_ext!(usize);
impl_num_ext!(i8);
impl_num_ext!(i16);
impl_num_ext!(i32);
impl_num_ext!(i64);
impl_num_ext!(i128);
impl_num_ext!(isize);
impl_num_ext!(f32);
impl_num_ext!(f64);
impl_num_ext!(Vec2);
impl_num_ext!(Pos2);
pub fn normalized_angle(mut angle: f32) -> f32 {
use std::f32::consts::{PI, TAU};
angle %= TAU;
if angle > PI {
angle -= TAU;
} else if angle < -PI {
angle += TAU;
}
angle
}
#[test]
fn test_normalized_angle() {
macro_rules! almost_eq {
($left: expr, $right: expr) => {
let left = $left;
let right = $right;
assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
};
}
use std::f32::consts::TAU;
almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
almost_eq!(normalized_angle(-TAU), 0.0);
almost_eq!(normalized_angle(0.0), 0.0);
almost_eq!(normalized_angle(TAU), 0.0);
almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
}
pub fn exponential_smooth_factor(
reach_this_fraction: f32,
in_this_many_seconds: f32,
dt: f32,
) -> f32 {
1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
}
pub fn interpolation_factor(
(start_time, end_time): (f64, f64),
current_time: f64,
dt: f32,
easing: impl Fn(f32) -> f32,
) -> f32 {
let animation_duration = (end_time - start_time) as f32;
let prev_time = current_time - dt as f64;
let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
let end_t = easing((current_time - start_time) as f32 / animation_duration);
if end_t < 1.0 {
(end_t - prev_t) / (1.0 - prev_t)
} else {
1.0
}
}
#[inline]
pub fn ease_in_ease_out(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
(3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
}