Skip to main content

wisp_storybook/stories/
s_text_shadow_glow.rs

1//! Story: drop shadow + glow on rendered text (M-TEXT.8 / AUT-82).
2//!
3//! Re-uses the existing `wisp::DropShadowFilter` — alpha-extract →
4//! blur → composite — applied to a text texture. Glow is just a
5//! drop shadow with `offset = (0, 0)` and a bright color.
6
7use glam::Vec2;
8use wisp::application::Application;
9use wisp::render::Renderer;
10use wisp::text::{TextTexturePipeline, WispText, WispTextStyle};
11use wisp::{Color, DropShadowFilter, RenderTexture, Sprite, Stage, Texture, WispFontWeight};
12
13use crate::story::Story;
14
15pub fn story() -> Story {
16    Story {
17        id: "text-shadow-glow",
18        category: "Text",
19        title: "Drop shadow + glow on text",
20        milestone: "M-TEXT.8",
21        writeup: include_str!("writeups/text_shadow_glow.md"),
22        build,
23        tick: None,
24    }
25}
26
27fn build(app: &Application, stage: &mut Stage) {
28    let format = wgpu::TextureFormat::Rgba8Unorm;
29    let renderer = Renderer::new(app, format).expect("renderer");
30    let pipeline = TextTexturePipeline::new(app, wgpu::TextureFormat::Rgba8UnormSrgb);
31    let root = stage.root();
32
33    // Paper-white backdrop so the shadow is visible (drop shadows are
34    // invisible against the storybook's near-black clear).
35    let backdrop_rt = RenderTexture::with_format(app, 256, 256, format);
36    let _ = renderer.render_stage(
37        app,
38        backdrop_rt.view(),
39        Color::rgba(0.92, 0.93, 0.95, 1.0),
40        &Stage::new(),
41    );
42    let backdrop_bytes = backdrop_rt.read_pixels(app);
43    let backdrop_tex = Texture::from_rgba(app, 256, 256, &backdrop_bytes);
44    let mut backdrop = Sprite::from_texture(backdrop_tex).with_anchor(Vec2::splat(0.5));
45    backdrop.container.transform.scale = Vec2::splat(2.0);
46    let _ = stage.add_child(root, backdrop);
47
48    // Render text once into a linear-format RT (glyphon writes +y
49    // down; we use the standard `scale.y = -1` flip when sampling
50    // back via Sprite). Filter math wants linear, so we keep the
51    // input RT in `Rgba8Unorm`.
52    let text = WispText::new("Glow").with_style(
53        WispTextStyle::default()
54            .with_size(0.50)
55            .with_weight(WispFontWeight::Bold)
56            .with_color(Color::WHITE),
57    );
58    let text_rt_arc = pipeline.render(app, &text, 512, 384);
59    let text_bytes = text_rt_arc.read_pixels(app);
60    // input_rt has the text bytes baked in directly via render_stage —
61    // sprite anchor at top-left, scale (2, 2) (no flip) so input_rt
62    // has glyphon's +y-down orientation preserved. Filter respects
63    // input orientation; final sprites apply the y-flip when sampling.
64    let staging = Texture::from_rgba(app, 512, 384, &text_bytes);
65    let input_rt = RenderTexture::with_format(app, 512, 384, format);
66    let mut staging_stage = Stage::new();
67    let mut staging_sprite = Sprite::from_texture(staging.clone()).with_anchor(Vec2::new(0.0, 0.0));
68    // Negative-y flips glyphon's +y-down texture into a +y-up RT so
69    // the filter math + final sprite consume the standard orientation.
70    staging_sprite.container.transform.position = Vec2::new(-1.0, 1.0);
71    staging_sprite.container.transform.scale = Vec2::new(2.0, -2.0);
72    let _ = staging_stage.add_child(staging_stage.root(), staging_sprite);
73    let _ = renderer.render_stage(app, input_rt.view(), Color::TRANSPARENT, &staging_stage);
74
75    // ── Drop shadow (offset down/right, dark) ───────────────────
76    let shadow_rt = RenderTexture::with_format(app, 512, 384, format);
77    renderer.apply_filter(
78        app,
79        &DropShadowFilter {
80            offset: Vec2::new(6.0, 6.0),
81            blur: 5.0,
82            color: Color::rgba(0.0, 0.0, 0.0, 0.6),
83        },
84        &input_rt,
85        &shadow_rt,
86    );
87    let shadow_bytes = shadow_rt.read_pixels(app);
88    let shadow_tex = Texture::from_rgba(app, 512, 384, &shadow_bytes);
89    let mut shadow_sprite = Sprite::from_texture(shadow_tex).with_anchor(Vec2::splat(0.5));
90    shadow_sprite.container.transform.position = Vec2::new(-0.45, 0.20);
91    // Negative y-scale flips the glyphon-oriented texture upright.
92    shadow_sprite.container.transform.scale = Vec2::new(0.9, 0.7);
93    let _ = stage.add_child(root, shadow_sprite);
94
95    // ── Glow (zero offset, bright color) ────────────────────────
96    let glow_rt = RenderTexture::with_format(app, 512, 384, format);
97    renderer.apply_filter(
98        app,
99        &DropShadowFilter {
100            offset: Vec2::new(0.0, 0.0),
101            blur: 8.0,
102            color: Color::rgba(1.0, 0.80, 0.30, 1.0),
103        },
104        &input_rt,
105        &glow_rt,
106    );
107    let glow_bytes = glow_rt.read_pixels(app);
108    let glow_tex = Texture::from_rgba(app, 512, 384, &glow_bytes);
109    let mut glow_sprite = Sprite::from_texture(glow_tex).with_anchor(Vec2::splat(0.5));
110    glow_sprite.container.transform.position = Vec2::new(0.45, -0.20);
111    glow_sprite.container.transform.scale = Vec2::new(0.9, 0.7);
112    let _ = stage.add_child(root, glow_sprite);
113}