grafo 0.16.0

A GPU-accelerated rendering library for Rust
Documentation
// TODO: This is needed to avoid false positives generated by the repr(C) expansion in compiler 1.89
#![allow(unused)]
/// Example: Analytical box shadow with rounded corners
///
/// Demonstrates an analytical (closed-form) box shadow using a group effect.
/// Instead of blurring a rasterised shape, the shader computes a Gaussian-blurred
/// rounded rectangle mathematically per-pixel using the error function. This runs
/// in a single pass with O(1) cost regardless of blur radius.
///
/// The scene shows several cards with different shadow parameters:
/// - A card with a soft, large-radius shadow
/// - A card with a tight, dark shadow
/// - A card with a colored shadow and an offset
use futures::executor::block_on;
use grafo::{BorderRadii, Shape};
use grafo::{Color, ShapeDrawCommandOptions, Stroke};
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};

const BOX_SHADOW_EFFECT: u64 = 1;

/// Parameters for the analytical box shadow effect.
///
/// All spatial values are in pixel coordinates. The shader converts
/// UV → pixels using `tex_size`, then does all math in pixel space.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct BoxShadowParams {
    /// Top-left corner of the box in pixels
    box_min: [f32; 2],
    /// Bottom-right corner of the box in pixels
    box_max: [f32; 2],
    /// Shadow color (premultiplied alpha RGBA)
    shadow_color: [f32; 4],
    /// Shadow offset in pixels
    offset: [f32; 2],
    /// Blur sigma in pixels (standard deviation of the Gaussian). Larger = softer shadow.
    sigma: f32,
    /// Corner radius in pixels
    corner_radius: f32,
    /// Render target size in pixels (for UV → pixel conversion)
    tex_size: [f32; 2],
    /// Padding to satisfy WGSL struct alignment (struct size must be multiple of 16)
    _pad: [f32; 2],
}

struct CardSpec {
    position: (f32, f32),
    size: (f32, f32),
    corner_radius: f32,
    shadow_sigma: f32,
    shadow_offset: [f32; 2],
    shadow_rgba: [f32; 4],
    card_color: Color,
}

/// Creates a card with a box shadow effect attached directly to it.
///
/// The shadow is computed analytically in the shader and composited behind the
/// card content. No wrapper shape needed — the offscreen texture is full-screen
/// and the effect output is a fullscreen quad, so shadow pixels outside the
/// card's bounds render correctly.
///
/// Returns the node id of the card, so children can be added to it.
fn draw_card(
    renderer: &mut grafo::Renderer,
    card_spec: CardSpec,
    viewport_size: (f32, f32),
) -> usize {
    let (x, y) = card_spec.position;
    let (width, height) = card_spec.size;
    let card_shape = Shape::rounded_rect(
        [(x, y), (x + width, y + height)],
        BorderRadii::new(card_spec.corner_radius),
        Stroke::new(0.0, Color::TRANSPARENT),
    );
    let card = renderer
        .add_shape(
            card_shape,
            None,
            None,
            ShapeDrawCommandOptions::new().color(card_spec.card_color),
        )
        .unwrap();

    let [red, green, blue, alpha] = card_spec.shadow_rgba;
    let params = BoxShadowParams {
        box_min: [x, y],
        box_max: [x + width, y + height],
        shadow_color: [red * alpha, green * alpha, blue * alpha, alpha],
        offset: card_spec.shadow_offset,
        sigma: card_spec.shadow_sigma,
        corner_radius: card_spec.corner_radius,
        tex_size: [viewport_size.0, viewport_size.1],
        _pad: [0.0, 0.0],
    };

    renderer
        .set_group_effect(card, BOX_SHADOW_EFFECT, bytemuck::bytes_of(&params))
        .expect("Failed to set box shadow effect");

    card
}

