1use crate::{RenderEffect, RuntimeShader};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
11pub enum CutDirection {
12 #[default]
14 LeftToRight,
15 RightToLeft,
17 TopToBottom,
19 BottomToTop,
21}
22
23impl CutDirection {
24 fn uniform_code(self) -> f32 {
25 match self {
26 CutDirection::LeftToRight => 0.0,
27 CutDirection::RightToLeft => 1.0,
28 CutDirection::TopToBottom => 2.0,
29 CutDirection::BottomToTop => 3.0,
30 }
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct GradientCutMaskSpec {
37 pub progress: f32,
39 pub feather: f32,
41 pub corner_radius: f32,
43 pub direction: CutDirection,
45}
46
47impl Default for GradientCutMaskSpec {
48 fn default() -> Self {
49 Self {
50 progress: 0.5,
51 feather: 24.0,
52 corner_radius: 16.0,
53 direction: CutDirection::LeftToRight,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct GradientFadeMaskSpec {
62 pub start: f32,
64 pub end: f32,
66 pub direction: CutDirection,
68}
69
70impl Default for GradientFadeMaskSpec {
71 fn default() -> Self {
72 Self {
73 start: 0.0,
74 end: 64.0,
75 direction: CutDirection::TopToBottom,
76 }
77 }
78}
79
80pub const GRADIENT_CUT_MASK_WGSL: &str = r#"
89struct VertexOutput {
90 @builtin(position) position: vec4<f32>,
91 @location(0) uv: vec2<f32>,
92}
93
94@vertex
95fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
96 var output: VertexOutput;
97 let x = f32(i32(vertex_index & 1u) * 2 - 1);
98 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
99 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
100 output.position = vec4<f32>(x, y, 0.0, 1.0);
101 return output;
102}
103
104@group(0) @binding(0) var input_texture: texture_2d<f32>;
105@group(0) @binding(1) var input_sampler: sampler;
106@group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
107
108fn get_float(index: u32) -> f32 {
109 return u[index / 4u][index % 4u];
110}
111
112fn get_vec2(index: u32) -> vec2<f32> {
113 return vec2<f32>(get_float(index), get_float(index + 1u));
114}
115
116fn sd_round_rect(p: vec2<f32>, half_size: vec2<f32>, radius: f32) -> f32 {
117 let q = abs(p) - half_size + vec2<f32>(radius);
118 return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - radius;
119}
120
121fn rounded_rect_alpha(local_px: vec2<f32>, size_px: vec2<f32>, corner_radius_px: f32) -> f32 {
122 let half = size_px * 0.5;
123 let p = local_px - half;
124 let d = sd_round_rect(p, half, corner_radius_px);
125 return 1.0 - smoothstep(-1.0, 1.0, d);
126}
127
128@fragment
129fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
130 let uv = input.uv;
131 let tex_size = vec2<f32>(textureDimensions(input_texture));
132
133 // Effect layer pixel rect injected by renderer in uniform slot 62.
134 let effect_rect = vec4<f32>(get_float(248u), get_float(249u), get_float(250u), get_float(251u));
135 let container_dp = get_vec2(0u);
136
137 // dp -> pixel mapping for local effect coordinates.
138 let dp_scale = effect_rect.zw / max(container_dp, vec2<f32>(1.0));
139 let s = min(dp_scale.x, dp_scale.y);
140
141 let local_px = uv * tex_size - effect_rect.xy;
142 let size_px = container_dp * dp_scale;
143
144 let progress = clamp(get_float(2u), 0.0, 1.0);
145 let feather_px = max(get_float(3u) * s, 0.001);
146 let corner_radius_px = max(get_float(4u) * s, 0.0);
147 let direction = get_float(5u);
148
149 var axis_value = local_px.x;
150 var axis_extent = max(size_px.x, 0.001);
151
152 if (direction >= 0.5 && direction < 1.5) {
153 axis_value = size_px.x - local_px.x;
154 axis_extent = max(size_px.x, 0.001);
155 } else if (direction >= 1.5 && direction < 2.5) {
156 axis_value = local_px.y;
157 axis_extent = max(size_px.y, 0.001);
158 } else if (direction >= 2.5) {
159 axis_value = size_px.y - local_px.y;
160 axis_extent = max(size_px.y, 0.001);
161 }
162
163 var directional_alpha = 1.0;
164 if (progress < 1.0) {
165 let cut_edge = progress * axis_extent;
166 directional_alpha = smoothstep(cut_edge + feather_px * 0.5, cut_edge - feather_px * 0.5, axis_value);
167 }
168 let shape_alpha = rounded_rect_alpha(local_px, size_px, corner_radius_px);
169 let mask = directional_alpha * shape_alpha;
170
171 let sample = textureSample(input_texture, input_sampler, uv);
172 return sample * mask;
173}
174"#;
175
176pub const ROUNDED_ALPHA_MASK_WGSL: &str = r#"
183struct VertexOutput {
184 @builtin(position) position: vec4<f32>,
185 @location(0) uv: vec2<f32>,
186}
187
188@vertex
189fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
190 var output: VertexOutput;
191 let x = f32(i32(vertex_index & 1u) * 2 - 1);
192 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
193 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
194 output.position = vec4<f32>(x, y, 0.0, 1.0);
195 return output;
196}
197
198@group(0) @binding(0) var input_texture: texture_2d<f32>;
199@group(0) @binding(1) var input_sampler: sampler;
200@group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
201
202fn get_float(index: u32) -> f32 {
203 return u[index / 4u][index % 4u];
204}
205
206fn get_vec2(index: u32) -> vec2<f32> {
207 return vec2<f32>(get_float(index), get_float(index + 1u));
208}
209
210fn sd_round_rect(p: vec2<f32>, half_size: vec2<f32>, radius: f32) -> f32 {
211 let q = abs(p) - half_size + vec2<f32>(radius);
212 return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - radius;
213}
214
215fn rounded_rect_alpha(local_px: vec2<f32>, size_px: vec2<f32>, corner_radius_px: f32, feather_px: f32) -> f32 {
216 let half = size_px * 0.5;
217 let p = local_px - half;
218 let d = sd_round_rect(p, half, corner_radius_px);
219 let half_feather = max(feather_px * 0.5, 0.001);
220 return 1.0 - smoothstep(-half_feather, half_feather, d);
221}
222
223@fragment
224fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
225 let uv = input.uv;
226 let tex_size = vec2<f32>(textureDimensions(input_texture));
227
228 // Effect layer pixel rect injected by renderer in uniform slot 62.
229 let effect_rect = vec4<f32>(get_float(248u), get_float(249u), get_float(250u), get_float(251u));
230 let container_dp = get_vec2(0u);
231
232 // dp -> pixel mapping for local effect coordinates.
233 let dp_scale = effect_rect.zw / max(container_dp, vec2<f32>(1.0));
234 let s = min(dp_scale.x, dp_scale.y);
235
236 let local_px = uv * tex_size - effect_rect.xy;
237 let size_px = container_dp * dp_scale;
238
239 let corner_radius_px = max(get_float(2u) * s, 0.0);
240 let feather_px = max(get_float(3u) * s, 0.0);
241 let mask = rounded_rect_alpha(local_px, size_px, corner_radius_px, feather_px);
242
243 let sample = textureSample(input_texture, input_sampler, uv);
244 return sample * mask;
245}
246"#;
247
248pub const GRADIENT_FADE_DST_OUT_WGSL: &str = r#"
256struct VertexOutput {
257 @builtin(position) position: vec4<f32>,
258 @location(0) uv: vec2<f32>,
259}
260
261@vertex
262fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
263 var output: VertexOutput;
264 let x = f32(i32(vertex_index & 1u) * 2 - 1);
265 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
266 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
267 output.position = vec4<f32>(x, y, 0.0, 1.0);
268 return output;
269}
270
271@group(0) @binding(0) var input_texture: texture_2d<f32>;
272@group(0) @binding(1) var input_sampler: sampler;
273@group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
274
275fn get_float(index: u32) -> f32 {
276 return u[index / 4u][index % 4u];
277}
278
279fn get_vec2(index: u32) -> vec2<f32> {
280 return vec2<f32>(get_float(index), get_float(index + 1u));
281}
282
283@fragment
284fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
285 let uv = input.uv;
286 let tex_size = vec2<f32>(textureDimensions(input_texture));
287
288 // Effect layer pixel rect injected by renderer in uniform slot 62.
289 let effect_rect = vec4<f32>(get_float(248u), get_float(249u), get_float(250u), get_float(251u));
290 let container_dp = get_vec2(0u);
291
292 // dp -> pixel mapping for local effect coordinates.
293 let dp_scale = effect_rect.zw / max(container_dp, vec2<f32>(1.0));
294
295 let local_px = uv * tex_size - effect_rect.xy;
296 let size_px = container_dp * dp_scale;
297 let direction = get_float(4u);
298
299 var axis_value = local_px.x;
300 var axis_scale = dp_scale.x;
301 if (direction >= 0.5 && direction < 1.5) {
302 axis_value = size_px.x - local_px.x;
303 axis_scale = dp_scale.x;
304 } else if (direction >= 1.5 && direction < 2.5) {
305 axis_value = local_px.y;
306 axis_scale = dp_scale.y;
307 } else if (direction >= 2.5) {
308 axis_value = size_px.y - local_px.y;
309 axis_scale = dp_scale.y;
310 }
311
312 let start_px = get_float(2u) * axis_scale;
313 let end_px = get_float(3u) * axis_scale;
314 let span = max(abs(end_px - start_px), 0.001);
315
316 var keep_alpha = 1.0;
317 if (end_px >= start_px) {
318 keep_alpha = clamp((axis_value - start_px) / span, 0.0, 1.0);
319 } else {
320 keep_alpha = clamp((start_px - axis_value) / span, 0.0, 1.0);
321 }
322
323 let sample = textureSample(input_texture, input_sampler, uv);
324 return sample * keep_alpha;
325}
326"#;
327
328pub fn gradient_cut_mask_effect(
333 spec: &GradientCutMaskSpec,
334 area_width: f32,
335 area_height: f32,
336) -> RenderEffect {
337 let mut shader = RuntimeShader::new(GRADIENT_CUT_MASK_WGSL);
338 shader.set_float2(0, area_width.max(1.0), area_height.max(1.0));
339 shader.set_float(2, spec.progress.clamp(0.0, 1.0));
340 shader.set_float(3, spec.feather.max(0.0));
341 shader.set_float(4, spec.corner_radius.max(0.0));
342 shader.set_float(5, spec.direction.uniform_code());
343 RenderEffect::runtime_shader(shader)
344}
345
346pub fn rounded_alpha_mask_effect(
351 area_width: f32,
352 area_height: f32,
353 corner_radius: f32,
354 edge_feather: f32,
355) -> RenderEffect {
356 let mut shader = RuntimeShader::new(ROUNDED_ALPHA_MASK_WGSL);
357 shader.set_float2(0, area_width.max(1.0), area_height.max(1.0));
358 shader.set_float(2, corner_radius.max(0.0));
359 shader.set_float(3, edge_feather.max(0.0));
360 RenderEffect::runtime_shader(shader)
361}
362
363pub fn gradient_fade_dst_out_effect(
369 spec: &GradientFadeMaskSpec,
370 area_width: f32,
371 area_height: f32,
372) -> RenderEffect {
373 let mut shader = RuntimeShader::new(GRADIENT_FADE_DST_OUT_WGSL);
374 shader.set_float2(0, area_width.max(1.0), area_height.max(1.0));
375 shader.set_float(2, spec.start);
376 shader.set_float(3, spec.end);
377 shader.set_float(4, spec.direction.uniform_code());
378 RenderEffect::runtime_shader(shader)
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn gradient_cut_spec_defaults() {
387 let spec = GradientCutMaskSpec::default();
388 assert_eq!(spec.progress, 0.5);
389 assert_eq!(spec.feather, 24.0);
390 assert_eq!(spec.corner_radius, 16.0);
391 assert_eq!(spec.direction, CutDirection::LeftToRight);
392 }
393
394 #[test]
395 fn gradient_fade_spec_defaults() {
396 let spec = GradientFadeMaskSpec::default();
397 assert_eq!(spec.start, 0.0);
398 assert_eq!(spec.end, 64.0);
399 assert_eq!(spec.direction, CutDirection::TopToBottom);
400 }
401
402 #[test]
403 fn gradient_cut_effect_sets_uniforms() {
404 let spec = GradientCutMaskSpec {
405 progress: 0.33,
406 feather: 18.0,
407 corner_radius: 20.0,
408 direction: CutDirection::BottomToTop,
409 };
410 let effect = gradient_cut_mask_effect(&spec, 320.0, 180.0);
411 let RenderEffect::Shader { shader } = effect else {
412 panic!("expected shader render effect");
413 };
414
415 let u = shader.uniforms();
416 assert_eq!(u[0], 320.0);
417 assert_eq!(u[1], 180.0);
418 assert_eq!(u[2], 0.33);
419 assert_eq!(u[3], 18.0);
420 assert_eq!(u[4], 20.0);
421 assert_eq!(u[5], 3.0);
422 }
423
424 #[test]
425 fn gradient_cut_effect_clamps_values() {
426 let spec = GradientCutMaskSpec {
427 progress: 2.4,
428 feather: -3.0,
429 corner_radius: -8.0,
430 direction: CutDirection::RightToLeft,
431 };
432 let effect = gradient_cut_mask_effect(&spec, 0.0, 0.0);
433 let RenderEffect::Shader { shader } = effect else {
434 panic!("expected shader render effect");
435 };
436
437 let u = shader.uniforms();
438 assert_eq!(u[0], 1.0);
439 assert_eq!(u[1], 1.0);
440 assert_eq!(u[2], 1.0);
441 assert_eq!(u[3], 0.0);
442 assert_eq!(u[4], 0.0);
443 assert_eq!(u[5], 1.0);
444 }
445
446 #[test]
447 fn rounded_alpha_mask_uses_dedicated_shader_uniforms() {
448 let effect = rounded_alpha_mask_effect(240.0, 120.0, 14.0, 6.0);
449 let RenderEffect::Shader { shader } = effect else {
450 panic!("expected shader render effect");
451 };
452
453 assert_eq!(shader.source(), ROUNDED_ALPHA_MASK_WGSL);
454 let u = shader.uniforms();
455 assert_eq!(u[0], 240.0);
456 assert_eq!(u[1], 120.0);
457 assert_eq!(u[2], 14.0);
458 assert_eq!(u[3], 6.0);
459 }
460
461 #[test]
462 fn gradient_fade_dst_out_effect_sets_uniforms() {
463 let spec = GradientFadeMaskSpec {
464 start: 24.0,
465 end: 52.0,
466 direction: CutDirection::BottomToTop,
467 };
468 let effect = gradient_fade_dst_out_effect(&spec, 300.0, 180.0);
469 let RenderEffect::Shader { shader } = effect else {
470 panic!("expected shader render effect");
471 };
472
473 assert_eq!(shader.source(), GRADIENT_FADE_DST_OUT_WGSL);
474 let u = shader.uniforms();
475 assert_eq!(u[0], 300.0);
476 assert_eq!(u[1], 180.0);
477 assert_eq!(u[2], 24.0);
478 assert_eq!(u[3], 52.0);
479 assert_eq!(u[4], 3.0);
480 }
481}