Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/rendering/wgpu/shaders/glyph_shader.wgsl
StratoSDK / crates / strato-ui-renderer / src / rendering / wgpu / shaders / glyph_shader.wgsl
1// Brightness-scaled contrast enhancement for glyph alpha masks.
2//
3// Linear sRGB blending makes light-on-dark text appear too thin because AA fringe
4// pixels blend perceptually darker than expected. Dark-on-light text has the opposite
5// problem — it already looks heavier than its geometric coverage.
6//
7// To compensate, we compute the text color's brightness (k) and use it to boost the
8// glyph alpha through enhance_contrast(). Brighter text gets a stronger boost;
9// dark text is left unchanged.
10//
11// enhance_contrast() adapted from DWrite_EnhanceContrast in Windows Terminal's DirectWrite shader:
12// https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
13// Copyright (c) Microsoft Corporation.
14// Licensed under the MIT license.
15fn glyph_color_brightness(color: vec3<f32>) -> f32 {
16 // REC. 601 luminance coefficients for perceived brightness.
17 return dot(color, vec3<f32>(0.30, 0.59, 0.11));
18}
19 
20fn enhance_contrast(alpha: f32, k: f32) -> f32 {
21 return alpha * (k + 1.0) / (alpha * k + 1.0);
22}
23 
24struct Uniforms {
25 viewport_size: vec2<f32>,
26 // Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require
27 // buffer bindings to be a multiple of 16 bytes.
28 padding: vec2<f32>
29}
30 
31@group(0) @binding(0) var<uniform> uniforms: Uniforms;
32 
33@group(1) @binding(0) var glyphAtlasTexture: texture_2d<f32>;
34@group(1) @binding(1) var glyphAtlasSampler: sampler;
35 
36struct GlyphVertexShaderInput {
37 // The position of the vertex in normalized device coordinates.
38 @location(0) vertex_position: vec2<f32>,
39 @location(1) bounds: vec4<f32>,
40 @location(2) uv_bounds: vec4<f32>,
41 @location(3) fade_start: f32,
42 @location(4) fade_end: f32,
43 @location(5) color: vec4<f32>,
44 @location(6) is_emoji: i32,
45}
46 
47struct GlyphVertexShaderOutput {
48 @builtin(position) position: vec4<f32>,
49 @location(0) rect_center: vec2<f32>,
50 @location(1) rect_corner: vec2<f32>,
51 @location(2) texture_coordinate: vec2<f32>,
52 @location(3) fade_alpha: f32,
53 @location(4) color: vec4<f32>,
54 @location(5) is_emoji: i32,
55}
56 
57@vertex
58fn vs_main(
59 glyph: GlyphVertexShaderInput,
60) -> GlyphVertexShaderOutput {
61 var out: GlyphVertexShaderOutput;
62 var origin: vec2<f32> = glyph.bounds.xy;
63 var size: vec2<f32> = glyph.bounds.zw;
64 var pixel_pos: vec2<f32> = glyph.vertex_position * size + origin;
65 
66 // Use floor here to vertically align the glyph to the pixel grid.
67 // If it's not aligned to the grid, the fragment shader will do its
68 // own interpolation, which makes it so we don't use the anti-aliasing
69 // from core text, which is what we want. We don't force the glyph to a
70 // horizontal pixel position because we rasterize the glyph at multiple
71 // subpixel positions, and so the very slight linear interpolation here
72 // won't produce a fuzzy glyph, just a correctly-positioned one.
73 pixel_pos = vec2(pixel_pos.x, floor(pixel_pos.y));
74 
75 // Evaluating the glyphs fade effect. Note that the fade may go in two different directions:
76 // - Right to left (default) - where the opaque side is on the right, and transparent on the left
77 // (in this case, the start_fade < end_fade; start is where the fade is transparent)
78 // - Left to right - where the opaque side is on the left, and it fades towards the right side.
79 // In this case, start_fade > end_fade, and the opaque side is on the left (end_fade).
80 // To clarify: fade_start is ALWAYS where the fade is transparent, and fade_end is ALWAYS where
81 // the opaque part is, this is reflected in how we compute width, dist, and alpha.
82 var fade_width: f32 = abs(glyph.fade_end - glyph.fade_start);
83 var fade_dist: f32 = pixel_pos.x - min(glyph.fade_start, glyph.fade_end);
84 
85 var fade_alpha: f32;
86 if glyph.fade_end < glyph.fade_start { // left-to-right case
87 fade_alpha = fade_dist / fade_width;
88 } else { // right-to-left case
89 fade_alpha = 1. - fade_dist / fade_width;
90 }
91 
92 // Convert the position of the item from screen coordinates into normalized device coordinates
93 var device_pos: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
94 
95 var texture_coordinate: vec2<f32> = glyph.uv_bounds.xy + glyph.vertex_position * glyph.uv_bounds.zw;
96 
97 out.position = vec4<f32>(device_pos, 0.0, 1.0);
98 out.rect_corner = size / 2.0;
99 out.rect_center = origin + out.rect_corner;
100 out.texture_coordinate = texture_coordinate;
101 out.fade_alpha = fade_alpha;
102 out.color = glyph.color;
103 out.is_emoji = glyph.is_emoji;
104 return out;
105}
106 
107@fragment
108fn fs_main(in: GlyphVertexShaderOutput) -> @location(0) vec4<f32> {
109 // Sample the texture to obtain a color.
110 var tex_color: vec4<f32> = textureSample(glyphAtlasTexture, glyphAtlasSampler, in.texture_coordinate);
111 // Use the input color for non-emoji, and the sampled color for emoji.
112 var color: vec4<f32> = mix(in.color, tex_color, f32(in.is_emoji));
113 
114 // Scale contrast boost by text brightness:
115 // light text (white=1) gets full boost; dark text (black=0) gets none.
116 let k = glyph_color_brightness(color.rgb);
117 let contrasted = enhance_contrast(tex_color.r, k);
118 color.a *= max(contrasted, f32(in.is_emoji));
119 
120 // Apply the fade.
121 color.a *= saturate(in.fade_alpha);
122 return color;
123}
124