struct ScreenUniforms {
screen_size: vec2<f32>,
padding: vec2<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: ScreenUniforms;
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
@group(1) @binding(1) var s_diffuse: sampler;
struct ImageInstance {
@location(0) pos: vec2<f32>,
@location(1) size: vec2<f32>,
@location(2) radius: vec4<f32>,
@location(3) border_width: f32,
@location(4) border_color: vec4<f32>,
@location(5) shadow_color: vec4<f32>,
@location(6) shadow_offset: vec2<f32>,
@location(7) shadow_blur: f32,
@location(8) clip_rect: vec4<f32>,
@location(9) grayscale: f32,
@location(10) brightness: f32,
@location(11) opacity: f32,
@location(12) uv_rect: vec4<f32>, // (u_start, v_start, u_size, v_size)
@location(13) bg_color: vec4<f32>,
@location(14) rotation: vec3<f32>,
@location(15) flip: vec2<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) local_pos: vec2<f32>,
@location(1) half_size: vec2<f32>,
@location(2) radius: vec4<f32>,
@location(3) border_width: f32,
@location(4) border_color: vec4<f32>,
@location(5) shadow_color: vec4<f32>,
@location(6) shadow_offset: vec2<f32>,
@location(7) shadow_blur: f32,
@location(8) clip_rect: vec4<f32>,
@location(9) grayscale: f32,
@location(10) brightness: f32,
@location(11) opacity: f32,
@location(12) uv_rect: vec4<f32>,
@location(13) uv: vec2<f32>,
@location(14) bg_color: vec4<f32>,
@location(15) world_pos: vec2<f32>,
}
@vertex
fn vs_main(
@builtin(vertex_index) in_vertex_index: u32,
instance: ImageInstance,
) -> 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);
// 1. Calculate Local Vertex Position (centred at 0,0 for rotation)
// We add expansion to the quad size to accommodate the shadow blur
let quad_size = instance.size + expansion * 2.0;
let local_v_pos = (corner - 0.5) * quad_size;
// 2. Apply 3D Rotation
// rotation: (x_tilt, y_tilt, z_rot)
let rot = instance.rotation;
// Rotation Z (2D Rotation)
let sz = sin(rot.z); let cz = cos(rot.z);
let rxz = local_v_pos.x * cz - local_v_pos.y * sz;
let ryz = local_v_pos.x * sz + local_v_pos.y * cz;
var rotated_pos = vec3<f32>(rxz, ryz, 0.0);
// Rotation X (Tilt forward/back)
let sx = sin(rot.x); let cx = cos(rot.x);
let ryx = rotated_pos.y * cx - rotated_pos.z * sx;
let rzx = rotated_pos.y * sx + rotated_pos.z * cx;
rotated_pos.y = ryx;
rotated_pos.z = rzx;
// Rotation Y (Tilt side-to-side)
let sy = sin(rot.y); let cy = cos(rot.y);
let rxy = rotated_pos.x * cy + rotated_pos.z * sy;
let rzy = -rotated_pos.x * sy + rotated_pos.z * cy;
rotated_pos.x = rxy;
rotated_pos.z = rzy;
// 3. Simple Perspective Projection
// We move the camera back a bit to see the tilt effect
let dist = 1000.0;
let perspective = dist / (dist - rotated_pos.z);
let final_local_pos = rotated_pos.xy * perspective;
// 4. Transform to Screen Coordinates
let center_pos = instance.pos + instance.size * 0.5;
let pixel_pos = center_pos + final_local_pos;
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;
// local_pos for SDF needs to be the UN-ROTATED, UN-EXPANDED position relative to image center
// But we need to handle the scale/perspective too.
// For now, we'll use the corner-based mapping which is more robust for SDFs with rotation.
out.local_pos = (corner - 0.5) * quad_size;
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.bg_color = instance.bg_color;
out.uv_rect = instance.uv_rect;
// 5. UV Mapping with Flipping
// We only map UVs for the actual image rect, excluding expansion
let img_norm = (out.local_pos / instance.size) + 0.5;
// Apply Flip
let flipped_norm = (img_norm - 0.5) * instance.flip + 0.5;
out.uv = vec2<f32>(
instance.uv_rect.x + flipped_norm.x * instance.uv_rect.z,
instance.uv_rect.y + flipped_norm.y * instance.uv_rect.w
);
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 sample_alpha(uv: vec2<f32>, uv_rect: vec4<f32>) -> f32 {
let min_uv = uv_rect.xy;
let max_uv = uv_rect.xy + uv_rect.zw;
if uv.x >= min_uv.x && uv.x <= max_uv.x && uv.y >= min_uv.y && uv.y <= max_uv.y {
return textureSampleLevel(t_diffuse, s_diffuse, uv, 0.0).a;
}
return 0.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 img_size = in.half_size * 2.0;
let uv_scale = in.uv_rect.zw / img_size;
let uv_offset = in.shadow_offset * uv_scale;
let sigma = in.shadow_blur * 0.5;
let dx = sigma * uv_scale.x;
let dy = sigma * uv_scale.y;
let center_uv = in.uv - uv_offset;
let t00 = sample_alpha(center_uv, in.uv_rect);
let t10 = sample_alpha(center_uv + vec2<f32>(-dx, 0.0), in.uv_rect);
let t20 = sample_alpha(center_uv + vec2<f32>(dx, 0.0), in.uv_rect);
let t01 = sample_alpha(center_uv + vec2<f32>(0.0, -dy), in.uv_rect);
let t02 = sample_alpha(center_uv + vec2<f32>(0.0, dy), in.uv_rect);
let t11 = sample_alpha(center_uv + vec2<f32>(-dx, -dy), in.uv_rect);
let t22 = sample_alpha(center_uv + vec2<f32>(dx, dy), in.uv_rect);
let t12 = sample_alpha(center_uv + vec2<f32>(-dx, dy), in.uv_rect);
let t21 = sample_alpha(center_uv + vec2<f32>(dx, -dy), in.uv_rect);
let blurred_alpha = 0.25 * t00 + 0.125 * (t10 + t20 + t01 + t02) + 0.0625 * (t11 + t22 + t12 + t21);
shadow_alpha = blurred_alpha;
}
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
let d = sdf_rounded_box(in.local_pos, in.half_size, r);
let aa_width = fwidth(d);
let rect_alpha = 1.0 - smoothstep(-aa_width, aa_width, d);
// Composite texture OVER background
var tex_color = vec4<f32>(0.0);
// Only sample if UVs are within the valid 0..1 range
// (This allows us to shrink the image inside the box using app.rs logic)
let is_inside_image = abs(in.local_pos.x) <= in.half_size.x && abs(in.local_pos.y) <= in.half_size.y;
if is_inside_image && in.uv.x >= 0.0 && in.uv.x <= 1.0 && in.uv.y >= 0.0 && in.uv.y <= 1.0 {
tex_color = textureSample(t_diffuse, s_diffuse, in.uv);
}
let premul_tex = vec4<f32>(tex_color.rgb * tex_color.a, tex_color.a);
let premul_bg = vec4<f32>(in.bg_color.rgb * in.bg_color.a, in.bg_color.a);
let premul_fill = premul_tex + premul_bg * (1.0 - premul_tex.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 = -border_width to d = 0
let border_factor = smoothstep(-in.border_width - aa_width, -in.border_width + aa_width, d);
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;
}