Skip to main content

bevy_react/filters/builtin/
pixelize.rs

1//! The `pixelize` built-in morph filter: a port of gl-transitions'
2//! `pixelize.glsl` (mosaic out, mosaic in). Like the other two morph
3//! built-ins it blends the frozen "from" snapshot into the live content via
4//! the prelude's morph helpers — single-pass, zero outset, premultiplied-
5//! direct blending.
6
7use std::sync::Arc;
8
9use bevy::asset::load_embedded_asset;
10use bevy::prelude::*;
11use bevy::shader::Shader;
12use serde::Deserialize;
13
14use crate::animations::ValueKind;
15use crate::filters::params::{ParamSlot, static_layout};
16use crate::filters::registry::{ReactFilter, ReactMorphFilter};
17
18fn default_squares_min() -> [f32; 2] {
19    [20.0, 20.0]
20}
21
22fn default_steps() -> f32 {
23    50.0
24}
25
26/// `pixelize`: both images sample the center of a shared mosaic cell whose
27/// size peaks mid-transition, over a plain progress crossfade. Upstream
28/// uniforms map 1:1 — `squaresMin` (ivec2, the cell count when the mosaic is
29/// coarsest) and `steps` (int, quantizes the cell-size ramp; `<= 0` disables
30/// the stepping for a continuous ramp).
31///
32/// Packed as `params[0] = (squaresMin.x, squaresMin.y, steps, 0)`.
33#[derive(Debug, Clone, Copy, PartialEq, Deserialize, ts_rs::TS)]
34#[serde(deny_unknown_fields)]
35pub struct PixelizeParams {
36    /// Cells across x/y at the mosaic's coarsest (upstream `squaresMin`).
37    #[serde(default = "default_squares_min", rename = "squaresMin")]
38    pub squares_min: [f32; 2],
39    /// Discrete cell-size levels; `<= 0` for a continuous ramp.
40    #[serde(default = "default_steps")]
41    pub steps: f32,
42}
43
44impl Default for PixelizeParams {
45    fn default() -> Self {
46        Self {
47            squares_min: default_squares_min(),
48            steps: default_steps(),
49        }
50    }
51}
52
53fn pixelize_layout() -> Arc<[ParamSlot]> {
54    static_layout![
55        ParamSlot {
56            name: "squaresMin",
57            kind: ValueKind::Scalar,
58            vec: 0,
59            comp: 0,
60            len: 2,
61        },
62        ParamSlot {
63            name: "steps",
64            kind: ValueKind::Scalar,
65            vec: 0,
66            comp: 2,
67            len: 1,
68        },
69    ]
70}
71
72impl ReactFilter for PixelizeParams {
73    const NAME: &'static str = "pixelize";
74    const IS_MORPH: bool = true;
75
76    fn shader(assets: &AssetServer) -> Handle<Shader> {
77        load_embedded_asset!(assets, "pixelize.wgsl")
78    }
79
80    /// The mosaic never paints outside the box.
81    fn outset(&self) -> Result<f32, String> {
82        Ok(0.0)
83    }
84
85    fn pack(&self) -> (Vec<Vec4>, Arc<[ParamSlot]>) {
86        (
87            vec![Vec4::new(
88                self.squares_min[0],
89                self.squares_min[1],
90                self.steps,
91                0.0,
92            )],
93            pixelize_layout(),
94        )
95    }
96}
97
98impl ReactMorphFilter for PixelizeParams {}
99
100#[cfg(test)]
101mod tests {
102    use serde_json::json;
103
104    use super::*;
105    use crate::filters::test_util::{asset_app, params};
106
107    /// Single zero-outset pass with the documented packing, user params
108    /// confined to `params[0..6]` (morph-capable).
109    #[test]
110    fn pixelize_resolves_to_single_zero_outset_pass() {
111        let app = asset_app();
112        let assets = app.world().resource::<AssetServer>();
113
114        let passes = params::<PixelizeParams>(json!({}))
115            .resolve(assets)
116            .expect("pixelize resolves");
117        assert_eq!(passes.len(), 1);
118        assert_eq!(passes[0].params[0], Vec4::new(20.0, 20.0, 50.0, 0.0));
119        assert!(passes[0].params.len() <= crate::filters::MORPH_MAX_USER_PARAM_VECS);
120        assert_eq!(params::<PixelizeParams>(json!({})).outset(), Ok(0.0));
121
122        let p = params::<PixelizeParams>(json!({ "squaresMin": [8, 4], "steps": 0 }));
123        assert_eq!(p.pack().0[0], Vec4::new(8.0, 4.0, 0.0, 0.0));
124        assert_eq!(p.pack().1[0].name, "squaresMin");
125        assert_eq!(p.pack().1[1].name, "steps");
126    }
127
128    /// `deny_unknown_fields`, and the wire name is the camelCase
129    /// `squaresMin` (serde rename — the field ident is snake_case).
130    #[test]
131    fn unknown_pixelize_params_reject() {
132        assert!(
133            serde_json::from_value::<PixelizeParams>(json!({ "squares_min": [4, 4] })).is_err()
134        );
135        assert!(serde_json::from_value::<PixelizeParams>(json!({ "blocks": 3 })).is_err());
136    }
137}