bevy_react/filters/builtin/
blur.rs1use 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::params::{ParamSlot, length_logical_px, static_layout};
13use crate::filters::registry::{ReactFilter, ResolvedFilterPass};
14use crate::protocol::units::Length;
15
16#[derive(Debug, Clone, Copy, PartialEq, Default, Deserialize, ts_rs::TS)]
24#[serde(deny_unknown_fields)]
25pub struct BlurParams {
26 #[serde(default)]
28 #[ts(type = "number | string")]
29 pub radius: Length,
30}
31
32fn blur_layout() -> Arc<[ParamSlot]> {
33 static_layout![ParamSlot {
34 name: "radius",
35 kind: ValueKind::Length,
36 vec: 0,
37 comp: 0,
38 len: 1,
39 }]
40}
41
42impl ReactFilter for BlurParams {
43 const NAME: &'static str = "blur";
44
45 fn shader(assets: &AssetServer) -> Handle<Shader> {
46 load_embedded_asset!(assets, "blur.wgsl")
47 }
48
49 fn identity_params() -> Option<Value> {
50 Some(serde_json::json!({ "radius": 0.0 }))
51 }
52
53 fn outset(&self) -> Result<f32, String> {
55 Ok(3.0 * length_logical_px(Self::NAME, "radius", self.radius)?)
56 }
57
58 fn pack(&self) -> (Vec<Vec4>, Arc<[ParamSlot]>) {
59 (
63 vec![Vec4::new(
64 length_logical_px(Self::NAME, "radius", self.radius).unwrap_or(0.0),
65 1.0,
66 0.0,
67 0.0,
68 )],
69 blur_layout(),
70 )
71 }
72
73 fn resolve(&self, assets: &AssetServer) -> Result<Vec<ResolvedFilterPass>, String> {
74 let radius = length_logical_px(Self::NAME, "radius", self.radius)?;
75 let shader = Self::shader(assets);
76 let pass = |dir: (f32, f32)| ResolvedFilterPass {
77 shader: shader.clone(),
78 params: vec![Vec4::new(radius, dir.0, dir.1, 0.0)],
79 layout: blur_layout(),
80 wire_index: 0,
81 };
82 Ok(vec![pass((1.0, 0.0)), pass((0.0, 1.0))])
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use serde_json::json;
89
90 use super::*;
91 use crate::filters::registry::FilterRegistry;
92 use crate::filters::test_util::{asset_app, params};
93
94 #[test]
98 fn blur_resolves_to_two_directional_passes() {
99 let app = asset_app();
100 let assets = app.world().resource::<AssetServer>();
101 let passes = params::<BlurParams>(json!({ "radius": 4 }))
102 .resolve(assets)
103 .expect("blur resolves");
104 assert_eq!(passes.len(), 2);
105 assert_eq!(passes[0].params, vec![Vec4::new(4.0, 1.0, 0.0, 0.0)]);
106 assert_eq!(passes[1].params, vec![Vec4::new(4.0, 0.0, 1.0, 0.0)]);
107 assert!(passes.iter().all(|p| p.wire_index == 0));
108 assert_eq!(passes[0].layout[0].kind, ValueKind::Length);
109 }
110
111 #[test]
113 fn blur_outset_is_three_radii() {
114 assert_eq!(
115 params::<BlurParams>(json!({ "radius": 4 })).outset(),
116 Ok(12.0)
117 );
118 assert_eq!(params::<BlurParams>(json!({})).outset(), Ok(0.0));
119 }
120
121 #[test]
125 fn non_px_blur_radius_rejects_from_registry() {
126 let app = asset_app();
127 let assets = app.world().resource::<AssetServer>();
128 let mut registry = FilterRegistry::default();
129 registry.register::<BlurParams>();
130 let blur = ®istry.entries["blur"];
131
132 let value = json!({ "radius": "50%" });
133 let err = (blur.resolve)(&value, assets).expect_err("percent radius must reject resolve");
134 assert!(
135 err.contains("px") && err.contains("%"),
136 "names the unit: {err}"
137 );
138 let err = (blur.outset)(&value).expect_err("percent radius must reject outset");
139 assert!(
140 err.contains("px") && err.contains("%"),
141 "names the unit: {err}"
142 );
143
144 let err = (blur.outset)(&json!({ "radius": "1vw" })).expect_err("vw radius must reject");
145 assert!(err.contains("vw"), "names the unit: {err}");
146
147 assert!((blur.resolve)(&json!({ "radius": 4 }), assets).is_ok());
149 assert_eq!((blur.outset)(&json!({ "radius": "4px" })), Ok(12.0));
150 }
151}