1struct Globals {
2 viewport_size: vec2<f32>,
3 pad: vec2<u32>,
4}
5
6var<uniform> globals: Globals;
7
8const M_PI_F: f32 = 3.1415926;
9
10struct ViewId {
11 lo: u32,
12 hi: u32,
13}
14
15struct Bounds {
16 origin: vec2<f32>,
17 size: vec2<f32>,
18}
19struct Corners {
20 top_left: f32,
21 top_right: f32,
22 bottom_right: f32,
23 bottom_left: f32,
24}
25struct Edges {
26 top: f32,
27 right: f32,
28 bottom: f32,
29 left: f32,
30}
31struct Hsla {
32 h: f32,
33 s: f32,
34 l: f32,
35 a: f32,
36}
37
38fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
39 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
40 let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
41 return vec4<f32>(device_position, 0.0, 1.0);
42}
43
44fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
45 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
46 let tl = position - clip_bounds.origin;
47 let br = clip_bounds.origin + clip_bounds.size - position;
48 return vec4<f32>(tl.x, br.x, tl.y, br.y);
49}
50
51fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
52 let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
53 let s = hsla.s;
54 let l = hsla.l;
55 let a = hsla.a;
56
57 let c = (1.0 - abs(2.0 * l - 1.0)) * s;
58 let x = c * (1.0 - abs(h % 2.0 - 1.0));
59 let m = l - c / 2.0;
60
61 var color = vec4<f32>(m, m, m, a);
62
63 if (h >= 0.0 && h < 1.0) {
64 color.r += c;
65 color.g += x;
66 } else if (h >= 1.0 && h < 2.0) {
67 color.r += x;
68 color.g += c;
69 } else if (h >= 2.0 && h < 3.0) {
70 color.g += c;
71 color.b += x;
72 } else if (h >= 3.0 && h < 4.0) {
73 color.g += x;
74 color.b += c;
75 } else if (h >= 4.0 && h < 5.0) {
76 color.r += x;
77 color.b += c;
78 } else {
79 color.r += c;
80 color.b += x;
81 }
82
83 return color;
84}
85
86fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
87 let alpha = above.a + below.a * (1.0 - above.a);
88 let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
89 return vec4<f32>(color, alpha);
90}
91
92// A standard gaussian function, used for weighting samples
93fn gaussian(x: f32, sigma: f32) -> f32{
94 return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
95}
96
97// This approximates the error function, needed for the gaussian integral
98fn erf(v: vec2<f32>) -> vec2<f32> {
99 let s = sign(v);
100 let a = abs(v);
101 let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
102 let r2 = r1 * r1;
103 return s - s / (r2 * r2);
104}
105
106fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
107 let delta = min(half_size.y - corner - abs(y), 0.0);
108 let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
109 let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
110 return integral.y - integral.x;
111}
112
113fn pick_corner_radius(point: vec2<f32>, radii: Corners) -> f32 {
114 if (point.x < 0.0) {
115 if (point.y < 0.0) {
116 return radii.top_left;
117 } else {
118 return radii.bottom_left;
119 }
120 } else {
121 if (point.y < 0.0) {
122 return radii.top_right;
123 } else {
124 return radii.bottom_right;
125 }
126 }
127}
128
129// --- quads --- //
130
131struct Quad {
132 view_id: ViewId,
133 layer_id: u32,
134 order: u32,
135 bounds: Bounds,
136 content_mask: Bounds,
137 background: Hsla,
138 border_color: Hsla,
139 corner_radii: Corners,
140 border_widths: Edges,
141}
142var<storage, read> b_quads: array<Quad>;
143
144struct QuadVarying {
145 @builtin(position) position: vec4<f32>,
146 @location(0) @interpolate(flat) background_color: vec4<f32>,
147 @location(1) @interpolate(flat) border_color: vec4<f32>,
148 @location(2) @interpolate(flat) quad_id: u32,
149 //TODO: use `clip_distance` once Naga supports it
150 @location(3) clip_distances: vec4<f32>,
151}
152
153@vertex
154fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
155 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
156 let quad = b_quads[instance_id];
157
158 var out = QuadVarying();
159 out.position = to_device_position(unit_vertex, quad.bounds);
160 out.background_color = hsla_to_rgba(quad.background);
161 out.border_color = hsla_to_rgba(quad.border_color);
162 out.quad_id = instance_id;
163 out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
164 return out;
165}
166
167@fragment
168fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
169 // Alpha clip first, since we don't have `clip_distance`.
170 if (any(input.clip_distances < vec4<f32>(0.0))) {
171 return vec4<f32>(0.0);
172 }
173
174 let quad = b_quads[input.quad_id];
175 let half_size = quad.bounds.size / 2.0;
176 let center = quad.bounds.origin + half_size;
177 let center_to_point = input.position.xy - center;
178
179 let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
180
181 let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
182 let distance =
183 length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
184 min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
185 corner_radius;
186
187 let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
188 let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
189 let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
190 let point_to_inset_corner = abs(center_to_point) - inset_size;
191
192 var border_width = 0.0;
193 if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
194 border_width = 0.0;
195 } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
196 border_width = horizontal_border;
197 } else {
198 border_width = vertical_border;
199 }
200
201 var color = input.background_color;
202 if (border_width > 0.0) {
203 let inset_distance = distance + border_width;
204 // Blend the border on top of the background and then linearly interpolate
205 // between the two as we slide inside the background.
206 let blended_border = over(input.background_color, input.border_color);
207 color = mix(blended_border, input.background_color,
208 saturate(0.5 - inset_distance));
209 }
210
211 return color * vec4<f32>(1.0, 1.0, 1.0, saturate(0.5 - distance));
212}
213
214// --- shadows --- //
215
216struct Shadow {
217 view_id: ViewId,
218 layer_id: u32,
219 order: u32,
220 bounds: Bounds,
221 corner_radii: Corners,
222 content_mask: Bounds,
223 color: Hsla,
224 blur_radius: f32,
225 pad: u32,
226}
227var<storage, read> b_shadows: array<Shadow>;
228
229struct ShadowVarying {
230 @builtin(position) position: vec4<f32>,
231 @location(0) @interpolate(flat) color: vec4<f32>,
232 @location(1) @interpolate(flat) shadow_id: u32,
233 //TODO: use `clip_distance` once Naga supports it
234 @location(3) clip_distances: vec4<f32>,
235}
236
237@vertex
238fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
239 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
240 let shadow = b_shadows[instance_id];
241
242 let margin = 3.0 * shadow.blur_radius;
243 // Set the bounds of the shadow and adjust its size based on the shadow's
244 // spread radius to achieve the spreading effect
245 var bounds = shadow.bounds;
246 bounds.origin -= vec2<f32>(margin);
247 bounds.size += 2.0 * vec2<f32>(margin);
248
249 var out = ShadowVarying();
250 out.position = to_device_position(unit_vertex, shadow.bounds);
251 out.color = hsla_to_rgba(shadow.color);
252 out.shadow_id = instance_id;
253 out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
254 return out;
255}
256
257@fragment
258fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
259 // Alpha clip first, since we don't have `clip_distance`.
260 if (any(input.clip_distances < vec4<f32>(0.0))) {
261 return vec4<f32>(0.0);
262 }
263
264 let shadow = b_shadows[input.shadow_id];
265 let half_size = shadow.bounds.size / 2.0;
266 let center = shadow.bounds.origin + half_size;
267 let center_to_point = input.position.xy - center;
268
269 let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
270
271 // The signal is only non-zero in a limited range, so don't waste samples
272 let low = center_to_point.y - half_size.y;
273 let high = center_to_point.y + half_size.y;
274 let start = clamp(-3.0 * shadow.blur_radius, low, high);
275 let end = clamp(3.0 * shadow.blur_radius, low, high);
276
277 // Accumulate samples (we can get away with surprisingly few samples)
278 let step = (end - start) / 4.0;
279 var y = start + step * 0.5;
280 var alpha = 0.0;
281 for (var i = 0; i < 4; i += 1) {
282 let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
283 shadow.blur_radius, corner_radius, half_size);
284 alpha += blur * gaussian(y, shadow.blur_radius) * step;
285 y += step;
286 }
287
288 return input.color * vec4<f32>(1.0, 1.0, 1.0, alpha);
289}