zenthra-render 0.2.2

Wgpu-based GPU renderer for the Zenthra UI framework
Documentation
struct ScreenUniforms {
    screen_size: vec2<f32>,
    padding: vec2<f32>,
}

@group(0) @binding(0) var<uniform> uniforms: ScreenUniforms;

struct RectInstance {
    @location(0)  pos:           vec2<f32>,
    @location(1)  size:          vec2<f32>,
    @location(2)  color:         vec4<f32>,
    @location(3)  radius:        vec4<f32>,
    @location(4)  border_width:  f32,
    @location(5)  border_color:  vec4<f32>,
    @location(6)  shadow_color:  vec4<f32>,
    @location(7)  shadow_offset: vec2<f32>,
    @location(8)  shadow_blur:   f32,
    @location(9)  clip_rect:     vec4<f32>,
    @location(10) grayscale:     f32,
    @location(11) brightness:    f32,
    @location(12) opacity:       f32,
    @location(13) border_alignment: f32,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0)  color:         vec4<f32>,
    @location(1)  local_pos:     vec2<f32>,
    @location(2)  half_size:     vec2<f32>,
    @location(3)  radius:        vec4<f32>,
    @location(4)  border_width:  f32,
    @location(5)  border_color:  vec4<f32>,
    @location(6)  shadow_color:  vec4<f32>,
    @location(7)  shadow_offset: vec2<f32>,
    @location(8)  shadow_blur:   f32,
    @location(9)  clip_rect:     vec4<f32>,
    @location(10) grayscale:     f32,
    @location(11) brightness:    f32,
    @location(12) opacity:       f32,
    @location(13) border_alignment: f32,
    @location(14) world_pos:      vec2<f32>,
}

@vertex
fn vs_main(
    @builtin(vertex_index) in_vertex_index: u32,
    instance: RectInstance,
) -> VertexOutput {
    var out: VertexOutput;

    var corners = array<vec2<f32>, 6>(
        vec2<f32>(0.0, 0.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(1.0, 1.0),
    );
    let corner = corners[in_vertex_index];

    let expansion    = max(2.0, instance.shadow_blur * 3.0);
    let expanded_size = instance.size + expansion * 2.0;
    let expanded_pos  = instance.pos  - expansion;

    let pixel_pos = expanded_pos + corner * expanded_size;

    let clip_x = (pixel_pos.x / uniforms.screen_size.x) * 2.0 - 1.0;
    let clip_y = 1.0 - (pixel_pos.y / uniforms.screen_size.y) * 2.0;

    out.clip_position = vec4<f32>(clip_x, clip_y, 0.0, 1.0);
    out.half_size     = instance.size * 0.5;
    out.local_pos     = pixel_pos - (instance.pos + out.half_size);

    out.color         = instance.color;
    out.radius        = instance.radius;
    out.border_width  = instance.border_width;
    out.border_color  = instance.border_color;
    out.shadow_color  = instance.shadow_color;
    out.shadow_offset = instance.shadow_offset;
    out.shadow_blur   = instance.shadow_blur;
    out.clip_rect     = instance.clip_rect;
    out.grayscale     = instance.grayscale;
    out.brightness    = instance.brightness;
    out.opacity       = instance.opacity;
    out.border_alignment = instance.border_alignment;
    out.world_pos     = pixel_pos;

    return out;
}

fn sdf_rounded_box(p: vec2<f32>, b: vec2<f32>, r: vec4<f32>) -> f32 {
    let corner = select(
        select(r.w, r.z, p.x > 0.0),
        select(r.x, r.y, p.x > 0.0),
        p.y > 0.0
    );
    let q = abs(p) - b + corner;
    return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - corner;
}

fn gaussian_shadow(d: f32, sigma: f32) -> f32 {
    if d > 0.0 {
        return exp(-0.5 * (d * d) / (sigma * sigma));
    }
    return 1.0;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // 0. Clip
    if in.world_pos.x < in.clip_rect.x ||
       in.world_pos.x > (in.clip_rect.x + in.clip_rect.z) ||
       in.world_pos.y < in.clip_rect.y ||
       in.world_pos.y > (in.clip_rect.y + in.clip_rect.w) {
        discard;
    }

    let r = in.radius;

    // 1. Shadow
    var shadow_alpha = 0.0;
    if in.shadow_blur > 0.1 && in.shadow_color.a > 0.0 {
        let shadow_p = in.local_pos - in.shadow_offset;
        let shadow_d = sdf_rounded_box(shadow_p, in.half_size, r);
        let sigma    = in.shadow_blur * 0.5;
        shadow_alpha = gaussian_shadow(shadow_d, sigma);
    }
    let premul_shadow = vec4<f32>(
        in.shadow_color.rgb * in.shadow_color.a * shadow_alpha,
        in.shadow_color.a * shadow_alpha,
    );

    // 2. Rect SDF + AA
    // Adjust SDF for border alignment: 0=inside, 0.5=center, 1=outside
    let border_offset = in.border_alignment * in.border_width;
    let d             = sdf_rounded_box(in.local_pos, in.half_size, r);
    let d_shifted     = d - border_offset;
    
    let aa_width      = fwidth(d);
    let rect_alpha    = 1.0 - smoothstep(-aa_width, aa_width, d_shifted);

    let premul_fill   = vec4<f32>(in.color.rgb * in.color.a, in.color.a);
    let premul_border = vec4<f32>(in.border_color.rgb * in.border_color.a, in.border_color.a);

    var rect_body = premul_fill;
    if in.border_width > 0.1 {
        // Border exists from d_shifted = -border_width to d_shifted = 0
        let border_factor = smoothstep(-in.border_width - aa_width, -in.border_width + aa_width, d_shifted);
        rect_body = mix(premul_fill, premul_border, border_factor);
    }
    let premul_rect = rect_body * rect_alpha;

    // 3. Composite shadow + rect (premultiplied Over)
    let out_alpha = premul_rect.a + premul_shadow.a * (1.0 - premul_rect.a);
    let out_rgb   = premul_rect.rgb + premul_shadow.rgb * (1.0 - premul_rect.a);
    var final_color = vec4<f32>(out_rgb, out_alpha);

    // 4. Filters
    let gray    = dot(final_color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
    final_color = vec4<f32>(mix(final_color.rgb, vec3<f32>(gray), in.grayscale), final_color.a);
    final_color = vec4<f32>(final_color.rgb * in.brightness, final_color.a * in.opacity);

    if final_color.a <= 0.001 { discard; }

    return final_color;
}