tessera-components 0.0.0

Basic components for tessera-ui, using md3e design principles.
Documentation
struct ShapeUniforms {
    corner_radii: vec4f,       // x:tl, y:tr, z:br, w:bl
    corner_g2: vec4f,          // x:tl, y:tr, z:br, w:bl
    primary_color: vec4f,
    border_color: vec4f,
    shadow_ambient_color: vec4f,
    shadow_ambient_params: vec3f,
    shadow_spot_color: vec4f,
    shadow_spot_params: vec3f,
    render_mode: f32,
    ripple_params: vec4f,
    ripple_color: vec4f,
    border_width: f32,
    position: vec4f,           // x, y, width, height
    screen_size: vec2f,
};

const MODE_FILL: f32 = 0.0;
const MODE_OUTLINE: f32 = 1.0;
const MODE_SHADOW: f32 = 2.0;
const MODE_RIPPLE_FILL: f32 = 3.0;
const MODE_RIPPLE_OUTLINE: f32 = 4.0;
const MODE_RIPPLE_FILLED_OUTLINE: f32 = 5.0;

const EPS_DISCARD: f32 = 0.001;
const SHADOW_AA_MARGIN_PX: f32 = 1.0;

struct ShapeInstances {
    instances: array<ShapeUniforms>,
};

@group(0) @binding(0)
var<storage, read> uniforms: ShapeInstances;

struct VertexInput {
    @location(0) position: vec2f,
    @builtin(instance_index) instance_index: u32,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4f,
    @location(0) local_pos: vec2f, // Normalized object space (scaled in fragment)
    @location(1) @interpolate(flat) instance_index: u32,
};

fn shadow_layer_pad(color: vec4f, params: vec3f) -> vec2f {
    if color.a <= 0.0 {
        return vec2f(0.0, 0.0);
    }

    let offset = params.xy;
    let smoothness = params.z;
    return vec2f(abs(offset.x), abs(offset.y)) + vec2f(smoothness, smoothness);
}

fn shadow_pad(instance: ShapeUniforms) -> vec2f {
    var pad = vec2f(0.0, 0.0);
    pad = max(pad, shadow_layer_pad(instance.shadow_ambient_color, instance.shadow_ambient_params));
    pad = max(pad, shadow_layer_pad(instance.shadow_spot_color, instance.shadow_spot_params));
    return pad + vec2f(SHADOW_AA_MARGIN_PX, SHADOW_AA_MARGIN_PX);
}

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
    let instance = uniforms.instances[in.instance_index];
    let screen_dimensions = instance.screen_size;

    // `in.position` is the unit quad vertex (0,0 to 1,1).
    // `instance.position` is the rect (x, y, width, height) in pixels.
    let instance_pos = instance.position.xy;
    let instance_size = instance.position.zw;

    var pad = vec2f(0.0, 0.0);
    if instance.render_mode == MODE_SHADOW {
        pad = shadow_pad(instance);
    }

    let expanded_size = instance_size + pad * 2.0;

    // Calculate the vertex's final pixel position.
    let pixel_pos = instance_pos - pad + in.position * expanded_size;

    // Convert final pixel position to NDC for the rasterizer.
    let clip_pos = vec2<f32>(
        (pixel_pos.x / screen_dimensions.x) * 2.0 - 1.0,
        (pixel_pos.y / screen_dimensions.y) * -2.0 + 1.0
    );

    var out: VertexOutput;
    out.clip_position = vec4<f32>(clip_pos, 0.0, 1.0);

    // local_pos is normalized object space (so `local_pos * size` yields pixels).
    // For shadows, expand the normalized range so the SDF can cover pixels
    // outside the layout box.
    if instance.render_mode == MODE_SHADOW {
        let safe_size = max(instance_size, vec2f(1.0, 1.0));
        out.local_pos = (in.position * expanded_size - pad) / safe_size - 0.5;
    } else {
        out.local_pos = in.position - 0.5; // Center around 0 for SDF
    }
    out.instance_index = in.instance_index;
    return out;
}

