1use geometry_core::{BorderRadius, Color, Point, Rect, Transform};
2
3pub trait Lerp: Clone {
12 fn lerp(&self, other: &Self, t: f32) -> Self;
14
15 fn add(&self, other: &Self) -> Self;
17
18 fn sub(&self, other: &Self) -> Self;
20
21 fn scale(&self, factor: f32) -> Self;
23
24 fn zero() -> Self;
26
27 fn magnitude_sq(&self) -> f32;
29}
30
31impl Lerp for f32 {
32 fn lerp(&self, other: &Self, t: f32) -> Self {
33 self + (other - self) * t
34 }
35 fn add(&self, other: &Self) -> Self {
36 self + other
37 }
38 fn sub(&self, other: &Self) -> Self {
39 self - other
40 }
41 fn scale(&self, factor: f32) -> Self {
42 self * factor
43 }
44 fn zero() -> Self {
45 0.0
46 }
47 fn magnitude_sq(&self) -> f32 {
48 self * self
49 }
50}
51
52impl Lerp for Point {
53 fn lerp(&self, other: &Self, t: f32) -> Self {
54 Point::new(self.x.lerp(&other.x, t), self.y.lerp(&other.y, t))
55 }
56 fn add(&self, other: &Self) -> Self {
57 Point::new(self.x + other.x, self.y + other.y)
58 }
59 fn sub(&self, other: &Self) -> Self {
60 Point::new(self.x - other.x, self.y - other.y)
61 }
62 fn scale(&self, factor: f32) -> Self {
63 Point::new(self.x * factor, self.y * factor)
64 }
65 fn zero() -> Self {
66 Point::new(0.0, 0.0)
67 }
68 fn magnitude_sq(&self) -> f32 {
69 self.x * self.x + self.y * self.y
70 }
71}
72
73impl Lerp for Rect {
74 fn lerp(&self, other: &Self, t: f32) -> Self {
75 Rect::new(
76 self.x.lerp(&other.x, t),
77 self.y.lerp(&other.y, t),
78 self.width.lerp(&other.width, t),
79 self.height.lerp(&other.height, t),
80 )
81 }
82 fn add(&self, other: &Self) -> Self {
83 Rect::new(
84 self.x + other.x,
85 self.y + other.y,
86 self.width + other.width,
87 self.height + other.height,
88 )
89 }
90 fn sub(&self, other: &Self) -> Self {
91 Rect::new(
92 self.x - other.x,
93 self.y - other.y,
94 self.width - other.width,
95 self.height - other.height,
96 )
97 }
98 fn scale(&self, factor: f32) -> Self {
99 Rect::new(
100 self.x * factor,
101 self.y * factor,
102 self.width * factor,
103 self.height * factor,
104 )
105 }
106 fn zero() -> Self {
107 Rect::new(0.0, 0.0, 0.0, 0.0)
108 }
109 fn magnitude_sq(&self) -> f32 {
110 self.x * self.x + self.y * self.y + self.width * self.width + self.height * self.height
111 }
112}
113
114impl Lerp for Transform {
115 fn lerp(&self, other: &Self, t: f32) -> Self {
116 Transform {
117 a: self.a.lerp(&other.a, t),
118 b: self.b.lerp(&other.b, t),
119 c: self.c.lerp(&other.c, t),
120 d: self.d.lerp(&other.d, t),
121 e: self.e.lerp(&other.e, t),
122 f: self.f.lerp(&other.f, t),
123 }
124 }
125 fn add(&self, other: &Self) -> Self {
126 Transform {
127 a: self.a + other.a,
128 b: self.b + other.b,
129 c: self.c + other.c,
130 d: self.d + other.d,
131 e: self.e + other.e,
132 f: self.f + other.f,
133 }
134 }
135 fn sub(&self, other: &Self) -> Self {
136 Transform {
137 a: self.a - other.a,
138 b: self.b - other.b,
139 c: self.c - other.c,
140 d: self.d - other.d,
141 e: self.e - other.e,
142 f: self.f - other.f,
143 }
144 }
145 fn scale(&self, factor: f32) -> Self {
146 Transform {
147 a: self.a * factor,
148 b: self.b * factor,
149 c: self.c * factor,
150 d: self.d * factor,
151 e: self.e * factor,
152 f: self.f * factor,
153 }
154 }
155 fn zero() -> Self {
156 Transform {
157 a: 0.0,
158 b: 0.0,
159 c: 0.0,
160 d: 0.0,
161 e: 0.0,
162 f: 0.0,
163 }
164 }
165 fn magnitude_sq(&self) -> f32 {
166 self.a * self.a
167 + self.b * self.b
168 + self.c * self.c
169 + self.d * self.d
170 + self.e * self.e
171 + self.f * self.f
172 }
173}
174
175impl Lerp for BorderRadius {
176 fn lerp(&self, other: &Self, t: f32) -> Self {
177 BorderRadius {
178 top_left: self.top_left.lerp(&other.top_left, t),
179 top_right: self.top_right.lerp(&other.top_right, t),
180 bottom_right: self.bottom_right.lerp(&other.bottom_right, t),
181 bottom_left: self.bottom_left.lerp(&other.bottom_left, t),
182 }
183 }
184 fn add(&self, other: &Self) -> Self {
185 BorderRadius {
186 top_left: self.top_left + other.top_left,
187 top_right: self.top_right + other.top_right,
188 bottom_right: self.bottom_right + other.bottom_right,
189 bottom_left: self.bottom_left + other.bottom_left,
190 }
191 }
192 fn sub(&self, other: &Self) -> Self {
193 BorderRadius {
194 top_left: self.top_left - other.top_left,
195 top_right: self.top_right - other.top_right,
196 bottom_right: self.bottom_right - other.bottom_right,
197 bottom_left: self.bottom_left - other.bottom_left,
198 }
199 }
200 fn scale(&self, factor: f32) -> Self {
201 BorderRadius {
202 top_left: self.top_left * factor,
203 top_right: self.top_right * factor,
204 bottom_right: self.bottom_right * factor,
205 bottom_left: self.bottom_left * factor,
206 }
207 }
208 fn zero() -> Self {
209 BorderRadius::all(0.0)
210 }
211 fn magnitude_sq(&self) -> f32 {
212 self.top_left * self.top_left
213 + self.top_right * self.top_right
214 + self.bottom_right * self.bottom_right
215 + self.bottom_left * self.bottom_left
216 }
217}
218
219const ACHROMATIC_EPS: f32 = 1e-4;
221
222fn lerp_hue(h1: f32, c1: f32, h2: f32, c2: f32, t: f32) -> f32 {
224 let gray1 = c1 < ACHROMATIC_EPS;
225 let gray2 = c2 < ACHROMATIC_EPS;
226 if gray1 && gray2 {
227 return h1;
228 }
229 if gray1 {
230 return h2;
231 }
232 if gray2 {
233 return h1;
234 }
235 let mut delta = (h2 - h1).rem_euclid(360.0);
236 if delta > 180.0 {
237 delta -= 360.0;
238 }
239 (h1 + delta * t).rem_euclid(360.0)
240}
241
242impl Lerp for Color {
243 fn lerp(&self, other: &Self, t: f32) -> Self {
244 if t <= 0.0 {
246 return *self;
247 }
248 if t >= 1.0 {
249 return *other;
250 }
251 let (l1, c1, h1, a1) = self.to_oklcha();
252 let (l2, c2, h2, a2) = other.to_oklcha();
253 let l = l1.lerp(&l2, t);
254 let c = c1.lerp(&c2, t);
255 let a = a1.lerp(&a2, t);
256 let h = lerp_hue(h1, c1, h2, c2, t);
257 Color::from_oklcha(l, c, h, a)
258 }
259 fn add(&self, other: &Self) -> Self {
261 Color::rgba(
262 self.r + other.r,
263 self.g + other.g,
264 self.b + other.b,
265 self.a + other.a,
266 )
267 }
268 fn sub(&self, other: &Self) -> Self {
269 Color::rgba(
270 self.r - other.r,
271 self.g - other.g,
272 self.b - other.b,
273 self.a - other.a,
274 )
275 }
276 fn scale(&self, factor: f32) -> Self {
277 Color::rgba(
278 self.r * factor,
279 self.g * factor,
280 self.b * factor,
281 self.a * factor,
282 )
283 }
284 fn zero() -> Self {
285 Color::rgba(0.0, 0.0, 0.0, 0.0)
286 }
287 fn magnitude_sq(&self) -> f32 {
288 self.r * self.r + self.g * self.g + self.b * self.b + self.a * self.a
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn f32_lerp_midpoint() {
298 assert!((2.0f32.lerp(&4.0, 0.5) - 3.0).abs() < 1e-6);
299 }
300
301 #[test]
302 fn point_lerp_is_component_wise() {
303 let mid = Point::new(0.0, 10.0).lerp(&Point::new(4.0, 20.0), 0.5);
304 assert_eq!(mid, Point::new(2.0, 15.0));
305 }
306
307 #[test]
308 fn rect_lerp_is_component_wise() {
309 let mid = Rect::new(0.0, 0.0, 10.0, 20.0).lerp(&Rect::new(2.0, 4.0, 30.0, 40.0), 0.5);
310 assert_eq!(mid, Rect::new(1.0, 2.0, 20.0, 30.0));
311 }
312
313 #[test]
314 fn border_radius_lerp_is_component_wise() {
315 let mid = BorderRadius::all(0.0).lerp(&BorderRadius::all(8.0), 0.25);
316 assert_eq!(mid, BorderRadius::all(2.0));
317 }
318
319 #[test]
320 fn color_lerp_endpoints_are_exact() {
321 let red = Color::RED;
322 let green = Color::GREEN;
323 assert_eq!(red.lerp(&green, 0.0), red);
324 assert_eq!(red.lerp(&green, 1.0), green);
325 }
326
327 #[test]
328 fn color_lerp_gray_to_gray_stays_neutral() {
329 let mid = Color::rgb(0.2, 0.2, 0.2).lerp(&Color::rgb(0.8, 0.8, 0.8), 0.5);
330 assert!((mid.r - mid.g).abs() < 2e-3, "{mid:?}");
331 assert!((mid.g - mid.b).abs() < 2e-3, "{mid:?}");
332 }
333
334 #[test]
335 fn color_lerp_hue_takes_short_arc() {
336 let mid = Color::RED.lerp(&Color::rgb(1.0, 1.0, 0.0), 0.5);
338 let (_, chroma, hue, _) = mid.to_oklcha();
339 assert!(
340 chroma > ACHROMATIC_EPS,
341 "midpoint unexpectedly gray: {mid:?}"
342 );
343 assert!(
344 (29.0..=110.0).contains(&hue),
345 "hue {hue} left the short arc"
346 );
347 }
348
349 #[test]
350 fn color_lerp_achromatic_endpoint_carries_the_other_hue() {
351 let mid = Color::rgb(0.5, 0.5, 0.5).lerp(&Color::RED, 0.5);
353 let (_, chroma, hue, _) = mid.to_oklcha();
354 let (_, _, red_hue, _) = Color::RED.to_oklcha();
355 assert!(chroma > ACHROMATIC_EPS);
356 assert!(
357 (hue - red_hue).abs() < 1.0,
358 "hue {hue} != red hue {red_hue}"
359 );
360 }
361}