Skip to main content

inset_embedder/geometry/
shadow.rs

1//! Flutter counterpart: dart:ui `Shadow` in `engine/src/flutter/lib/ui/painting.dart`.
2
3use std::fmt::{self, Debug};
4
5use super::color::Color;
6use super::geometry::Offset;
7use super::lerp::lerp_double_non_null;
8
9/// A representation of a shadow.
10#[derive(Clone, Copy, PartialEq)]
11pub struct Shadow {
12    /// Color that the shadow will be drawn with.
13    ///
14    /// The shadows are shapes composited directly over the base canvas, and do not
15    /// represent optical occlusion.
16    pub color: Color,
17    /// The displacement of the shadow from the casting element.
18    ///
19    /// Positive x/y offsets will shift the shadow to the right and down, while
20    /// negative offsets shift the shadow to the left and up. The offsets are
21    /// relative to the position of the element that is casting it.
22    pub offset: Offset,
23    /// The standard deviation of the Gaussian to convolve with the shadow's shape.
24    pub blur_radius: f64,
25}
26
27impl Shadow {
28    /// Construct a shadow.
29    ///
30    /// The default shadow is a black shadow with zero offset and zero blur.
31    /// Default shadows should be completely covered by the casting element,
32    /// and not be visible.
33    ///
34    /// Transparency should be adjusted through the [`color`](Shadow::color) alpha.
35    ///
36    /// Shadow order matters due to compositing multiple translucent objects not
37    /// being commutative.
38    pub fn new(color: Color, offset: Offset, blur_radius: f64) -> Shadow {
39        debug_assert!(
40            blur_radius >= 0.0,
41            "Text shadow blur radius should be non-negative."
42        );
43        Shadow {
44            color,
45            offset,
46            blur_radius,
47        }
48    }
49
50    /// Converts a blur radius in pixels to sigmas.
51    ///
52    /// See SkBlurMask::ConvertRadiusToSigma().
53    pub fn convert_radius_to_sigma(radius: f64) -> f64 {
54        if radius > 0.0 {
55            radius * 0.57735 + 0.5
56        } else {
57            0.0
58        }
59    }
60
61    /// The [`blur_radius`](Shadow::blur_radius) in sigmas instead of logical pixels.
62    pub fn blur_sigma(&self) -> f64 {
63        Self::convert_radius_to_sigma(self.blur_radius)
64    }
65
66    /// Returns a new shadow with its [`offset`](Shadow::offset) and
67    /// [`blur_radius`](Shadow::blur_radius) scaled by the given factor.
68    pub fn scale(&self, factor: f64) -> Shadow {
69        Shadow::new(self.color, self.offset * factor, self.blur_radius * factor)
70    }
71
72    /// Create the [`Paint`](crate::Paint) object that corresponds to this shadow
73    /// description.
74    ///
75    /// The [`offset`](Shadow::offset) is not represented in the [`Paint`](crate::Paint)
76    /// object. To honor this as well, the shape should be translated by
77    /// [`offset`](Shadow::offset) before being filled using this paint.
78    pub fn to_paint(&self) -> crate::Paint {
79        crate::Paint {
80            color: self.color.into(),
81            mask_blur: Some(crate::MaskBlur::new(self.blur_sigma() as f32)),
82            ..crate::Paint::default()
83        }
84    }
85
86    /// Linearly interpolate between two shadows.
87    ///
88    /// If either shadow is null, this function linearly interpolates from
89    /// a shadow that matches the other shadow in color but has a zero
90    /// offset and a zero blurRadius.
91    pub fn lerp(a: Option<Shadow>, b: Option<Shadow>, t: f64) -> Option<Shadow> {
92        match (a, b) {
93            (None, None) => None,
94            (Some(a), None) => Some(a.scale(1.0 - t)),
95            (None, Some(b)) => Some(b.scale(t)),
96            (Some(a), Some(b)) => Some(Shadow::new(
97                Color::lerp(Some(a.color), Some(b.color), t).unwrap(),
98                Offset::lerp(Some(a.offset), Some(b.offset), t).unwrap(),
99                lerp_double_non_null(a.blur_radius, b.blur_radius, t),
100            )),
101        }
102    }
103
104    /// Linearly interpolate between two lists of shadows.
105    ///
106    /// If the lists differ in length, excess items are lerped with null.
107    pub fn lerp_list(a: Option<&[Shadow]>, b: Option<&[Shadow]>, t: f64) -> Option<Vec<Shadow>> {
108        if a.is_none() && b.is_none() {
109            return None;
110        }
111        let a = a.unwrap_or(&[]);
112        let b = b.unwrap_or(&[]);
113        let common_length = a.len().min(b.len());
114        let mut result = Vec::new();
115        for i in 0..common_length {
116            result.push(Shadow::lerp(Some(a[i]), Some(b[i]), t).unwrap());
117        }
118        for shadow in &a[common_length..] {
119            result.push(shadow.scale(1.0 - t));
120        }
121        for shadow in &b[common_length..] {
122            result.push(shadow.scale(t));
123        }
124        Some(result)
125    }
126}
127
128impl Default for Shadow {
129    fn default() -> Shadow {
130        Shadow::new(Color::new(0xFF000000), Offset::ZERO, 0.0)
131    }
132}
133
134impl Debug for Shadow {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(
137            f,
138            "TextShadow({:?}, {:?}, {})",
139            self.color, self.offset, self.blur_radius
140        )
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn convert_radius_to_sigma_matches_skia() {
150        assert_eq!(Shadow::convert_radius_to_sigma(0.0), 0.0);
151        assert_eq!(Shadow::convert_radius_to_sigma(-1.0), 0.0);
152        assert_eq!(Shadow::convert_radius_to_sigma(1.0), 1.0 * 0.57735 + 0.5);
153    }
154
155    #[test]
156    fn lerp_null_arms_scale_toward_nothing() {
157        let shadow = Shadow::new(Color::new(0xFF000000), Offset::new(8.0, 0.0), 4.0);
158        assert_eq!(
159            Shadow::lerp(None, Some(shadow), 0.25).unwrap(),
160            shadow.scale(0.25)
161        );
162        assert_eq!(
163            Shadow::lerp(Some(shadow), None, 0.25).unwrap(),
164            shadow.scale(0.75)
165        );
166        assert_eq!(Shadow::lerp(None, None, 0.25), None);
167    }
168}