StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | struct Uniforms { |
| 2 | viewport_size: vec2<f32>, |
| 3 | // Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require |
| 4 | // buffer bindings to be a multiple of 16 bytes. |
| 5 | padding: vec2<f32> |
| 6 | } |
| 7 | |
| 8 | const EPSILON: f32 = 0.0000001; |
| 9 | const PI: f32 = 3.141592653589793; |
| 10 | |
| 11 | @group(0) @binding(0) var<uniform> uniforms: Uniforms; |
| 12 | |
| 13 | struct RectVertexShaderInput { |
| 14 | // The position of the vertex in normalized device coordinates. |
| 15 | @location(0) vertex_position: vec2<f32>, |
| 16 | // Bounds of the item in screen coordinates. Origin is contained in `xy`, size is contained in `zw`. |
| 17 | @location(1) bounds: vec4<f32>, |
| 18 | @location(2) background_start: vec2<f32>, |
| 19 | @location(3) background_start_color: vec4<f32>, |
| 20 | @location(4) background_end: vec2<f32>, |
| 21 | @location(5) background_end_color: vec4<f32>, |
| 22 | // Width of the border in the order top, left, right, bottom. |
| 23 | @location(6) border_width: vec4<f32>, |
| 24 | @location(7) border_start: vec2<f32>, |
| 25 | @location(8) border_start_color: vec4<f32>, |
| 26 | @location(9) border_end: vec2<f32>, |
| 27 | @location(10) border_end_color: vec4<f32>, |
| 28 | // Corner radius in the order top_left, top_right, bottom_left, bottom_right. |
| 29 | @location(11) corner_radius: vec4<f32>, |
| 30 | // The sigma and padding factor values packed into a single vec2. We pack them together in order |
| 31 | // to reduce the total number of attributes, which maxes out at 16. See here: |
| 32 | // https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_vertex_attributes |
| 33 | @location(12) drop_shadow_data: vec2<f32>, |
| 34 | // The length of the dash and the gaps for the x and y dimensions, packed into a single vec3. |
| 35 | @location(13) dashed_border_data: vec3<f32>, |
| 36 | }; |
| 37 | |
| 38 | struct RectVertexShaderOutput { |
| 39 | @builtin(position) position: vec4<f32>, |
| 40 | @location(0) background_start: vec2<f32>, |
| 41 | @location(1) background_start_color: vec4<f32>, |
| 42 | @location(2) background_end: vec2<f32>, |
| 43 | @location(3) background_end_color: vec4<f32>, |
| 44 | @location(4) border_width: vec4<f32>, |
| 45 | @location(5) border_start: vec2<f32>, |
| 46 | @location(6) border_start_color: vec4<f32>, |
| 47 | @location(7) border_end: vec2<f32>, |
| 48 | @location(8) border_end_color: vec4<f32>, |
| 49 | @location(9) rect_corner: vec2<f32>, |
| 50 | @location(10) rect_center: vec2<f32>, |
| 51 | @location(11) corner_radius: vec4<f32>, |
| 52 | @location(12) drop_shadow_data: vec2<f32>, |
| 53 | @location(13) dashed_border_data: vec3<f32>, |
| 54 | }; |
| 55 | |
| 56 | @vertex |
| 57 | fn vs_main( |
| 58 | in: RectVertexShaderInput, |
| 59 | ) -> RectVertexShaderOutput { |
| 60 | var out: RectVertexShaderOutput; |
| 61 | var origin: vec2<f32> = in.bounds.xy; |
| 62 | var size: vec2<f32> = in.bounds.zw; |
| 63 | var pixel_pos: vec2<f32> = in.vertex_position * size + origin; |
| 64 | // Convert the position of the item from screen coordinates into normalized device coordinates |
| 65 | var ndc_position: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0); |
| 66 | |
| 67 | out.position = vec4<f32>(ndc_position, 0.0, 1.0); |
| 68 | out.background_start = in.background_start * size + origin; |
| 69 | out.background_start_color = in.background_start_color; |
| 70 | out.background_end = in.background_end * size + origin; |
| 71 | out.background_end_color = in.background_end_color; |
| 72 | out.border_start = in.border_start * size + origin; |
| 73 | out.border_start_color = in.border_start_color; |
| 74 | out.border_end = in.border_end * size + origin; |
| 75 | out.border_end_color = in.border_end_color; |
| 76 | out.border_width = in.border_width; |
| 77 | out.corner_radius = in.corner_radius; |
| 78 | out.rect_corner = size / 2.; |
| 79 | out.rect_center = origin + out.rect_corner; |
| 80 | out.drop_shadow_data = in.drop_shadow_data; |
| 81 | out.dashed_border_data = in.dashed_border_data; |
| 82 | |
| 83 | return out; |
| 84 | } |
| 85 | |
| 86 | @fragment |
| 87 | fn rect_fs_main(in: RectVertexShaderOutput) -> @location(0) vec4<f32> { |
| 88 | var background_color: vec4<f32> = derive_color( |
| 89 | in.position.xy, |
| 90 | in.background_start, |
| 91 | in.background_end, |
| 92 | in.background_start_color, |
| 93 | in.background_end_color |
| 94 | ); |
| 95 | var border_color: vec4<f32> = derive_color( |
| 96 | in.position.xy, |
| 97 | in.border_start, |
| 98 | in.border_end, |
| 99 | in.border_start_color, |
| 100 | in.border_end_color |
| 101 | ); |
| 102 | |
| 103 | // There are actually two different radii at play here - the inner |
| 104 | // (background) and outer (shape) radii. The inner radius is equal to the |
| 105 | // outer radius minus the border width, in order for the two curves to |
| 106 | // maintain a constant distance from each other. |
| 107 | var inner_corner_radius: f32; |
| 108 | var outer_corner_radius: f32; |
| 109 | |
| 110 | var border_inner_corner: vec2<f32> = in.rect_corner; |
| 111 | if in.position.y >= in.rect_center.y { |
| 112 | // Bottom half |
| 113 | border_inner_corner.y -= in.border_width.z; |
| 114 | if in.position.x >= in.rect_center.x { |
| 115 | // Bottom right quadrant |
| 116 | border_inner_corner.x -= in.border_width.y; |
| 117 | outer_corner_radius = in.corner_radius.w; |
| 118 | inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z); |
| 119 | } else { |
| 120 | // Bottom left quadrant |
| 121 | border_inner_corner.x -= in.border_width.w; |
| 122 | outer_corner_radius = in.corner_radius.z; |
| 123 | inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z); |
| 124 | } |
| 125 | } else { |
| 126 | // Top half |
| 127 | border_inner_corner.y -= in.border_width.x; |
| 128 | if in.position.x >= in.rect_center.x { |
| 129 | // Top right quadrant |
| 130 | border_inner_corner.x -= in.border_width.y; |
| 131 | outer_corner_radius = in.corner_radius.y; |
| 132 | inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x); |
| 133 | } else { |
| 134 | // Top left quadrant |
| 135 | border_inner_corner.x -= in.border_width.w; |
| 136 | outer_corner_radius = in.corner_radius.x; |
| 137 | inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | var rect_origin: vec2<f32> = in.rect_center - in.rect_corner; |
| 142 | var outer_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius); |
| 143 | var inner_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, border_inner_corner, inner_corner_radius); |
| 144 | |
| 145 | var drop_shadow_sigma = in.drop_shadow_data.x; |
| 146 | var drop_shadow_padding_factor = in.drop_shadow_data.y; |
| 147 | if drop_shadow_sigma > 0.0 { |
| 148 | var rect_size: vec2<f32> = in.rect_corner * 2.0; |
| 149 | // When we are rendering a drop shadow we need to pass in the positions |
| 150 | // of the original rect, so we figure them out from the padding. |
| 151 | // Note we subtract twice the padding, because the padding is specified |
| 152 | // in terms of padding on a single side. |
| 153 | var shadowed_rect_origin: vec2<f32> = rect_origin + drop_shadow_padding_factor; |
| 154 | var shadowed_rect_size: vec2<f32> = rect_size - 2.0 * drop_shadow_padding_factor; |
| 155 | background_color.a *= rounded_box_shadow( |
| 156 | shadowed_rect_origin, |
| 157 | shadowed_rect_origin + shadowed_rect_size, |
| 158 | in.position.xy, |
| 159 | drop_shadow_sigma, |
| 160 | outer_corner_radius |
| 161 | ); |
| 162 | } else { |
| 163 | // Adjust the opacity of the border color based on where the pixel lies |
| 164 | // between the background and the border_width. |
| 165 | border_color.a *= saturate(inner_distance + 0.5); |
| 166 | |
| 167 | // Force the alpha value to 0 (fully transparent) if the pixel is |
| 168 | // outside the border_width. |
| 169 | // |
| 170 | // When we are outside the border, outer_distance is a larger positive |
| 171 | // value than inner_distance. When we are inside the border itself, |
| 172 | // outer_distance is negative and inner_distance is positive. When we |
| 173 | // are inside the inner border edge, outer_distance is more negative |
| 174 | // than inner_distance. |
| 175 | border_color.a *= f32(inner_distance > outer_distance); |
| 176 | |
| 177 | var rect_bottom_right = in.rect_center + in.rect_corner; |
| 178 | var pos_from_origin = in.position.xy - rect_origin; |
| 179 | |
| 180 | // Masks for pixels outside of inner rectangle or on border |
| 181 | var is_horizontal_border = (in.position.y <= rect_origin.y + in.border_width.x) || (in.position.y >= rect_bottom_right.y - in.border_width.z); |
| 182 | var is_vertical_border = (in.position.x <= rect_origin.x + in.border_width.w) || (in.position.x >= rect_bottom_right.x - in.border_width.y); |
| 183 | |
| 184 | var dash_length = in.dashed_border_data.x; |
| 185 | var gap_lengths = in.dashed_border_data.yz; |
| 186 | |
| 187 | // Get length along the dash and gap segment and determine if pixel is in dash or gap |
| 188 | var length_on_dash_and_gap_segment_x = pos_from_origin.x % (dash_length + gap_lengths.x); |
| 189 | var length_on_dash_and_gap_segment_y = pos_from_origin.y % (dash_length + gap_lengths.y); |
| 190 | var is_horizontal_dash = is_horizontal_border && (length_on_dash_and_gap_segment_x < dash_length); |
| 191 | var is_vertical_dash = is_vertical_border && (length_on_dash_and_gap_segment_y < dash_length); |
| 192 | |
| 193 | // Mask out any gaps in the border |
| 194 | border_color.a *= f32(dash_length <= 0.0 || is_horizontal_dash || is_vertical_dash); |
| 195 | |
| 196 | // Perform proper alpha blending on the two colors, avoiding a |
| 197 | // divide-by-zero if both colors are fully transparent. |
| 198 | // |
| 199 | // See formula for "over" compositing here: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending |
| 200 | var alpha: f32 = border_color.a + background_color.a * (1.0 - border_color.a); |
| 201 | var new_background_color: vec3<f32> = (border_color.rgb * border_color.a + background_color.rgb * background_color.a * (1.0 - border_color.a)) / (alpha + EPSILON); |
| 202 | background_color = vec4(new_background_color, alpha); |
| 203 | } |
| 204 | |
| 205 | // If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect. |
| 206 | if outer_corner_radius > 0. { |
| 207 | background_color.a *= 1.0 - saturate(outer_distance + 0.5); |
| 208 | } |
| 209 | |
| 210 | return background_color; |
| 211 | } |
| 212 | |
| 213 | fn derive_color( |
| 214 | position: vec2<f32>, |
| 215 | start: vec2<f32>, |
| 216 | end: vec2<f32>, |
| 217 | start_color: vec4<f32>, |
| 218 | end_color: vec4<f32> |
| 219 | ) -> vec4<f32> { |
| 220 | var adjusted_end: vec2<f32> = end - start; |
| 221 | var h: f32 = dot(position - start, adjusted_end) / dot(adjusted_end, adjusted_end); |
| 222 | return mix(start_color, end_color, h); |
| 223 | } |
| 224 | |
| 225 | // Based on the fragement position and the center of the quad, select one of the 4 radi. |
| 226 | // Order matches CSS border radius attribute: |
| 227 | // radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left |
| 228 | fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { |
| 229 | var rx = radi.x; |
| 230 | var ry = radi.y; |
| 231 | rx = select(radi.x, radi.y, position.x > center.x); |
| 232 | ry = select(radi.w, radi.z, position.x > center.x); |
| 233 | rx = select(rx, ry, position.y > center.y); |
| 234 | return rx; |
| 235 | } |
| 236 | |
| 237 | fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, corner_radius: f32) -> f32 { |
| 238 | var p: vec2<f32> = pixel_pos - rect_center; |
| 239 | var q: vec2<f32> = abs(p) - rect_corner + corner_radius; |
| 240 | return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - corner_radius; |
| 241 | } |
| 242 | |
| 243 | // Drop shadow code *heavily* inspired by this post: |
| 244 | // http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ |
| 245 | |
| 246 | // Return the mask for the shadow of a box from lower to upper |
| 247 | fn rounded_box_shadow(lower: vec2<f32>, upper: vec2<f32>, in_point: vec2<f32>, sigma: f32, corner: f32) -> f32 { |
| 248 | // Center everything to make the math easier |
| 249 | var center: vec2<f32> = (lower + upper) * 0.5; |
| 250 | var half_size: vec2<f32> = (upper - lower) * 0.5; |
| 251 | var point = in_point - center; |
| 252 | |
| 253 | // The signal is only non-zero in a limited range, so don't waste samples |
| 254 | var low: f32 = point.y - half_size.y; |
| 255 | var high: f32 = point.y + half_size.y; |
| 256 | var start: f32 = clamp(-3.0 * sigma, low, high); |
| 257 | var end: f32 = clamp(3.0 * sigma, low, high); |
| 258 | |
| 259 | // Accumulate samples (we can get away with surprisingly few samples) |
| 260 | var step: f32 = (end - start) / 4.0; |
| 261 | var y: f32 = start + step * 0.5; |
| 262 | var value: f32 = 0.0; |
| 263 | for (var i = 0; i < 4; i++) { |
| 264 | value += rounded_box_shadow_x(point.x, point.y - y, sigma, corner, half_size) * gaussian(y, sigma) * step; |
| 265 | y += step; |
| 266 | } |
| 267 | |
| 268 | return value; |
| 269 | } |
| 270 | |
| 271 | // Return the blurred mask along the x dimension |
| 272 | fn rounded_box_shadow_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 { |
| 273 | var delta: f32 = min(half_size.y - corner - abs(y), 0.0); |
| 274 | var curved: f32 = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); |
| 275 | var integral: vec2<f32> = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); |
| 276 | return integral.y - integral.x; |
| 277 | } |
| 278 | |
| 279 | // This approximates the error function, needed for the gaussian integral |
| 280 | fn erf(x: vec2<f32>) -> vec2<f32> { |
| 281 | var s = sign(x); |
| 282 | var a = abs(x); |
| 283 | var denom = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; |
| 284 | denom *= denom; |
| 285 | return s - s / (denom * denom); |
| 286 | } |
| 287 | |
| 288 | // A standard gaussian function, used for weighting samples |
| 289 | fn gaussian(x: f32, sigma: f32) -> f32 { |
| 290 | return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * PI) * sigma); |
| 291 | } |
| 292 |