teksilo-render 0.9.0

wgpu renderer for Teksilo — rect, SDF and path pipelines with a glyph atlas.
Documentation
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech

// Shadow shader — renders soft box shadows using SDF + Gaussian blur approximation.

struct VertexInput {
    @location(0) position: vec2<f32>,
    @location(1) local_uv: vec2<f32>,
    @location(2) shadow_color: vec4<f32>,
    @location(3) corner_radii: vec4<f32>,
    // [shape_width, shape_height, blur_radius, spread]
    @location(4) shadow_params: vec4<f32>,
    // [shape_offset_x, shape_offset_y, 0, 0] — offset of inner shape within the shadow quad
    @location(5) shape_offset: vec4<f32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) local_uv: vec2<f32>,
    @location(1) shadow_color: vec4<f32>,
    @location(2) corner_radii: vec4<f32>,
    @location(3) shadow_params: vec4<f32>,
    @location(4) shape_offset: vec4<f32>,
};

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
    out.local_uv = in.local_uv;
    out.shadow_color = in.shadow_color;
    out.corner_radii = in.corner_radii;
    out.shadow_params = in.shadow_params;
    out.shape_offset = in.shape_offset;
    return out;
}

// Signed distance to a rounded rectangle.
fn sd_rounded_rect(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
    let q = abs(p) - b + vec2<f32>(r, r);
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0, 0.0))) - r;
}

// Approximate Gaussian integral using erf-like function.
// sigma = blur_radius / 2.5 gives a good visual match.
fn gaussian_alpha(dist: f32, sigma: f32) -> f32 {
    // Use smoothstep as a fast approximation of the Gaussian CDF
    // The transition happens over roughly 3*sigma
    return 1.0 - smoothstep(-sigma * 1.5, sigma * 1.5, dist);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let shape_w = in.shadow_params.x;
    let shape_h = in.shadow_params.y;
    let blur_radius = in.shadow_params.z;
    let spread = in.shadow_params.w;

    // The shadow quad is larger than the shape. We need to compute the
    // distance from the current pixel to the inner shape boundary.
    // shape_offset tells us where the inner shape center is within the shadow quad.
    let shadow_quad_size = vec2<f32>(
        shape_w + (blur_radius + spread) * 2.0,
        shape_h + (blur_radius + spread) * 2.0,
    );

    // Map UV to shadow quad coordinates centered at origin
    let p_shadow = (in.local_uv - vec2<f32>(0.5, 0.5)) * shadow_quad_size;

    // Offset to get position relative to inner shape center
    let p = p_shadow - vec2<f32>(in.shape_offset.x, in.shape_offset.y);

    // Inner shape half-size (expanded by spread)
    let half_shape = vec2<f32>((shape_w + spread * 2.0) * 0.5, (shape_h + spread * 2.0) * 0.5);

    // Select corner radius based on quadrant of the inner shape
    var r: f32;
    if (p.x < 0.0) {
        if (p.y < 0.0) {
            r = in.corner_radii.x; // top-left
        } else {
            r = in.corner_radii.w; // bottom-left
        }
    } else {
        if (p.y < 0.0) {
            r = in.corner_radii.y; // top-right
        } else {
            r = in.corner_radii.z; // bottom-right
        }
    }

    let dist = sd_rounded_rect(p, half_shape, r);

    // Use blur_radius to control the softness
    let sigma = max(blur_radius / 2.5, 0.5);
    let alpha = gaussian_alpha(dist, sigma);

    return vec4<f32>(in.shadow_color.rgb, in.shadow_color.a * alpha);
}