Skip to content

Commit ccdd991

Browse files
author
Ericky Dos Santos
committed
feat: improve lerp utils for shadow/borders/vector
1 parent 3c79460 commit ccdd991

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

core/src/border.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,30 @@ impl Border {
112112
pub fn widths(&self) -> [f32; 4] {
113113
self.sides.unwrap_or([self.width; 4])
114114
}
115+
116+
/// Linearly interpolates between two [`Border`]s by the given amount.
117+
///
118+
/// A uniform border and a per-side one blend through their resolved
119+
/// [`widths`](Self::widths), so the result is per-side for as long as either
120+
/// end is — stepping straight to `other`'s sides instead would pop the border
121+
/// on and off mid-transition. Two uniform borders stay uniform.
122+
pub fn lerp(self, other: Self, amount: f32) -> Self {
123+
let lerp = |from: f32, to: f32| from + (to - from) * amount;
124+
125+
let sides = if self.sides.is_none() && other.sides.is_none() {
126+
None
127+
} else {
128+
let (from, to) = (self.widths(), other.widths());
129+
Some(std::array::from_fn(|i| lerp(from[i], to[i])))
130+
};
131+
132+
Self {
133+
color: self.color.lerp(other.color, amount),
134+
width: lerp(self.width, other.width),
135+
radius: self.radius.lerp(other.radius, amount),
136+
sides,
137+
}
138+
}
115139
}
116140

117141
/// The border radii for the corners of a graphics primitive in the order:
@@ -261,6 +285,22 @@ impl Radius {
261285
..self
262286
}
263287
}
288+
289+
/// Linearly interpolates between `self` and `other` by the given amount,
290+
/// corner by corner.
291+
///
292+
/// At `amount = 0.0` returns `self`, at `amount = 1.0` returns `other`.
293+
pub const fn lerp(self, other: Self, amount: f32) -> Self {
294+
Self {
295+
top_left: self.top_left + (other.top_left - self.top_left) * amount,
296+
top_right: self.top_right
297+
+ (other.top_right - self.top_right) * amount,
298+
bottom_right: self.bottom_right
299+
+ (other.bottom_right - self.bottom_right) * amount,
300+
bottom_left: self.bottom_left
301+
+ (other.bottom_left - self.bottom_left) * amount,
302+
}
303+
}
264304
}
265305

