Skip to main content

gpui_kit/motion/
interpolate.rs

1//! Values that can be sampled part way between two states.
2
3use gpui::{Hsla, Pixels, Point, Rems, Size, px, rems};
4
5/// A value an animation can move through.
6///
7/// `t` outside 0..1 is meaningful: overshoot curves and underdamped springs
8/// deliberately pass their target, so implementations extrapolate rather than
9/// clamp.
10pub trait Interpolate: Copy {
11    fn lerp(self, other: Self, t: f32) -> Self;
12
13    /// How far apart two values are, in one number, so a retarget can rescale
14    /// a velocity it is carrying into the new distance.
15    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    /// Interpolates hue the short way around the wheel, so a red-to-magenta
50    /// transition does not sweep through the entire spectrum.
51    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    /// Measured over the same short way around the wheel that `lerp` travels,
61    /// so the distance is the distance actually covered.
62    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
71/// The signed hue step from `from` to `to` the short way around the wheel.
72fn 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        // The short path wraps past 1.0 rather than sweeping back through green.
136        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}