motion_core/authoring/
elevation.rs1use crate::{Color, Filter, FilterOperation, Node, NodeModifiers, ValidationError, Vector2};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct ShadowLayer {
8 pub sigma: f64,
10 pub offset: Vector2,
12 pub opacity: f64,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct Elevation {
31 height: f64,
32 direction: Vector2,
33 color: Color,
34}
35
36impl Elevation {
37 pub const MAX_HEIGHT: f64 = 1024.0;
39
40 pub fn shadow(height: f64) -> Result<Self, ValidationError> {
42 if !height.is_finite() || !(0.0..=Self::MAX_HEIGHT).contains(&height) {
43 return Err(ValidationError::invalid(
44 "elevation",
45 "height",
46 "Require a finite height in 0..=1024",
47 ));
48 }
49 Ok(Self {
50 height,
51 direction: Vector2::new(0.0, 1.0),
52 color: Color::BLACK,
53 })
54 }
55
56 pub fn direction(mut self, direction: Vector2) -> Result<Self, ValidationError> {
58 let length = libm::hypot(direction.x, direction.y);
59 if !length.is_finite() || length <= 0.0 {
60 return Err(ValidationError::invalid(
61 "elevation",
62 "direction",
63 "Require a finite nonzero direction",
64 ));
65 }
66 self.direction = Vector2::new(direction.x / length, direction.y / length);
67 Ok(self)
68 }
69
70 pub fn color(mut self, color: Color) -> Self {
72 self.color = color;
73 self
74 }
75
76 pub fn height(self) -> f64 {
78 self.height
79 }
80
81 pub fn layers(self) -> Vec<ShadowLayer> {
83 let height = self.height;
84 if height == 0.0 {
85 return Vec::new();
86 }
87 let fade = 1.0 / (1.0 + height / 64.0);
89 [
90 (0.1 * height + 0.5, 0.06 * height, 0.55 * fade),
91 (0.5 * height, 0.4 * height, 0.32 * fade),
92 (1.0 * height + 1.0, 0.8 * height, 0.2 * fade),
93 ]
94 .map(|(sigma, distance, opacity)| ShadowLayer {
95 sigma,
96 offset: Vector2::new(self.direction.x * distance, self.direction.y * distance),
97 opacity,
98 })
99 .into()
100 }
101
102 pub fn apply(self, source: Node) -> Node {
104 let layers = self.layers();
105 if layers.is_empty() {
106 return source;
107 }
108 shadow_stack(
109 source,
110 self.color,
111 layers
112 .into_iter()
113 .map(|layer| (layer.sigma, layer.offset, layer.opacity)),
114 )
115 }
116}
117
118pub(super) fn shadow_stack(
120 source: Node,
121 color: Color,
122 layers: impl IntoIterator<Item = (f64, Vector2, f64)>,
123) -> Node {
124 let linear = color.to_linear_premultiplied();
125 let straight = if linear[3] > 0.0 {
126 [
127 linear[0] / linear[3],
128 linear[1] / linear[3],
129 linear[2] / linear[3],
130 ]
131 } else {
132 [0.0; 3]
133 };
134 let mut nodes = Vec::new();
135 for (sigma, offset, opacity) in layers {
136 let mut matrix = [0.0; 20];
137 matrix[4] = straight[0];
138 matrix[9] = straight[1];
139 matrix[14] = straight[2];
140 matrix[18] = linear[3] * opacity;
141 nodes.push(source.clone().filter(Filter::Custom(vec![
142 FilterOperation::ColorMatrix(matrix),
143 FilterOperation::Blur { sigma },
144 FilterOperation::Offset {
145 x: offset.x,
146 y: offset.y,
147 },
148 ])));
149 }
150 nodes.push(source);
151 Node::group(nodes)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::Rect;
158
159 #[test]
160 fn layers_grow_with_height_and_fade_in_opacity() {
161 assert!(Elevation::shadow(0.0).unwrap().layers().is_empty());
162 let low = Elevation::shadow(4.0).unwrap().layers();
163 let high = Elevation::shadow(40.0).unwrap().layers();
164 assert_eq!(low.len(), 3);
165 for (low, high) in low.iter().zip(&high) {
166 assert!(high.sigma > low.sigma);
167 assert!(high.offset.y > low.offset.y);
168 assert!(high.opacity < low.opacity);
169 }
170 assert!(low[0].sigma < low[2].sigma && low[0].opacity > low[2].opacity);
172 }
173
174 #[test]
175 fn direction_is_normalized_and_inputs_are_validated() {
176 let layers = Elevation::shadow(10.0)
177 .unwrap()
178 .direction(Vector2::new(3.0, 4.0))
179 .unwrap()
180 .layers();
181 let ratio = layers[1].offset.x / layers[1].offset.y;
182 assert!((ratio - 0.75).abs() < 1e-12);
183 for height in [-1.0, f64::NAN, f64::INFINITY, 2000.0] {
184 assert!(Elevation::shadow(height).is_err());
185 }
186 let elevation = Elevation::shadow(1.0).unwrap();
187 assert!(elevation.direction(Vector2::new(0.0, 0.0)).is_err());
188 assert!(elevation.direction(Vector2::new(f64::NAN, 1.0)).is_err());
189 }
190
191 #[test]
192 fn apply_builds_layer_clones_before_the_untouched_source() {
193 let card: Node = Rect::new(0.0, 0.0, 8.0, 8.0).fill(Color::WHITE).into();
194 let raised = card.clone().elevated(Elevation::shadow(8.0).unwrap());
195 let children = |node: &Node| match &node.data.kind {
196 super::super::NodeKind::Group(children) => Some(children.len()),
197 _ => None,
198 };
199 assert_eq!(children(&raised), Some(4));
200 let flat = card.elevated(Elevation::shadow(0.0).unwrap());
201 assert_eq!(children(&flat), None);
202 }
203}