266306
impl From<f32> for Radius {

core/src/shadow.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,109 @@ impl Shadow {
5858
self.inset = inset;
5959
self
6060
}
61+
62+
/// Returns the [`Shadow`] with a fully transparent color.
63+
///
64+
/// The geometry is kept, so this is the "absent" end of a fade — a shadow
65+
/// appearing or disappearing rather than one moving.
66+
pub const fn transparent(self) -> Self {
67+
Self {
68+
color: Color {
69+
a: 0.0,
70+
..self.color
71+
},
72+
..self
73+
}
74+
}
75+
76+
/// Linearly interpolates between two [`Shadow`]s by the given amount.
77+
///
78+
/// `inset` has no representable in-between — an inset shadow and an outset one
79+
/// are drawn by different branches entirely — so it steps to `other`'s.
80+
pub const fn lerp(self, other: Self, amount: f32) -> Self {
81+
Self {
82+
color: self.color.lerp(other.color, amount),
83+
offset: self.offset.lerp(other.offset, amount),
84+
blur_radius: self.blur_radius
85+
+ (other.blur_radius - self.blur_radius) * amount,
86+
spread_radius: self.spread_radius
87+
+ (other.spread_radius - self.spread_radius) * amount,
88+
inset: other.inset,
89+
}
90+
}
91+
92+
/// Linearly interpolates between two stacks of [`Shadow`]s by the given
93+
/// amount, pairing them up layer by layer.
94+
///
95+
/// Elevation scales stack several layers, and the two ends of a transition
96+
/// need not use the same number of them. A layer present at only one end keeps
97+
/// that end's geometry and fades its alpha from (or to) zero, so a stack can
98+
/// grow and shrink across a transition.
99+
///
100+
/// The result always holds one layer per index rather than both stacks
101+
/// concatenated: overlapping translucent layers composite, so drawing both
102+
/// would darken the shadow mid-transition even when the endpoints match.
103+
pub fn lerp_stacks(from: &[Self], to: &[Self], amount: f32) -> Vec<Self> {
104+
(0..from.len().max(to.len()))
105+
.filter_map(|i| match (from.get(i), to.get(i)) {
106+
(Some(from), Some(to)) => Some(from.lerp(*to, amount)),
107+
(Some(from), None) => {
108+
Some(from.lerp(from.transparent(), amount))
109+
}
110+
(None, Some(to)) => Some(to.transparent().lerp(*to, amount)),
111+
// Unreachable: `i` is below the longer stack's length.
112+
(None, None) => None,
113+
})
114+
.collect()
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
122+
fn shadow(alpha: f32, blur: f32) -> Shadow {
123+
Shadow {
124+
color: Color::from_rgba(0.0, 0.0, 0.0, alpha),
125+
offset: Vector::new(0.0, 2.0),
126+
blur_radius: blur,
127+
spread_radius: -1.0,
128+
inset: false,
129+
}
130+
}
131+
132+
#[test]
133+
fn lerp_stacks_pairs_layers_instead_of_concatenating() {
134+
// Two overlapping translucent layers composite to more than either one,
135+
// so a stack transition that drew both ends would visibly darken halfway
136+
// through even when the endpoints are identical.
137+
let from = vec![shadow(0.1, 6.0), shadow(0.1, 15.0)];
138+
let to = vec![shadow(0.2, 6.0), shadow(0.2, 15.0)];
139+
140+
assert_eq!(Shadow::lerp_stacks(&from, &to, 0.5).len(), 2);
141+
assert_eq!(Shadow::lerp_stacks(&from, &to, 0.0), from);
142+
assert_eq!(Shadow::lerp_stacks(&from, &to, 1.0), to);
143+
}
144+
145+
#[test]
146+
fn lerp_stacks_fades_unpaired_layers_from_transparent() {
147+
// A stack that grows mid-transition (an extra ring layer appearing, say)
148+
// must fade the new layer in by alpha while it already holds its final
149+
// geometry — interpolating its geometry from a neighbour would slide it
150+
// into place from the wrong shape.
151+
let from = vec![shadow(0.1, 6.0)];
152+
let to = vec![shadow(0.1, 6.0), shadow(0.4, 20.0)];
153+
154+
let mid = Shadow::lerp_stacks(&from, &to, 0.5);
155+
assert_eq!(mid.len(), 2);
156+
assert!((mid[1].color.a - 0.2).abs() < 1e-6, "alpha fades in");
157+
assert!(
158+
(mid[1].blur_radius - 20.0).abs() < 1e-6,
159+
"geometry is already final"
160+
);
161+
162+
// Endpoints stay exact in both directions.
163+
assert_eq!(Shadow::lerp_stacks(&from, &to, 1.0), to);
164+
assert_eq!(Shadow::lerp_stacks(&to, &from, 1.0)[1], to[1].transparent());
165+
}
61166
}

core/src/vector.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ impl Vector {
2727
y: self.y.round(),
2828
}
2929
}
30+
31+
/// Linearly interpolates between `self` and `other` by the given amount.
32+
///
33+
/// At `amount = 0.0` returns `self`, at `amount = 1.0` returns `other`.
34+
pub const fn lerp(self, other: Self, amount: f32) -> Self {
35+
Self {
36+
x: self.x + (other.x - self.x) * amount,
37+
y: self.y + (other.y - self.y) * amount,
38+
}
39+
}
3040
}
3141

3242
impl<T> std::ops::Neg for Vector<T>

0 commit comments

Comments
 (0)