1use crate::model::EffectKind;
26
27pub const VERT: &str = r#"#version 330 core
29out vec2 v_uv;
30void main() {
31 vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
32 v_uv = p;
33 // uv.y = 0 lands on GL row 0, so a top-down upload stays top-down in the FBO, in glReadPixels
34 // output and in egui — no flip anywhere.
35 gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
36}
37"#;
38
39pub const PRELUDE: &str = r#"#version 330 core
42precision highp float;
43
44in vec2 v_uv;
45out vec4 out_color;
46
47uniform sampler2D tex;
48uniform vec2 u_res;
49uniform float u_time;
50uniform float u_scale;
51uniform float p0;
52uniform float p1;
53uniform float p2;
54uniform float p3;
55uniform float p4;
56uniform float p5;
57uniform float p6;
58uniform float p7;
59uniform sampler2D u_mask;
60uniform int u_has_mask;
61
62float luma(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
63
64float hash12(vec2 p) {
65 vec3 q = fract(vec3(p.xyx) * 0.1031);
66 q += dot(q, q.yzx + 33.33);
67 return fract((q.x + q.y) * q.z);
68}
69
70float hash11(float x) { return hash12(vec2(x, x * 1.7 + 0.37)); }
71
72vec3 rgb2hsl(vec3 c) {
73 float mx = max(c.r, max(c.g, c.b));
74 float mn = min(c.r, min(c.g, c.b));
75 float l = (mx + mn) * 0.5;
76 float d = mx - mn;
77 float h = 0.0;
78 float s = 0.0;
79 if (d > 1e-6) {
80 s = (l > 0.5) ? d / max(2.0 - mx - mn, 1e-6) : d / max(mx + mn, 1e-6);
81 if (mx == c.r) {
82 h = (c.g - c.b) / d + ((c.g < c.b) ? 6.0 : 0.0);
83 } else if (mx == c.g) {
84 h = (c.b - c.r) / d + 2.0;
85 } else {
86 h = (c.r - c.g) / d + 4.0;
87 }
88 h /= 6.0;
89 }
90 return vec3(h, s, l);
91}
92
93float hue2rgb(float p, float q, float t) {
94 t = fract(t);
95 if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; }
96 if (t < 0.5) { return q; }
97 if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
98 return p;
99}
100
101vec3 hsl2rgb(vec3 c) {
102 if (c.y <= 0.0) { return vec3(c.z); }
103 float q = (c.z < 0.5) ? c.z * (1.0 + c.y) : c.z + c.y - c.z * c.y;
104 float p = 2.0 * c.z - q;
105 return vec3(hue2rgb(p, q, c.x + 1.0 / 3.0), hue2rgb(p, q, c.x), hue2rgb(p, q, c.x - 1.0 / 3.0));
106}
107
108/// Sample `t` with the uv clamped to the half-texel border of a `u_res`-sized image.
109vec4 sample_clamped(sampler2D t, vec2 uv) {
110 vec2 half_texel = 0.5 / max(u_res, vec2(1.0));
111 return texture(t, clamp(uv, half_texel, 1.0 - half_texel));
112}
113
114float mask_at(vec2 uv) { return (u_has_mask == 0) ? 1.0 : texture(u_mask, uv).r; }
115
116/// Blend an effect result back over the untouched source through the mask.
117vec4 apply_mask(vec4 src, vec4 fx, vec2 uv) { return mix(src, fx, mask_at(uv)); }
118"#;
119
120pub const MAIN: &str = r#"
124void main() {
125 vec2 uv = v_uv;
126 vec4 src = texture(tex, uv);
127 out_color = apply_mask(src, effect(src, uv), uv);
128}
129"#;
130
131pub fn body(kind: crate::model::EffectKind) -> Option<&'static str> {
138 use crate::model::EffectKind as K;
139 Some(match kind {
140 K::Blur => BLUR,
141 K::Pixelate => PIXELATE,
142 K::Tint => TINT,
143 K::Color => COLOR,
144 K::Vignette => VIGNETTE,
145 K::Sharpen => SHARPEN,
146 K::Invert => INVERT,
147 K::Grayscale => GRAYSCALE,
148 K::Flip => FLIP,
149 K::Crop => CROP,
150 K::ChromaKey => CHROMA_KEY,
151 K::ColorReplace => COLOR_REPLACE,
152 K::Curves => CURVES,
153 K::Levels => LEVELS,
154 K::HueShift => HUE_SHIFT,
155 K::JpegCompress => JPEG_COMPRESS,
156 K::MotionBlur => MOTION_BLUR,
157 K::EdgeGlow => EDGE_GLOW,
158 K::Threshold => THRESHOLD,
159 K::BlobTrack => BLOB_TRACK,
160 K::Vhs => VHS,
161 K::RecDot => REC_DOT,
162 K::Wobble | K::Plane3d | K::Shader => return None,
165 })
166}
167
168pub fn fragment(kind: EffectKind, user_src: &str) -> Option<String> {
170 if kind == EffectKind::Shader {
171 return Some(user_shader(user_src));
172 }
173 Some(format!("{PRELUDE}\n{}", body(kind)?))
176}
177
178pub const BLEND: &str = r#"
181float blend_1(int mode, float s, float d) {
182 if (mode == 1) { return s * d; } // Multiply
183 if (mode == 2) { return 1.0 - (1.0 - s) * (1.0 - d); } // Screen
184 if (mode == 3) { // Overlay
185 return (d <= 0.5) ? 2.0 * s * d : 1.0 - 2.0 * (1.0 - s) * (1.0 - d);
186 }
187 if (mode == 4) { return min(s, d); } // Darken
188 if (mode == 5) { return max(s, d); } // Lighten
189 if (mode == 6) { return min(s + d, 1.0); } // Add
190 if (mode == 7) { return max(d - s, 0.0); } // Subtract
191 if (mode == 8) { return abs(s - d); } // Difference
192 if (mode == 9) { // SoftLight
193 if (s <= 0.5) { return d - (1.0 - 2.0 * s) * d * (1.0 - d); }
194 float g = (d <= 0.25) ? ((16.0 * d - 12.0) * d + 4.0) * d : sqrt(d);
195 return d + (2.0 * s - 1.0) * (g - d);
196 }
197 if (mode == 10) { // HardLight
198 return (s <= 0.5) ? 2.0 * s * d : 1.0 - 2.0 * (1.0 - s) * (1.0 - d);
199 }
200 if (mode == 11) { // ColorDodge
201 if (d <= 0.0) { return 0.0; }
202 if (s >= 1.0) { return 1.0; }
203 return min(d / (1.0 - s), 1.0);
204 }
205 if (mode == 12) { // ColorBurn
206 if (d >= 1.0) { return 1.0; }
207 if (s <= 0.0) { return 0.0; }
208 return 1.0 - min((1.0 - d) / s, 1.0);
209 }
210 return s; // Normal
211}
212
213vec3 blend(int mode, vec3 s, vec3 d) {
214 if (mode == 0) { return s; }
215 return vec3(blend_1(mode, s.r, d.r), blend_1(mode, s.g, d.g), blend_1(mode, s.b, d.b));
216}
217"#;
218
219pub const MASK: &str = r#"
223uniform int m_shape;
224uniform vec2 m_center;
225uniform vec2 m_radius;
226uniform float m_rot;
227uniform float m_feather;
228uniform float m_expand;
229uniform float m_opacity;
230uniform int m_invert;
231uniform int m_count;
232// ponytail: 64 points is plenty for hand-drawn masks; a point texture would lift the cap.
233uniform vec2 m_points[64];
234
235float sd_box(vec2 p, vec2 b) {
236 vec2 d = abs(p) - b;
237 return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
238}
239
240// ponytail: cheap ellipse SDF (exact only on circles); good enough for a feathered edge.
241float sd_ellipse(vec2 p, vec2 r) {
242 vec2 rr = max(r, vec2(1e-4));
243 return (length(p / rr) - 1.0) * min(rr.x, rr.y);
244}
245
246float sd_poly(vec2 p) {
247 int n = min(m_count, 64);
248 if (n < 3) { return 1e6; }
249 vec2 w0 = p - m_points[0];
250 float d = dot(w0, w0);
251 float s = 1.0;
252 for (int i = 0, j = n - 1; i < n; j = i, i++) {
253 vec2 e = m_points[j] - m_points[i];
254 vec2 w = p - m_points[i];
255 vec2 b = w - e * clamp(dot(w, e) / max(dot(e, e), 1e-9), 0.0, 1.0);
256 d = min(d, dot(b, b));
257 bvec3 c = bvec3(p.y >= m_points[i].y, p.y < m_points[j].y, e.x * w.y > e.y * w.x);
258 if (all(c) || all(not(c))) { s = -s; }
259 }
260 return s * sqrt(d);
261}
262
263void main() {
264 vec2 q = v_uv * u_res - m_center;
265 float a = radians(m_rot);
266 float ca = cos(a);
267 float sa = sin(a);
268 q = vec2(q.x * ca + q.y * sa, -q.x * sa + q.y * ca);
269 float d;
270 if (m_shape == 0) {
271 d = sd_box(q, m_radius);
272 } else if (m_shape == 1) {
273 d = sd_ellipse(q, m_radius);
274 } else {
275 d = sd_poly(q);
276 }
277 d -= m_expand;
278 float cov = clamp(0.5 - d / max(m_feather, 1.0), 0.0, 1.0);
279 if (m_invert != 0) { cov = 1.0 - cov; }
280 out_color = vec4(cov * m_opacity);
281}
282"#;
283
284pub const COMPOSITE: &str = r#"
288uniform sampler2D u_dst;
289uniform vec2 u_layer;
290uniform mat3 u_inv;
291uniform int u_scaler;
292uniform int u_mode;
293uniform float u_opacity;
294
295vec4 tap(vec2 ip) {
296 return texelFetch(tex, ivec2(clamp(ip, vec2(0.0), u_layer - 1.0)), 0);
297}
298
299vec4 catmull_w(float t) {
300 float t2 = t * t;
301 float t3 = t2 * t;
302 return 0.5 * vec4(-t3 + 2.0 * t2 - t, 3.0 * t3 - 5.0 * t2 + 2.0, -3.0 * t3 + 4.0 * t2 + t, t3 - t2);
303}
304
305// Alpha-weighted accumulation (matches the CPU compositor: transparent texels must not darken edges).
306vec4 sample_layer(vec2 uv) {
307 vec2 sp = uv * u_layer - 0.5;
308 if (u_scaler == 0) { return tap(floor(sp + 0.5)); }
309 vec4 acc = vec4(0.0);
310 if (u_scaler == 1) {
311 vec2 f = floor(sp);
312 vec2 t = sp - f;
313 for (int j = 0; j < 2; j++) {
314 for (int i = 0; i < 2; i++) {
315 float w = (i == 0 ? 1.0 - t.x : t.x) * (j == 0 ? 1.0 - t.y : t.y);
316 vec4 c = tap(f + vec2(float(i), float(j)));
317 acc += vec4(c.rgb * c.a * w, c.a * w);
318 }
319 }
320 } else {
321 vec2 f = floor(sp);
322 vec4 wx = catmull_w(sp.x - f.x);
323 vec4 wy = catmull_w(sp.y - f.y);
324 for (int j = 0; j < 4; j++) {
325 for (int i = 0; i < 4; i++) {
326 float w = wx[i] * wy[j];
327 vec4 c = tap(f + vec2(float(i - 1), float(j - 1)));
328 acc += vec4(c.rgb * c.a * w, c.a * w);
329 }
330 }
331 }
332 if (acc.a <= 0.0) { return vec4(0.0); }
333 return vec4(clamp(acc.rgb / acc.a, 0.0, 1.0), clamp(acc.a, 0.0, 1.0));
334}
335
336void main() {
337 vec3 h = u_inv * vec3(v_uv * u_res, 1.0);
338 float z = (abs(h.z) < 1e-9) ? 1e-9 : h.z;
339 vec2 uv = h.xy / z;
340 // derivatives must be taken outside any branch
341 vec2 e = max(fwidth(uv), vec2(1e-8));
342 vec2 dd = min(uv, 1.0 - uv);
343 float cov = clamp(min(dd.x / e.x, dd.y / e.y) + 0.5, 0.0, 1.0);
344 vec4 d = texture(u_dst, v_uv);
345 if (cov <= 0.0 || h.z <= 0.0) { out_color = d; return; }
346 vec4 s = sample_layer(uv);
347 float a = s.a * u_opacity * cov * mask_at(v_uv);
348 if (a <= 0.0) { out_color = d; return; }
349 float ao = a + d.a * (1.0 - a);
350 if (ao <= 0.0) { out_color = vec4(0.0); return; }
351 vec3 b = blend(u_mode, s.rgb, d.rgb);
352 vec3 co = ((1.0 - d.a) * a * s.rgb + (1.0 - a) * d.a * d.rgb + a * d.a * b) / ao;
353 out_color = vec4(co, ao);
354}
355"#;
356
357pub fn user_shader(src: &str) -> String {
360 let mut s = String::with_capacity(PRELUDE.len() + src.len() + 512);
361 s.push_str(PRELUDE);
362 for i in 0..8 {
363 s.push_str(&format!("#define u{} p{}\n", i + 1, i));
364 }
365 s.push('\n');
366 s.push_str(src);
367 s.push_str(MAIN);
368 s
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 fn balanced(src: &str) -> bool {
376 let mut n = 0i32;
377 for c in src.chars() {
378 match c {
379 '{' => n += 1,
380 '}' => {
381 n -= 1;
382 if n < 0 {
383 return false;
384 }
385 }
386 _ => {}
387 }
388 }
389 n == 0
390 }
391
392 fn declared(src: &str) -> Vec<String> {
394 let mut out = Vec::new();
395 for line in src.lines() {
396 let l = line.trim();
397 if let Some(rest) = l.strip_prefix("uniform ") {
398 if let Some(name) = rest.split_whitespace().nth(1) {
400 out.push(name.trim_end_matches(';').split('[').next().unwrap_or(name).to_string());
401 }
402 } else if let Some(rest) = l.strip_prefix("#define ") {
403 if let Some(name) = rest.split_whitespace().next() {
404 out.push(name.to_string());
405 }
406 } else if let Some(i) = l.find('(') {
407 let head = &l[..i];
409 let mut w = head.split_whitespace();
410 if let (Some(_ty), Some(name)) = (w.next(), w.next()) {
411 if w.next().is_none() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
412 out.push(name.to_string());
413 }
414 }
415 }
416 if let Some(rest) = l.strip_prefix("in ").or_else(|| l.strip_prefix("out ")) {
417 if let Some(name) = rest.split_whitespace().nth(1) {
418 out.push(name.trim_end_matches(';').to_string());
419 }
420 }
421 }
422 out
423 }
424
425 fn undeclared_uniforms(src: &str) -> Vec<String> {
427 let decl = declared(src);
428 let mut bad = Vec::new();
429 let mut word = String::new();
430 for c in src.chars().chain(std::iter::once(' ')) {
431 if c.is_alphanumeric() || c == '_' {
432 word.push(c);
433 continue;
434 }
435 let w = std::mem::take(&mut word);
436 let interesting = w.starts_with("u_")
437 || w.starts_with("m_")
438 || (w.len() == 2 && w.starts_with('p') && w.as_bytes()[1].is_ascii_digit());
439 if interesting && !decl.contains(&w) && !bad.contains(&w) {
440 bad.push(w);
441 }
442 }
443 bad
444 }
445
446 const SAMPLE_BODY: &str = "vec4 effect(vec4 src, vec2 uv) {\n vec4 c = sample_clamped(tex, uv);\n \
447 return vec4(mix(c.rgb, vec3(luma(c.rgb)), p0) * u_scale, c.a);\n}\n";
448
449 #[test]
450 fn prelude_plus_body_is_well_formed() {
451 let src = format!("{PRELUDE}\n{SAMPLE_BODY}\n{MAIN}");
452 assert!(balanced(&src), "unbalanced braces");
453 assert!(src.starts_with("#version 330 core"));
454 assert_eq!(undeclared_uniforms(&src), Vec::<String>::new());
455 for name in ["tex", "u_res", "u_time", "u_scale", "u_mask", "u_has_mask"] {
456 assert!(src.contains(&format!("uniform {name};")) || src.contains(&format!(" {name};")), "{name}");
457 }
458 for i in 0..8 {
459 assert!(src.contains(&format!("uniform float p{i};")), "p{i}");
460 }
461 for f in ["luma(", "rgb2hsl(", "hsl2rgb(", "hash12(", "sample_clamped(", "apply_mask("] {
463 assert!(src.contains(f), "missing helper {f}");
464 }
465 }
466
467 #[test]
468 fn shared_programs_are_well_formed() {
469 for (name, src) in [
470 ("vert", VERT.to_string()),
471 ("composite", format!("{PRELUDE}{BLEND}{COMPOSITE}")),
472 ("mask", format!("{PRELUDE}{MASK}")),
473 ] {
474 assert!(balanced(&src), "{name}: unbalanced braces");
475 assert!(src.contains("void main()"), "{name}: no entry point");
476 assert_eq!(undeclared_uniforms(&src), Vec::<String>::new(), "{name}");
477 }
478 }
479
480 #[test]
481 fn user_shader_wraps_body_with_knobs_and_entry_point() {
482 let s = user_shader(crate::model::DEFAULT_SHADER);
483 assert!(s.starts_with("#version 330 core"));
484 assert!(balanced(&s));
485 for i in 1..=8 {
486 assert!(s.contains(&format!("#define u{} p{}\n", i, i - 1)), "knob u{i}");
487 }
488 assert!(s.contains("vec4 effect(vec4 src, vec2 uv)"), "entry point missing");
489 assert!(s.contains("out_color = apply_mask("), "main must call effect through the mask");
490 assert_eq!(undeclared_uniforms(&s), Vec::<String>::new());
491 assert!(s.contains("u1") && s.contains("u_time"));
493 }
494
495 #[test]
498 fn built_in_fragments_have_one_entry_point_and_no_undefined_effect() {
499 for k in crate::model::EffectKind::ALL {
500 if k == EffectKind::Shader {
501 continue;
502 }
503 let Some(src) = fragment(k, "") else { continue };
504 assert_eq!(src.matches("void main()").count(), 1, "{k:?}: not exactly one entry point");
505 assert!(!src.contains("effect(src, uv)"), "{k:?}: calls an effect() no body defines");
506 assert!(balanced(&src), "{k:?}: unbalanced braces");
507 assert!(src.starts_with("#version 330 core"), "{k:?}");
508 assert_eq!(undeclared_uniforms(&src), Vec::<String>::new(), "{k:?}");
509 }
510 }
511
512 #[test]
513 fn fragment_uses_the_user_source_for_shader_kind() {
514 let f = fragment(EffectKind::Shader, "vec4 effect(vec4 s, vec2 uv) { return s * p3; }").unwrap();
515 assert!(f.contains("return s * p3;"));
516 assert!(f.ends_with(MAIN));
517 assert!(fragment(EffectKind::Wobble, "").is_none());
519 assert!(fragment(EffectKind::Plane3d, "").is_none());
520 }
521}
522
523const BLUR: &str = r#"
543void main() {
544 vec2 uv = gl_FragCoord.xy / u_res;
545 vec4 src = texture(tex, uv);
546 vec4 res = src;
547 float r = p0 * u_scale;
548 if (r >= 0.5) {
549 // ponytail: one-pass Vogel disc instead of a separable Gaussian - if quality matters,
550 // add a direction knob and let gpu.rs run this program twice (H then V).
551 vec4 acc = vec4(0.0);
552 float wsum = 0.0;
553 for (int i = 0; i < 24; i++) {
554 float fi = float(i) + 0.5;
555 float rad = sqrt(fi / 24.0) * r;
556 float ang = fi * 2.39996323;
557 float w = exp(-2.0 * rad * rad / (r * r));
558 acc += texture(tex, uv + vec2(cos(ang), sin(ang)) * rad / u_res) * w;
559 wsum += w;
560 }
561 res = acc / wsum;
562 }
563 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
564 out_color = mix(src, res, m);
565}
566"#;
567
568const PIXELATE: &str = r#"
570void main() {
571 vec2 uv = gl_FragCoord.xy / u_res;
572 vec4 src = texture(tex, uv);
573 float b = max(p0, 1.0) * max(u_scale, 0.01);
574 vec4 res = src;
575 if (b > 1.0) {
576 vec2 q = (floor(gl_FragCoord.xy / b) + 0.5) * b;
577 res = texture(tex, q / u_res);
578 }
579 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
580 out_color = mix(src, res, m);
581}
582"#;
583
584const TINT: &str = r#"
586void main() {
587 vec2 uv = gl_FragCoord.xy / u_res;
588 vec4 src = texture(tex, uv);
589 vec4 res = vec4(mix(src.rgb, vec3(p0, p1, p2) / 255.0, clamp(p3, 0.0, 1.0)), src.a);
590 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
591 out_color = mix(src, res, m);
592}
593"#;
594
595const COLOR: &str = r#"
597void main() {
598 vec2 uv = gl_FragCoord.xy / u_res;
599 vec4 src = texture(tex, uv);
600 vec3 c = src.rgb;
601 float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
602 c = mix(vec3(l), c, p2);
603 float a = radians(p3), cs = cos(a), sn = sin(a);
604 mat3 hm = mat3(
605 0.213 + cs * 0.787 - sn * 0.213, 0.213 - cs * 0.213 + sn * 0.143, 0.213 - cs * 0.213 - sn * 0.787,
606 0.715 - cs * 0.715 - sn * 0.715, 0.715 + cs * 0.285 + sn * 0.140, 0.715 - cs * 0.715 + sn * 0.715,
607 0.072 - cs * 0.072 + sn * 0.928, 0.072 - cs * 0.072 - sn * 0.283, 0.072 + cs * 0.928 + sn * 0.072);
608 c = clamp((clamp(hm * c, 0.0, 1.0) - 0.5) * p1 + 0.5 + p0, 0.0, 1.0);
609 c = pow(c, vec3(1.0 / max(p4, 0.001)));
610 vec4 res = vec4(c, src.a);
611 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
612 out_color = mix(src, res, m);
613}
614"#;
615
616const VIGNETTE: &str = r#"
618void main() {
619 vec2 uv = gl_FragCoord.xy / u_res;
620 vec4 src = texture(tex, uv);
621 float d = length((uv - 0.5) * u_res) / max(length(u_res * 0.5), 1.0);
622 float fall = smoothstep(0.0, 1.0, (d - p0) / max(p1, 0.001));
623 float k = 1.0 - clamp(p2, 0.0, 1.0) * fall;
624 vec4 res = vec4(src.rgb * k, src.a);
625 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
626 out_color = mix(src, res, m);
627}
628"#;
629
630const SHARPEN: &str = r#"
632void main() {
633 vec2 uv = gl_FragCoord.xy / u_res;
634 vec4 src = texture(tex, uv);
635 vec2 o = vec2(max(p1, 0.5) * u_scale) / u_res;
636 vec3 b = texture(tex, uv + vec2(-o.x, -o.y)).rgb + texture(tex, uv + vec2(0.0, -o.y)).rgb
637 + texture(tex, uv + vec2(o.x, -o.y)).rgb + texture(tex, uv + vec2(-o.x, 0.0)).rgb
638 + src.rgb + texture(tex, uv + vec2(o.x, 0.0)).rgb
639 + texture(tex, uv + vec2(-o.x, o.y)).rgb + texture(tex, uv + vec2(0.0, o.y)).rgb
640 + texture(tex, uv + vec2(o.x, o.y)).rgb;
641 vec4 res = vec4(clamp(src.rgb + max(p0, 0.0) * (src.rgb - b / 9.0), 0.0, 1.0), src.a);
642 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
643 out_color = mix(src, res, m);
644}
645"#;
646
647const INVERT: &str = r#"
648void main() {
649 vec2 uv = gl_FragCoord.xy / u_res;
650 vec4 src = texture(tex, uv);
651 vec4 res = vec4(mix(src.rgb, 1.0 - src.rgb, clamp(p0, 0.0, 1.0)), src.a);
652 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
653 out_color = mix(src, res, m);
654}
655"#;
656
657const GRAYSCALE: &str = r#"
658void main() {
659 vec2 uv = gl_FragCoord.xy / u_res;
660 vec4 src = texture(tex, uv);
661 float l = dot(src.rgb, vec3(0.2126, 0.7152, 0.0722));
662 vec4 res = vec4(mix(src.rgb, vec3(l), clamp(p0, 0.0, 1.0)), src.a);
663 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
664 out_color = mix(src, res, m);
665}
666"#;
667
668const FLIP: &str = r#"
670void main() {
671 vec2 uv = gl_FragCoord.xy / u_res;
672 vec4 src = texture(tex, uv);
673 vec2 f = vec2(p0 >= 0.5 ? 1.0 - uv.x : uv.x, p1 >= 0.5 ? 1.0 - uv.y : uv.y);
674 vec4 res = texture(tex, f);
675 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
676 out_color = mix(src, res, m);
677}
678"#;
679
680const CROP: &str = r#"
682void main() {
683 vec2 uv = gl_FragCoord.xy / u_res;
684 vec4 src = texture(tex, uv);
685 vec2 q = uv * u_res;
686 float x0 = clamp(p0, 0.0, 0.5) * u_res.x;
687 float x1 = u_res.x - clamp(p1, 0.0, 0.5) * u_res.x;
688 float y0 = clamp(p2, 0.0, 0.5) * u_res.y;
689 float y1 = u_res.y - clamp(p3, 0.0, 0.5) * u_res.y;
690 float d = min(min(q.x - x0, x1 - q.x), min(q.y - y0, y1 - q.y));
691 float fe = clamp(p4, 0.0, 0.5) * min(u_res.x, u_res.y);
692 float a = d <= 0.0 ? 0.0 : (fe > 0.0 ? min(d / fe, 1.0) : 1.0);
693 vec4 res = vec4(src.rgb, src.a * a);
694 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
695 out_color = mix(src, res, m);
696}
697"#;
698
699const CHROMA_KEY: &str = r#"
702vec3 fx_ycc(vec3 c) {
703 return vec3(dot(c, vec3(0.299, 0.587, 0.114)),
704 dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5,
705 dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5);
706}
707vec3 fx_rgb(vec3 y) {
708 float cb = y.y - 0.5;
709 float cr = y.z - 0.5;
710 return vec3(y.x + 1.402 * cr, y.x - 0.344136 * cb - 0.714136 * cr, y.x + 1.772 * cb);
711}
712float fx_alpha(vec2 uv) {
713 vec2 c = fx_ycc(texture(tex, uv).rgb).yz;
714 vec2 k = fx_ycc(clamp(vec3(p0, p1, p2) / 255.0, 0.0, 1.0)).yz;
715 float sim = clamp(p3, 0.0, 1.0) * 0.5;
716 return smoothstep(sim, sim + max(clamp(p4, 0.0, 1.0) * 0.5, 0.0005), distance(c, k));
717}
718void main() {
719 vec2 uv = gl_FragCoord.xy / u_res;
720 vec4 src = texture(tex, uv);
721 float a = fx_alpha(uv);
722 float sh = p7 * u_scale;
723 if (abs(sh) > 0.01) {
724 vec2 step_ = vec2(abs(sh)) / u_res;
725 for (int i = 0; i < 8; i++) {
726 float ang = float(i) * 0.78539816;
727 float n = fx_alpha(uv + vec2(cos(ang), sin(ang)) * step_);
728 a = sh > 0.0 ? min(a, n) : max(a, n);
729 }
730 }
731 vec3 rgb = src.rgb;
732 float spill = clamp(p6, 0.0, 1.0);
733 if (spill > 0.0) {
734 vec3 y = fx_ycc(rgb);
735 vec2 kc = fx_ycc(clamp(vec3(p0, p1, p2) / 255.0, 0.0, 1.0)).yz - 0.5;
736 if (length(kc) > 0.001) {
737 vec2 dir = normalize(kc);
738 vec2 cc = y.yz - 0.5;
739 float proj = dot(cc, dir);
740 if (proj > 0.0) {
741 cc -= dir * proj * spill;
742 rgb = clamp(fx_rgb(vec3(y.x, cc + 0.5)), 0.0, 1.0);
743 }
744 }
745 }
746 vec4 res = vec4(rgb, src.a * a);
747 if (p5 >= 0.5) {
748 res = vec4(vec3(a), 1.0);
749 }
750 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
751 out_color = mix(src, res, m);
752}
753"#;
754
755const COLOR_REPLACE: &str = r#"
758void main() {
759 vec2 uv = gl_FragCoord.xy / u_res;
760 vec4 src = texture(tex, uv);
761 vec3 from = clamp(vec3(p0, p1, p2) / 255.0, 0.0, 1.0);
762 vec3 to = clamp(vec3(p3, p4, p5) / 255.0, 0.0, 1.0);
763 float tol = clamp(p6, 0.0, 1.0);
764 float soft = max(clamp(p7, 0.0, 1.0), 0.0005);
765 float d = distance(src.rgb, from) / 1.7320508;
766 float hit = 1.0 - smoothstep(tol, tol + soft, d);
767 vec4 res = vec4(mix(src.rgb, to, hit), src.a);
768 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
769 out_color = mix(src, res, m);
770}
771"#;
772
773const CURVES: &str = r#"
777uniform float p8;
778uniform float p9;
779uniform float p10;
780uniform float p11;
781float fx_curve(float x, float a, float b, float c) {
782 float y[5];
783 y[0] = 0.0; y[1] = a; y[2] = b; y[3] = c; y[4] = 1.0;
784 float d[4];
785 for (int i = 0; i < 4; i++) {
786 d[i] = (y[i + 1] - y[i]) * 4.0;
787 }
788 float m[5];
789 m[0] = d[0];
790 m[4] = d[3];
791 for (int i = 1; i < 4; i++) {
792 m[i] = (d[i - 1] * d[i] <= 0.0) ? 0.0 : (d[i - 1] + d[i]) * 0.5;
793 }
794 for (int i = 0; i < 4; i++) {
795 if (abs(d[i]) < 1e-6) {
796 m[i] = 0.0;
797 m[i + 1] = 0.0;
798 } else {
799 float ai = m[i] / d[i];
800 float bi = m[i + 1] / d[i];
801 float s = ai * ai + bi * bi;
802 if (s > 9.0) {
803 float k = 3.0 / sqrt(s);
804 m[i] = k * ai * d[i];
805 m[i + 1] = k * bi * d[i];
806 }
807 }
808 }
809 float xc = clamp(x, 0.0, 1.0);
810 int i = int(min(floor(xc * 4.0), 3.0));
811 float t = xc * 4.0 - float(i);
812 float t2 = t * t;
813 float t3 = t2 * t;
814 return (2.0 * t3 - 3.0 * t2 + 1.0) * y[i] + (t3 - 2.0 * t2 + t) * 0.25 * m[i]
815 + (-2.0 * t3 + 3.0 * t2) * y[i + 1] + (t3 - t2) * 0.25 * m[i + 1];
816}
817void main() {
818 vec2 uv = gl_FragCoord.xy / u_res;
819 vec4 src = texture(tex, uv);
820 vec3 c = vec3(fx_curve(src.r, p3, p4, p5), fx_curve(src.g, p6, p7, p8), fx_curve(src.b, p9, p10, p11));
821 c = vec3(fx_curve(c.r, p0, p1, p2), fx_curve(c.g, p0, p1, p2), fx_curve(c.b, p0, p1, p2));
822 vec4 res = vec4(clamp(c, 0.0, 1.0), src.a);
823 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
824 out_color = mix(src, res, m);
825}
826"#;
827
828const LEVELS: &str = r#"
830void main() {
831 vec2 uv = gl_FragCoord.xy / u_res;
832 vec4 src = texture(tex, uv);
833 vec3 c = clamp((src.rgb - p0) / max(p1 - p0, 0.0001), 0.0, 1.0);
834 c = pow(c, vec3(1.0 / max(p2, 0.01)));
835 c = clamp(p3 + c * (p4 - p3), 0.0, 1.0);
836 vec4 res = vec4(c, src.a);
837 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
838 out_color = mix(src, res, m);
839}
840"#;
841
842const HUE_SHIFT: &str = r#"
844vec3 fx_rgb2hsl(vec3 c) {
845 float mx = max(c.r, max(c.g, c.b));
846 float mn = min(c.r, min(c.g, c.b));
847 float l = (mx + mn) * 0.5;
848 float h = 0.0;
849 float s = 0.0;
850 float d = mx - mn;
851 if (d > 1e-5) {
852 s = d / max(1.0 - abs(2.0 * l - 1.0), 1e-5);
853 if (mx == c.r) {
854 h = mod((c.g - c.b) / d, 6.0);
855 } else if (mx == c.g) {
856 h = (c.b - c.r) / d + 2.0;
857 } else {
858 h = (c.r - c.g) / d + 4.0;
859 }
860 h /= 6.0;
861 }
862 return vec3(h, s, l);
863}
864float fx_h2c(float a, float b, float t) {
865 float u = fract(t);
866 if (u < 1.0 / 6.0) return a + (b - a) * 6.0 * u;
867 if (u < 0.5) return b;
868 if (u < 2.0 / 3.0) return a + (b - a) * (2.0 / 3.0 - u) * 6.0;
869 return a;
870}
871vec3 fx_hsl2rgb(vec3 hsl) {
872 float l = clamp(hsl.z, 0.0, 1.0);
873 float s = clamp(hsl.y, 0.0, 1.0);
874 if (s <= 0.0) return vec3(l);
875 float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
876 float a = 2.0 * l - q;
877 return vec3(fx_h2c(a, q, hsl.x + 1.0 / 3.0), fx_h2c(a, q, hsl.x), fx_h2c(a, q, hsl.x - 1.0 / 3.0));
878}
879void main() {
880 vec2 uv = gl_FragCoord.xy / u_res;
881 vec4 src = texture(tex, uv);
882 vec3 hsl = fx_rgb2hsl(clamp(src.rgb, 0.0, 1.0));
883 hsl.x = fract(hsl.x + p0 / 360.0);
884 hsl.y = clamp(hsl.y * max(p1, 0.0), 0.0, 1.0);
885 hsl.z = clamp(hsl.z + p2, 0.0, 1.0);
886 vec4 res = vec4(clamp(fx_hsl2rgb(hsl), 0.0, 1.0), src.a);
887 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
888 out_color = mix(src, res, m);
889}
890"#;
891
892const JPEG_COMPRESS: &str = r#"
896vec3 fx_ycc(vec3 c) {
897 return vec3(dot(c, vec3(0.299, 0.587, 0.114)),
898 dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5,
899 dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5);
900}
901vec3 fx_rgb(vec3 y) {
902 float cb = y.y - 0.5;
903 float cr = y.z - 0.5;
904 return vec3(y.x + 1.402 * cr, y.x - 0.344136 * cb - 0.714136 * cr, y.x + 1.772 * cb);
905}
906void main() {
907 vec2 uv = gl_FragCoord.xy / u_res;
908 vec4 src = texture(tex, uv);
909 float sc = max(u_scale, 0.05);
910 float b = max(p1, 2.0) * sc;
911 vec2 org = floor(gl_FragCoord.xy / b) * b;
912 vec3 dc = texture(tex, (org + b * vec2(0.25, 0.25)) / u_res).rgb
913 + texture(tex, (org + b * vec2(0.75, 0.25)) / u_res).rgb
914 + texture(tex, (org + b * vec2(0.25, 0.75)) / u_res).rgb
915 + texture(tex, (org + b * vec2(0.75, 0.75)) / u_res).rgb;
916 dc *= 0.25;
917 float q = clamp(p0, 1.0, 100.0);
918 float qs = q < 50.0 ? 50.0 / q : 2.0 - q / 50.0;
919 float step_ = clamp(qs * 0.05, 0.004, 0.6);
920 vec3 ydc = fx_ycc(dc);
921 vec3 ypx = fx_ycc(src.rgb);
922 float dcs = step_ * 0.5;
923 float luma = floor(ydc.x / dcs + 0.5) * dcs + floor((ypx.x - ydc.x) / step_ + 0.5) * step_;
924 vec2 ch = ypx.yz;
925 if (p2 >= 0.5) {
926 vec2 sub = (floor(gl_FragCoord.xy / (2.0 * sc)) + 0.5) * (2.0 * sc);
927 ch = fx_ycc(texture(tex, sub / u_res).rgb).yz;
928 }
929 float cstep = step_ * 2.0;
930 ch = floor((ch - 0.5) / cstep + 0.5) * cstep + 0.5;
931 vec4 res = vec4(clamp(fx_rgb(vec3(luma, ch)), 0.0, 1.0), src.a);
932 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
933 out_color = mix(src, res, m);
934}
935"#;
936
937const MOTION_BLUR: &str = r#"
944uniform sampler2D u_prev;
945uniform sampler2D u_next;
946uniform float u_frames;
947uniform vec2 u_motion;
948void main() {
949 vec2 uv = gl_FragCoord.xy / u_res;
950 vec4 src = texture(tex, uv);
951 float sh = clamp(p0, 0.0, 360.0) / 360.0;
952 int n = int(clamp(p1, 2.0, 32.0));
953 vec4 acc = vec4(0.0);
954 vec4 prev = src;
955 vec4 next = src;
956 if (u_frames > 0.5) {
957 prev = texture(u_prev, uv);
958 next = u_frames > 1.5 ? texture(u_next, uv) : prev;
959 for (int i = 0; i < 32; i++) {
960 if (i >= n) break;
961 float o = ((float(i) + 0.5) / float(n) - 0.5) * sh;
962 acc += o < 0.0 ? mix(src, prev, clamp(-o, 0.0, 1.0)) : mix(src, next, clamp(o, 0.0, 1.0));
963 }
964 } else {
965 vec2 d = u_motion / u_res * sh;
966 for (int i = 0; i < 32; i++) {
967 if (i >= n) break;
968 acc += texture(tex, uv + d * ((float(i) + 0.5) / float(n) - 0.5));
969 }
970 }
971 vec4 res = acc / float(n);
972 if (p2 >= 0.5 && u_frames > 0.5) {
973 float mv = length(prev.rgb - src.rgb) + length(next.rgb - src.rgb);
974 res = mix(src, res, clamp(mv * 4.0, 0.0, 1.0));
975 }
976 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
977 out_color = mix(src, res, m);
978}
979"#;
980
981const EDGE_GLOW: &str = r#"
984float fx_l(vec2 uv) {
985 return dot(texture(tex, uv).rgb, vec3(0.2126, 0.7152, 0.0722));
986}
987void main() {
988 vec2 uv = gl_FragCoord.xy / u_res;
989 vec4 src = texture(tex, uv);
990 vec2 o = vec2(max(p1, 0.5) * max(u_scale, 0.05)) / u_res;
991 float tl = fx_l(uv + vec2(-o.x, -o.y));
992 float tc = fx_l(uv + vec2(0.0, -o.y));
993 float tr = fx_l(uv + vec2(o.x, -o.y));
994 float ml = fx_l(uv + vec2(-o.x, 0.0));
995 float mr = fx_l(uv + vec2(o.x, 0.0));
996 float bl = fx_l(uv + vec2(-o.x, o.y));
997 float bc = fx_l(uv + vec2(0.0, o.y));
998 float br = fx_l(uv + vec2(o.x, o.y));
999 float gx = (tr + 2.0 * mr + br) - (tl + 2.0 * ml + bl);
1000 float gy = (bl + 2.0 * bc + br) - (tl + 2.0 * tc + tr);
1001 float e = length(vec2(gx, gy)) * 0.25;
1002 float edge = smoothstep(p0, p0 + 0.15, e);
1003 vec3 glow = vec3(p3, p4, p5) / 255.0 * max(p2, 0.0) * edge;
1004 vec4 res;
1005 if (p6 >= 0.5) {
1006 res = vec4(clamp(src.rgb + glow, 0.0, 1.0), src.a);
1007 } else {
1008 res = vec4(clamp(glow, 0.0, 1.0), edge * src.a);
1009 }
1010 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
1011 out_color = mix(src, res, m);
1012}
1013"#;
1014
1015const THRESHOLD: &str = r#"
1017void main() {
1018 vec2 uv = gl_FragCoord.xy / u_res;
1019 vec4 src = texture(tex, uv);
1020 float sm = max(p1, 0.0001) * 0.5;
1021 vec3 c;
1022 if (p2 >= 0.5) {
1023 c = smoothstep(vec3(p0 - sm), vec3(p0 + sm), src.rgb);
1024 } else {
1025 c = vec3(smoothstep(p0 - sm, p0 + sm, dot(src.rgb, vec3(0.2126, 0.7152, 0.0722))));
1026 }
1027 vec4 res = vec4(c, src.a);
1028 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
1029 out_color = mix(src, res, m);
1030}
1031"#;
1032
1033const BLOB_TRACK: &str = r#"
1039uniform vec3 u_blob;
1040void main() {
1041 vec2 uv = gl_FragCoord.xy / u_res;
1042 vec4 src = texture(tex, uv);
1043 vec4 res = src;
1044 if (p4 >= 0.5) {
1045 float d = distance(src.rgb, clamp(vec3(p0, p1, p2) / 255.0, 0.0, 1.0));
1046 float tol = clamp(p3, 0.0, 1.0) * 1.2;
1047 float hit = 1.0 - smoothstep(tol, tol + 0.05, d);
1048 res = vec4(mix(src.rgb, vec3(1.0, 0.25, 0.25), hit * 0.5), src.a);
1049 if (u_blob.z > 0.0) {
1050 vec2 dxy = abs(uv - u_blob.xy) * u_res;
1051 float lw = max(1.0, u_scale);
1052 float arm = 16.0 * max(u_scale, 0.25);
1053 float cross_ = ((dxy.y <= lw && dxy.x <= arm) || (dxy.x <= lw && dxy.y <= arm)) ? 1.0 : 0.0;
1054 res = vec4(mix(res.rgb, vec3(1.0, 1.0, 0.2), cross_), max(res.a, cross_));
1055 }
1056 }
1057 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
1058 out_color = mix(src, res, m);
1059}
1060"#;
1061
1062const VHS: &str = r#"
1066float fx_hash(vec2 p) {
1067 return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
1068}
1069vec3 fx_ycc(vec3 c) {
1070 return vec3(dot(c, vec3(0.299, 0.587, 0.114)),
1071 dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5,
1072 dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5);
1073}
1074vec3 fx_rgb(vec3 y) {
1075 float cb = y.y - 0.5;
1076 float cr = y.z - 0.5;
1077 return vec3(y.x + 1.402 * cr, y.x - 0.344136 * cb - 0.714136 * cr, y.x + 1.772 * cb);
1078}
1079void main() {
1080 vec2 uv = gl_FragCoord.xy / u_res;
1081 vec4 src = texture(tex, uv);
1082 float full = p6 >= 0.5 ? 0.0 : 1.0; // "colour bleed only" keeps just the chroma stage
1083 float noise = clamp(p0, 0.0, 1.0) * full;
1084 float bleed = clamp(p1, 0.0, 1.0);
1085 float scan = clamp(p2, 0.0, 1.0) * full;
1086 float jit = clamp(p3, 0.0, 1.0) * full;
1087 float head = clamp(p4, 0.0, 1.0) * full;
1088 float ring = clamp(p5, 0.0, 1.0);
1089 float wear = clamp(p7, 0.0, 1.0) * full;
1090 float sc = max(u_scale, 0.1);
1091 float line = floor(gl_FragCoord.y / sc);
1092 float t = u_time;
1093 float band = 1.0 - smoothstep(0.0, max(0.05 * head, 0.001), 1.0 - uv.y);
1094 band *= step(0.001, head);
1095 float j = (fx_hash(vec2(line, floor(t * 30.0))) - 0.5) * jit * 0.02
1096 + sin(line * 0.31 + t * 6.0) * jit * 0.002
1097 + band * head * 0.08 * (fx_hash(vec2(line, floor(t * 12.0))) * 2.0 - 1.0);
1098 vec2 suv = clamp(vec2(uv.x + j, uv.y), vec2(0.0), vec2(1.0));
1099 vec3 base = texture(tex, suv).rgb;
1100 float bpx = bleed * 6.0 * sc / max(u_res.x, 1.0);
1101 vec3 b1 = texture(tex, clamp(suv - vec2(bpx, 0.0), 0.0, 1.0)).rgb;
1102 vec3 b2 = texture(tex, clamp(suv - vec2(bpx * 2.0, 0.0), 0.0, 1.0)).rgb;
1103 vec3 y = fx_ycc(base);
1104 y.yz = mix(y.yz, fx_ycc((b1 + b2) * 0.5).yz, bleed);
1105 y.x += (y.x - fx_ycc(b1).x) * ring * 1.5;
1106 y.x *= 1.0 - scan * 0.5 * (0.5 + 0.5 * cos(gl_FragCoord.y * 3.14159265 / sc));
1107 y.x += (fx_hash(gl_FragCoord.xy + vec2(t * 97.0, t * 31.0)) - 0.5) * noise * 0.25;
1108 if (wear > 0.0 && fx_hash(vec2(floor(line * 0.5), floor(t * 6.0))) > 1.0 - wear * 0.06) {
1109 float drop = fx_hash(vec2(floor(uv.x * 40.0), line));
1110 y.x = mix(y.x, 0.75 + 0.25 * drop, 0.7);
1111 y.yz = mix(y.yz, vec2(0.5), 0.8);
1112 }
1113 y.x = mix(y.x, 0.45 + 0.55 * fx_hash(gl_FragCoord.xy + vec2(t)), band * 0.8);
1114 y.yz = mix(y.yz, vec2(0.5), band);
1115 vec4 res = vec4(clamp(fx_rgb(y), 0.0, 1.0), src.a);
1116 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
1117 out_color = mix(src, res, m);
1118}
1119"#;
1120
1121const REC_DOT: &str = r#"
1124float fx_rect(vec2 q, vec2 c, vec2 h) {
1125 vec2 d = abs(q - c) - h;
1126 return max(d.x, d.y) < 0.0 ? 1.0 : 0.0;
1127}
1128float fx_digit(vec2 q, int d) {
1129 if (q.x < 0.0 || q.x > 1.0 || q.y < 0.0 || q.y > 1.0) return 0.0;
1130 int seg[10] = int[10](63, 6, 91, 79, 102, 109, 125, 7, 127, 111);
1131 int m = seg[clamp(d, 0, 9)];
1132 float t = 0.11;
1133 float o = 0.0;
1134 if ((m & 1) != 0) o = max(o, fx_rect(q, vec2(0.5, t), vec2(0.5 - t, t)));
1135 if ((m & 2) != 0) o = max(o, fx_rect(q, vec2(1.0 - t, 0.25), vec2(t, 0.25 - t)));
1136 if ((m & 4) != 0) o = max(o, fx_rect(q, vec2(1.0 - t, 0.75), vec2(t, 0.25 - t)));
1137 if ((m & 8) != 0) o = max(o, fx_rect(q, vec2(0.5, 1.0 - t), vec2(0.5 - t, t)));
1138 if ((m & 16) != 0) o = max(o, fx_rect(q, vec2(t, 0.75), vec2(t, 0.25 - t)));
1139 if ((m & 32) != 0) o = max(o, fx_rect(q, vec2(t, 0.25), vec2(t, 0.25 - t)));
1140 if ((m & 64) != 0) o = max(o, fx_rect(q, vec2(0.5, 0.5), vec2(0.5 - t, t)));
1141 return o;
1142}
1143void main() {
1144 vec2 uv = gl_FragCoord.xy / u_res;
1145 vec4 src = texture(tex, uv);
1146 vec2 q = gl_FragCoord.xy;
1147 float sc = max(u_scale, 0.05);
1148 float sz = max(p0, 2.0) * sc;
1149 float mg = max(p4, 0.0) * sc;
1150 int corner = int(clamp(p2, 0.0, 3.0) + 0.5);
1151 bool right = (corner == 1 || corner == 3);
1152 bool bottom = (corner == 2 || corner == 3);
1153 vec2 anchor = vec2(right ? u_res.x - mg - sz * 0.5 : mg + sz * 0.5,
1154 bottom ? u_res.y - mg - sz * 0.5 : mg + sz * 0.5);
1155 float hz = max(p1, 0.0);
1156 float on = hz <= 0.0 ? 1.0 : (fract(u_time * hz) < 0.5 ? 1.0 : 0.0);
1157 float dot_ = 1.0 - smoothstep(sz * 0.5 - 1.0, sz * 0.5 + 1.0, distance(q, anchor));
1158 vec3 c = mix(src.rgb, vec3(0.9, 0.12, 0.12), dot_ * on);
1159 float cover = dot_ * on;
1160 if (p3 >= 0.5) {
1161 float dh = sz;
1162 float dw = sz * 0.55;
1163 float gap = sz * 0.12;
1164 float cw = dw + gap;
1165 float colw = sz * 0.3 + gap;
1166 float total = 6.0 * cw + 2.0 * colw;
1167 float ox = right ? anchor.x - sz * 0.5 - gap * 2.0 - total : anchor.x + sz * 0.5 + gap * 2.0;
1168 float oy = anchor.y - dh * 0.5;
1169 int tot = int(max(u_time, 0.0));
1170 int hh = (tot / 3600) - (tot / 360000) * 100;
1171 int mm = (tot / 60) - (tot / 3600) * 60;
1172 int ss = tot - (tot / 60) * 60;
1173 int digs[6] = int[6](hh / 10, hh - (hh / 10) * 10, mm / 10, mm - (mm / 10) * 10,
1174 ss / 10, ss - (ss / 10) * 10);
1175 float ink = 0.0;
1176 float x = ox;
1177 for (int i = 0; i < 6; i++) {
1178 ink = max(ink, fx_digit(vec2((q.x - x) / dw, (q.y - oy) / dh), digs[i]));
1179 x += cw;
1180 if (i == 1 || i == 3) {
1181 float cx = x + colw * 0.5;
1182 ink = max(ink, fx_rect(q, vec2(cx, oy + dh * 0.33), vec2(sz * 0.07)));
1183 ink = max(ink, fx_rect(q, vec2(cx, oy + dh * 0.7), vec2(sz * 0.07)));
1184 x += colw;
1185 }
1186 }
1187 c = mix(c, vec3(1.0), ink * 0.9);
1188 cover = max(cover, ink * 0.9);
1189 }
1190 vec4 res = vec4(c, max(src.a, cover));
1191 float m = u_has_mask > 0.5 ? texture(u_mask, uv).r : 1.0;
1192 out_color = mix(src, res, m);
1193}
1194"#;
1195
1196#[cfg(test)]
1197mod shader_body_tests {
1198 use super::*;
1199 use crate::model::EffectKind as K;
1200
1201 const PRELUDE_UNIFORMS: [&str; 5] = ["tex", "u_res", "u_time", "u_scale", "u_mask"];
1203
1204 const UNUSED_PARAMS: [(K, usize); 1] = [(K::BlobTrack, 5)];
1206
1207 fn bodies() -> Vec<(K, &'static str)> {
1208 K::ALL.iter().filter_map(|&k| body(k).map(|s| (k, s))).collect()
1209 }
1210
1211 fn identifiers(src: &str) -> (Vec<String>, Vec<usize>) {
1213 let mut words: Vec<String> = Vec::new();
1214 let mut cur = String::new();
1215 for ch in src.chars() {
1216 if ch.is_ascii_alphanumeric() || ch == '_' {
1217 cur.push(ch);
1218 } else if !cur.is_empty() {
1219 words.push(std::mem::take(&mut cur));
1220 }
1221 }
1222 if !cur.is_empty() {
1223 words.push(cur);
1224 }
1225 let params = words
1226 .iter()
1227 .filter_map(|w| w.strip_prefix('p').and_then(|n| n.parse::<usize>().ok().filter(|_| !n.is_empty())))
1228 .collect();
1229 (words, params)
1230 }
1231
1232 fn declared(src: &str) -> Vec<String> {
1234 src.split("uniform ")
1235 .skip(1)
1236 .filter_map(|rest| {
1237 let mut it = rest.split_whitespace();
1238 let _ty = it.next()?;
1239 Some(it.next()?.trim_end_matches(';').to_string())
1240 })
1241 .collect()
1242 }
1243
1244 #[test]
1245 fn every_gpu_kind_has_a_body() {
1246 for k in K::ALL {
1247 let has = body(k).is_some();
1248 let want = !matches!(k, K::Wobble | K::Plane3d | K::Shader);
1249 assert_eq!(has, want, "{:?}", k);
1250 }
1251 assert_eq!(bodies().len(), 22);
1252 }
1253
1254 #[test]
1255 fn bodies_are_well_formed_glsl_text() {
1256 for (k, src) in bodies() {
1257 assert!(!src.contains("#version"), "{k:?}: the prelude owns #version");
1258 assert!(!src.contains("precision "), "{k:?}: the prelude owns precision");
1259 assert!(src.contains("void main()"), "{k:?}: no entry point");
1260 assert!(src.contains("out_color ="), "{k:?}: never writes out_color");
1261 for (open, close) in [('{', '}'), ('(', ')'), ('[', ']')] {
1262 let mut depth = 0i32;
1263 for ch in src.chars() {
1264 if ch == open {
1265 depth += 1;
1266 } else if ch == close {
1267 depth -= 1;
1268 assert!(depth >= 0, "{k:?}: unbalanced {close}");
1269 }
1270 }
1271 assert_eq!(depth, 0, "{k:?}: unbalanced {open}{close}");
1272 }
1273 for line in src.lines() {
1275 assert!(!line.contains('\t'), "{k:?}: tab in {line:?}");
1276 assert_eq!(line.trim_end(), line, "{k:?}: trailing space in {line:?}");
1277 }
1278 }
1279 }
1280
1281 #[test]
1282 fn bodies_only_touch_declared_uniforms() {
1283 for (k, src) in bodies() {
1284 let own = declared(src);
1285 let (words, _) = identifiers(src);
1286 for w in words.iter().filter(|w| w.starts_with("u_") || *w == "tex") {
1287 let ok = PRELUDE_UNIFORMS.contains(&w.as_str()) || w == "u_has_mask" || own.contains(w);
1288 assert!(ok, "{k:?}: undeclared uniform {w}");
1289 }
1290 }
1291 }
1292
1293 #[test]
1294 fn bodies_match_the_parameter_list() {
1295 for (k, src) in bodies() {
1296 let n = k.params().len();
1297 let own = declared(src);
1298 let (_, used) = identifiers(src);
1299 for i in &used {
1300 assert!(*i < n, "{k:?}: reads p{i} but only has {n} params");
1301 if *i >= 8 {
1303 assert!(own.contains(&format!("p{i}")), "{k:?}: p{i} is past the prelude and undeclared");
1304 }
1305 }
1306 for i in 0..n {
1307 let known_unused = UNUSED_PARAMS.contains(&(k, i));
1308 assert!(used.contains(&i) || known_unused, "{k:?}: {} (p{i}) is never read", k.params()[i].name);
1309 }
1310 }
1311 }
1312
1313 #[test]
1314 fn masked_and_scaled_where_it_matters() {
1315 for (k, src) in bodies() {
1316 assert!(src.contains("u_has_mask"), "{k:?}: ignores its mask");
1317 }
1318 for k in [K::Blur, K::Pixelate, K::Sharpen, K::EdgeGlow, K::RecDot, K::ChromaKey, K::Vhs, K::JpegCompress] {
1320 assert!(body(k).unwrap().contains("u_scale"), "{k:?}: pixel sizes not scaled");
1321 }
1322 }
1323
1324 #[test]
1325 fn extra_uniforms_are_documented() {
1326 assert_eq!(declared(MOTION_BLUR), ["u_prev", "u_next", "u_frames", "u_motion"]);
1328 assert_eq!(declared(BLOB_TRACK), ["u_blob"]);
1329 assert_eq!(declared(CURVES), ["p8", "p9", "p10", "p11"]);
1330 for (_, src) in bodies() {
1331 if src != MOTION_BLUR && src != BLOB_TRACK && src != CURVES {
1332 assert!(declared(src).is_empty(), "undocumented extra uniform in {:?}", declared(src));
1333 }
1334 }
1335 }
1336}