Skip to main content

wisp_storybook/stories/
s_text_stroke.rs

1//! Story: stroked text — light glyphs with a dark outline over a
2//! busy / colorful background (M-TEXT.7 / AUT-81).
3//!
4//! Demonstrates the M-TEXT.7 stroke API: a `TextTexturePipeline` renders
5//! the glyphs once, then `stroked_text_sprites` stamps the texture
6//! eight times in stroke color around a central fill stamp. The text
7//! stays readable over the chaotic backdrop without any shader
8//! modification.
9
10use glam::Vec2;
11use wisp::application::Application;
12use wisp::math::Rect;
13use wisp::render::Renderer;
14use wisp::text::{
15    StrokedTextLayer, TextTexturePipeline, WispText, WispTextStyle, stroked_text_sprites,
16};
17use wisp::texture::render_texture::RenderTexture;
18use wisp::{Color, Container, Fill, Graphics, Sprite, Stage, WispFontWeight};
19
20use crate::story::Story;
21
22pub fn story() -> Story {
23    Story {
24        id: "text-stroke",
25        category: "Text",
26        title: "Stroked text on busy backdrop",
27        milestone: "M-TEXT.7",
28        writeup: include_str!("writeups/text_stroke.md"),
29        build,
30        tick: None,
31    }
32}
33
34fn build(app: &Application, stage: &mut Stage) {
35    let root = stage.root();
36
37    // ── Busy backdrop ──────────────────────────────────────────
38    // Pre-render to a RenderTexture so the backdrop ships through the
39    // sprite pipeline (and stays UNDER the text sprites). Graphics
40    // primitives paint AFTER sprites in `render_stage`, so a raw
41    // Graphics backdrop would overpaint the text. See CLAUDE.md
42    // "Renderer batching / draw order".
43    let backdrop_rt = {
44        let renderer = Renderer::new(app, wgpu::TextureFormat::Rgba8UnormSrgb).expect("renderer");
45        let rt = RenderTexture::with_format(app, 256, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
46        let mut bg_stage = Stage::new();
47        let mut bg = Graphics::new();
48        bg.fill(Fill::Solid(Color::rgba_u8(232, 60, 100, 255))); // pink
49        bg.draw_rect(Rect::new(-1.0, -1.0, 1.0, 2.0));
50        bg.fill(Fill::Solid(Color::rgba_u8(255, 220, 60, 255))); // yellow
51        bg.draw_rect(Rect::new(0.0, -1.0, 1.0, 2.0));
52        bg.fill(Fill::Solid(Color::rgba_u8(40, 180, 220, 255))); // cyan strip
53        bg.draw_rounded_rect(Rect::new(-0.85, -0.7, 1.7, 0.45), 0.05);
54        bg.fill(Fill::Solid(Color::rgba_u8(30, 30, 40, 255))); // dark strip
55        bg.draw_rounded_rect(Rect::new(-0.85, 0.15, 1.7, 0.45), 0.05);
56        let _ = bg_stage.add_child(bg_stage.root(), bg);
57        let _ = renderer.render_stage(app, rt.view(), Color::rgba(0.1, 0.1, 0.12, 1.0), &bg_stage);
58        rt
59    };
60    let mut backdrop = Sprite::from_texture(backdrop_rt.as_texture());
61    backdrop.container.transform.position = Vec2::new(-1.0, -1.0);
62    backdrop.container.transform.scale = Vec2::new(2.0, 2.0);
63    let _ = stage.add_child(root, backdrop);
64
65    // ── Rendered text texture ──────────────────────────────────
66    let pipeline = TextTexturePipeline::new(app, wgpu::TextureFormat::Rgba8UnormSrgb);
67    let text = WispText::new("READ ME").with_style(
68        WispTextStyle::default()
69            .with_size(0.22)
70            .with_weight(WispFontWeight::Bold)
71            .with_color(Color::WHITE),
72    );
73    let rt = pipeline.render(app, &text, 1024, 256);
74
75    // ── Stroked + centered (the "with stroke" demo) ────────────
76    let mut layer_container = Container::new();
77    layer_container.transform.position = Vec2::new(0.0, 0.30);
78    layer_container.transform.scale = Vec2::new(1.6, -0.40);
79    let layer_id = stage
80        .add_child(root, layer_container)
81        .expect("attach container");
82
83    let layer = StrokedTextLayer {
84        fill: Color::WHITE,
85        stroke: Color::rgba_u8(10, 10, 18, 255),
86        stroke_width_ndc: 0.04,
87    };
88    for sprite in stroked_text_sprites(&rt, &layer) {
89        let _ = stage.add_child(layer_id, sprite);
90    }
91
92    // ── No-stroke control below — same content, no outline ────
93    let control = WispText::new("no stroke").with_style(
94        WispTextStyle::default()
95            .with_size(0.22)
96            .with_weight(WispFontWeight::Bold)
97            .with_color(Color::WHITE),
98    );
99    let ctl_rt = pipeline.render(app, &control, 1024, 256);
100    let mut ctl_sprite = Sprite::from_texture(ctl_rt.as_texture()).with_anchor(Vec2::splat(0.5));
101    ctl_sprite.container.transform.position = Vec2::new(0.0, -0.40);
102    ctl_sprite.container.transform.scale = Vec2::new(1.6, -0.40);
103    let _ = stage.add_child(root, ctl_sprite);
104}