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
//! Flutter counterpart: dart:ui `Shadow` in `engine/src/flutter/lib/ui/painting.dart`.
use std::fmt::{self, Debug};
use super::color::Color;
use super::geometry::Offset;
use super::lerp::lerp_double_non_null;
/// A representation of a shadow.
#[derive(Clone, Copy, PartialEq)]
pub struct Shadow {
/// Color that the shadow will be drawn with.
///
/// The shadows are shapes composited directly over the base canvas, and do not
/// represent optical occlusion.
pub color: Color,
/// The displacement of the shadow from the casting element.
///
/// Positive x/y offsets will shift the shadow to the right and down, while
/// negative offsets shift the shadow to the left and up. The offsets are
/// relative to the position of the element that is casting it.
pub offset: Offset,
/// The standard deviation of the Gaussian to convolve with the shadow's shape.
pub blur_radius: f64,
}
impl Shadow {
/// Construct a shadow.
///
/// The default shadow is a black shadow with zero offset and zero blur.
/// Default shadows should be completely covered by the casting element,
/// and not be visible.
///
/// Transparency should be adjusted through the [`color`](Shadow::color) alpha.
///
/// Shadow order matters due to compositing multiple translucent objects not
/// being commutative.
pub fn new(color: Color, offset: Offset, blur_radius: f64) -> Shadow {
debug_assert!(
blur_radius >= 0.0,
"Text shadow blur radius should be non-negative."
);
Shadow {
color,
offset,
blur_radius,
}
}
/// Converts a blur radius in pixels to sigmas.
///
/// See SkBlurMask::ConvertRadiusToSigma().
pub fn convert_radius_to_sigma(radius: f64) -> f64 {
if radius > 0.0 {
radius * 0.57735 + 0.5
} else {
0.0
}
}
/// The [`blur_radius`](Shadow::blur_radius) in sigmas instead of logical pixels.
pub fn blur_sigma(&self) -> f64 {
Self::convert_radius_to_sigma(self.blur_radius)
}
/// Returns a new shadow with its [`offset`](Shadow::offset) and
/// [`blur_radius`](Shadow::blur_radius) scaled by the given factor.
pub fn scale(&self, factor: f64) -> Shadow {
Shadow::new(self.color, self.offset * factor, self.blur_radius * factor)
}
/// Create the [`Paint`](crate::Paint) object that corresponds to this shadow
/// description.
///
/// The [`offset`](Shadow::offset) is not represented in the [`Paint`](crate::Paint)
/// object. To honor this as well, the shape should be translated by
/// [`offset`](Shadow::offset) before being filled using this paint.
pub fn to_paint(&self) -> crate::Paint {
crate::Paint {
color: self.color.into(),
mask_blur: Some(crate::MaskBlur::new(self.blur_sigma() as f32)),
..crate::Paint::default()
}
}
/// Linearly interpolate between two shadows.
///
/// If either shadow is null, this function linearly interpolates from
/// a shadow that matches the other shadow in color but has a zero
/// offset and a zero blurRadius.
pub fn lerp(a: Option<Shadow>, b: Option<Shadow>, t: f64) -> Option<Shadow> {
match (a, b) {
(None, None) => None,
(Some(a), None) => Some(a.scale(1.0 - t)),
(None, Some(b)) => Some(b.scale(t)),
(Some(a), Some(b)) => Some(Shadow::new(
Color::lerp(Some(a.color), Some(b.color), t).unwrap(),
Offset::lerp(Some(a.offset), Some(b.offset), t).unwrap(),
lerp_double_non_null(a.blur_radius, b.blur_radius, t),
)),
}
}
/// Linearly interpolate between two lists of shadows.
///
/// If the lists differ in length, excess items are lerped with null.
pub fn lerp_list(a: Option<&[Shadow]>, b: Option<&[Shadow]>, t: f64) -> Option<Vec<Shadow>> {
if a.is_none() && b.is_none() {
return None;
}
let a = a.unwrap_or(&[]);
let b = b.unwrap_or(&[]);
let common_length = a.len().min(b.len());
let mut result = Vec::new();
for i in 0..common_length {
result.push(Shadow::lerp(Some(a[i]), Some(b[i]), t).unwrap());
}
for shadow in &a[common_length..] {
result.push(shadow.scale(1.0 - t));
}
for shadow in &b[common_length..] {
result.push(shadow.scale(t));
}
Some(result)
}
}
impl Default for Shadow {
fn default() -> Shadow {
Shadow::new(Color::new(0xFF000000), Offset::ZERO, 0.0)
}
}
impl Debug for Shadow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TextShadow({:?}, {:?}, {})",
self.color, self.offset, self.blur_radius
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_radius_to_sigma_matches_skia() {
assert_eq!(Shadow::convert_radius_to_sigma(0.0), 0.0);
assert_eq!(Shadow::convert_radius_to_sigma(-1.0), 0.0);
assert_eq!(Shadow::convert_radius_to_sigma(1.0), 1.0 * 0.57735 + 0.5);
}
#[test]
fn lerp_null_arms_scale_toward_nothing() {
let shadow = Shadow::new(Color::new(0xFF000000), Offset::new(8.0, 0.0), 4.0);
assert_eq!(
Shadow::lerp(None, Some(shadow), 0.25).unwrap(),
shadow.scale(0.25)
);
assert_eq!(
Shadow::lerp(Some(shadow), None, 0.25).unwrap(),
shadow.scale(0.75)
);
assert_eq!(Shadow::lerp(None, None, 0.25), None);
}
}