1use gpui::{Hsla, Pixels, Point, Rems, Size, px, rems};
4
5pub trait Interpolate: Copy {
11 fn lerp(self, other: Self, t: f32) -> Self;
12
13 fn distance(self, other: Self) -> f32;
16}
17
18impl Interpolate for f32 {
19 fn lerp(self, other: Self, t: f32) -> Self {
20 self + (other - self) * t
21 }
22
23 fn distance(self, other: Self) -> f32 {
24 (other - self).abs()
25 }
26}
27
28impl Interpolate for Pixels {
29 fn lerp(self, other: Self, t: f32) -> Self {
30 px(f32::from(self).lerp(f32::from(other), t))
31 }
32
33 fn distance(self, other: Self) -> f32 {
34 f32::from(self).distance(f32::from(other))
35 }
36}
37
38impl Interpolate for Rems {
39 fn lerp(self, other: Self, t: f32) -> Self {
40 rems(self.0.lerp(other.0, t))
41 }
42
43 fn distance(self, other: Self) -> f32 {
44 self.0.distance(other.0)
45 }
46}
47
48impl Interpolate for Hsla {
49 fn lerp(self, other: Self, t: f32) -> Self {
52 Hsla {
53 h: (self.h + hue_delta(self.h, other.h) * t).rem_euclid(1.0),
54 s: self.s.lerp(other.s, t).clamp(0.0, 1.0),
55 l: self.l.lerp(other.l, t).clamp(0.0, 1.0),
56 a: self.a.lerp(other.a, t).clamp(0.0, 1.0),
57 }
58 }
59
60 fn distance(self, other: Self) -> f32 {
63 let hue = hue_delta(self.h, other.h);
64 let saturation = other.s - self.s;
65 let lightness = other.l - self.l;
66 let alpha = other.a - self.a;
67 (hue * hue + saturation * saturation + lightness * lightness + alpha * alpha).sqrt()
68 }
69}
70
71fn hue_delta(from: f32, to: f32) -> f32 {
73 let delta = to - from;
74 if delta > 0.5 {
75 delta - 1.0
76 } else if delta < -0.5 {
77 delta + 1.0
78 } else {
79 delta
80 }
81}
82
83impl<T: Interpolate + Clone + std::fmt::Debug + Default + PartialEq> Interpolate for Point<T> {
84 fn lerp(self, other: Self, t: f32) -> Self {
85 Point {
86 x: self.x.lerp(other.x, t),
87 y: self.y.lerp(other.y, t),
88 }
89 }
90
91 fn distance(self, other: Self) -> f32 {
92 let x = self.x.distance(other.x);
93 let y = self.y.distance(other.y);
94 (x * x + y * y).sqrt()
95 }
96}
97
98impl<T: Interpolate + Clone + std::fmt::Debug + Default + PartialEq> Interpolate for Size<T> {
99 fn lerp(self, other: Self, t: f32) -> Self {
100 Size {
101 width: self.width.lerp(other.width, t),
102 height: self.height.lerp(other.height, t),
103 }
104 }
105
106 fn distance(self, other: Self) -> f32 {
107 let width = self.width.distance(other.width);
108 let height = self.height.distance(other.height);
109 (width * width + height * height).sqrt()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use gpui::{hsla, point, size};
117
118 #[test]
119 fn endpoints_are_exact() {
120 assert_eq!(2.0f32.lerp(10.0, 0.0), 2.0);
121 assert_eq!(2.0f32.lerp(10.0, 1.0), 10.0);
122 assert_eq!(px(0.0).lerp(px(8.0), 0.5), px(4.0));
123 }
124
125 #[test]
126 fn overshoot_extrapolates_instead_of_clamping() {
127 assert_eq!(0.0f32.lerp(10.0, 1.2), 12.0);
128 }
129
130 #[test]
131 fn hue_takes_the_short_way_around_the_wheel() {
132 let magenta = hsla(0.9, 1.0, 0.5, 1.0);
133 let red = hsla(0.05, 1.0, 0.5, 1.0);
134 let middle = magenta.lerp(red, 0.5);
135 assert!(
137 middle.h > 0.9 || middle.h < 0.05,
138 "hue took the long way: {middle:?}"
139 );
140 }
141
142 #[test]
143 fn color_channels_stay_in_range_under_overshoot() {
144 let from = hsla(0.0, 0.2, 0.2, 0.4);
145 let to = hsla(0.1, 0.9, 0.9, 1.0);
146 let past = from.lerp(to, 1.4);
147 assert!((0.0..=1.0).contains(&past.s));
148 assert!((0.0..=1.0).contains(&past.l));
149 assert!((0.0..=1.0).contains(&past.a));
150 }
151
152 #[test]
153 fn distance_is_how_far_a_value_has_to_travel() {
154 assert_eq!(2.0f32.distance(10.0), 8.0);
155 assert_eq!(10.0f32.distance(2.0), 8.0);
156 assert_eq!(px(1.0).distance(px(4.0)), 3.0);
157 assert_eq!(rems(1.0).distance(rems(2.5)), 1.5);
158 assert_eq!(
159 point(px(0.0), px(0.0)).distance(point(px(3.0), px(4.0))),
160 5.0
161 );
162 assert_eq!(
163 size(px(0.0), px(0.0)).distance(size(px(6.0), px(8.0))),
164 10.0
165 );
166 }
167
168 #[test]
169 fn hue_distance_takes_the_short_way_around_the_wheel() {
170 let magenta = hsla(0.9, 0.5, 0.5, 1.0);
171 let red = hsla(0.05, 0.5, 0.5, 1.0);
172 assert!(
173 (magenta.distance(red) - 0.15).abs() < 1e-5,
174 "hue took the long way: {}",
175 magenta.distance(red)
176 );
177 assert_eq!(magenta.distance(red), red.distance(magenta));
178 }
179
180 #[test]
181 fn compound_values_interpolate_component_wise() {
182 let moved = point(px(0.0), px(10.0)).lerp(point(px(10.0), px(0.0)), 0.5);
183 assert_eq!(moved, point(px(5.0), px(5.0)));
184 let grown = size(px(0.0), px(0.0)).lerp(size(px(4.0), px(8.0)), 0.5);
185 assert_eq!(grown, size(px(2.0), px(4.0)));
186 }
187}