Skip to main content

wisp_storybook/stories/
s_text_composition.rs

1//! Story: text composed with filters + non-Normal blend modes
2//! (M-TEXT.6 / AUT-80).
3
4use glam::Vec2;
5use wisp::application::Application;
6use wisp::blend::BlendMode;
7use wisp::filter::ColorMatrixFilter;
8use wisp::render::Renderer;
9use wisp::text::{TextTexturePipeline, WispText, WispTextStyle};
10use wisp::texture::render_texture::RenderTexture;
11use wisp::{Color, Sprite, Stage, WispFontWeight};
12
13use crate::story::Story;
14
15pub fn story() -> Story {
16    Story {
17        id: "text-composition",
18        category: "Scene Graph",
19        title: "Text + filter + blend composition",
20        milestone: "M-TEXT.6",
21        writeup: include_str!("writeups/text_composition.md"),
22        build,
23        tick: None,
24    }
25}
26
27fn build(app: &Application, stage: &mut Stage) {
28    let pipeline = TextTexturePipeline::new(app, wgpu::TextureFormat::Rgba8UnormSrgb);
29
30    // Backdrop sprite — a warm gradient-y RT we draw into via a stage
31    // render. Demonstrates blend modes need something to blend against.
32    let backdrop_rt = {
33        let renderer = Renderer::new(app, wgpu::TextureFormat::Rgba8UnormSrgb).expect("renderer");
34        let rt = RenderTexture::with_format(app, 256, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
35        let empty = Stage::new();
36        let _ = renderer.render_stage(app, rt.view(), Color::rgba(0.85, 0.35, 0.25, 1.0), &empty);
37        rt
38    };
39    let mut backdrop = Sprite::from_texture(backdrop_rt.as_texture());
40    backdrop.container.transform.position = Vec2::new(-1.0, -1.0);
41    backdrop.container.transform.scale = Vec2::new(2.0, 2.0);
42    let _ = stage.add_child(stage.root(), backdrop);
43
44    // Top half: white text at Normal blend — the baseline.
45    let normal_text = WispText::new("Normal").with_style(
46        WispTextStyle::default()
47            .with_size(0.35)
48            .with_weight(WispFontWeight::Bold)
49            .with_color(Color::WHITE),
50    );
51    let normal_rt = pipeline.render(app, &normal_text, 512, 192);
52    let mut normal_sprite =
53        Sprite::from_texture(normal_rt.as_texture()).with_anchor(Vec2::new(0.5, 0.5));
54    normal_sprite.container.transform.position = Vec2::new(0.0, 0.5);
55    normal_sprite.container.transform.scale = Vec2::new(1.4, -0.45);
56    normal_sprite.container.blend_mode = BlendMode::Normal;
57    let _ = stage.add_child(stage.root(), normal_sprite);
58
59    // Middle: text at Subtract — `dst - src` clamps to a dark stamp
60    // where the glyph alpha is high. Picked over Multiply because
61    // Multiply with white text against a warm backdrop gives back
62    // the backdrop color (invisible).
63    let sub_text = WispText::new("Subtract").with_style(
64        WispTextStyle::default()
65            .with_size(0.35)
66            .with_weight(WispFontWeight::Bold)
67            .with_color(Color::WHITE),
68    );
69    let sub_rt = pipeline.render(app, &sub_text, 512, 192);
70    let mut sub_sprite = Sprite::from_texture(sub_rt.as_texture()).with_anchor(Vec2::new(0.5, 0.5));
71    sub_sprite.container.transform.position = Vec2::new(0.0, 0.0);
72    sub_sprite.container.transform.scale = Vec2::new(1.4, -0.45);
73    sub_sprite.container.blend_mode = BlendMode::Subtract;
74    let _ = stage.add_child(stage.root(), sub_sprite);
75
76    // Bottom: text → grayscale color-matrix → sprite. Demonstrates a
77    // filter chain feeding the sprite pipeline. The text is colored
78    // orange in the source but the filter erases the hue.
79    let orange_text = WispText::new("Filtered").with_style(
80        WispTextStyle::default()
81            .with_size(0.35)
82            .with_weight(WispFontWeight::Bold)
83            .with_color(Color::rgba(1.0, 0.4, 0.1, 1.0)),
84    );
85    let source = pipeline.render(app, &orange_text, 512, 192);
86    let filtered = RenderTexture::with_format(app, 512, 192, wgpu::TextureFormat::Rgba8UnormSrgb);
87    {
88        let renderer = Renderer::new(app, wgpu::TextureFormat::Rgba8UnormSrgb).expect("renderer");
89        renderer.apply_filter(app, &ColorMatrixFilter::grayscale(), &source, &filtered);
90    }
91    let mut filtered_sprite =
92        Sprite::from_texture(filtered.as_texture()).with_anchor(Vec2::new(0.5, 0.5));
93    filtered_sprite.container.transform.position = Vec2::new(0.0, -0.5);
94    filtered_sprite.container.transform.scale = Vec2::new(1.4, -0.45);
95    let _ = stage.add_child(stage.root(), filtered_sprite);
96}