1/* Functions useful for debugging:
2
3// A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
4fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2<f32>) -> vec4<f32> {
5 // Normalize value to 0-1 range
6 let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0);
7
8 // Heat map color calculation
9 let r = t * t;
10 let g = 4.0 * t * (1.0 - t);
11 let b = (1.0 - t) * (1.0 - t);
12 let heat_color = vec3<f32>(r, g, b);
13
14 // Create a checkerboard pattern (black and white)
15 let sum = floor(position.x / 3) + floor(position.y / 3);
16 let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd
17 let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd
18 let checker_color = vec3<f32>(checker_value);
19
20 // Determine if value is in range (1.0 if in range, 0.0 if out of range)
21 let in_range = step(minValue, value) * step(value, maxValue);
22
23 // Mix checkerboard and heat map based on whether value is in range
24 let final_color = mix(checker_color, heat_color, in_range);
25
26 return vec4<f32>(final_color, 1.0);
27}
28
29*/
30
31// Contrast and gamma correction adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
32// Copyright (c) Microsoft Corporation.
33// Licensed under the MIT license.
34fn color_brightness(color: vec3<f32>) -> f32 {
35 // REC. 601 luminance coefficients for perceived brightness
36 return dot(color, vec3<f32>(0.30, 0.59, 0.11));
37}
38
39fn light_on_dark_contrast(enhancedContrast: f32, color: vec3<f32>) -> f32 {
40 let brightness = color_brightness(color);
41 let multiplier = saturate(4.0 * (0.75 - brightness));
42 return enhancedContrast * multiplier;
43}
44
45fn enhance_contrast(alpha: f32, k: f32) -> f32 {
46 return alpha * (k + 1.0) / (alpha * k + 1.0);
47}
48
49fn enhance_contrast3(alpha: vec3<f32>, k: f32) -> vec3<f32> {
50 return alpha * (k + 1.0) / (alpha * k + 1.0);
51}
52
53fn apply_alpha_correction(a: f32, b: f32, g: vec4<f32>) -> f32 {
54 let brightness_adjustment = g.x * b + g.y;
55 let correction = brightness_adjustment * a + (g.z * b + g.w);
56 return a + a * (1.0 - a) * correction;
57}
58
59fn apply_alpha_correction3(a: vec3<f32>, b: vec3<f32>, g: vec4<f32>) -> vec3<f32> {
60 let brightness_adjustment = g.x * b + g.y;
61 let correction = brightness_adjustment * a + (g.z * b + g.w);
62 return a + a * (1.0 - a) * correction;
63}
64
65fn apply_contrast_and_gamma_correction(sample: f32, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> f32 {
66 let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
67 let brightness = color_brightness(color);
68
69 let contrasted = enhance_contrast(sample, enhanced_contrast);
70 return apply_alpha_correction(contrasted, brightness, gamma_ratios);
71}
72
73fn apply_contrast_and_gamma_correction3(sample: vec3<f32>, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> vec3<f32> {
74 let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
75
76 let contrasted = enhance_contrast3(sample, enhanced_contrast);
77 return apply_alpha_correction3(contrasted, color, gamma_ratios);
78}
79
80struct GlobalParams {
81 viewport_size: vec2<f32>,
82 premultiplied_alpha: u32,
83 pad: u32,
84}
85
86struct GammaParams {
87 gamma_ratios: vec4<f32>,
88 grayscale_enhanced_contrast: f32,
89 subpixel_enhanced_contrast: f32,
90 pad: vec2<f32>,
91}
92
93@group(0) @binding(0) var<uniform> globals: GlobalParams;
94@group(0) @binding(1) var<uniform> gamma_params: GammaParams;
95@group(1) @binding(1) var t_sprite: texture_2d<f32>;
96@group(1) @binding(2) var s_sprite: sampler;
97
98const M_PI_F: f32 = 3.1415926;
99const GRAYSCALE_FACTORS: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
100
101struct Bounds {
102 origin: vec2<f32>,
103 size: vec2<f32>,
104}
105
106struct Corners {
107 top_left: f32,
108 top_right: f32,
109 bottom_right: f32,
110 bottom_left: f32,
111}
112
113struct Edges {
114 top: f32,
115 right: f32,
116 bottom: f32,
117 left: f32,
118}
119
120struct Hsla {
121 h: f32,
122 s: f32,
123 l: f32,
124 a: f32,
125}
126
127struct LinearColorStop {
128 color: Hsla,
129 percentage: f32,
130}
131
132struct Background {
133 // 0u is Solid
134 // 1u is LinearGradient
135 // 2u is PatternSlash
136 // 3u is Checkerboard
137 tag: u32,
138 // 0u is sRGB linear color
139 // 1u is Oklab color
140 color_space: u32,
141 solid: Hsla,
142 gradient_angle_or_pattern_height: f32,
143 colors: array<LinearColorStop, 2>,
144 pad: u32,
145}
146
147struct AtlasTextureId {
148 index: u32,
149 kind: u32,
150}
151
152struct AtlasBounds {
153 origin: vec2<i32>,
154 size: vec2<i32>,
155}
156
157struct AtlasTile {
158 texture_id: AtlasTextureId,
159 tile_id: u32,
160 padding: u32,
161 bounds: AtlasBounds,
162}
163
164struct TransformationMatrix {
165 rotation_scale: mat2x2<f32>,
166 translation: vec2<f32>,
167}
168
169fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
170 let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
171 return vec4<f32>(device_position, 0.0, 1.0);
172}
173
174fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
175 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
176 return to_device_position_impl(position);
177}
178
179fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
180 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
181 //Note: Rust side stores it as row-major, so transposing here
182 let transformed = transpose(transform.rotation_scale) * position + transform.translation;
183 return to_device_position_impl(transformed);
184}
185
186fn to_tile_position(unit_vertex: vec2<f32>, tile: AtlasTile) -> vec2<f32> {
187 let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
188 return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
189}
190
191fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec4<f32> {
192 let tl = position - clip_bounds.origin;
193 let br = clip_bounds.origin + clip_bounds.size - position;
194 return vec4<f32>(tl.x, br.x, tl.y, br.y);
195}
196
197fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
198 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
199 return distance_from_clip_rect_impl(position, clip_bounds);
200}
201
202fn distance_from_clip_rect_transformed(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
203 let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
204 let transformed = transpose(transform.rotation_scale) * position + transform.translation;
205 return distance_from_clip_rect_impl(transformed, clip_bounds);
206}
207
208// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
209fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
210 let cutoff = srgb < vec3<f32>(0.04045);
211 let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
212 let lower = srgb / vec3<f32>(12.92);
213 return select(higher, lower, cutoff);
214}
215
216fn srgb_to_linear_component(a: f32) -> f32 {
217 let cutoff = a < 0.04045;
218 let higher = pow((a + 0.055) / 1.055, 2.4);
219 let lower = a / 12.92;
220 return select(higher, lower, cutoff);
221}
222
223fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
224 let cutoff = linear < vec3<f32>(0.0031308);
225 let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
226 let lower = linear * vec3<f32>(12.92);
227 return select(higher, lower, cutoff);
228}
229
230/// Convert a linear color to sRGBA space.
231fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
232 return vec4<f32>(linear_to_srgb(color.rgb), color.a);
233}
234
235/// Convert a sRGBA color to linear space.
236fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
237 return vec4<f32>(srgb_to_linear(color.rgb), color.a);
238}
239
240/// Hsla to linear RGBA conversion.
241fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
242 let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
243 let s = hsla.s;
244 let l = hsla.l;
245 let a = hsla.a;
246
247 let c = (1.0 - abs(2.0 * l - 1.0)) * s;
248 let x = c * (1.0 - abs(h % 2.0 - 1.0));
249 let m = l - c / 2.0;
250 var color = vec3<f32>(m);
251
252 if (h >= 0.0 && h < 1.0) {
253 color.r += c;
254 color.g += x;
255 } else if (h >= 1.0 && h < 2.0) {
256 color.r += x;
257 color.g += c;
258 } else if (h >= 2.0 && h < 3.0) {
259 color.g += c;
260 color.b += x;
261 } else if (h >= 3.0 && h < 4.0) {
262 color.g += x;
263 color.b += c;
264 } else if (h >= 4.0 && h < 5.0) {
265 color.r += x;
266 color.b += c;
267 } else {
268 color.r += c;
269 color.b += x;
270 }
271
272 return vec4<f32>(color, a);
273}
274
275/// Convert a linear sRGB to Oklab space.
276/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
277fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
278 let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
279 let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
280 let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
281
282 let l_ = pow(l, 1.0 / 3.0);
283 let m_ = pow(m, 1.0 / 3.0);
284 let s_ = pow(s, 1.0 / 3.0);
285
286 return vec4<f32>(
287 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
288 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
289 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
290 color.a
291 );
292}
293
294/// Convert an Oklab color to linear sRGB space.
295fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
296 let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
297 let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
298 let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
299
300 let l = l_ * l_ * l_;
301 let m = m_ * m_ * m_;
302 let s = s_ * s_ * s_;
303
304 return vec4<f32>(
305 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
306 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
307 -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
308 color.a
309 );
310}
311
312fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
313 let alpha = above.a + below.a * (1.0 - above.a);
314 let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
315 return vec4<f32>(color, alpha);
316}
317
318// A standard gaussian function, used for weighting samples
319fn gaussian(x: f32, sigma: f32) -> f32{
320 return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
321}
322
323// This approximates the error function, needed for the gaussian integral
324fn erf(v: vec2<f32>) -> vec2<f32> {
325 let s = sign(v);
326 let a = abs(v);
327 let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
328 let r2 = r1 * r1;
329 return s - s / (r2 * r2);
330}
331
332fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
333 let delta = min(half_size.y - corner - abs(y), 0.0);
334 let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
335 let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
336 return integral.y - integral.x;
337}
338
339// Selects corner radius based on quadrant.
340fn pick_corner_radius(center_to_point: vec2<f32>, radii: Corners) -> f32 {
341 if (center_to_point.x < 0.0) {
342 if (center_to_point.y < 0.0) {
343 return radii.top_left;
344 } else {
345 return radii.bottom_left;
346 }
347 } else {
348 if (center_to_point.y < 0.0) {
349 return radii.top_right;
350 } else {
351 return radii.bottom_right;
352 }
353 }
354}
355
356// Signed distance of the point to the quad's border - positive outside the
357// border, and negative inside.
358//
359// See comments on similar code using `quad_sdf_impl` in `fs_quad` for
360// explanation.
361fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
362 let half_size = bounds.size / 2.0;
363 let center = bounds.origin + half_size;
364 let center_to_point = point - center;
365 let corner_radius = pick_corner_radius(center_to_point, corner_radii);
366 let corner_to_point = abs(center_to_point) - half_size;
367 let corner_center_to_point = corner_to_point + corner_radius;
368 return quad_sdf_impl(corner_center_to_point, corner_radius);
369}
370
371fn quad_sdf_impl(corner_center_to_point: vec2<f32>, corner_radius: f32) -> f32 {
372 if (corner_radius == 0.0) {
373 // Fast path for unrounded corners.
374 return max(corner_center_to_point.x, corner_center_to_point.y);
375 } else {
376 // Signed distance of the point from a quad that is inset by corner_radius.
377 // It is negative inside this quad, and positive outside.
378 let signed_distance_to_inset_quad =
379 // 0 inside the inset quad, and positive outside.
380 length(max(vec2<f32>(0.0), corner_center_to_point)) +
381 // 0 outside the inset quad, and negative inside.
382 min(0.0, max(corner_center_to_point.x, corner_center_to_point.y));
383
384 return signed_distance_to_inset_quad - corner_radius;
385 }
386}
387
388// Abstract away the final color transformation based on the
389// target alpha compositing mode.
390fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
391 let alpha = color.a * alpha_factor;
392 let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
393 return vec4<f32>(color.rgb * multiplier, alpha);
394}
395
396
397struct GradientColor {
398 solid: vec4<f32>,
399 color0: vec4<f32>,
400 color1: vec4<f32>,
401}
402
403fn prepare_gradient_color(tag: u32, color_space: u32,
404 solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
405 var result = GradientColor();
406
407 if (tag == 0u || tag == 2u || tag == 3u) {
408 result.solid = hsla_to_rgba(solid);
409 } else if (tag == 1u) {
410 // The hsla_to_rgba is returns a linear sRGB color
411 result.color0 = hsla_to_rgba(colors[0].color);
412 result.color1 = hsla_to_rgba(colors[1].color);
413
414 // Prepare color space in vertex for avoid conversion
415 // in fragment shader for performance reasons
416 if (color_space == 0u) {
417 // sRGB
418 result.color0 = linear_to_srgba(result.color0);
419 result.color1 = linear_to_srgba(result.color1);
420 } else if (color_space == 1u) {
421 // Oklab
422 result.color0 = linear_srgb_to_oklab(result.color0);
423 result.color1 = linear_srgb_to_oklab(result.color1);
424 }
425 }
426
427 return result;
428}
429
430fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
431 solid_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
432 var background_color = vec4<f32>(0.0);
433
434 switch (background.tag) {
435 default: {
436 return solid_color;
437 }
438 case 1u: {
439 // Linear gradient background.
440 // -90 degrees to match the CSS gradient angle.
441 let angle = background.gradient_angle_or_pattern_height;
442 let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0;
443 var direction = vec2<f32>(cos(radians), sin(radians));
444 let stop0_percentage = background.colors[0].percentage;
445 let stop1_percentage = background.colors[1].percentage;
446
447 // Expand the short side to be the same as the long side
448 if (bounds.size.x > bounds.size.y) {
449 direction.y *= bounds.size.y / bounds.size.x;
450 } else {
451 direction.x *= bounds.size.x / bounds.size.y;
452 }
453
454 // Get the t value for the linear gradient with the color stop percentages.
455 let half_size = bounds.size / 2.0;
456 let center = bounds.origin + half_size;
457 let center_to_point = position - center;
458 var t = dot(center_to_point, direction) / length(direction);
459 // Check the direct to determine the use x or y
460 if (abs(direction.x) > abs(direction.y)) {
461 t = (t + half_size.x) / bounds.size.x;
462 } else {
463 t = (t + half_size.y) / bounds.size.y;
464 }
465
466 // Adjust t based on the stop percentages
467 t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
468 t = clamp(t, 0.0, 1.0);
469
470 switch (background.color_space) {
471 default: {
472 background_color = srgba_to_linear(mix(color0, color1, t));
473 }
474 case 1u: {
475 let oklab_color = mix(color0, color1, t);
476 background_color = oklab_to_linear_srgb(oklab_color);
477 }
478 }
479 }
480 case 2u: {
481 // pattern slash
482 let gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height;
483 let pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
484 let pattern_interval = (gradient_angle_or_pattern_height % 65535.0f) / 255.0f;
485 let pattern_height = pattern_width + pattern_interval;
486 let stripe_angle = M_PI_F / 4.0;
487 let pattern_period = pattern_height * sin(stripe_angle);
488 let rotation = mat2x2<f32>(
489 cos(stripe_angle), -sin(stripe_angle),
490 sin(stripe_angle), cos(stripe_angle)
491 );
492 let relative_position = position - bounds.origin;
493 let rotated_point = rotation * relative_position;
494 let pattern = rotated_point.x % pattern_period;
495 let distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f;
496 background_color = solid_color;
497 background_color.a *= saturate(0.5 - distance);
498 }
499 case 3u: {
500 // checkerboard
501 let size = background.gradient_angle_or_pattern_height;
502 let relative_position = position - bounds.origin;
503
504 let x_index = floor(relative_position.x / size);
505 let y_index = floor(relative_position.y / size);
506 let should_be_colored = (x_index + y_index) % 2.0;
507
508 background_color = solid_color;
509 background_color.a *= saturate(should_be_colored);
510 }
511 }
512
513 return background_color;
514}
515
516// --- quads --- //
517
518struct Quad {
519 order: u32,
520 border_style: u32,
521 bounds: Bounds,
522 content_mask: Bounds,
523 background: Background,
524 border_color: Hsla,
525 corner_radii: Corners,
526 border_widths: Edges,
527}
528@group(1) @binding(0) var<storage, read> b_quads: array<Quad>;
529
530struct QuadVarying {
531 @builtin(position) position: vec4<f32>,
532 @location(0) @interpolate(flat) border_color: vec4<f32>,
533 @location(1) @interpolate(flat) quad_id: u32,
534 // TODO: use `clip_distance` once Naga supports it
535 @location(2) clip_distances: vec4<f32>,
536 @location(3) @interpolate(flat) background_solid: vec4<f32>,
537 @location(4) @interpolate(flat) background_color0: vec4<f32>,
538 @location(5) @interpolate(flat) background_color1: vec4<f32>,
539}
540
541@vertex
542fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
543 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
544 let quad = b_quads[instance_id];
545
546 var out = QuadVarying();
547 out.position = to_device_position(unit_vertex, quad.bounds);
548
549 let gradient = prepare_gradient_color(
550 quad.background.tag,
551 quad.background.color_space,
552 quad.background.solid,
553 quad.background.colors
554 );
555 out.background_solid = gradient.solid;
556 out.background_color0 = gradient.color0;
557 out.background_color1 = gradient.color1;
558 out.border_color = hsla_to_rgba(quad.border_color);
559 out.quad_id = instance_id;
560 out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
561 return out;
562}
563
564@fragment
565fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
566 // Alpha clip first, since we don't have `clip_distance`.
567 if (any(input.clip_distances < vec4<f32>(0.0))) {
568 return vec4<f32>(0.0);
569 }
570
571 let quad = b_quads[input.quad_id];
572
573 let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
574 input.background_solid, input.background_color0, input.background_color1);
575
576 let unrounded = quad.corner_radii.top_left == 0.0 &&
577 quad.corner_radii.bottom_left == 0.0 &&
578 quad.corner_radii.top_right == 0.0 &&
579 quad.corner_radii.bottom_right == 0.0;
580
581 // Fast path when the quad is not rounded and doesn't have any border
582 if (quad.border_widths.top == 0.0 &&
583 quad.border_widths.left == 0.0 &&
584 quad.border_widths.right == 0.0 &&
585 quad.border_widths.bottom == 0.0 &&
586 unrounded) {
587 return blend_color(background_color, 1.0);
588 }
589
590 let size = quad.bounds.size;
591 let half_size = size / 2.0;
592 let point = input.position.xy - quad.bounds.origin;
593 let center_to_point = point - half_size;
594
595 // Signed distance field threshold for inclusion of pixels. 0.5 is the
596 // minimum distance between the center of the pixel and the edge.
597 let antialias_threshold = 0.5;
598
599 // Radius of the nearest corner
600 let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
601
602 // Width of the nearest borders
603 let border = vec2<f32>(
604 select(
605 quad.border_widths.right,
606 quad.border_widths.left,
607 center_to_point.x < 0.0),
608 select(
609 quad.border_widths.bottom,
610 quad.border_widths.top,
611 center_to_point.y < 0.0));
612
613 // 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
614 // The purpose of this is to not draw antialiasing pixels in this case.
615 let reduced_border =
616 vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
617 select(border.y, -antialias_threshold, border.y == 0.0));
618
619 // Vector from the corner of the quad bounds to the point, after mirroring
620 // the point into the bottom right quadrant. Both components are <= 0.
621 let corner_to_point = abs(center_to_point) - half_size;
622
623 // Vector from the point to the center of the rounded corner's circle, also
624 // mirrored into bottom right quadrant.
625 let corner_center_to_point = corner_to_point + corner_radius;
626
627 // Whether the nearest point on the border is rounded
628 let is_near_rounded_corner =
629 corner_center_to_point.x >= 0 &&
630 corner_center_to_point.y >= 0;
631
632 // Vector from straight border inner corner to point.
633 let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
634
635 // Whether the point is beyond the inner edge of the straight border.
636 let is_beyond_inner_straight_border =
637 straight_border_inner_corner_to_point.x > 0 ||
638 straight_border_inner_corner_to_point.y > 0;
639
640 // Whether the point is far enough inside the quad, such that the pixels are
641 // not affected by the straight border.
642 let is_within_inner_straight_border =
643 straight_border_inner_corner_to_point.x < -antialias_threshold &&
644 straight_border_inner_corner_to_point.y < -antialias_threshold;
645
646 // Fast path for points that must be part of the background.
647 //
648 // This could be optimized further for large rounded corners by including
649 // points in an inscribed rectangle, or some other quick linear check.
650 // However, that might negatively impact performance in the case of
651 // reasonable sizes for rounded corners.
652 if (is_within_inner_straight_border && !is_near_rounded_corner) {
653 return blend_color(background_color, 1.0);
654 }
655
656 // Signed distance of the point to the outside edge of the quad's border. It
657 // is positive outside this edge, and negative inside.
658 let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius);
659
660 // Approximate signed distance of the point to the inside edge of the quad's
661 // border. It is negative outside this edge (within the border), and
662 // positive inside.
663 //
664 // This is not always an accurate signed distance:
665 // * The rounded portions with varying border width use an approximation of
666 // nearest-point-on-ellipse.
667 // * When it is quickly known to be outside the edge, -1.0 is used.
668 var inner_sdf = 0.0;
669 if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) {
670 // Fast paths for straight borders.
671 inner_sdf = -max(straight_border_inner_corner_to_point.x,
672 straight_border_inner_corner_to_point.y);
673 } else if (is_beyond_inner_straight_border) {
674 // Fast path for points that must be outside the inner edge.
675 inner_sdf = -1.0;
676 } else if (reduced_border.x == reduced_border.y) {
677 // Fast path for circular inner edge.
678 inner_sdf = -(outer_sdf + reduced_border.x);
679 } else {
680 let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
681 inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
682 }
683
684 // Negative when inside the border
685 let border_sdf = max(inner_sdf, outer_sdf);
686
687 var color = background_color;
688 if (border_sdf < antialias_threshold) {
689 var border_color = input.border_color;
690
691 // Dashed border logic when border_style == 1
692 if (quad.border_style == 1) {
693 // Position along the perimeter in "dash space", where each dash
694 // period has length 1
695 var t = 0.0;
696
697 // Total number of dash periods, so that the dash spacing can be
698 // adjusted to evenly divide it
699 var max_t = 0.0;
700
701 // Border width is proportional to dash size. This is the behavior
702 // used by browsers, but also avoids dashes from different segments
703 // overlapping when dash size is smaller than the border width.
704 //
705 // Dash pattern: (2 * border width) dash, (1 * border width) gap
706 let dash_length_per_width = 2.0;
707 let dash_gap_per_width = 1.0;
708 let dash_period_per_width = dash_length_per_width + dash_gap_per_width;
709
710 // Since the dash size is determined by border width, the density of
711 // dashes varies. Multiplying a pixel distance by this returns a
712 // position in dash space - it has units (dash period / pixels). So
713 // a dash velocity of (1 / 10) is 1 dash every 10 pixels.
714 var dash_velocity = 0.0;
715
716 // Dividing this by the border width gives the dash velocity
717 let dv_numerator = 1.0 / dash_period_per_width;
718
719 if (unrounded) {
720 // When corners aren't rounded, the dashes are separately laid
721 // out on each straight line, rather than around the whole
722 // perimeter. This way each line starts and ends with a dash.
723 let is_horizontal =
724 corner_center_to_point.x <
725 corner_center_to_point.y;
726
727 // When applying dashed borders to just some, not all, the sides.
728 // The way we chose border widths above sometimes comes with a 0 width value.
729 // So we choose again to avoid division by zero.
730 // TODO: A better solution exists taking a look at the whole file.
731 // this does not fix single dashed borders at the corners
732 let dashed_border = vec2<f32>(
733 max(
734 quad.border_widths.bottom,
735 quad.border_widths.top,
736 ),
737 max(
738 quad.border_widths.right,
739 quad.border_widths.left,
740 )
741 );
742
743 let border_width = select(dashed_border.y, dashed_border.x, is_horizontal);
744 dash_velocity = dv_numerator / border_width;
745 t = select(point.y, point.x, is_horizontal) * dash_velocity;
746 max_t = select(size.y, size.x, is_horizontal) * dash_velocity;
747 } else {
748 // When corners are rounded, the dashes are laid out clockwise
749 // around the whole perimeter.
750
751 let r_tr = quad.corner_radii.top_right;
752 let r_br = quad.corner_radii.bottom_right;
753 let r_bl = quad.corner_radii.bottom_left;
754 let r_tl = quad.corner_radii.top_left;
755
756 let w_t = quad.border_widths.top;
757 let w_r = quad.border_widths.right;
758 let w_b = quad.border_widths.bottom;
759 let w_l = quad.border_widths.left;
760
761 // Straight side dash velocities
762 let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0);
763 let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0);
764 let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0);
765 let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0);
766
767 // Straight side lengths in dash space
768 let s_t = (size.x - r_tl - r_tr) * dv_t;
769 let s_r = (size.y - r_tr - r_br) * dv_r;
770 let s_b = (size.x - r_br - r_bl) * dv_b;
771 let s_l = (size.y - r_bl - r_tl) * dv_l;
772
773 let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r);
774 let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r);
775 let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l);
776 let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l);
777
778 // Corner lengths in dash space
779 let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr;
780 let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br;
781 let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl;
782 let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl;
783
784 // Cumulative dash space upto each segment
785 let upto_tr = s_t;
786 let upto_r = upto_tr + c_tr;
787 let upto_br = upto_r + s_r;
788 let upto_b = upto_br + c_br;
789 let upto_bl = upto_b + s_b;
790 let upto_l = upto_bl + c_bl;
791 let upto_tl = upto_l + s_l;
792 max_t = upto_tl + c_tl;
793
794 if (is_near_rounded_corner) {
795 let radians = atan2(corner_center_to_point.y,
796 corner_center_to_point.x);
797 let corner_t = radians * corner_radius;
798
799 if (center_to_point.x >= 0.0) {
800 if (center_to_point.y < 0.0) {
801 dash_velocity = corner_dash_velocity_tr;
802 // Subtracted because radians is pi/2 to 0 when
803 // going clockwise around the top right corner,
804 // since the y axis has been flipped
805 t = upto_r - corner_t * dash_velocity;
806 } else {
807 dash_velocity = corner_dash_velocity_br;
808 // Added because radians is 0 to pi/2 when going
809 // clockwise around the bottom-right corner
810 t = upto_br + corner_t * dash_velocity;
811 }
812 } else {
813 if (center_to_point.y >= 0.0) {
814 dash_velocity = corner_dash_velocity_bl;
815 // Subtracted because radians is pi/2 to 0 when
816 // going clockwise around the bottom-left corner,
817 // since the x axis has been flipped
818 t = upto_l - corner_t * dash_velocity;
819 } else {
820 dash_velocity = corner_dash_velocity_tl;
821 // Added because radians is 0 to pi/2 when going
822 // clockwise around the top-left corner, since both
823 // axis were flipped
824 t = upto_tl + corner_t * dash_velocity;
825 }
826 }
827 } else {
828 // Straight borders
829 let is_horizontal =
830 corner_center_to_point.x <
831 corner_center_to_point.y;
832 if (is_horizontal) {
833 if (center_to_point.y < 0.0) {
834 dash_velocity = dv_t;
835 t = (point.x - r_tl) * dash_velocity;
836 } else {
837 dash_velocity = dv_b;
838 t = upto_bl - (point.x - r_bl) * dash_velocity;
839 }
840 } else {
841 if (center_to_point.x < 0.0) {
842 dash_velocity = dv_l;
843 t = upto_tl - (point.y - r_tl) * dash_velocity;
844 } else {
845 dash_velocity = dv_r;
846 t = upto_r + (point.y - r_tr) * dash_velocity;
847 }
848 }
849 }
850 }
851
852 let dash_length = dash_length_per_width / dash_period_per_width;
853 let desired_dash_gap = dash_gap_per_width / dash_period_per_width;
854
855 // Straight borders should start and end with a dash, so max_t is
856 // reduced to cause this.
857 max_t -= select(0.0, dash_length, unrounded);
858 if (max_t >= 1.0) {
859 // Adjust dash gap to evenly divide max_t.
860 let dash_count = floor(max_t);
861 let dash_period = max_t / dash_count;
862 border_color.a *= dash_alpha(
863 t,
864 dash_period,
865 dash_length,
866 dash_velocity,
867 antialias_threshold);
868 } else if (unrounded) {
869 // When there isn't enough space for the full gap between the
870 // two start / end dashes of a straight border, reduce gap to
871 // make them fit.
872 let dash_gap = max_t - dash_length;
873 if (dash_gap > 0.0) {
874 let dash_period = dash_length + dash_gap;
875 border_color.a *= dash_alpha(
876 t,
877 dash_period,
878 dash_length,
879 dash_velocity,
880 antialias_threshold);
881 }
882 }
883 }
884
885 // Blend the border on top of the background and then linearly interpolate
886 // between the two as we slide inside the background.
887 let blended_border = over(background_color, border_color);
888 color = mix(background_color, blended_border,
889 saturate(antialias_threshold - inner_sdf));
890 }
891
892 return blend_color(color, saturate(antialias_threshold - outer_sdf));
893}
894
895// Returns the dash velocity of a corner given the dash velocity of the two
896// sides, by returning the slower velocity (larger dashes).
897//
898// Since 0 is used for dash velocity when the border width is 0 (instead of
899// +inf), this returns the other dash velocity in that case.
900//
901// An alternative to this might be to appropriately interpolate the dash
902// velocity around the corner, but that seems overcomplicated.
903fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 {
904 if (dv1 == 0.0) {
905 return dv2;
906 } else if (dv2 == 0.0) {
907 return dv1;
908 } else {
909 return min(dv1, dv2);
910 }
911}
912
913// Returns alpha used to render antialiased dashes.
914// `t` is within the dash when `fmod(t, period) < length`.
915fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 {
916 let half_period = period / 2;
917 let half_length = length / 2;
918 // Value in [-half_period, half_period].
919 // The dash is in [-half_length, half_length].
920 let centered = fmod(t + half_period - half_length, period) - half_period;
921 // Signed distance for the dash, negative values are inside the dash.
922 let signed_distance = abs(centered) - half_length;
923 // Antialiased alpha based on the signed distance.
924 return saturate(antialias_threshold - signed_distance / dash_velocity);
925}
926
927// This approximates distance to the nearest point to a quarter ellipse in a way
928// that is sufficient for anti-aliasing when the ellipse is not very eccentric.
929// The components of `point` are expected to be positive.
930//
931// Negative on the outside and positive on the inside.
932fn quarter_ellipse_sdf(point: vec2<f32>, radii: vec2<f32>) -> f32 {
933 // Scale the space to treat the ellipse like a unit circle.
934 let circle_vec = point / radii;
935 let unit_circle_sdf = length(circle_vec) - 1.0;
936 // Approximate up-scaling of the length by using the average of the radii.
937 //
938 // TODO: A better solution would be to use the gradient of the implicit
939 // function for an ellipse to approximate a scaling factor.
940 return unit_circle_sdf * (radii.x + radii.y) * -0.5;
941}
942
943// Modulus that has the same sign as `a`.
944fn fmod(a: f32, b: f32) -> f32 {
945 return a - b * trunc(a / b);
946}
947
948// --- shadows --- //
949
950struct Shadow {
951 order: u32,
952 blur_radius: f32,
953 bounds: Bounds,
954 corner_radii: Corners,
955 content_mask: Bounds,
956 color: Hsla,
957}
958@group(1) @binding(0) var<storage, read> b_shadows: array<Shadow>;
959
960struct ShadowVarying {
961 @builtin(position) position: vec4<f32>,
962 @location(0) @interpolate(flat) color: vec4<f32>,
963 @location(1) @interpolate(flat) shadow_id: u32,
964 //TODO: use `clip_distance` once Naga supports it
965 @location(3) clip_distances: vec4<f32>,
966}
967
968@vertex
969fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
970 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
971 var shadow = b_shadows[instance_id];
972
973 let margin = 3.0 * shadow.blur_radius;
974 // Set the bounds of the shadow and adjust its size based on the shadow's
975 // spread radius to achieve the spreading effect
976 shadow.bounds.origin -= vec2<f32>(margin);
977 shadow.bounds.size += 2.0 * vec2<f32>(margin);
978
979 var out = ShadowVarying();
980 out.position = to_device_position(unit_vertex, shadow.bounds);
981 out.color = hsla_to_rgba(shadow.color);
982 out.shadow_id = instance_id;
983 out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
984 return out;
985}
986
987@fragment
988fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
989 // Alpha clip first, since we don't have `clip_distance`.
990 if (any(input.clip_distances < vec4<f32>(0.0))) {
991 return vec4<f32>(0.0);
992 }
993
994 let shadow = b_shadows[input.shadow_id];
995 let half_size = shadow.bounds.size / 2.0;
996 let center = shadow.bounds.origin + half_size;
997 let center_to_point = input.position.xy - center;
998
999 let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
1000
1001 // The signal is only non-zero in a limited range, so don't waste samples
1002 let low = center_to_point.y - half_size.y;
1003 let high = center_to_point.y + half_size.y;
1004 let start = clamp(-3.0 * shadow.blur_radius, low, high);
1005 let end = clamp(3.0 * shadow.blur_radius, low, high);
1006
1007 // Accumulate samples (we can get away with surprisingly few samples)
1008 let step = (end - start) / 4.0;
1009 var y = start + step * 0.5;
1010 var alpha = 0.0;
1011 for (var i = 0; i < 4; i += 1) {
1012 let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
1013 shadow.blur_radius, corner_radius, half_size);
1014 alpha += blur * gaussian(y, shadow.blur_radius) * step;
1015 y += step;
1016 }
1017
1018 return blend_color(input.color, alpha);
1019}
1020
1021// --- path rasterization --- //
1022
1023struct PathRasterizationVertex {
1024 xy_position: vec2<f32>,
1025 st_position: vec2<f32>,
1026 color: Background,
1027 bounds: Bounds,
1028}
1029
1030@group(1) @binding(0) var<storage, read> b_path_vertices: array<PathRasterizationVertex>;
1031
1032struct PathRasterizationVarying {
1033 @builtin(position) position: vec4<f32>,
1034 @location(0) st_position: vec2<f32>,
1035 @location(1) @interpolate(flat) vertex_id: u32,
1036 //TODO: use `clip_distance` once Naga supports it
1037 @location(3) clip_distances: vec4<f32>,
1038}
1039
1040@vertex
1041fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
1042 let v = b_path_vertices[vertex_id];
1043
1044 var out = PathRasterizationVarying();
1045 out.position = to_device_position_impl(v.xy_position);
1046 out.st_position = v.st_position;
1047 out.vertex_id = vertex_id;
1048 out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.bounds);
1049 return out;
1050}
1051
1052@fragment
1053fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4<f32> {
1054 let dx = dpdx(input.st_position);
1055 let dy = dpdy(input.st_position);
1056 if (any(input.clip_distances < vec4<f32>(0.0))) {
1057 return vec4<f32>(0.0);
1058 }
1059
1060 let v = b_path_vertices[input.vertex_id];
1061 let background = v.color;
1062 let bounds = v.bounds;
1063
1064 var alpha: f32;
1065 if (length(vec2<f32>(dx.x, dy.x)) < 0.001) {
1066 // If the gradient is too small, return a solid color.
1067 alpha = 1.0;
1068 } else {
1069 let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
1070 let f = input.st_position.x * input.st_position.x - input.st_position.y;
1071 let distance = f / length(gradient);
1072 alpha = saturate(0.5 - distance);
1073 }
1074 let prepared_gradient = prepare_gradient_color(
1075 background.tag,
1076 background.color_space,
1077 background.solid,
1078 background.colors,
1079 );
1080 let color = gradient_color(background, input.position.xy, bounds,
1081 prepared_gradient.solid, prepared_gradient.color0, prepared_gradient.color1);
1082 return vec4<f32>(color.rgb * color.a * alpha, color.a * alpha);
1083}
1084
1085// --- paths --- //
1086
1087struct PathSprite {
1088 bounds: Bounds,
1089}
1090@group(1) @binding(0) var<storage, read> b_path_sprites: array<PathSprite>;
1091
1092struct PathVarying {
1093 @builtin(position) position: vec4<f32>,
1094 @location(0) texture_coords: vec2<f32>,
1095}
1096
1097@vertex
1098fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
1099 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1100 let sprite = b_path_sprites[instance_id];
1101 // Don't apply content mask because it was already accounted for when rasterizing the path.
1102 let device_position = to_device_position(unit_vertex, sprite.bounds);
1103 // For screen-space intermediate texture, convert screen position to texture coordinates
1104 let screen_position = sprite.bounds.origin + unit_vertex * sprite.bounds.size;
1105 let texture_coords = screen_position / globals.viewport_size;
1106
1107 var out = PathVarying();
1108 out.position = device_position;
1109 out.texture_coords = texture_coords;
1110
1111 return out;
1112}
1113
1114@fragment
1115fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
1116 let sample = textureSample(t_sprite, s_sprite, input.texture_coords);
1117 return sample;
1118}
1119
1120// --- underlines --- //
1121
1122struct Underline {
1123 order: u32,
1124 pad: u32,
1125 bounds: Bounds,
1126 content_mask: Bounds,
1127 color: Hsla,
1128 thickness: f32,
1129 wavy: u32,
1130}
1131@group(1) @binding(0) var<storage, read> b_underlines: array<Underline>;
1132
1133struct UnderlineVarying {
1134 @builtin(position) position: vec4<f32>,
1135 @location(0) @interpolate(flat) color: vec4<f32>,
1136 @location(1) @interpolate(flat) underline_id: u32,
1137 //TODO: use `clip_distance` once Naga supports it
1138 @location(3) clip_distances: vec4<f32>,
1139}
1140
1141@vertex
1142fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying {
1143 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1144 let underline = b_underlines[instance_id];
1145
1146 var out = UnderlineVarying();
1147 out.position = to_device_position(unit_vertex, underline.bounds);
1148 out.color = hsla_to_rgba(underline.color);
1149 out.underline_id = instance_id;
1150 out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
1151 return out;
1152}
1153
1154@fragment
1155fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
1156 const WAVE_FREQUENCY: f32 = 2.0;
1157 const WAVE_HEIGHT_RATIO: f32 = 0.8;
1158
1159 // Alpha clip first, since we don't have `clip_distance`.
1160 if (any(input.clip_distances < vec4<f32>(0.0))) {
1161 return vec4<f32>(0.0);
1162 }
1163
1164 let underline = b_underlines[input.underline_id];
1165 if ((underline.wavy & 0xFFu) == 0u)
1166 {
1167 return blend_color(input.color, input.color.a);
1168 }
1169
1170 let half_thickness = underline.thickness * 0.5;
1171
1172 let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
1173 let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y;
1174 let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y;
1175
1176 let sine = sin(st.x * frequency) * amplitude;
1177 let dSine = cos(st.x * frequency) * amplitude * frequency;
1178 let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);
1179 let distance_in_pixels = distance * underline.bounds.size.y;
1180 let distance_from_top_border = distance_in_pixels - half_thickness;
1181 let distance_from_bottom_border = distance_in_pixels + half_thickness;
1182 let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
1183 return blend_color(input.color, alpha * input.color.a);
1184}
1185
1186// --- monochrome sprites --- //
1187
1188struct MonochromeSprite {
1189 order: u32,
1190 pad: u32,
1191 bounds: Bounds,
1192 content_mask: Bounds,
1193 color: Hsla,
1194 tile: AtlasTile,
1195 transformation: TransformationMatrix,
1196}
1197@group(1) @binding(0) var<storage, read> b_mono_sprites: array<MonochromeSprite>;
1198
1199struct MonoSpriteVarying {
1200 @builtin(position) position: vec4<f32>,
1201 @location(0) tile_position: vec2<f32>,
1202 @location(1) @interpolate(flat) color: vec4<f32>,
1203 @location(3) clip_distances: vec4<f32>,
1204}
1205
1206@vertex
1207fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying {
1208 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1209 let sprite = b_mono_sprites[instance_id];
1210
1211 var out = MonoSpriteVarying();
1212 out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
1213
1214 out.tile_position = to_tile_position(unit_vertex, sprite.tile);
1215 out.color = hsla_to_rgba(sprite.color);
1216 out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
1217 return out;
1218}
1219
1220@fragment
1221fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
1222 let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
1223 let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, gamma_params.grayscale_enhanced_contrast, gamma_params.gamma_ratios);
1224
1225 // Alpha clip after using the derivatives.
1226 if (any(input.clip_distances < vec4<f32>(0.0))) {
1227 return vec4<f32>(0.0);
1228 }
1229
1230 return blend_color(input.color, alpha_corrected);
1231}
1232
1233// --- polychrome sprites --- //
1234
1235struct PolychromeSprite {
1236 order: u32,
1237 pad: u32,
1238 grayscale: u32,
1239 opacity: f32,
1240 bounds: Bounds,
1241 content_mask: Bounds,
1242 corner_radii: Corners,
1243 tile: AtlasTile,
1244}
1245@group(1) @binding(0) var<storage, read> b_poly_sprites: array<PolychromeSprite>;
1246
1247struct PolySpriteVarying {
1248 @builtin(position) position: vec4<f32>,
1249 @location(0) tile_position: vec2<f32>,
1250 @location(1) @interpolate(flat) sprite_id: u32,
1251 @location(3) clip_distances: vec4<f32>,
1252}
1253
1254@vertex
1255fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
1256 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1257 let sprite = b_poly_sprites[instance_id];
1258
1259 var out = PolySpriteVarying();
1260 out.position = to_device_position(unit_vertex, sprite.bounds);
1261 out.tile_position = to_tile_position(unit_vertex, sprite.tile);
1262 out.sprite_id = instance_id;
1263 out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
1264 return out;
1265}
1266
1267@fragment
1268fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
1269 let sample = textureSample(t_sprite, s_sprite, input.tile_position);
1270 // Alpha clip after using the derivatives.
1271 if (any(input.clip_distances < vec4<f32>(0.0))) {
1272 return vec4<f32>(0.0);
1273 }
1274
1275 let sprite = b_poly_sprites[input.sprite_id];
1276 let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
1277
1278 var color = sample;
1279 if ((sprite.grayscale & 0xFFu) != 0u) {
1280 let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
1281 color = vec4<f32>(vec3<f32>(grayscale), sample.a);
1282 }
1283 return blend_color(color, sprite.opacity * saturate(0.5 - distance));
1284}
1285
1286// --- surfaces --- //
1287
1288struct SurfaceParams {
1289 bounds: Bounds,
1290 content_mask: Bounds,
1291}
1292
1293@group(1) @binding(0) var<uniform> surface_locals: SurfaceParams;
1294@group(1) @binding(1) var t_y: texture_2d<f32>;
1295@group(1) @binding(2) var t_cb_cr: texture_2d<f32>;
1296@group(1) @binding(3) var s_surface: sampler;
1297
1298const ycbcr_to_RGB = mat4x4<f32>(
1299 vec4<f32>( 1.0000f, 1.0000f, 1.0000f, 0.0),
1300 vec4<f32>( 0.0000f, -0.3441f, 1.7720f, 0.0),
1301 vec4<f32>( 1.4020f, -0.7141f, 0.0000f, 0.0),
1302 vec4<f32>(-0.7010f, 0.5291f, -0.8860f, 1.0),
1303);
1304
1305struct SurfaceVarying {
1306 @builtin(position) position: vec4<f32>,
1307 @location(0) texture_position: vec2<f32>,
1308 @location(3) clip_distances: vec4<f32>,
1309}
1310
1311@vertex
1312fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
1313 let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
1314
1315 var out = SurfaceVarying();
1316 out.position = to_device_position(unit_vertex, surface_locals.bounds);
1317 out.texture_position = unit_vertex;
1318 out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
1319 return out;
1320}
1321
1322@fragment
1323fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
1324 // Alpha clip after using the derivatives.
1325 if (any(input.clip_distances < vec4<f32>(0.0))) {
1326 return vec4<f32>(0.0);
1327 }
1328
1329 let y_cb_cr = vec4<f32>(
1330 textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
1331 textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,
1332 1.0);
1333
1334 return ycbcr_to_RGB * y_cb_cr;
1335}