// p: point to sample (in object space, centered at 0,0)
// b: half-size of the box
// r: corner radii (tl, tr, br, bl) -> (x, y, z, w)
// k: exponent for p-norm per corner
fn sdf_g2_rounded_box(p: vec2f, b: vec2f, r: vec4f, k: vec4f) -> f32 {
    // Select radius based on the quadrant p is in.
    // In our local coordinates, p.y is negative for the top half and positive for the bottom half.
    var radius: f32;
    var corner_k: f32;
    if (p.y < 0.0) { // Top half
        if (p.x < 0.0) { // Top-Left
            radius = r.x;
            corner_k = k.x;
        } else { // Top-Right
            radius = r.y;
            corner_k = k.y;
        }
    } else { // Bottom half
        if (p.x < 0.0) { // Bottom-Left
            radius = r.w;
            corner_k = k.w;
        } else { // Bottom-Right
            radius = r.z;
            corner_k = k.z;
        }
    }

    let q = abs(p) - b + radius;

    let v_x = max(q.x, 0.0);
    let v_y = max(q.y, 0.0);

    var dist_corner_shape: f32;
    if abs(corner_k - 2.0) < 0.001 {
        dist_corner_shape = length(vec2f(v_x, v_y));
    } else {
        if v_x == 0.0 && v_y == 0.0 {
            dist_corner_shape = 0.0;
        } else {
            dist_corner_shape = pow(pow(v_x, corner_k) + pow(v_y, corner_k), 1.0 / corner_k);
        }
    }

    return dist_corner_shape + min(max(q.x, q.y), 0.0) - radius;
}

fn is_ellipse(corner_radii: vec4f) -> bool {
    return corner_radii.x < 0.0;
}

fn sdf_shape(p: vec2f, half_size: vec2f, corner_radii: vec4f, corner_g2: vec4f) -> f32 {
    if is_ellipse(corner_radii) {
        return sdf_ellipse(p, half_size);
    }
    return sdf_g2_rounded_box(p, half_size, corner_radii, corner_g2);
}

// SDF for an ellipse
// p: point to sample
// r: radii of the ellipse
fn sdf_ellipse(p: vec2f, r: vec2f) -> f32 {
    if r.x <= 0.0 || r.y <= 0.0 {
        // Return a large value to prevent rendering if radii are invalid
        return 1.0e6;
    }
    // Scales the distance to be in pixel units, which is important for anti-aliasing.
    return (length(p / r) - 1.0) * min(r.x, r.y);
}

fn aa_mask(dist: f32) -> f32 {
    let aa = fwidth(dist);
    return 1.0 - smoothstep(-aa, aa, dist);
}

fn outline_mask(dist: f32, border_width: f32) -> f32 {
    let aa = fwidth(dist);
    let outer = 1.0 - smoothstep(-aa, aa, dist);
    let inner = 1.0 - smoothstep(-aa, aa, dist + border_width);
    return max(0.0, outer - inner);
}

fn shadow_layer_alpha(
    p_object: vec2f,
    half_size: vec2f,
    corner_radii: vec4f,
    corner_g2: vec4f,
    offset: vec2f,
    smoothness: f32,
) -> f32 {
    let dist = sdf_shape(p_object - offset, half_size, corner_radii, corner_g2);
    let mask = aa_mask(dist);
    let soft = smoothstep(smoothness, 0.0, dist);
    return mask * soft;
}

