#![allow(unused)]
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;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct BoxShadowParams {
box_min: [f32; 2],
box_max: [f32; 2],
shadow_color: [f32; 4],
offset: [f32; 2],
sigma: f32,
corner_radius: f32,
tex_size: [f32; 2],
_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,
}
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(¶ms))
.expect("Failed to set box shadow effect");
card
}
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,
));
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;
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);
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,
);
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,
);
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,
);
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,
);
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);
}