struct Globals {
ndc_to_px: vec2<f32>,
_pad: vec2<f32>,
};
@group(0) @binding(0) var<uniform> G: Globals;
struct VSOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) xywh: vec4<f32>,
@location(2) start_angle: f32,
@location(3) sweep_angle: f32,
@location(4) stroke_ndc: f32,
@location(5) pos_ndc: vec2<f32>,
@location(6) sin_cos: vec2<f32>,
@location(7) @interpolate(flat) start_endpoint: vec2<f32>,
@location(8) @interpolate(flat) end_endpoint: vec2<f32>,
@location(9) @interpolate(flat) cap: f32,
};
fn ellipse_pt_at_angle(center: vec2<f32>, half: vec2<f32>, angle: f32, sc: vec2<f32>) -> vec2<f32> {
let c = cos(angle);
let s = sin(angle);
let denom = sqrt(half.y * half.y * c * c + half.x * half.x * s * s);
let local = vec2(half.x * half.y * c / denom, -half.x * half.y * s / denom);
return center + vec2(
local.x * sc.x - local.y * sc.y,
local.x * sc.y + local.y * sc.x
);
}
@vertex
fn vs_main(
@location(0) xywh: vec4<f32>,
@location(1) start_angle: f32,
@location(2) sweep_angle: f32,
@location(3) stroke_ndc: f32,
@location(4) pad: f32,
@location(5) color: vec4<f32>,
@location(6) sin_cos: vec2<f32>,
@location(7) cap: f32,
@builtin(vertex_index) v: u32
) -> VSOut {
var positions = array<vec2<f32>, 6>(
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0),
vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0)
);
let p = positions[v];
let half = 0.5 * xywh.zw;
// Expand quad to accommodate the full stroke width + AA
let quad_half = half + pad;
let corner = (p * 2.0 - 1.0) * quad_half;
let rotated = vec2(corner.x * sin_cos.x - corner.y * sin_cos.y, corner.x * sin_cos.y + corner.y * sin_cos.x);
let pos_ndc = xywh.xy + rotated;
let center = xywh.xy;
// Compensate for round cap (slightly smaller on one side, but looks good as is)
let half_px = 0.5 * stroke_ndc;
let r_px = half.x * G.ndc_to_px.x;
let cap_offset = half_px / max(r_px, 1.0);
let adjusted_start = start_angle + cap_offset;
// Compute points on the ellipse at polar angles matching local_angle's convention.
let start_endpoint = ellipse_pt_at_angle(center, half, adjusted_start, sin_cos);
let end_endpoint = ellipse_pt_at_angle(center, half, adjusted_start + sweep_angle, sin_cos);
var out: VSOut;
out.pos = vec4(pos_ndc, 0.0, 1.0);
out.xywh = xywh;
out.start_angle = adjusted_start;
out.sweep_angle = sweep_angle;
out.stroke_ndc = stroke_ndc;
out.color = color;
out.pos_ndc = pos_ndc;
out.sin_cos = sin_cos;
out.start_endpoint = start_endpoint;
out.end_endpoint = end_endpoint;
out.cap = cap;
return out;
}
fn sdf_ellipse(pos_ndc: vec2<f32>, xywh: vec4<f32>, sin_cos: vec2<f32>) -> f32 {
let center = xywh.xy;
let unrotated = center + vec2(
(pos_ndc.x - center.x) * sin_cos.x + (pos_ndc.y - center.y) * sin_cos.y,
-(pos_ndc.x - center.x) * sin_cos.y + (pos_ndc.y - center.y) * sin_cos.x
);
let radii = 0.5 * xywh.zw;
let p = (unrotated - center) / radii;
return length(p) - 1.0;
}
fn local_angle(pos_ndc: vec2<f32>, xywh: vec4<f32>, sin_cos: vec2<f32>) -> f32 {
let center = xywh.xy;
let unrotated = center + vec2(
(pos_ndc.x - center.x) * sin_cos.x + (pos_ndc.y - center.y) * sin_cos.y,
-(pos_ndc.x - center.x) * sin_cos.y + (pos_ndc.y - center.y) * sin_cos.x
);
let dx = unrotated.x - center.x;
let dy = unrotated.y - center.y;
return -atan2(dy, dx);
}
// Arc coverage: 1.0 inside the sweep, smoothly fading to 0.0 at the boundaries.
fn arc_coverage(angle: f32, start: f32, sweep: f32, half_px: f32) -> f32 {
if sweep >= 6.2831853 {
return 1.0;
}
let end = start + sweep;
let a = (angle % 6.2831853 + 6.2831853) % 6.2831853;
let s = (start % 6.2831853 + 6.2831853) % 6.2831853;
let e = (end % 6.2831853 + 6.2831853) % 6.2831853;
var da: f32;
if s <= e {
if a < s || a > e {
da = min((s - a + 6.2831853) % 6.2831853, (a - e + 6.2831853) % 6.2831853);
} else {
return 1.0;
}
} else {
if a > e && a < s {
da = min((a - e + 6.2831853) % 6.2831853, (s - a + 6.2831853) % 6.2831853);
} else {
return 1.0;
}
}
return 1.0 - smoothstep(0.0, half_px, da);
}
// Round cap coverage: semicircle of radius = half_px centered at the endpoint.
fn round_cap_coverage(pos_ndc: vec2<f32>, endpoint: vec2<f32>, half_px: f32) -> f32 {
let delta = (pos_ndc - endpoint) * G.ndc_to_px;
let dist = length(delta);
return 1.0 - smoothstep(max(half_px - 1.0, 0.0), half_px + 1.0, dist);
}
@fragment
fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
let d = sdf_ellipse(in.pos_ndc, in.xywh, in.sin_cos);
let grad = vec2(dpdx(d), dpdy(d));
let w = max(length(grad), 1e-5);
let half_px = 0.5 * in.stroke_ndc;
let half = half_px * w;
let stroke_cov = 1.0 - smoothstep(-w, w, abs(d) - half);
let angle = local_angle(in.pos_ndc, in.xywh, in.sin_cos);
let angle_w = max(length(fwidth(in.pos_ndc)) / length(in.xywh.zw), 1e-4);
let angle_cov = arc_coverage(angle, in.start_angle, in.sweep_angle, angle_w * 2.0);
var cap_cov = 0.0;
if in.cap >= 0.5 {
let start_cap = round_cap_coverage(in.pos_ndc, in.start_endpoint, half_px);
let end_cap = round_cap_coverage(in.pos_ndc, in.end_endpoint, half_px);
cap_cov = max(start_cap, end_cap);
}
let alpha_cov = stroke_cov * max(angle_cov, cap_cov);
let a = in.color.a * alpha_cov;
return vec4(in.color.rgb * a, a);
}