StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 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. |
| 15 | fn 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 | |
| 20 | fn enhance_contrast(alpha: f32, k: f32) -> f32 { |
| 21 | return alpha * (k + 1.0) / (alpha * k + 1.0); |
| 22 | } |
| 23 | |
| 24 | struct 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 | |
| 36 | struct 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 | |
| 47 | struct 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 |
| 58 | fn 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 |
| 108 | fn 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 |