// Calculate ripple effect based on distance from ripple center
fn calculate_ripple_mask(dist_to_center: f32, ripple_radius: f32, aa: f32) -> f32 {
    if ripple_radius <= 0.0 {
        return 0.0;
    }

    // Filled circle that expands over time, with a soft edge.
    return 1.0 - smoothstep(ripple_radius - aa, ripple_radius + aa, dist_to_center);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
    let instance = uniforms.instances[in.instance_index];
    let mode = instance.render_mode;

    let size = instance.position.zw;
    let half_size = size * 0.5;

    let p_normalized = in.local_pos;
    let p_object = p_normalized * size;

    var final_color: vec4f;

    if mode == MODE_SHADOW {
        var ambient_alpha: f32 = 0.0;
        if instance.shadow_ambient_color.a > 0.0 {
            ambient_alpha = shadow_layer_alpha(
                p_object,
                half_size,
                instance.corner_radii,
                instance.corner_g2,
                instance.shadow_ambient_params.xy,
                instance.shadow_ambient_params.z,
            );
        }

        var spot_alpha: f32 = 0.0;
        if instance.shadow_spot_color.a > 0.0 {
            spot_alpha = shadow_layer_alpha(
                p_object,
                half_size,
                instance.corner_radii,
                instance.corner_g2,
                instance.shadow_spot_params.xy,
                instance.shadow_spot_params.z,
            );
        }

        let ambient_a = ambient_alpha * instance.shadow_ambient_color.a;
        let spot_a = spot_alpha * instance.shadow_spot_color.a;
        let out_a = ambient_a + spot_a * (1.0 - ambient_a);
        if out_a <= EPS_DISCARD {
            discard;
        }

        let out_pm_rgb = instance.shadow_ambient_color.rgb * ambient_a
            + instance.shadow_spot_color.rgb * spot_a * (1.0 - ambient_a);
        final_color = vec4f(out_pm_rgb / out_a, out_a);
    } else {
        let dist = sdf_shape(p_object, half_size, instance.corner_radii, instance.corner_g2);
        let shape_mask = aa_mask(dist);

        if mode == MODE_FILL {
            if shape_mask <= EPS_DISCARD {
                discard;
            }
            final_color = vec4f(instance.primary_color.rgb, instance.primary_color.a * shape_mask);
        } else if mode == MODE_OUTLINE {
            if instance.border_width <= 0.0 {
                discard;
            }
            let mask = outline_mask(dist, instance.border_width);
            if mask <= EPS_DISCARD {
                discard;
            }
            final_color = vec4f(instance.primary_color.rgb, instance.primary_color.a * mask);
        } else if mode == MODE_RIPPLE_FILL || mode == MODE_RIPPLE_OUTLINE || mode == MODE_RIPPLE_FILLED_OUTLINE {
            var base_rgb: vec3f;
            var base_a: f32;

            if mode == MODE_RIPPLE_FILL {
                base_rgb = instance.primary_color.rgb;
                base_a = instance.primary_color.a * shape_mask;
            } else if mode == MODE_RIPPLE_OUTLINE {
                if instance.border_width <= 0.0 {
                    discard;
                }
                let mask = outline_mask(dist, instance.border_width);
                base_rgb = instance.primary_color.rgb;
                base_a = instance.primary_color.a * mask;
            } else {
                if instance.border_width <= 0.0 {
                    if shape_mask <= EPS_DISCARD {
                        discard;
                    }
                    base_rgb = instance.primary_color.rgb;
                    base_a = instance.primary_color.a * shape_mask;
                } else {
                    let dist_inner_edge = dist + instance.border_width;
                    let aa = fwidth(dist);
                    let t = smoothstep(-aa, aa, dist_inner_edge);
                    base_rgb = mix(instance.primary_color.rgb, instance.border_color.rgb, t);
                    base_a = mix(instance.primary_color.a, instance.border_color.a, t) * shape_mask;
                }
            }

            let ripple_center = instance.ripple_params.xy;
            let ripple_radius = instance.ripple_params.z;
            let ripple_alpha = instance.ripple_params.w;
            let ripple_bounded = instance.ripple_color.a > 0.5;

            let p_pixel = p_normalized * size;
            let center_pixel = ripple_center * size;
            let dist_to_center_pixel = distance(p_pixel, center_pixel);
            let min_dimension = min(size.x, size.y);
            let dist_norm = dist_to_center_pixel / max(min_dimension, 1.0);
            let aa_ripple = max(fwidth(dist_norm), EPS_DISCARD);
            let ripple_mask = calculate_ripple_mask(dist_norm, ripple_radius, aa_ripple);
            let bounded_mask = select(1.0, shape_mask, ripple_bounded);
            let overlay_a = clamp(ripple_mask * ripple_alpha * bounded_mask, 0.0, 1.0);

            let out_a = overlay_a + base_a * (1.0 - overlay_a);
            if out_a <= EPS_DISCARD {
                discard;
            }

            let out_pm_rgb = instance.ripple_color.rgb * overlay_a + base_rgb * base_a * (1.0 - overlay_a);
            final_color = vec4f(out_pm_rgb / out_a, out_a);
        } else {
            discard;
        }
    }

    return vec4f(final_color.rgb * final_color.a, final_color.a);
}