struct GlobalUniforms {
projection: mat4x4<f32>,
}
struct RectInstance {
position_size: vec4<f32>,
color: vec4<f32>,
border_color: vec4<f32>,
clip_rect: vec4<f32>,
params: vec4<f32>,
shadow_color: vec4<f32>,
shadow_params: vec4<f32>,
effect_params: vec4<f32>,
quad_corner_01: vec4<f32>,
quad_corner_23: vec4<f32>,
effect_kind: u32,
is_quad: u32,
_padding0: u32,
_padding1: u32,
}
@group(0) @binding(0) var<uniform> globals: GlobalUniforms;
@group(1) @binding(0) var<storage, read> instances: array<RectInstance>;
@group(1) @binding(1) var<storage, read> draw_order: array<u32>;
struct VertexInput {
@location(0) position: vec2<f32>,
@builtin(instance_index) instance_index: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) local_pos: vec2<f32>,
@location(1) rect_size: vec2<f32>,
@location(2) screen_pos: vec2<f32>,
@location(3) @interpolate(flat) slot_index: u32,
}
fn shadow_margin(inst: RectInstance) -> f32 {
let offset = inst.shadow_params.xy;
let blur = inst.shadow_params.z;
let spread = inst.shadow_params.w;
if inst.shadow_color.a <= 0.0 {
return 0.0;
}
return blur + spread + max(abs(offset.x), abs(offset.y));
}
@vertex
fn vs_main(vertex: VertexInput) -> VertexOutput {
var output: VertexOutput;
let slot = draw_order[vertex.instance_index];
let inst = instances[slot];
let rect_pos = inst.position_size.xy;
let rect_size = inst.position_size.zw;
var world_pos: vec2<f32>;
var local: vec2<f32>;
if inst.is_quad != 0u {
let p0 = inst.quad_corner_01.xy;
let p1 = inst.quad_corner_01.zw;
let p2 = inst.quad_corner_23.xy;
let p3 = inst.quad_corner_23.zw;
let top = mix(p0, p1, vertex.position.x);
let bottom = mix(p3, p2, vertex.position.x);
world_pos = mix(top, bottom, vertex.position.y);
local = vertex.position * rect_size;
} else {
let margin = shadow_margin(inst);
let extended_size = rect_size + vec2<f32>(2.0 * margin, 2.0 * margin);
local = vertex.position * extended_size - vec2<f32>(margin, margin);
let rotation = inst.params.w;
let center = rect_size * 0.5;
let centered = local - center;
let cos_r = cos(rotation);
let sin_r = sin(rotation);
let rotated = vec2<f32>(
centered.x * cos_r - centered.y * sin_r,
centered.x * sin_r + centered.y * cos_r,
);
world_pos = rect_pos + center + rotated;
}
output.position = globals.projection * vec4<f32>(world_pos, 0.0, 1.0);
output.position.z = inst.params.z;
output.local_pos = local;
output.rect_size = rect_size;
output.screen_pos = world_pos;
output.slot_index = slot;
return output;
}
fn rounded_rect_sdf(pos: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 {
let half_size = size * 0.5;
let center_pos = pos - half_size;
let clamped_radius = min(radius, min(half_size.x, half_size.y));
let q = abs(center_pos) - half_size + clamped_radius;
return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - clamped_radius;
}
fn linear_to_oklab(c: vec3<f32>) -> vec3<f32> {
let l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
let m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
let s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
let l_ = pow(max(l, 0.0), 1.0 / 3.0);
let m_ = pow(max(m, 0.0), 1.0 / 3.0);
let s_ = pow(max(s, 0.0), 1.0 / 3.0);
return vec3<f32>(
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
);
}
fn oklab_to_linear(c: vec3<f32>) -> vec3<f32> {
let l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
let m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
let s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
return vec3<f32>(
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
);
}
fn mix_oklab(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let lab_a = linear_to_oklab(a.rgb);
let lab_b = linear_to_oklab(b.rgb);
let blended = oklab_to_linear(mix(lab_a, lab_b, t));
let alpha = mix(a.a, b.a, t);
return vec4<f32>(blended, alpha);
}
fn apply_effect(
local_pos: vec2<f32>,
rect_size: vec2<f32>,
base: vec4<f32>,
effect_kind: u32,
params: vec4<f32>,
) -> vec4<f32> {
if effect_kind == 1u {
let denom = max(rect_size.y, 1.0);
let t = clamp(local_pos.y / denom, 0.0, 1.0);
let secondary = vec4<f32>(params.x, params.y, params.z, params.w);
return mix_oklab(base, secondary, t);
}
if effect_kind == 2u {
let denom = max(rect_size.x, 1.0);
let t = clamp(local_pos.x / denom, 0.0, 1.0);
let secondary = vec4<f32>(params.x, params.y, params.z, params.w);
return mix_oklab(base, secondary, t);
}
if effect_kind == 3u {
let center = rect_size * 0.5;
let half_diag = max(length(center), 1.0);
let dist = length(local_pos - center) / half_diag;
let glow = vec4<f32>(params.x, params.y, params.z, base.a);
let intensity = params.w;
return mix(base, glow, smoothstep(0.0, 0.85, dist) * intensity);
}
if effect_kind == 4u {
let strength = params.x;
let edge = max(params.y, 0.5);
let top_edge = smoothstep(0.0, edge, local_pos.y);
let left_edge = smoothstep(0.0, edge, local_pos.x);
let bottom_edge = smoothstep(0.0, edge, rect_size.y - local_pos.y);
let right_edge = smoothstep(0.0, edge, rect_size.x - local_pos.x);
let inset_dark = (1.0 - top_edge) * 0.6 + (1.0 - left_edge) * 0.4;
let inset_light = (1.0 - bottom_edge) * 0.4 + (1.0 - right_edge) * 0.3;
let darkened = base.rgb * (1.0 - inset_dark * strength);
let lightened = darkened + inset_light * strength * 0.5;
return vec4<f32>(lightened, base.a);
}
if effect_kind == 5u {
let strength = params.x;
let edge = max(params.y, 0.5);
let top_edge = smoothstep(0.0, edge, local_pos.y);
let left_edge = smoothstep(0.0, edge, local_pos.x);
let bottom_edge = smoothstep(0.0, edge, rect_size.y - local_pos.y);
let right_edge = smoothstep(0.0, edge, rect_size.x - local_pos.x);
let outset_light = (1.0 - top_edge) * 0.5 + (1.0 - left_edge) * 0.3;
let outset_dark = (1.0 - bottom_edge) * 0.5 + (1.0 - right_edge) * 0.3;
let lightened = base.rgb + outset_light * strength * 0.6;
let darkened = lightened * (1.0 - outset_dark * strength * 0.4);
return vec4<f32>(darkened, base.a);
}
if effect_kind == 6u {
let tint = vec4<f32>(params.x, params.y, params.z, params.w);
let highlight_band = smoothstep(0.0, max(rect_size.y * 0.5, 1.0), rect_size.y - local_pos.y);
let frosted = mix(tint, vec4<f32>(1.0, 1.0, 1.0, tint.a * 0.18), highlight_band * 0.25);
return frosted;
}
if effect_kind == 8u {
let nx = local_pos.x / max(rect_size.x, 1.0);
let half_h = max(rect_size.y * 0.5, 1.0);
let ny = abs(local_pos.y - rect_size.y * 0.5) / half_h;
let edge = nx + ny;
let triangle_alpha = 1.0 - smoothstep(0.96, 1.04, edge);
return vec4<f32>(base.rgb, base.a * triangle_alpha);
}
if effect_kind == 7u {
let center = rect_size * 0.5;
let radius = min(center.x, center.y);
if radius < 1.0 {
return base;
}
let offset = local_pos - center;
let distance_from_center = length(offset);
let saturation = clamp(distance_from_center / radius, 0.0, 1.0);
let pi = 3.14159265358979;
let angle = atan2(offset.y, offset.x);
let hue = (angle / (2.0 * pi)) + 0.5;
let value = clamp(params.x, 0.0, 1.0);
let wheel_alpha = clamp(params.y, 0.0, 1.0);
let chroma = value * saturation;
let hue_segment = hue * 6.0;
let secondary = chroma * (1.0 - abs((hue_segment % 2.0) - 1.0));
var rgb = vec3<f32>(0.0, 0.0, 0.0);
if hue_segment < 1.0 {
rgb = vec3<f32>(chroma, secondary, 0.0);
} else if hue_segment < 2.0 {
rgb = vec3<f32>(secondary, chroma, 0.0);
} else if hue_segment < 3.0 {
rgb = vec3<f32>(0.0, chroma, secondary);
} else if hue_segment < 4.0 {
rgb = vec3<f32>(0.0, secondary, chroma);
} else if hue_segment < 5.0 {
rgb = vec3<f32>(secondary, 0.0, chroma);
} else {
rgb = vec3<f32>(chroma, 0.0, secondary);
}
let match_value = value - chroma;
rgb = rgb + vec3<f32>(match_value, match_value, match_value);
let edge_softness = 1.5;
let mask = 1.0 - smoothstep(radius - edge_softness, radius, distance_from_center);
return vec4<f32>(rgb, wheel_alpha * mask);
}
return base;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
let inst = instances[input.slot_index];
let clip = inst.clip_rect;
if clip.z > clip.x {
let frag_pos = input.screen_pos;
if frag_pos.x < clip.x || frag_pos.x > clip.z ||
frag_pos.y < clip.y || frag_pos.y > clip.w {
discard;
}
}
if inst.is_quad != 0u {
if inst.color.a < 0.001 {
discard;
}
return inst.color;
}
let corner_radius = inst.params.x;
let border_width = inst.params.y;
var rect_color = apply_effect(
input.local_pos,
input.rect_size,
inst.color,
inst.effect_kind,
inst.effect_params,
);
let rect_dist = rounded_rect_sdf(input.local_pos, input.rect_size, corner_radius);
let edge_softness = 0.5;
let rect_alpha = 1.0 - smoothstep(-edge_softness, edge_softness, rect_dist);
if border_width > 0.0 {
let inner_dist = rect_dist + border_width;
let border_alpha = 1.0 - smoothstep(-edge_softness, edge_softness, inner_dist);
let is_border = rect_alpha - border_alpha;
rect_color = mix(rect_color, inst.border_color, is_border);
}
let rect_eff_a = rect_alpha * rect_color.a;
let shadow_color = inst.shadow_color;
if shadow_color.a > 0.0 {
let shadow_offset = inst.shadow_params.xy;
let shadow_blur = inst.shadow_params.z;
let shadow_spread = inst.shadow_params.w;
let shadow_local = input.local_pos - shadow_offset + vec2<f32>(shadow_spread, shadow_spread);
let shadow_size = input.rect_size + vec2<f32>(2.0 * shadow_spread, 2.0 * shadow_spread);
let shadow_radius = corner_radius + shadow_spread;
let shadow_dist = rounded_rect_sdf(shadow_local, shadow_size, shadow_radius);
let blur_safe = max(shadow_blur, 0.5);
let shadow_sdf_a = 1.0 - smoothstep(-blur_safe, blur_safe, shadow_dist);
let shadow_eff_a = shadow_sdf_a * shadow_color.a;
let visible_shadow_a = shadow_eff_a * (1.0 - rect_eff_a);
let total_a = rect_eff_a + visible_shadow_a;
if total_a < 0.001 {
discard;
}
let total_rgb = rect_color.rgb * rect_eff_a + shadow_color.rgb * visible_shadow_a;
return vec4<f32>(total_rgb / total_a, total_a);
}
if rect_eff_a < 0.001 {
discard;
}
return vec4<f32>(rect_color.rgb, rect_eff_a);
}