/// The analytical box shadow WGSL shader.
///
/// All geometry is passed in pixel coordinates. The shader converts UV → pixels
/// first, then uses the error function to compute the exact integral of a Gaussian
/// over a rounded rectangle. Single pass, O(1) cost regardless of blur radius.
const BOX_SHADOW_WGSL: &str = r#"
struct Params {
    box_min: vec2<f32>,
    box_max: vec2<f32>,
    shadow_color: vec4<f32>,
    offset: vec2<f32>,
    sigma: f32,
    corner_radius: f32,
    tex_size: vec2<f32>,
    _pad: vec2<f32>,
}
@group(1) @binding(0) var<uniform> params: Params;

// Approximate erf (Abramowitz & Stegun 7.1.26), max error ~1.5e-7.
fn erf_approx(x: f32) -> f32 {
    let s = sign(x);
    let a = abs(x);
    let t = 1.0 / (1.0 + 0.3275911 * a);
    let poly = ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t
        - 0.284496736) * t + 0.254829592) * t;
    return s * (1.0 - poly * exp(-a * a));
}

// Integral of a 1D Gaussian (mean=0, std=sigma) over [lo, hi]. Returns [0, 1].
fn gauss_integral(lo: f32, hi: f32, sigma: f32) -> f32 {
    let s = sigma * 1.4142135; // sigma * sqrt(2)
    return 0.5 * (erf_approx(hi / s) - erf_approx(lo / s));
}

// Signed distance from point p to a rounded box centred at the origin,
// with half-extents `half` and corner radius `r`.
fn sd_rounded_box(p: vec2<f32>, half: vec2<f32>, r: f32) -> f32 {
    let q = abs(p) - half + vec2<f32>(r);
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r;
}

@fragment
fn effect_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    // The original composited group (the card itself)
    let original = textureSample(t_input, s_input, uv);

    // Convert UV [0,1] → pixel coordinates
    let pixel_pos = uv * params.tex_size;

    // Box center and half-extents in pixel space, with shadow offset applied
    let center = (params.box_min + params.box_max) * 0.5 + params.offset;
    let half = (params.box_max - params.box_min) * 0.5;

    let sigma = max(params.sigma, 0.0001);

    // Position relative to the shadow box center, in pixels
    let p = pixel_pos - center;

    // Separable Gaussian integral over the rectangle [-half, +half].
    // This is exact for a sharp (non-rounded) box in pixel space.
    let rect_shadow = gauss_integral(-half.x - p.x, half.x - p.x, sigma)
                    * gauss_integral(-half.y - p.y, half.y - p.y, sigma);

    // Corner rounding: use the rounded-box SDF through a Gaussian CDF.
    // Gaussian CDF = 0.5 * (1 + erf(-d / (sigma * sqrt(2))))
    let d = sd_rounded_box(p, half, params.corner_radius);
    let corner_factor = 0.5 * (1.0 + erf_approx(-d / (sigma * 1.4142135)));

    // Blend: for large sigma, the rect integral provides a smooth shape
    // and the corner correction improves the corners. For very small sigma
    // (nearly hard shadow), the SDF-based approach is more accurate.
    let blend = smoothstep(0.0, 2.0, sigma);
    let shadow_alpha = mix(corner_factor, rect_shadow, blend);

    let shadow = params.shadow_color * shadow_alpha;

    // Composite: original (card) OVER shadow.
    // Premultiplied alpha "over": result = src + dst * (1 - src.a)
    return original + shadow * (1.0 - original.a);
}
"#;

#[derive(Default)]
struct App<'a> {
    window: Option<Arc<Window>>,
    renderer: Option<grafo::Renderer<'a>>,
}

