struct Camera {
center: vec2<f32>,
zoom: f32,
fade: f32,
viewport: vec2<f32>,
_pad1: vec2<f32>,
};
@group(0) @binding(0) var<uniform> cam: Camera;
// Half-width of the antialiasing ramp in pixels, and the amount the quad grows
// on each side to make room for it. See the same constant in edge.wgsl.
const AA_PAD: f32 = 1.0;
// 1/sqrt(2), for converting the diamond's L1 distance to a perpendicular one.
const INV_SQRT2: f32 = 0.70710678;
struct VsOut {
@builtin(position) clip: vec4<f32>,
// Position within the shape, normalized so |local| == 1 lands exactly on the
// rim. The quad extends past that (see `pad`), so `local` runs slightly
// beyond 1 at the corners — that overshoot is the AA band.
@location(0) local: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) opacity: f32,
@location(3) @interpolate(flat) shape: u32,
// Drawn radius in pixels. Converts the normalized distance above back into
// pixels so the AA ramp stays one pixel wide at every radius and zoom —
// which the old radius-relative smoothstep(0.9, 1.0, ...) did not: it was a
// sub-pixel ramp on a small node and a several-pixel smear on a large one.
@location(4) @interpolate(flat) radius_px: f32,
};
// Unit quad corners (two triangles) provided as vertex_index 0..6
var<private> QUAD: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, 1.0), vec2<f32>(-1.0, 1.0),
);
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
@location(0) pos: vec2<f32>,
@location(1) radius: f32,
@location(2) opacity: f32,
@location(3) shape: u32,
@location(4) color: vec4<f32>,
) -> VsOut {
let corner = QUAD[vi];
// A zero radius would make the normalization below divide by zero; such a
// node covers nothing anyway, so any finite `local` is fine.
let r = max(radius, 1e-6);
let pad = AA_PAD / cam.zoom; // pixels -> world units
let r_out = r + pad;
let world = pos + corner * r_out;
let rel = (world - cam.center) * cam.zoom;
let clip = vec2<f32>(rel.x / (cam.viewport.x * 0.5), rel.y / (cam.viewport.y * 0.5));
var out: VsOut;
out.clip = vec4<f32>(clip, 0.0, 1.0);
out.local = corner * (r_out / r);
out.color = color;
out.opacity = opacity;
out.shape = shape;
out.radius_px = r * cam.zoom;
return out;
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let p = in.local;
// Signed distance from the rim in pixels: negative inside, positive outside.
var dist_px: f32;
if (in.shape == 1u) { // square
dist_px = (max(abs(p.x), abs(p.y)) - 1.0) * in.radius_px;
} else if (in.shape == 2u) { // diamond
// |x|+|y| is an L1 distance, which overshoots the true perpendicular
// distance from a 45-degree edge by sqrt(2). Without the correction the
// diamond's AA band would be 1.41px wide while the circle's is 1px, and
// the two would visibly disagree side by side.
dist_px = (abs(p.x) + abs(p.y) - 1.0) * in.radius_px * INV_SQRT2;
} else { // circle
dist_px = (length(p) - 1.0) * in.radius_px;
}
// Coverage of a one-pixel sample straddling the rim.
let coverage = clamp(0.5 - dist_px, 0.0, 1.0);
if (coverage <= 0.0) { discard; }
return vec4<f32>(in.color.rgb, in.color.a * in.opacity * coverage * cam.fade);
}