tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4
llimphi-compositor/examples/backdrop_blur_demo.rs
History
Who touched what
raw
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}