Skip to main content

ply_engine/
lerp.rs

1use crate::color::Color;
2use crate::math::Vector2;
3
4/// Linear interpolation trait.
5pub trait Lerp {
6    fn lerp(self, other: Self, t: f32) -> Self;
7}
8
9#[inline]
10fn clamp01(t: f32) -> f32 {
11    t.clamp(0.0, 1.0)
12}
13
14impl Lerp for f32 {
15    #[inline]
16    fn lerp(self, other: Self, t: f32) -> Self {
17        let t = clamp01(t);
18        if self == other {
19            return self;
20        }
21        self + (other - self) * t
22    }
23}
24
25impl Lerp for u16 {
26    #[inline]
27    fn lerp(self, other: Self, t: f32) -> Self {
28        let t = clamp01(t);
29        if self == other {
30            return self;
31        }
32        let v = (self as f32)
33            .lerp(other as f32, t)
34            .round()
35            .clamp(u16::MIN as f32, u16::MAX as f32);
36        v as u16
37    }
38}
39
40impl Lerp for Vector2 {
41    #[inline]
42    fn lerp(self, other: Self, t: f32) -> Self {
43        let t = clamp01(t);
44        if self == other {
45            return self;
46        }
47        Self {
48            x: self.x.lerp(other.x, t),
49            y: self.y.lerp(other.y, t),
50        }
51    }
52}
53
54impl Lerp for macroquad::prelude::Vec2 {
55    #[inline]
56    fn lerp(self, other: Self, t: f32) -> Self {
57        let t = clamp01(t);
58        if self == other {
59            return self;
60        }
61        Self::new(
62            self.x.lerp(other.x, t),
63            self.y.lerp(other.y, t),
64        )
65    }
66}
67
68impl Lerp for (f32, f32, f32, f32) {
69    #[inline]
70    fn lerp(self, other: Self, t: f32) -> Self {
71        let t = clamp01(t);
72        if self == other {
73            return self;
74        }
75        (
76            self.0.lerp(other.0, t),
77            self.1.lerp(other.1, t),
78            self.2.lerp(other.2, t),
79            self.3.lerp(other.3, t),
80        )
81    }
82}
83
84impl Lerp for (u16, u16, u16, u16) {
85    #[inline]
86    fn lerp(self, other: Self, t: f32) -> Self {
87        let t = clamp01(t);
88        if self == other {
89            return self;
90        }
91        (
92            self.0.lerp(other.0, t),
93            self.1.lerp(other.1, t),
94            self.2.lerp(other.2, t),
95            self.3.lerp(other.3, t),
96        )
97    }
98}
99
100impl Lerp for Color {
101    #[inline]
102    fn lerp(self, other: Self, t: f32) -> Self {
103        let t = clamp01(t);
104        if self == other {
105            return self;
106        }
107        Color {
108            r: self.r.lerp(other.r, t),
109            g: self.g.lerp(other.g, t),
110            b: self.b.lerp(other.b, t),
111            a: self.a.lerp(other.a, t),
112        }
113    }
114}
115
116impl Color {
117    /// Interpolate in sRGB transfer space (gamma-aware).
118    #[inline]
119    pub fn lerp_srgb(self, other: Self, t: f32) -> Self {
120        if self == other {
121            return self;
122        }
123
124        let t = clamp01(t);
125        if t == 0.0 {
126            return self;
127        }
128        if t == 1.0 {
129            return other;
130        }
131        let a = self.a.lerp(other.a, t);
132
133        let r0 = srgb_to_linear(channel_to_unit(self.r));
134        let g0 = srgb_to_linear(channel_to_unit(self.g));
135        let b0 = srgb_to_linear(channel_to_unit(self.b));
136
137        let r1 = srgb_to_linear(channel_to_unit(other.r));
138        let g1 = srgb_to_linear(channel_to_unit(other.g));
139        let b1 = srgb_to_linear(channel_to_unit(other.b));
140
141        let r = linear_to_srgb(r0.lerp(r1, t));
142        let g = linear_to_srgb(g0.lerp(g1, t));
143        let b = linear_to_srgb(b0.lerp(b1, t));
144
145        Color::rgba(unit_to_channel(r), unit_to_channel(g), unit_to_channel(b), a)
146    }
147
148    /// Interpolate in Oklab color space (perceptually uniform).
149    #[inline]
150    pub fn lerp_oklab(self, other: Self, t: f32) -> Self {
151        if self == other {
152            return self;
153        }
154
155        let t = clamp01(t);
156        if t == 0.0 {
157            return self;
158        }
159        if t == 1.0 {
160            return other;
161        }
162        let a = self.a.lerp(other.a, t);
163
164        let c0 = rgb_to_oklab(self);
165        let c1 = rgb_to_oklab(other);
166
167        let l = c0.0.lerp(c1.0, t);
168        let a_lab = c0.1.lerp(c1.1, t);
169        let b_lab = c0.2.lerp(c1.2, t);
170
171        let (r, g, b) = oklab_to_rgb(l, a_lab, b_lab);
172        Color::rgba(unit_to_channel(r), unit_to_channel(g), unit_to_channel(b), a)
173    }
174}
175
176#[inline]
177fn channel_to_unit(v: f32) -> f32 {
178    (v / 255.0).clamp(0.0, 1.0)
179}
180
181#[inline]
182fn unit_to_channel(v: f32) -> f32 {
183    (v.clamp(0.0, 1.0) * 255.0).clamp(0.0, 255.0)
184}
185
186#[inline]
187fn srgb_to_linear(v: f32) -> f32 {
188    if v <= 0.04045 {
189        v / 12.92
190    } else {
191        ((v + 0.055) / 1.055).powf(2.4)
192    }
193}
194
195#[inline]
196fn linear_to_srgb(v: f32) -> f32 {
197    if v <= 0.003_130_8 {
198        12.92 * v
199    } else {
200        1.055 * v.powf(1.0 / 2.4) - 0.055
201    }
202}
203
204#[inline]
205fn rgb_to_oklab(color: Color) -> (f32, f32, f32) {
206    let r = srgb_to_linear(channel_to_unit(color.r));
207    let g = srgb_to_linear(channel_to_unit(color.g));
208    let b = srgb_to_linear(channel_to_unit(color.b));
209
210    let l = 0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b;
211    let m = 0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b;
212    let s = 0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b;
213
214    let l_ = l.cbrt();
215    let m_ = m.cbrt();
216    let s_ = s.cbrt();
217
218    (
219        0.210_454_26 * l_ + 0.793_617_8 * m_ - 0.004_072_047 * s_,
220        1.977_998_5 * l_ - 2.428_592_2 * m_ + 0.450_593_7 * s_,
221        0.025_904_037 * l_ + 0.782_771_77 * m_ - 0.808_675_77 * s_,
222    )
223}
224
225#[inline]
226fn oklab_to_rgb(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
227    let l_ = l + 0.396_337_78 * a + 0.215_803_76 * b;
228    let m_ = l - 0.105_561_346 * a - 0.063_854_17 * b;
229    let s_ = l - 0.089_484_18 * a - 1.291_485_5 * b;
230
231    let l3 = l_ * l_ * l_;
232    let m3 = m_ * m_ * m_;
233    let s3 = s_ * s_ * s_;
234
235    let r = 4.076_741_7 * l3 - 3.307_711_6 * m3 + 0.230_969_94 * s3;
236    let g = -1.268_438 * l3 + 2.609_757_4 * m3 - 0.341_319_38 * s3;
237    let b = -0.004_196_086_3 * l3 - 0.703_418_6 * m3 + 1.707_614_7 * s3;
238
239    (
240        linear_to_srgb(r).clamp(0.0, 1.0),
241        linear_to_srgb(g).clamp(0.0, 1.0),
242        linear_to_srgb(b).clamp(0.0, 1.0),
243    )
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn assert_close(a: f32, b: f32) {
251        assert!(
252            (a - b).abs() <= 0.001,
253            "expected {} ~= {} (delta {})",
254            a,
255            b,
256            (a - b).abs()
257        );
258    }
259
260    fn generic_lerp<T: Lerp>(a: T, b: T, t: f32) -> T {
261        a.lerp(b, t)
262    }
263
264    #[test]
265    fn test_f32_lerp_clamps_and_identity() {
266        assert_eq!(generic_lerp(10.0_f32, 20.0_f32, -2.0), 10.0);
267        assert_eq!(generic_lerp(10.0_f32, 20.0_f32, 2.0), 20.0);
268        assert_eq!(generic_lerp(7.5_f32, 7.5_f32, 0.25), 7.5);
269        assert_close(generic_lerp(10.0_f32, 20.0_f32, 0.25), 12.5);
270    }
271
272    #[test]
273    fn test_u16_lerp_clamps_rounds_and_identity() {
274        assert_eq!(10_u16.lerp(20, -1.0), 10);
275        assert_eq!(10_u16.lerp(20, 2.0), 20);
276        assert_eq!(10_u16.lerp(20, 0.49), 15);
277        assert_eq!(42_u16.lerp(42, 0.66), 42);
278    }
279
280    #[test]
281    fn test_vector2_and_vec2_lerp() {
282        let a = Vector2::new(0.0, 10.0);
283        let b = Vector2::new(20.0, 30.0);
284        let v = a.lerp(b, 0.5);
285        assert_close(v.x, 10.0);
286        assert_close(v.y, 20.0);
287
288        let mq_a = macroquad::prelude::Vec2::new(0.0, 10.0);
289        let mq_b = macroquad::prelude::Vec2::new(20.0, 30.0);
290        let mq_v = mq_a.lerp(mq_b, 0.5);
291        assert_close(mq_v.x, 10.0);
292        assert_close(mq_v.y, 20.0);
293    }
294
295    #[test]
296    fn test_tuple_lerp() {
297        let tf = (0.0_f32, 1.0_f32, 2.0_f32, 3.0_f32).lerp((4.0, 5.0, 6.0, 7.0), 0.5);
298        assert_close(tf.0, 2.0);
299        assert_close(tf.1, 3.0);
300        assert_close(tf.2, 4.0);
301        assert_close(tf.3, 5.0);
302
303        let tu = (0_u16, 10_u16, 20_u16, 30_u16).lerp((10, 20, 30, 40), 0.5);
304        assert_eq!(tu, (5, 15, 25, 35));
305    }
306
307    #[test]
308    fn test_color_lerp_linear_and_alpha() {
309        let a = Color::rgba(10.0, 20.0, 30.0, 40.0);
310        let b = Color::rgba(110.0, 220.0, 130.0, 240.0);
311        let mid = a.lerp(b, 0.5);
312
313        assert_close(mid.r, 60.0);
314        assert_close(mid.g, 120.0);
315        assert_close(mid.b, 80.0);
316        assert_close(mid.a, 140.0);
317
318        assert_eq!(a.lerp(b, -1.0), a);
319        assert_eq!(a.lerp(b, 2.0), b);
320    }
321
322    #[test]
323    fn test_color_lerp_srgb_clamps_and_is_brighter_midpoint_than_linear() {
324        let black = Color::rgba(0.0, 0.0, 0.0, 10.0);
325        let white = Color::rgba(255.0, 255.0, 255.0, 210.0);
326
327        let linear_mid = black.lerp(white, 0.5);
328        let srgb_mid = black.lerp_srgb(white, 0.5);
329
330        assert!(srgb_mid.r > linear_mid.r, "sRGB midpoint should be perceptually brighter");
331        assert_close(srgb_mid.a, 110.0);
332
333        assert_eq!(black.lerp_srgb(white, -0.5), black);
334        assert_eq!(black.lerp_srgb(white, 1.5), white);
335        assert_eq!(black.lerp_srgb(black, 0.42), black);
336    }
337
338    #[test]
339    fn test_color_lerp_oklab_clamps_alpha_and_identity() {
340        let a = Color::rgba(255.0, 0.0, 0.0, 10.0);
341        let b = Color::rgba(0.0, 0.0, 255.0, 250.0);
342        let mid = a.lerp_oklab(b, 0.5);
343
344        assert_close(mid.a, 130.0);
345        assert!(mid.r >= 0.0 && mid.r <= 255.0);
346        assert!(mid.g >= 0.0 && mid.g <= 255.0);
347        assert!(mid.b >= 0.0 && mid.b <= 255.0);
348
349        assert_eq!(a.lerp_oklab(b, -0.5), a);
350        assert_eq!(a.lerp_oklab(b, 1.5), b);
351        assert_eq!(a.lerp_oklab(a, 0.42), a);
352    }
353}