Skip to main content

wisp_storybook/stories/
s_motion_blur.rs

1//! Story: `MotionBlurFilter` — diagonal-velocity smear (M0.18).
2
3use glam::Vec2;
4use wisp::application::Application;
5use wisp::render::Renderer;
6use wisp::{Color, MotionBlurFilter, RenderTexture, Sprite, Stage, Texture};
7
8use crate::story::Story;
9
10pub fn story() -> Story {
11    Story {
12        id: "filter-motion-blur",
13        category: "Filters",
14        title: "Motion blur",
15        milestone: "M0.18",
16        writeup: include_str!("writeups/motion_blur.md"),
17        build,
18        tick: None,
19    }
20}
21
22fn build(app: &Application, stage: &mut Stage) {
23    let format = wgpu::TextureFormat::Rgba8Unorm;
24    let renderer = Renderer::new(app, format).expect("renderer");
25    let source = dot_texture(app);
26
27    // Source on the left.
28    let mut sharp = Sprite::from_texture(source.clone()).with_anchor(Vec2::splat(0.5));
29    sharp.container.transform.position = Vec2::new(-0.5, 0.0);
30    sharp.container.transform.scale = Vec2::splat(0.35);
31    let _ = stage.add_child(stage.root(), sharp);
32
33    // Motion-blurred copy on the right.
34    let input = RenderTexture::with_format(app, 256, 256, format);
35    let output = RenderTexture::with_format(app, 256, 256, format);
36    let mut sprite_stage = Stage::new();
37    let mut sprite = Sprite::from_texture(source).with_anchor(Vec2::splat(0.5));
38    sprite.container.transform.scale = Vec2::splat(0.95);
39    let _ = sprite_stage.add_child(sprite_stage.root(), sprite);
40    let _ = renderer.render_stage(app, input.view(), Color::BLACK, &sprite_stage);
41
42    // Dramatic kernel so the static-frame PNG shows the smear
43    // clearly. At 14 px the trail was barely visible on the 96-px
44    // dot; 60 px gives a clear comet shape.
45    let filter = MotionBlurFilter {
46        velocity: Vec2::new(900.0, 600.0),
47        peak_velocity_pps: 1100.0,
48        max_kernel_px: 60.0,
49    };
50    renderer.apply_filter(app, &filter, &input, &output);
51
52    let bytes = output.read_pixels(app);
53    let blurred_texture = Texture::from_rgba(app, 256, 256, &bytes);
54    let mut blurred = Sprite::from_texture(blurred_texture).with_anchor(Vec2::splat(0.5));
55    blurred.container.transform.position = Vec2::new(0.5, 0.0);
56    blurred.container.transform.scale = Vec2::splat(0.35);
57    let _ = stage.add_child(stage.root(), blurred);
58}
59
60fn dot_texture(app: &Application) -> Texture {
61    let size = 96u32;
62    let mut bytes = Vec::with_capacity((size * size * 4) as usize);
63    let center = (
64        i32::try_from(size / 2).unwrap_or(48),
65        i32::try_from(size / 2).unwrap_or(48),
66    );
67    let radius_sq = 28 * 28;
68    for y in 0..size {
69        for x in 0..size {
70            let dx = i32::try_from(x).unwrap_or(0) - center.0;
71            let dy = i32::try_from(y).unwrap_or(0) - center.1;
72            if dx * dx + dy * dy < radius_sq {
73                bytes.extend_from_slice(&[255, 200, 80, 255]);
74            } else {
75                bytes.extend_from_slice(&[20, 20, 30, 255]);
76            }
77        }
78    }
79    Texture::from_rgba(app, size, size, &bytes)
80}