impl<'a> ApplicationHandler for App<'a> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window = Arc::new(
            event_loop
                .create_window(
                    Window::default_attributes().with_title("Grafo – Analytical Box Shadow"),
                )
                .unwrap(),
        );

        let window_size = window.inner_size();
        let scale_factor = window.scale_factor();
        let physical_size = (window_size.width, window_size.height);

        let mut renderer = block_on(grafo::Renderer::new(
            window.clone(),
            physical_size,
            scale_factor,
            true,
            false,
            1,
        ));

        // Load the box shadow effect shader once
        renderer
            .load_effect(BOX_SHADOW_EFFECT, &[BOX_SHADOW_WGSL])
            .expect("Failed to compile box shadow effect");

        self.window = Some(window);
        self.renderer = Some(renderer);
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    ) {
        let Some(window) = &self.window else { return };
        let Some(renderer) = &mut self.renderer else {
            return;
        };

        if window_id != window.id() {
            return;
        }

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::Resized(physical_size) => {
                let new_size = (physical_size.width, physical_size.height);
                renderer.resize(new_size);
                window.request_redraw();
            }
            WindowEvent::RedrawRequested => {
                let (pw, ph) = renderer.size();
                let pw = pw as f32;
                let ph = ph as f32;

                // ── Scene background ─────────────────────────────────────
                let scene_bg =
                    Shape::rect([(0.0, 0.0), (pw, ph)], Stroke::new(0.0, Color::TRANSPARENT));
                renderer
                    .add_shape(
                        scene_bg,
                        None,
                        None,
                        ShapeDrawCommandOptions::new().color(Color::rgb(235, 235, 240)),
                    )
                    .unwrap();

                let vp = (pw, ph);

                // ── Card 1: Soft, large shadow ───────────────────────────
                draw_card(
                    renderer,
                    CardSpec {
                        position: (80.0, 60.0),
                        size: (280.0, 180.0),
                        corner_radius: 16.0,
                        shadow_sigma: 12.0,
                        shadow_offset: [0.0, 4.0],
                        shadow_rgba: [0.0, 0.0, 0.0, 0.35],
                        card_color: Color::WHITE,
                    },
                    vp,
                );

                // ── Card 2: Tight, dark shadow ───────────────────────────
                draw_card(
                    renderer,
                    CardSpec {
                        position: (420.0, 60.0),
                        size: (280.0, 180.0),
                        corner_radius: 8.0,
                        shadow_sigma: 4.0,
                        shadow_offset: [0.0, 2.0],
                        shadow_rgba: [0.0, 0.0, 0.0, 0.6],
                        card_color: Color::WHITE,
                    },
                    vp,
                );

                // ── Card 3: Colored shadow with offset ───────────────────
                draw_card(
                    renderer,
                    CardSpec {
                        position: (80.0, 320.0),
                        size: (280.0, 180.0),
                        corner_radius: 20.0,
                        shadow_sigma: 16.0,
                        shadow_offset: [8.0, 8.0],
                        shadow_rgba: [0.2, 0.0, 0.5, 0.4],
                        card_color: Color::rgb(240, 245, 255),
                    },
                    vp,
                );

                // ── Card 4: Subtle elevation shadow ──────────────────────
                draw_card(
                    renderer,
                    CardSpec {
                        position: (420.0, 320.0),
                        size: (280.0, 180.0),
                        corner_radius: 12.0,
                        shadow_sigma: 8.0,
                        shadow_offset: [0.0, 6.0],
                        shadow_rgba: [0.0, 0.0, 0.0, 0.2],
                        card_color: Color::rgb(255, 250, 240),
                    },
                    vp,
                );

                // ── Render ───────────────────────────────────────────────
                match renderer.render() {
                    Ok(_) => {
                        renderer.clear_draw_queue();
                    }
                    Err(wgpu::SurfaceError::Lost) => renderer.resize(renderer.size()),
                    Err(wgpu::SurfaceError::OutOfMemory) => event_loop.exit(),
                    Err(e) => eprintln!("{e:?}"),
                }
            }
            _ => {}
        }
    }
}

pub fn main() {
    env_logger::init();
    let event_loop = EventLoop::new().expect("To create the event loop");

    let mut app = App::default();
    let _ = event_loop.run_app(&mut app);
}