Skip to main content

ph_curves/
math.rs

1//! Fixed-point math helpers and the [`UnitValue`] abstraction.
2//!
3//! All arithmetic uses the [`fixed`] crate so that the library remains
4//! `no_std` and avoids floating-point operations at runtime.
5
6use fixed::types::{I16F16, I32F32, U16F16};
7
8/// Rounding policy used by [`quantize`] and the tickless scheduler.
9#[derive(Copy, Clone, Debug, Eq, PartialEq)]
10pub enum Rounding {
11    /// Round down to the nearest multiple of `step`.
12    Floor,
13    /// Round up to the nearest multiple of `step`.
14    Ceil,
15    /// Round to the nearest multiple of `step` (half-up).
16    Nearest,
17}
18
19// ---------------------------------------------------------------------------
20// UnitValue trait
21// ---------------------------------------------------------------------------
22
23/// A normalized value type usable as a curve domain / range.
24///
25/// Implementors map a unit interval onto a discrete integer type so that the
26/// tickless scheduler can convert between wall-clock time fractions and curve
27/// positions.
28pub trait UnitValue: Copy + 'static {
29    /// The value representing 0 (start of the interval).
30    fn zero() -> Self;
31    /// The value representing 1 (end of the interval).
32    fn one() -> Self;
33
34    /// Convert this value to a LUT index (0-based).
35    fn to_index(self) -> usize;
36
37    /// Create a value from a time fraction `elapsed / duration`.
38    ///
39    /// Returns [`Self::zero()`] when `elapsed == 0` and [`Self::one()`] when
40    /// `elapsed >= duration`.
41    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self;
42
43    /// Convert this value back to a wall-clock offset in milliseconds within
44    /// `duration_ms`, rounding up.
45    fn to_time_offset(self, duration_ms: u32) -> u32;
46
47    /// Linearly interpolate between two `u16` endpoints using `self` as the
48    /// blend weight.
49    fn lerp_u16(self, a: u16, b: u16) -> u16;
50
51    /// Inverse linear interpolation: find the blend weight `T` such that
52    /// `T::lerp_u16(a, b) ≈ target`, rounding toward `b`.
53    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self;
54}
55
56// ---------------------------------------------------------------------------
57// UnitValue implementation for u8
58// ---------------------------------------------------------------------------
59
60impl UnitValue for u8 {
61    fn zero() -> Self {
62        0
63    }
64
65    fn one() -> Self {
66        255
67    }
68
69    fn to_index(self) -> usize {
70        self as usize
71    }
72
73    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self {
74        if duration_ms == 0 || elapsed_ms >= duration_ms {
75            return 255;
76        }
77        if elapsed_ms == 0 {
78            return 0;
79        }
80        let frac = U16F16::from_num(elapsed_ms) / U16F16::from_num(duration_ms);
81        let u = frac * U16F16::from_num(255u16);
82        u.to_num::<u32>().min(255) as u8
83    }
84
85    fn to_time_offset(self, duration_ms: u32) -> u32 {
86        if duration_ms == 0 {
87            return 0;
88        }
89        let frac = U16F16::from_num(self) / U16F16::from_num(255u16);
90        (frac * U16F16::from_num(duration_ms))
91            .ceil()
92            .to_num::<u32>()
93    }
94
95    fn lerp_u16(self, a: u16, b: u16) -> u16 {
96        let a_fix = I32F32::from_num(a);
97        let b_fix = I32F32::from_num(b);
98        let t = I32F32::from_num(self) / I32F32::from_num(255);
99        let result = a_fix + t * (b_fix - a_fix);
100        result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
101    }
102
103    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self {
104        if a == b {
105            return 255;
106        }
107        let delta = I32F32::from_num(b) - I32F32::from_num(a);
108        let numer = I32F32::from_num(target) - I32F32::from_num(a);
109        let frac = numer / delta;
110        let w = (frac * I32F32::from_num(255)).ceil();
111        w.to_num::<i32>().clamp(0, 255) as u8
112    }
113}
114
115// ---------------------------------------------------------------------------
116// UnitValue implementation for u16
117// ---------------------------------------------------------------------------
118
119impl UnitValue for u16 {
120    fn zero() -> Self {
121        0
122    }
123
124    fn one() -> Self {
125        65535
126    }
127
128    fn to_index(self) -> usize {
129        self as usize
130    }
131
132    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self {
133        if duration_ms == 0 || elapsed_ms >= duration_ms {
134            return 65535;
135        }
136        if elapsed_ms == 0 {
137            return 0;
138        }
139        let frac = I32F32::from_num(elapsed_ms) / I32F32::from_num(duration_ms);
140        let u = frac * I32F32::from_num(65535u32);
141        u.to_num::<u32>().min(65535) as u16
142    }
143
144    fn to_time_offset(self, duration_ms: u32) -> u32 {
145        if duration_ms == 0 {
146            return 0;
147        }
148        let frac = I32F32::from_num(self) / I32F32::from_num(65535u32);
149        (frac * I32F32::from_num(duration_ms))
150            .ceil()
151            .to_num::<u32>()
152    }
153
154    fn lerp_u16(self, a: u16, b: u16) -> u16 {
155        let a_fix = I32F32::from_num(a);
156        let b_fix = I32F32::from_num(b);
157        let t = I32F32::from_num(self) / I32F32::from_num(65535);
158        let result = a_fix + t * (b_fix - a_fix);
159        result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
160    }
161
162    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self {
163        if a == b {
164            return 65535;
165        }
166        let delta = I32F32::from_num(b) - I32F32::from_num(a);
167        let numer = I32F32::from_num(target) - I32F32::from_num(a);
168        let frac = numer / delta;
169        let w = (frac * I32F32::from_num(65535)).ceil();
170        w.to_num::<i64>().clamp(0, 65535) as u16
171    }
172}
173
174// ---------------------------------------------------------------------------
175// Interpolation helpers
176// ---------------------------------------------------------------------------
177
178/// Convert a `u8` weight (`0..=255`) to a fixed-point fraction in `[0, 1]`.
179fn weight_frac(w: u8) -> I16F16 {
180    I16F16::from_num(w) / I16F16::from_num(255)
181}
182
183/// Linearly interpolate between `a` and `b` with a `u8` blend weight.
184///
185/// `w = 0` returns `a`, `w = 255` returns `b`.  Intermediate values are
186/// computed with fixed-point arithmetic and clamped to `0..=255`.
187pub fn lerp_u8(a: u8, b: u8, w: u8) -> u8 {
188    let a_fix = I16F16::from_num(a);
189    let b_fix = I16F16::from_num(b);
190    let t = weight_frac(w);
191    let result = a_fix + t * (b_fix - a_fix);
192    result.to_num::<i32>().clamp(0, 255) as u8
193}
194
195/// Linearly interpolate between two `u16` values with a `u8` blend weight.
196///
197/// `w = 0` returns `a`, `w = 255` returns `b`.  The result is clamped to
198/// `0..=u16::MAX`.
199pub fn lerp_u16(a: u16, b: u16, w: u8) -> u16 {
200    let a_fix = I32F32::from_num(a);
201    let b_fix = I32F32::from_num(b);
202    let t = I32F32::from_num(w) / I32F32::from_num(255);
203    let result = a_fix + t * (b_fix - a_fix);
204    result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
205}
206
207/// Scale a normalized `u8` value (`0..=255`) into a `u16` range `0..=max`.
208///
209/// `w = 0` returns `0`, `w = 255` returns `max`.
210pub fn map_u8_to_u16(w: u8, max: u16) -> u16 {
211    let t = U16F16::from_num(w) / U16F16::from_num(255);
212    let result = t * U16F16::from_num(max);
213    result.to_num::<u32>().min(u16::MAX as u32) as u16
214}
215
216// ---------------------------------------------------------------------------
217// Quantization helpers
218// ---------------------------------------------------------------------------
219
220/// Snap `value` to the nearest multiple of `step` according to `rounding`.
221///
222/// # Panics
223///
224/// Panics if `step` is `0` (division by zero).
225pub fn quantize(value: u16, step: u16, rounding: Rounding) -> u16 {
226    let v = u32::from(value);
227    let s = u32::from(step);
228    let result = match rounding {
229        Rounding::Floor => (v / s) * s,
230        Rounding::Ceil => v.div_ceil(s) * s,
231        Rounding::Nearest => ((v + s / 2) / s) * s,
232    };
233    result.min(u32::from(u16::MAX)) as u16
234}
235
236/// Return the next quantized target value one `step` closer to `end`.
237///
238/// If `increasing` is `true` the value advances upward; otherwise downward.
239/// The result is clamped so it never overshoots `end`.
240pub fn next_target_value(current: u16, end: u16, step: u16, increasing: bool) -> u16 {
241    if increasing {
242        let next = current.saturating_add(step);
243        next.min(end)
244    } else {
245        let next = current.saturating_sub(step);
246        next.max(end)
247    }
248}