Files
llimphi/llimphi-compositor/examples/backdrop_blur_demo.rs
T
Sergio ccab39f140 refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la
estructura vieja de eventloop) y suma el 3D:

- bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 /
  accesskit_winit 0.33 / vello_hybrid 0.0.9.
- nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido,
  montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel
  (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox).
- README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif).
- excluido modules/allichay (arrastra deps fuera del alcance del front-door).
- cargo check --workspace: verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:40:00 +00:00

249 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Filmstrip headless del **backdrop blur** (Bloque 11 de PARIDAD-FLUTTER):
//! sobre un fondo con franjas de colores fuertes, una fila de cuatro paneles
//! `.backdrop_blur(σ)` con `σ ∈ {0, 4, 8, 16}` — el primero es la referencia
//! sin blur, el resto muestra el Gauss separable cada vez más fuerte.
//!
//! Prueba el camino `View::backdrop_blur` → `collect_backdrop_blurs` →
//! `BlurCompositor::blur` (post-pasada wgpu sobre la intermediate). Render
//! headless: vello pinta a una textura, el compositor de blur la modifica
//! in-place sobre los rects de cada panel, y volcamos a PNG.
//!
//! `cargo run -p llimphi-compositor --example backdrop_blur_demo -- [out.png]`
use std::fs::File;
use std::io::BufWriter;
use llimphi_compositor::{collect_backdrop_blurs, mount, paint, View};
use llimphi_hal::{wgpu, BlurCompositor, Hal};
use llimphi_layout::taffy::prelude::{auto, length, percent, FlexDirection, Size, Style};
use llimphi_layout::taffy::{Position, Rect};
use llimphi_layout::LayoutTree;
use llimphi_raster::peniko::Color;
use llimphi_raster::{vello, Renderer};
use llimphi_text::Typesetter;
const W: u32 = 1200;
const H: u32 = 360;
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const SIGMAS: [f32; 4] = [0.0, 4.0, 8.0, 16.0];
fn rgb(r: u8, g: u8, b: u8) -> Color {
Color::from_rgba8(r, g, b, 255)
}
fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::from_rgba8(r, g, b, a)
}
fn main() {
let out = std::env::args()
.nth(1)
.unwrap_or_else(|| "backdrop_blur.png".to_string());
// Fondo: cuatro franjas verticales saturadas — el blur tiene que mezclar
// los bordes entre franjas, así el efecto se ve aun sin texto/detalle.
let franjas: Vec<View<()>> = [
rgb(231, 76, 60),
rgb(241, 196, 15),
rgb(46, 204, 113),
rgb(52, 152, 219),
]
.iter()
.map(|c| {
View::<()>::new(Style {
size: Size {
width: percent(0.25),
height: percent(1.0),
},
..Default::default()
})
.fill(*c)
})
.collect();
let fondo = View::<()>::new(Style {
size: Size {
width: percent(1.0),
height: percent(1.0),
},
position: Position::Absolute,
inset: Rect {
left: length(0.0),
top: length(0.0),
right: auto(),
bottom: auto(),
},
flex_direction: FlexDirection::Row,
..Default::default()
})
.children(franjas);
// Fila de paneles "vidrio": cada uno apunta a un σ distinto. Todos
// `Position::Absolute` con inset calculado para superponerse al fondo.
// El panel es un rect translúcido sin contenido propio (el blur post-
// pasada borronea TODO lo que está dentro del rect, así que un texto
// *dentro* del panel saldría borroso — limitación documentada del v1).
let panel_w = 240.0_f32;
let panel_h = 220.0_f32;
let gap = 24.0_f32;
let fila_w = SIGMAS.len() as f32 * panel_w + (SIGMAS.len() as f32 - 1.0) * gap;
let inicio_x = (W as f32 - fila_w) * 0.5;
let panel_y = (H as f32 - panel_h) * 0.5;
let mut hijos: Vec<View<()>> = vec![fondo];
for (i, &sigma) in SIGMAS.iter().enumerate() {
let x = inicio_x + i as f32 * (panel_w + gap);
let panel = View::<()>::new(Style {
position: Position::Absolute,
inset: Rect {
left: length(x),
top: length(panel_y),
right: auto(),
bottom: auto(),
},
size: Size {
width: length(panel_w),
height: length(panel_h),
},
..Default::default()
})
.fill(rgba(255, 255, 255, 96))
.radius(20.0)
.border(1.5, rgba(255, 255, 255, 180))
.backdrop_blur(sigma);
hijos.push(panel);
}
let root = View::<()>::new(Style {
size: Size {
width: length(W as f32),
height: length(H as f32),
},
..Default::default()
})
.children(hijos);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, root);
let computed = layout
.compute(mounted.root, (W as f32, H as f32))
.expect("layout");
let mut ts = Typesetter::new();
let mut scene = vello::Scene::new();
paint(&mut scene, &mounted, &computed, &mut ts, None, None);
// Render + post-pase de blur con BlurCompositor — el mismo camino que
// toma `llimphi-ui` durante un redraw real.
let hal = pollster::block_on(Hal::new(None)).expect("hal");
let mut renderer = Renderer::new(&hal).expect("renderer");
let mut blur = BlurCompositor::new(&hal.device);
let target = hal.device.create_texture(&wgpu::TextureDescriptor {
label: Some("dump-blur"),
size: wgpu::Extent3d {
width: W,
height: H,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FMT,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
renderer
.render_to_view(&hal, &scene, &view, W, H, Color::BLACK)
.expect("render_to_view");
let blurs = collect_backdrop_blurs(&mounted, &computed);
let mut encoder = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("blur-demo-encoder"),
});
for b in &blurs {
blur.blur(
&hal.device,
&hal.queue,
&mut encoder,
&view,
(W, H),
b.rect,
b.sigma,
);
}
hal.queue.submit(std::iter::once(encoder.finish()));
write_png(&hal, &target, &out);
eprintln!(
"backdrop_blur_demo: escrito {out} ({W}x{H}) — {} paneles σ={:?}; \
σ=0 queda nítido (el compositor no-op'ea); los demás muestran el \
Gauss separable sobre las franjas. {} blur node(s) detectado(s).",
SIGMAS.len(),
SIGMAS,
blurs.len(),
);
}
fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
let unpadded = (W * 4) as usize;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
let padded = unpadded.div_ceil(align) * align;
let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: (padded * H as usize) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: Some(H),
},
},
wgpu::Extent3d {
width: W,
height: H,
depth_or_array_layers: 1,
},
);
hal.queue.submit(std::iter::once(enc.finish()));
let slice = buf.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
hal.device.poll(wgpu::PollType::wait_indefinitely());
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range();
let mut pixels = Vec::with_capacity((W * H * 4) as usize);
for row in 0..H as usize {
let s = row * padded;
pixels.extend_from_slice(&data[s..s + unpadded]);
}
drop(data);
buf.unmap();
let file = File::create(path).expect("png");
let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut w = enc.write_header().unwrap();
w.write_image_data(&pixels).unwrap();
}