1struct Vertex {
2 @builtin(position) position: vec4<f32>,
3 @location(0) uv: vec2<f32>,
4 @location(1) color: vec4<f32>,
5 @location(2) local: vec2<f32>,
6 @location(3) @interpolate(flat) shape: vec4<f32>,
7 @location(4) @interpolate(flat) stroke: f32,
8 @location(5) clip_local: vec2<f32>,
9 @location(6) @interpolate(flat) clip: vec3<f32>,
10 @location(7) @interpolate(flat) blur: f32,
11}
12
13@group(0) @binding(0) var atlas: texture_2d<f32>;
14@group(0) @binding(1) var atlas_sampler: sampler;
15
16@vertex
17fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>,
18 @location(2) color: vec4<f32>, @location(3) local: vec2<f32>,
19 @location(4) shape: vec4<f32>, @location(5) stroke: f32,
20 @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>,
21 @location(8) blur: f32) -> Vertex {
22 return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke, clip_local, clip, blur);
23}
24
25fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 {
26 if radius.x == radius.y {
27 return angle * radius.x;
28 }
29 // Four-point Gauss-Legendre quadrature.
30 let nodes = vec4<f32>(-0.86113631, -0.33998104, 0.33998104, 0.86113631);
31 let weights = vec4<f32>(0.34785485, 0.65214515, 0.65214515, 0.34785485);
32 let half = angle * 0.5;
33 let scale = max(radius.x, radius.y);
34 var sum = 0.0;
35 for (var i = 0u; i < 4u; i += 1u) {
36 let t = half * (nodes[i] + 1.0);
37 sum += weights[i] * length((radius / scale) * vec2<f32>(sin(t), cos(t)));
38 }
39 return half * sum * scale;
40}
41
42fn erf(x: vec2<f32>) -> vec2<f32> {
43 let s = sign(x);
44 let a = abs(x);
45 var y = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
46 y *= y;
47 return s - s / (y * y);
48}
49
50// Coverage at `p` of a rectangle `half` about the origin with round corners `corner`,
51// blurred by a Gaussian of deviation `sigma`: exact across, sampled four times along
52// (Evan Wallace, "Fast Rounded Rectangle Shadows").
53fn shadow(p: vec2<f32>, half: vec2<f32>, corner: f32, sigma: f32) -> f32 {
54 let start = clamp(-3.0 * sigma, p.y - half.y, p.y + half.y);
55 let end = clamp(3.0 * sigma, p.y - half.y, p.y + half.y);
56 let stride = (end - start) / 4.0;
57 var y = start + stride * 0.5;
58 var value = 0.0;
59 for (var i = 0; i < 4; i += 1) {
60 let delta = min(half.y - corner - abs(p.y - y), 0.0);
61 let curved = half.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
62 let integral = 0.5 + 0.5 * erf((p.x + vec2<f32>(-curved, curved)) * (0.70710678 / sigma));
63 let weight = exp(-y * y / (2.0 * sigma * sigma)) / (2.50662827 * sigma);
64 value += (integral.y - integral.x) * weight * stride;
65 y += stride;
66 }
67 return value;
68}
69
70// Distance from `p` to the tapered capsule from (-h, 0) to (h, 0) with round ends of radii
71// `r0` and `r1` (Inigo Quilez's uneven capsule).
72fn taper(p: vec2<f32>, h: f32, r0: f32, r1: f32) -> f32 {
73 let q = vec2<f32>(abs(p.y), p.x + h);
74 let span = 2.0 * h;
75 let b = (r0 - r1) / max(span, 1e-6);
76 let far = length(q - vec2<f32>(0.0, span)) - r1;
77 if abs(b) >= 1.0 {
78 return min(length(q) - r0, far);
79 }
80 let a = sqrt(1.0 - b * b);
81 let k = dot(q, vec2<f32>(-b, a));
82 if k < 0.0 {
83 return length(q) - r0;
84 }
85 if k > a * span {
86 return far;
87 }
88 return dot(q, vec2<f32>(a, b)) - r0;
89}
90
91@fragment
92fn fragment(input: Vertex) -> @location(0) vec4<f32> {
93 var color = textureSample(atlas, atlas_sampler, input.uv) * input.color;
94 if input.blur > 0.0 {
95 color *= shadow(input.local, input.shape.xy, input.shape.z, input.blur);
96 } else if input.blur < 0.0 {
97 color *= clamp(0.5 - taper(input.local, input.shape.x, input.shape.y, input.shape.z), 0.0, 1.0);
98 } else if input.shape.x > 0.0 && input.shape.y > 0.0 {
99 let q = abs(input.local) - input.shape.xy + input.shape.zw;
100 var distance: f32;
101 if input.shape.z == input.shape.w {
102 distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.shape.z;
103 } else if all(q > vec2<f32>(0.0)) {
104 let radius = max(input.shape.zw, vec2<f32>(0.0001));
105 let normalized = q / radius;
106 let k0 = length(normalized);
107 distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20);
108 } else {
109 let edge = abs(input.local) - input.shape.xy;
110 distance = max(edge.x, edge.y);
111 }
112 var coverage = clamp(0.5 - distance, 0.0, 1.0);
113 let width = abs(input.stroke);
114 if width > 0.0 {
115 coverage *= clamp(distance + width + 0.5, 0.0, 1.0);
116 }
117 if input.stroke < 0.0 {
118 var radius = max(input.shape.zw - width * 0.5, vec2<f32>(0.0));
119 if any(radius == vec2<f32>(0.0)) {
120 radius = vec2<f32>(0.0);
121 }
122 let straight = input.shape.xy - width * 0.5 - radius;
123 let p = abs(input.local);
124 let quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius);
125 var along: f32;
126 if p.y <= straight.y {
127 along = p.y;
128 } else if p.x <= straight.x || radius.x == 0.0 {
129 along = quarter - p.x;
130 } else {
131 let angle = atan2((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x);
132 along = straight.y + ellipse_arc(angle, radius);
133 }
134 if input.local.x < 0.0 {
135 along = select(2.0 * quarter - along, 2.0 * quarter + along, input.local.y < 0.0);
136 } else if input.local.y < 0.0 {
137 along = 4.0 * quarter - along;
138 }
139 let phase = fract(along / (4.0 * width)) * (4.0 * width);
140 coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0);
141 }
142 color *= coverage;
143 }
144 if input.clip.x > 0.0 {
145 let q = abs(input.clip_local) - input.clip.xy + input.clip.z;
146 let distance = length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - input.clip.z;
147 color *= clamp(0.5 - distance, 0.0, 1.0);
148 }
149 return color;
150}