1#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
20#![allow(clippy::float_cmp)]
23
24use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
25
26pub mod align;
29pub mod easing;
30mod history;
31mod numeric;
32mod ordered_float;
33mod pos2;
34mod range;
35mod rect;
36mod rect_transform;
37mod rot2;
38pub mod smart_aim;
39mod ts_transform;
40mod vec2;
41mod vec2b;
42
43pub use self::{
44 align::{Align, Align2},
45 history::History,
46 numeric::*,
47 ordered_float::*,
48 pos2::*,
49 range::Rangef,
50 rect::*,
51 rect_transform::*,
52 rot2::*,
53 ts_transform::*,
54 vec2::*,
55 vec2b::*,
56};
57
58pub trait One {
62 const ONE: Self;
63}
64
65impl One for f32 {
66 const ONE: Self = 1.0;
67}
68
69impl One for f64 {
70 const ONE: Self = 1.0;
71}
72
73pub trait Real:
75 Copy
76 + PartialEq
77 + PartialOrd
78 + One
79 + Add<Self, Output = Self>
80 + Sub<Self, Output = Self>
81 + Mul<Self, Output = Self>
82 + Div<Self, Output = Self>
83{
84}
85
86impl Real for f32 {}
87
88impl Real for f64 {}
89
90#[inline(always)]
102pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
103where
104 T: Real + Mul<R, Output = R>,
105 R: Copy + Add<R, Output = R>,
106{
107 let range = range.into();
108 (T::ONE - t) * *range.start() + t * *range.end()
109}
110
111#[inline]
126pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
127where
128 R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
129{
130 let min = *range.start();
131 let max = *range.end();
132 if min == max {
133 None
134 } else {
135 Some((value - min) / (max - min))
136 }
137}
138
139pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
143where
144 T: Real,
145{
146 let from = from.into();
147 let to = to.into();
148 debug_assert!(from.start() != from.end());
149 let t = (x - *from.start()) / (*from.end() - *from.start());
150 lerp(to, t)
151}
152
153pub fn remap_clamp<T>(
155 x: T,
156 from: impl Into<RangeInclusive<T>>,
157 to: impl Into<RangeInclusive<T>>,
158) -> T
159where
160 T: Real,
161{
162 let from = from.into();
163 let to = to.into();
164 if from.end() < from.start() {
165 return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
166 }
167 if x <= *from.start() {
168 *to.start()
169 } else if *from.end() <= x {
170 *to.end()
171 } else {
172 debug_assert!(from.start() != from.end());
173 let t = (x - *from.start()) / (*from.end() - *from.start());
174 if T::ONE <= t {
176 *to.end()
177 } else {
178 lerp(to, t)
179 }
180 }
181}
182
183pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
185 format!("{value:.decimal_places$}").parse().unwrap_or(value)
187}
188
189pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
190 format_with_decimals_in_range(value, decimals..=6)
191}
192
193pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
197 let min_decimals = *decimal_range.start();
198 let max_decimals = *decimal_range.end();
199 debug_assert!(min_decimals <= max_decimals);
200 debug_assert!(max_decimals < 100);
201 let max_decimals = max_decimals.min(16);
202 let min_decimals = min_decimals.min(max_decimals);
203
204 if min_decimals < max_decimals {
205 for decimals in min_decimals..max_decimals {
207 let text = format!("{value:.decimals$}");
208 let epsilon = 16.0 * f32::EPSILON; if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
210 return text;
212 }
213 }
214 }
218 format!("{value:.max_decimals$}")
219}
220
221pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
227 if a == b {
228 true } else {
230 let abs_max = a.abs().max(b.abs());
231 abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
232 }
233}
234
235#[allow(clippy::approx_constant)]
236#[test]
237fn test_format() {
238 assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
239 assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
240 assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
241 assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
242 assert_eq!(
243 format_with_minimum_decimals(std::f64::consts::PI, 2),
244 "3.14159"
245 );
246}
247
248#[test]
249fn test_almost_equal() {
250 for &x in &[
251 0.0_f32,
252 f32::MIN_POSITIVE,
253 1e-20,
254 1e-10,
255 f32::EPSILON,
256 0.1,
257 0.99,
258 1.0,
259 1.001,
260 1e10,
261 f32::MAX / 100.0,
262 f32::INFINITY,
264 ] {
265 for &x in &[-x, x] {
266 for roundtrip in &[
267 |x: f32| x.to_degrees().to_radians(),
268 |x: f32| x.to_radians().to_degrees(),
269 ] {
270 let epsilon = f32::EPSILON;
271 assert!(
272 almost_equal(x, roundtrip(x), epsilon),
273 "{} vs {}",
274 x,
275 roundtrip(x)
276 );
277 }
278 }
279 }
280}
281
282#[test]
283fn test_remap() {
284 assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
285 assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
286 assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
287}
288
289pub trait NumExt {
293 #[must_use]
295 fn at_least(self, lower_limit: Self) -> Self;
296
297 #[must_use]
299 fn at_most(self, upper_limit: Self) -> Self;
300}
301
302macro_rules! impl_num_ext {
303 ($t: ty) => {
304 impl NumExt for $t {
305 #[inline(always)]
306 fn at_least(self, lower_limit: Self) -> Self {
307 self.max(lower_limit)
308 }
309
310 #[inline(always)]
311 fn at_most(self, upper_limit: Self) -> Self {
312 self.min(upper_limit)
313 }
314 }
315 };
316}
317
318impl_num_ext!(u8);
319impl_num_ext!(u16);
320impl_num_ext!(u32);
321impl_num_ext!(u64);
322impl_num_ext!(u128);
323impl_num_ext!(usize);
324impl_num_ext!(i8);
325impl_num_ext!(i16);
326impl_num_ext!(i32);
327impl_num_ext!(i64);
328impl_num_ext!(i128);
329impl_num_ext!(isize);
330impl_num_ext!(f32);
331impl_num_ext!(f64);
332impl_num_ext!(Vec2);
333impl_num_ext!(Pos2);
334
335pub fn normalized_angle(mut angle: f32) -> f32 {
339 use std::f32::consts::{PI, TAU};
340 angle %= TAU;
341 if angle > PI {
342 angle -= TAU;
343 } else if angle < -PI {
344 angle += TAU;
345 }
346 angle
347}
348
349#[test]
350fn test_normalized_angle() {
351 macro_rules! almost_eq {
352 ($left: expr, $right: expr) => {
353 let left = $left;
354 let right = $right;
355 assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
356 };
357 }
358
359 use std::f32::consts::TAU;
360 almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
361 almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
362 almost_eq!(normalized_angle(-TAU), 0.0);
363 almost_eq!(normalized_angle(0.0), 0.0);
364 almost_eq!(normalized_angle(TAU), 0.0);
365 almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
366}
367
368pub fn exponential_smooth_factor(
383 reach_this_fraction: f32,
384 in_this_many_seconds: f32,
385 dt: f32,
386) -> f32 {
387 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
388}
389
390pub fn interpolation_factor(
411 (start_time, end_time): (f64, f64),
412 current_time: f64,
413 dt: f32,
414 easing: impl Fn(f32) -> f32,
415) -> f32 {
416 let animation_duration = (end_time - start_time) as f32;
417 let prev_time = current_time - dt as f64;
418 let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
419 let end_t = easing((current_time - start_time) as f32 / animation_duration);
420 if end_t < 1.0 {
421 (end_t - prev_t) / (1.0 - prev_t)
422 } else {
423 1.0
424 }
425}
426
427#[inline]
431pub fn ease_in_ease_out(t: f32) -> f32 {
432 let t = t.clamp(0.0, 1.0);
433 (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
434}