1use std::sync::Arc;
4
5use bevy::asset::load_embedded_asset;
6use bevy::prelude::*;
7use bevy::shader::Shader;
8use serde::Deserialize;
9use serde_json::Value;
10
11use crate::animations::ValueKind;
12use crate::filters::builtin::BlurParams;
13use crate::filters::params::{FilterColor, ParamSlot, length_logical_px, static_layout};
14use crate::filters::registry::{ReactFilter, ResolvedFilterPass};
15use crate::protocol::units::Length;
16
17const MODE_PREP: f32 = 0.0;
19const MODE_COMBINE: f32 = 1.0;
21
22fn default_offset_y() -> Length {
23 Length::Px(4.0)
24}
25
26fn default_spread() -> Length {
27 Length::Px(6.0)
28}
29
30fn default_shadow_color() -> FilterColor {
31 FilterColor([0.0, 0.0, 0.0, 0.6])
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Deserialize, ts_rs::TS)]
53#[serde(deny_unknown_fields, rename_all = "camelCase")]
54pub struct ShadowParams {
55 #[serde(default = "default_shadow_color")]
56 pub color: FilterColor,
57 #[serde(default)]
60 #[ts(type = "number | string")]
61 pub offset_x: Length,
62 #[serde(default = "default_offset_y")]
63 #[ts(type = "number | string")]
64 pub offset_y: Length,
65 #[serde(default = "default_spread")]
66 #[ts(type = "number | string")]
67 pub spread: Length,
68}
69
70impl Default for ShadowParams {
71 fn default() -> Self {
72 Self {
73 color: default_shadow_color(),
74 offset_x: Length::Px(0.0),
75 offset_y: default_offset_y(),
76 spread: default_spread(),
77 }
78 }
79}
80
81fn shadow_layout() -> Arc<[ParamSlot]> {
82 static_layout![
83 ParamSlot {
84 name: "spread",
85 kind: ValueKind::Length,
86 vec: 0,
87 comp: 0,
88 len: 1,
89 },
90 ParamSlot {
91 name: "offsetX",
92 kind: ValueKind::Length,
93 vec: 1,
94 comp: 0,
95 len: 1,
96 },
97 ParamSlot {
98 name: "offsetY",
99 kind: ValueKind::Length,
100 vec: 1,
101 comp: 1,
102 len: 1,
103 },
104 ParamSlot {
105 name: "color",
106 kind: ValueKind::Color,
107 vec: 2,
108 comp: 0,
109 len: 4,
110 },
111 ]
112}
113
114impl ShadowParams {
115 fn lengths_px(&self) -> Result<(f32, f32, f32), String> {
116 Ok((
117 length_logical_px(Self::NAME, "offsetX", self.offset_x)?,
118 length_logical_px(Self::NAME, "offsetY", self.offset_y)?,
119 length_logical_px(Self::NAME, "spread", self.spread)?,
120 ))
121 }
122}
123
124impl ReactFilter for ShadowParams {
125 const NAME: &'static str = "shadow";
126
127 fn shader(assets: &AssetServer) -> Handle<Shader> {
128 load_embedded_asset!(assets, "shadow.wgsl")
129 }
130
131 fn identity_params() -> Option<Value> {
132 Some(serde_json::json!({ "color": "transparent" }))
136 }
137
138 fn outset(&self) -> Result<f32, String> {
141 let (offset_x, offset_y, spread) = self.lengths_px()?;
142 Ok(offset_x.abs().max(offset_y.abs()) + 3.0 * spread)
143 }
144
145 fn pack(&self) -> (Vec<Vec4>, Arc<[ParamSlot]>) {
146 let (offset_x, offset_y, spread) = self.lengths_px().unwrap_or((0.0, 0.0, 0.0));
150 (
151 vec![
152 Vec4::new(spread, 0.0, 0.0, MODE_PREP),
153 Vec4::new(offset_x, offset_y, 0.0, 0.0),
154 Vec4::from_array(self.color.0),
155 ],
156 shadow_layout(),
157 )
158 }
159
160 fn resolve(&self, assets: &AssetServer) -> Result<Vec<ResolvedFilterPass>, String> {
161 let (offset_x, offset_y, spread) = self.lengths_px()?;
162 let shadow_shader = Self::shader(assets);
163 let blur_shader = BlurParams::shader(assets);
164 let offsets = Vec4::new(offset_x, offset_y, 0.0, 0.0);
165 let color = Vec4::from_array(self.color.0);
166 let pass = |shader: &Handle<Shader>, internal: (f32, f32, f32)| ResolvedFilterPass {
167 shader: shader.clone(),
168 params: vec![
169 Vec4::new(spread, internal.0, internal.1, internal.2),
170 offsets,
171 color,
172 ],
173 layout: shadow_layout(),
174 wire_index: 0,
175 };
176 Ok(vec![
177 pass(&shadow_shader, (0.0, 0.0, MODE_PREP)),
178 pass(&blur_shader, (1.0, 0.0, 0.0)),
179 pass(&blur_shader, (0.0, 1.0, 0.0)),
180 pass(&shadow_shader, (0.0, 0.0, MODE_COMBINE)),
181 ])
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use serde_json::json;
188
189 use super::*;
190 use crate::filters::registry::FilterRegistry;
191 use crate::filters::test_util::{asset_app, params};
192
193 #[test]
199 fn shadow_resolves_to_four_passes() {
200 let app = asset_app();
201 let assets = app.world().resource::<AssetServer>();
202 let passes = params::<ShadowParams>(json!({
203 "offsetX": 3, "offsetY": -2, "spread": 5, "color": "#ff0000",
204 }))
205 .resolve(assets)
206 .expect("shadow resolves");
207 assert_eq!(passes.len(), 4);
208
209 let offsets = Vec4::new(3.0, -2.0, 0.0, 0.0);
210 let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
211 assert_eq!(
212 passes[0].params,
213 vec![Vec4::new(5.0, 0.0, 0.0, 0.0), offsets, color]
214 );
215 assert_eq!(
216 passes[1].params,
217 vec![Vec4::new(5.0, 1.0, 0.0, 0.0), offsets, color]
218 );
219 assert_eq!(
220 passes[2].params,
221 vec![Vec4::new(5.0, 0.0, 1.0, 0.0), offsets, color]
222 );
223 assert_eq!(
224 passes[3].params,
225 vec![Vec4::new(5.0, 0.0, 0.0, 1.0), offsets, color]
226 );
227 assert!(passes.iter().all(|p| p.wire_index == 0));
228 assert_eq!(passes[0].layout[0].name, "spread");
229 assert_eq!(passes[0].layout[0].kind, ValueKind::Length);
230 assert_eq!(passes[0].layout[3].kind, ValueKind::Color);
231
232 assert_eq!(passes[0].shader, passes[3].shader);
235 assert_eq!(passes[1].shader, passes[2].shader);
236 assert_eq!(passes[1].shader, BlurParams::shader(assets));
237 assert_ne!(passes[0].shader, passes[1].shader);
238 }
239
240 #[test]
243 fn shadow_outset_is_offset_plus_three_spreads() {
244 assert_eq!(
245 params::<ShadowParams>(json!({ "offsetX": -8, "offsetY": 2, "spread": 4 })).outset(),
246 Ok(20.0)
247 );
248 assert_eq!(params::<ShadowParams>(json!({})).outset(), Ok(22.0));
249 }
250
251 #[test]
253 fn shadow_empty_params_default_to_a_soft_shadow() {
254 let p = params::<ShadowParams>(json!({}));
255 assert_eq!(p, ShadowParams::default());
256 assert_eq!(p.offset_x, Length::Px(0.0));
257 assert_eq!(p.offset_y, Length::Px(4.0));
258 assert_eq!(p.spread, Length::Px(6.0));
259 assert_eq!(p.color.0, [0.0, 0.0, 0.0, 0.6]);
260 }
261
262 #[test]
265 fn identity_is_a_transparent_shadow() {
266 let identity = ShadowParams::identity_params().expect("has identity");
267 let p: ShadowParams = serde_json::from_value(identity).expect("identity decodes");
268 assert_eq!(p.color.0, [0.0, 0.0, 0.0, 0.0]);
269 assert_eq!(p.pack().0[2], Vec4::ZERO);
270 }
271
272 #[test]
275 fn non_px_lengths_reject_from_registry() {
276 let app = asset_app();
277 let assets = app.world().resource::<AssetServer>();
278 let mut registry = FilterRegistry::default();
279 registry.register::<ShadowParams>();
280 let entry = ®istry.entries["shadow"];
281
282 for value in [
283 json!({ "offsetX": "50%" }),
284 json!({ "offsetY": "2vw" }),
285 json!({ "spread": "1vh" }),
286 ] {
287 let err = (entry.resolve)(&value, assets).expect_err("non-px must reject resolve");
288 assert!(err.contains("px"), "names the unit: {err}");
289 let err = (entry.outset)(&value).expect_err("non-px must reject outset");
290 assert!(err.contains("px"), "names the unit: {err}");
291 }
292 assert!((entry.resolve)(&json!({ "offsetX": -6, "spread": "3px" }), assets).is_ok());
293 assert_eq!(
294 (entry.outset)(&json!({ "offsetY": -10, "spread": 2 })),
295 Ok(16.0)
296 );
297 }
298
299 #[test]
301 fn unknown_shadow_param_rejects() {
302 assert!(serde_json::from_value::<ShadowParams>(json!({ "offsetx": 2 })).is_err());
303 }
304}