1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crate::{Color, Vector};
/// A shadow, painted as CSS paints a `box-shadow`.
///
/// An outset shadow shows only outside the box casting it; an inset one fills
/// the box's padding box (inside its border) outside the shape it would cast.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Shadow {
/// The color of the shadow.
pub color: Color,
/// The offset of the shadow.
pub offset: Vector,
/// The blur radius of the shadow.
///
/// As in CSS, the shadow is blurred by a Gaussian whose standard deviation
/// is half this radius, so it fades over about one and a half radii.
pub blur_radius: f32,
/// The spread radius of the shadow.
///
/// Positive values expand the shadow outward (larger than the element).
/// Negative values contract the shadow inward (smaller than the element).
/// Its corners grow and shrink with it as CSS's do: a square corner stays
/// square.
pub spread_radius: f32,
/// Whether the shadow is inset (inside the element) or outset (outside).
/// Default is `false` (outset shadow).
pub inset: bool,
}
impl Shadow {
/// Creates a new outset (default) shadow.
pub fn new(color: Color, offset: Vector, blur_radius: f32) -> Self {
Self {
color,
offset,
blur_radius,
spread_radius: 0.0,
inset: false,
}
}
/// Creates a new inset shadow.
pub fn inset(color: Color, offset: Vector, blur_radius: f32) -> Self {
Self {
color,
offset,
blur_radius,
spread_radius: 0.0,
inset: true,
}
}
/// Sets the spread radius of the shadow.
pub fn with_spread(mut self, spread_radius: f32) -> Self {
self.spread_radius = spread_radius;
self
}
/// Sets whether the shadow is inset.
pub fn with_inset(mut self, inset: bool) -> Self {
self.inset = inset;
self
}
/// Returns the [`Shadow`] with a fully transparent color.
///
/// The geometry is kept, so this is the "absent" end of a fade — a shadow
/// appearing or disappearing rather than one moving.
pub const fn transparent(self) -> Self {
Self {
color: Color {
a: 0.0,
..self.color
},
..self
}
}
/// Linearly interpolates between two [`Shadow`]s by the given amount.
///
/// `inset` has no representable in-between — an inset shadow and an outset one
/// are drawn by different branches entirely — so it steps to `other`'s.
pub const fn lerp(self, other: Self, amount: f32) -> Self {
Self {
color: self.color.lerp(other.color, amount),
offset: self.offset.lerp(other.offset, amount),
blur_radius: self.blur_radius + (other.blur_radius - self.blur_radius) * amount,
spread_radius: self.spread_radius + (other.spread_radius - self.spread_radius) * amount,
inset: other.inset,
}
}
/// Linearly interpolates between two stacks of [`Shadow`]s by the given
/// amount, pairing them up layer by layer.
///
/// Elevation scales stack several layers, and the two ends of a transition
/// need not use the same number of them. A layer present at only one end keeps
/// that end's geometry and fades its alpha from (or to) zero, so a stack can
/// grow and shrink across a transition.
///
/// The result always holds one layer per index rather than both stacks
/// concatenated: overlapping translucent layers composite, so drawing both
/// would darken the shadow mid-transition even when the endpoints match.
pub fn lerp_stacks(from: &[Self], to: &[Self], amount: f32) -> Vec<Self> {
(0..from.len().max(to.len()))
.filter_map(|i| match (from.get(i), to.get(i)) {
(Some(from), Some(to)) => Some(from.lerp(*to, amount)),
(Some(from), None) => Some(from.lerp(from.transparent(), amount)),
(None, Some(to)) => Some(to.transparent().lerp(*to, amount)),
// Unreachable: `i` is below the longer stack's length.
(None, None) => None,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn shadow(alpha: f32, blur: f32) -> Shadow {
Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, alpha),
offset: Vector::new(0.0, 2.0),
blur_radius: blur,
spread_radius: -1.0,
inset: false,
}
}
#[test]
fn lerp_stacks_pairs_layers_instead_of_concatenating() {
// Two overlapping translucent layers composite to more than either one,
// so a stack transition that drew both ends would visibly darken halfway
// through even when the endpoints are identical.
let from = vec![shadow(0.1, 6.0), shadow(0.1, 15.0)];
let to = vec![shadow(0.2, 6.0), shadow(0.2, 15.0)];
assert_eq!(Shadow::lerp_stacks(&from, &to, 0.5).len(), 2);
assert_eq!(Shadow::lerp_stacks(&from, &to, 0.0), from);
assert_eq!(Shadow::lerp_stacks(&from, &to, 1.0), to);
}
#[test]
fn lerp_stacks_fades_unpaired_layers_from_transparent() {
// A stack that grows mid-transition (an extra ring layer appearing, say)
// must fade the new layer in by alpha while it already holds its final
// geometry — interpolating its geometry from a neighbour would slide it
// into place from the wrong shape.
let from = vec![shadow(0.1, 6.0)];
let to = vec![shadow(0.1, 6.0), shadow(0.4, 20.0)];
let mid = Shadow::lerp_stacks(&from, &to, 0.5);
assert_eq!(mid.len(), 2);
assert!((mid[1].color.a - 0.2).abs() < 1e-6, "alpha fades in");
assert!(
(mid[1].blur_radius - 20.0).abs() < 1e-6,
"geometry is already final"
);
// Endpoints stay exact in both directions.
assert_eq!(Shadow::lerp_stacks(&from, &to, 1.0), to);
assert_eq!(Shadow::lerp_stacks(&to, &from, 1.0)[1], to[1].transparent());
}
}