gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4 llimphi-compositor/examples/backdrop_blur_demo.rs Rust · 248 lines · 8.3 KB History Who touched what raw

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! Filmstrip headless del **backdrop blur** (Bloque 11 de PARIDAD-FLUTTER):
2//! sobre un fondo con franjas de colores fuertes, una fila de cuatro paneles
3//! `.backdrop_blur(σ)` con `σ ∈ {0, 4, 8, 16}` — el primero es la referencia
4//! sin blur, el resto muestra el Gauss separable cada vez más fuerte.
5//!
6//! Prueba el camino `View::backdrop_blur` → `collect_backdrop_blurs` →
7//! `BlurCompositor::blur` (post-pasada wgpu sobre la intermediate). Render
8//! headless: vello pinta a una textura, el compositor de blur la modifica
9//! in-place sobre los rects de cada panel, y volcamos a PNG.
10//!
11//! `cargo run -p llimphi-compositor --example backdrop_blur_demo -- [out.png]`
12
13use std::fs::File;
14use std::io::BufWriter;
15
16use llimphi_compositor::{collect_backdrop_blurs, mount, paint, View};
17use llimphi_hal::{wgpu, BlurCompositor, Hal};
18use llimphi_layout::taffy::prelude::{auto, length, percent, FlexDirection, Size, Style};
19use llimphi_layout::taffy::{Position, Rect};
20use llimphi_layout::LayoutTree;
21use llimphi_raster::peniko::Color;
22use llimphi_raster::{vello, Renderer};
23use llimphi_text::Typesetter;
24
25const W: u32 = 1200;
26const H: u32 = 360;
27const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
28const SIGMAS: [f32; 4] = [0.0, 4.0, 8.0, 16.0];
29
30fn rgb(r: u8, g: u8, b: u8) -> Color {
31    Color::from_rgba8(r, g, b, 255)
32}
33
34fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
35    Color::from_rgba8(r, g, b, a)
36}
37
38fn main() {
39    let out = std::env::args()
40        .nth(1)
41        .unwrap_or_else(|| "backdrop_blur.png".to_string());
42
43    // Fondo: cuatro franjas verticales saturadas — el blur tiene que mezclar
44    // los bordes entre franjas, así el efecto se ve aun sin texto/detalle.
45    let franjas: Vec<View<()>> = [
46        rgb(231, 76, 60),
47        rgb(241, 196, 15),
48        rgb(46, 204, 113),
49        rgb(52, 152, 219),
50    ]
51    .iter()
52    .map(|c| {
53        View::<()>::new(Style {
54            size: Size {
55                width: percent(0.25),
56                height: percent(1.0),
57            },
58            ..Default::default()
59        })
60        .fill(*c)
61    })
62    .collect();
63    let fondo = View::<()>::new(Style {
64        size: Size {
65            width: percent(1.0),
66            height: percent(1.0),
67        },
68        position: Position::Absolute,
69        inset: Rect {
70            left: length(0.0),
71            top: length(0.0),
72            right: auto(),
73            bottom: auto(),
74        },
75        flex_direction: FlexDirection::Row,
76        ..Default::default()
77    })
78    .children(franjas);
79
80    // Fila de paneles "vidrio": cada uno apunta a un σ distinto. Todos
81    // `Position::Absolute` con inset calculado para superponerse al fondo.
82    // El panel es un rect translúcido sin contenido propio (el blur post-
83    // pasada borronea TODO lo que está dentro del rect, así que un texto
84    // *dentro* del panel saldría borroso — limitación documentada del v1).
85    let panel_w = 240.0_f32;
86    let panel_h = 220.0_f32;
87    let gap = 24.0_f32;
88    let fila_w = SIGMAS.len() as f32 * panel_w + (SIGMAS.len() as f32 - 1.0) * gap;
89    let inicio_x = (W as f32 - fila_w) * 0.5;
90    let panel_y = (H as f32 - panel_h) * 0.5;
91
92    let mut hijos: Vec<View<()>> = vec![fondo];
93    for (i, &sigma) in SIGMAS.iter().enumerate() {
94        let x = inicio_x + i as f32 * (panel_w + gap);
95        let panel = View::<()>::new(Style {
96            position: Position::Absolute,
97            inset: Rect {
98                left: length(x),
99                top: length(panel_y),
100                right: auto(),
101                bottom: auto(),
102            },
103            size: Size {
104                width: length(panel_w),
105                height: length(panel_h),
106            },
107            ..Default::default()
108        })
109        .fill(rgba(255, 255, 255, 96))
110        .radius(20.0)
111        .border(1.5, rgba(255, 255, 255, 180))
112        .backdrop_blur(sigma);
113        hijos.push(panel);
114    }
115
116    let root = View::<()>::new(Style {
117        size: Size {
118            width: length(W as f32),
119            height: length(H as f32),
120        },
121        ..Default::default()
122    })
123    .children(hijos);
124
125    let mut layout = LayoutTree::new();
126    let mounted = mount(&mut layout, root);
127    let computed = layout
128        .compute(mounted.root, (W as f32, H as f32))
129        .expect("layout");
130
131    let mut ts = Typesetter::new();
132    let mut scene = vello::Scene::new();
133    paint(&mut scene, &mounted, &computed, &mut ts, None, None);
134
135    // Render + post-pase de blur con BlurCompositor — el mismo camino que
136    // toma `llimphi-ui` durante un redraw real.
137    let hal = pollster::block_on(Hal::new(None)).expect("hal");
138    let mut renderer = Renderer::new(&hal).expect("renderer");
139    let mut blur = BlurCompositor::new(&hal.device);
140    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
141        label: Some("dump-blur"),
142        size: wgpu::Extent3d {
143            width: W,
144            height: H,
145            depth_or_array_layers: 1,
146        },
147        mip_level_count: 1,
148        sample_count: 1,
149        dimension: wgpu::TextureDimension::D2,
150        format: FMT,
151        usage: wgpu::TextureUsages::STORAGE_BINDING
152            | wgpu::TextureUsages::TEXTURE_BINDING
153            | wgpu::TextureUsages::RENDER_ATTACHMENT
154            | wgpu::TextureUsages::COPY_SRC,
155        view_formats: &[],
156    });
157    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
158    renderer
159        .render_to_view(&hal, &scene, &view, W, H, Color::BLACK)
160        .expect("render_to_view");
161
162    let blurs = collect_backdrop_blurs(&mounted, &computed);
163    let mut encoder = hal
164        .device
165        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
166            label: Some("blur-demo-encoder"),
167        });
168    for b in &blurs {
169        blur.blur(
170            &hal.device,
171            &hal.queue,
172            &mut encoder,
173            &view,
174            (W, H),
175            b.rect,
176            b.sigma,
177        );
178    }
179    hal.queue.submit(std::iter::once(encoder.finish()));
180
181    write_png(&hal, &target, &out);
182    eprintln!(
183        "backdrop_blur_demo: escrito {out} ({W}x{H}) — {} paneles σ={:?}; \
184         σ=0 queda nítido (el compositor no-op'ea); los demás muestran el \
185         Gauss separable sobre las franjas. {} blur node(s) detectado(s).",
186        SIGMAS.len(),
187        SIGMAS,
188        blurs.len(),
189    );
190}
191
192fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
193    let unpadded = (W * 4) as usize;
194    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
195    let padded = unpadded.div_ceil(align) * align;
196    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
197        label: Some("readback"),
198        size: (padded * H as usize) as u64,
199        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
200        mapped_at_creation: false,
201    });
202    let mut enc = hal
203        .device
204        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
205    enc.copy_texture_to_buffer(
206        wgpu::TexelCopyTextureInfo {
207            texture: target,
208            mip_level: 0,
209            origin: wgpu::Origin3d::ZERO,
210            aspect: wgpu::TextureAspect::All,
211        },
212        wgpu::TexelCopyBufferInfo {
213            buffer: &buf,
214            layout: wgpu::TexelCopyBufferLayout {
215                offset: 0,
216                bytes_per_row: Some(padded as u32),
217                rows_per_image: Some(H),
218            },
219        },
220        wgpu::Extent3d {
221            width: W,
222            height: H,
223            depth_or_array_layers: 1,
224        },
225    );
226    hal.queue.submit(std::iter::once(enc.finish()));
227    let slice = buf.slice(..);
228    let (tx, rx) = std::sync::mpsc::channel();
229    slice.map_async(wgpu::MapMode::Read, move |r| {
230        let _ = tx.send(r);
231    });
232    hal.device.poll(wgpu::PollType::wait_indefinitely());
233    rx.recv().unwrap().unwrap();
234    let data = slice.get_mapped_range();
235    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
236    for row in 0..H as usize {
237        let s = row * padded;
238        pixels.extend_from_slice(&data[s..s + unpadded]);
239    }
240    drop(data);
241    buf.unmap();
242    let file = File::create(path).expect("png");
243    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
244    enc.set_color(png::ColorType::Rgba);
245    enc.set_depth(png::BitDepth::Eight);
246    let mut w = enc.write_header().unwrap();
247    w.write_image_data(&pixels).unwrap();
248}