Skip to main content

cardinal_gui/
effects.rs

1/// Map effect name (case-insensitive) to effect index. Returns Some(index) or None if not found.
2pub fn effect_name_to_index(name: &str) -> Option<usize> {
3    let name = name.to_ascii_lowercase();
4    (0..EFFECT_COUNT).find(|&idx| effect_name(idx).to_ascii_lowercase() == name)
5}
6// Effect module for cardinal-gui
7// Provides effect count, names, and application logic
8
9use egui::Color32;
10
11pub const EFFECT_COUNT: usize = 38;
12
13pub fn effect_name(index: usize) -> &'static str {
14    match index {
15        0 => "Normal",
16        1 => "Invert",
17        2 => "Grayscale",
18        3 => "Plasma (horizontal)",
19        4 => "Plasma (vertical)",
20        5 => "Plasma (circle)",
21        6 => "Plasma (diagonal)",
22        7 => "Rainbow (horizontal)",
23        8 => "Rainbow (vertical)",
24        9 => "Rainbow (circle)",
25        10 => "Rainbow (diagonal)",
26        11 => "Waves (horizontal)",
27        12 => "Waves (vertical)",
28        13 => "Waves (circle)",
29        14 => "Waves (diagonal)",
30        15 => "Noise (horizontal)",
31        16 => "Noise (vertical)",
32        17 => "Noise (circle)",
33        18 => "Noise (diagonal)",
34        // Inverted input versions
35        19 => "Normal (inverted input)",
36        20 => "Invert (inverted input)",
37        21 => "Grayscale (inverted input)",
38        22 => "Plasma (horizontal, inverted input)",
39        23 => "Plasma (vertical, inverted input)",
40        24 => "Plasma (circle, inverted input)",
41        25 => "Plasma (diagonal, inverted input)",
42        26 => "Rainbow (horizontal, inverted input)",
43        27 => "Rainbow (vertical, inverted input)",
44        28 => "Rainbow (circle, inverted input)",
45        29 => "Rainbow (diagonal, inverted input)",
46        30 => "Waves (horizontal, inverted input)",
47        31 => "Waves (vertical, inverted input)",
48        32 => "Waves (circle, inverted input)",
49        33 => "Waves (diagonal, inverted input)",
50        34 => "Noise (horizontal, inverted input)",
51        35 => "Noise (vertical, inverted input)",
52        36 => "Noise (circle, inverted input)",
53        37 => "Noise (diagonal, inverted input)",
54        _ => "Unknown",
55    }
56}
57
58#[allow(clippy::too_many_arguments)]
59pub fn apply_effect(
60    index: usize,
61    r: u8,
62    g: u8,
63    b: u8,
64    a: u8,
65    x: usize,
66    y: usize,
67    w: usize,
68    h: usize,
69    t: f32,
70    zebra_offset: usize,
71) -> Color32 {
72    let fx = x as f32;
73    let fy = y as f32;
74    let cx = (w as f32) / 2.0;
75    let cy = (h as f32) / 2.0;
76    let dist = ((fx - cx).powi(2) + (fy - cy).powi(2)).sqrt();
77    let diag = (fx + fy) * 0.5;
78    // Debug print: show which effect is being applied for the first pixel each frame
79    // t is now passed as-is; dynamic scaling is handled in apply_effect_blend for FullDyn
80    if index >= 19 {
81        // Only affect nearly black: threshold 32
82        if r <= 32 && g <= 32 && b <= 32 {
83            // Use original color, but apply the effect as if the input was white (255,255,255)
84            let (r, g, b) = (255, 255, 255);
85            return match index % 19 {
86                0 => Color32::from_rgba_unmultiplied(r, g, b, a),
87                1 => Color32::from_rgba_unmultiplied(255 - r, 255 - g, 255 - b, a),
88                2 => {
89                    let avg = ((r as u16 + g as u16 + b as u16) / 3) as u8;
90                    Color32::from_rgba_unmultiplied(avg, avg, avg, a)
91                }
92                3 => {
93                    let v = (fx * 0.08 + t * 0.12).sin() + (fy * 0.08 + t * 0.15).cos();
94                    let v = v * 0.5;
95                    let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
96                    let g = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
97                    let b = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
98                    Color32::from_rgba_unmultiplied(r, g, b, a)
99                }
100                4 => {
101                    let v = (fy * 0.08 + t * 0.12).sin() + (fx * 0.08 + t * 0.15).cos();
102                    let v = v * 0.5;
103                    let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
104                    let g = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
105                    let b = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
106                    Color32::from_rgba_unmultiplied(r, g, b, a)
107                }
108                5 => {
109                    let theta = (fy - cy).atan2(fx - cx);
110                    let v = (dist * 0.15 + t * 0.12).sin() + (theta * 6.0 + t * 0.2).cos();
111                    let v = v * 0.5;
112                    let r = ((v * 127.0 + 128.0) as u8)
113                        .saturating_add((theta.sin() * 127.0 + 128.0) as u8);
114                    let g = ((v * 127.0 + 128.0) as u8)
115                        .saturating_add((theta.cos() * 127.0 + 128.0) as u8);
116                    let b = ((v * 127.0 + 128.0) as u8)
117                        .saturating_add((dist.sin() * 127.0 + 128.0) as u8);
118                    Color32::from_rgba_unmultiplied(r, g, b, a)
119                }
120                6 => {
121                    let v = (diag * 0.08 + t * 0.12).sin() + (t * 0.15).cos();
122                    let v = v * 0.5;
123                    let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
124                    let g = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
125                    let b = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
126                    Color32::from_rgba_unmultiplied(r, g, b, a)
127                }
128                7 => {
129                    let r = (fx * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
130                    let g = (fy * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
131                    let b = ((fx + fy) * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
132                    Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
133                }
134                8 => {
135                    let r = (fy * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
136                    let g = (fx * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
137                    let b = ((fx + fy) * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
138                    Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
139                }
140                9 => {
141                    let theta = (fy - cy).atan2(fx - cx);
142                    let r = (dist * 0.18 + t * 0.2).sin() * 127.0 + 128.0;
143                    let g = (theta * 4.0 + t * 0.3).cos() * 127.0 + 128.0;
144                    let b = (dist * 0.09 + theta + t * 0.4).sin() * 127.0 + 128.0;
145                    Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
146                }
147                10 => {
148                    let r = (diag * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
149                    let g = (diag * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
150                    let b = (diag * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
151                    Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
152                }
153                11 => {
154                    let v = (fx * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
155                    Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
156                }
157                12 => {
158                    let v = (fy * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
159                    Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
160                }
161                13 => {
162                    let v = (dist * 0.25 + t * 0.25).sin() * 127.0 + 128.0;
163                    let w = (dist * 0.12 + t * 0.1).cos() * 127.0 + 128.0;
164                    Color32::from_rgba_unmultiplied(v as u8, 255 - w as u8, (w as u8) / 2, a)
165                }
166                14 => {
167                    let v = (diag * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
168                    Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
169                }
170                15 => {
171                    let seed = (x as u32).wrapping_mul(73856093)
172                        ^ (zebra_offset as u32).wrapping_mul(83492791);
173                    let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
174                    Color32::from_rgba_unmultiplied(
175                        val,
176                        val.wrapping_mul(3),
177                        val.wrapping_mul(7),
178                        a,
179                    )
180                }
181                16 => {
182                    let seed = (y as u32).wrapping_mul(19349663)
183                        ^ (zebra_offset as u32).wrapping_mul(83492791);
184                    let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
185                    Color32::from_rgba_unmultiplied(
186                        val,
187                        val.wrapping_mul(3),
188                        val.wrapping_mul(7),
189                        a,
190                    )
191                }
192                17 => {
193                    let theta = (fy - cy).atan2(fx - cx);
194                    let seed = (dist as u32).wrapping_mul(1234567)
195                        ^ (theta.to_bits().wrapping_mul(314159))
196                        ^ (zebra_offset as u32).wrapping_mul(83492791);
197                    let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
198                    Color32::from_rgba_unmultiplied(val, val.rotate_left(2), val.rotate_left(4), a)
199                }
200                18 => {
201                    let seed = (diag as u32).wrapping_mul(9876543)
202                        ^ (zebra_offset as u32).wrapping_mul(83492791);
203                    let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
204                    Color32::from_rgba_unmultiplied(
205                        val,
206                        val.wrapping_mul(3),
207                        val.wrapping_mul(7),
208                        a,
209                    )
210                }
211                _ => Color32::from_rgba_unmultiplied(255, 255, 255, a),
212            };
213        } else {
214            // Not nearly black: leave unchanged
215            return Color32::from_rgba_unmultiplied(r, g, b, a);
216        }
217    }
218    // Non-inverted effects
219    let (r, g, b) = (r, g, b);
220    match index % 19 {
221        1 => Color32::from_rgba_unmultiplied(255 - r, 255 - g, 255 - b, a),
222        2 => {
223            let avg = ((r as u16 + g as u16 + b as u16) / 3) as u8;
224            Color32::from_rgba_unmultiplied(avg, avg, avg, a)
225        }
226        3 => {
227            let v = (fx * 0.08 + t * 0.12).sin() + (fy * 0.08 + t * 0.15).cos();
228            let v = v * 0.5;
229            let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
230            let g = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
231            let b = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
232            Color32::from_rgba_unmultiplied(r, g, b, a)
233        }
234        4 => {
235            let v = (fy * 0.08 + t * 0.12).sin() + (fx * 0.08 + t * 0.15).cos();
236            let v = v * 0.5;
237            let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
238            let g = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
239            let b = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
240            Color32::from_rgba_unmultiplied(r, g, b, a)
241        }
242        5 => {
243            let theta = (fy - cy).atan2(fx - cx);
244            let v = (dist * 0.15 + t * 0.12).sin() + (theta * 6.0 + t * 0.2).cos();
245            let v = v * 0.5;
246            let r = ((v * 127.0 + 128.0) as u8).saturating_add((theta.sin() * 127.0 + 128.0) as u8);
247            let g = ((v * 127.0 + 128.0) as u8).saturating_add((theta.cos() * 127.0 + 128.0) as u8);
248            let b = ((v * 127.0 + 128.0) as u8).saturating_add((dist.sin() * 127.0 + 128.0) as u8);
249            Color32::from_rgba_unmultiplied(r, g, b, a)
250        }
251        6 => {
252            let v = (diag * 0.08 + t * 0.12).sin() + (t * 0.15).cos();
253            let v = v * 0.5;
254            let r = ((v * 127.0 + 128.0) as u8).saturating_add((t as u8) % 128);
255            let g = ((v * 127.0 + 128.0) as u8).saturating_add((x as u8) % 128);
256            let b = ((v * 127.0 + 128.0) as u8).saturating_add((y as u8) % 128);
257            Color32::from_rgba_unmultiplied(r, g, b, a)
258        }
259        7 => {
260            let r = (fx * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
261            let g = (fy * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
262            let b = ((fx + fy) * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
263            Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
264        }
265        8 => {
266            let r = (fy * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
267            let g = (fx * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
268            let b = ((fx + fy) * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
269            Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
270        }
271        9 => {
272            let theta = (fy - cy).atan2(fx - cx);
273            let r = (dist * 0.18 + t * 0.2).sin() * 127.0 + 128.0;
274            let g = (theta * 4.0 + t * 0.3).cos() * 127.0 + 128.0;
275            let b = (dist * 0.09 + theta + t * 0.4).sin() * 127.0 + 128.0;
276            Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
277        }
278        10 => {
279            let r = (diag * 0.12 + t * 0.2).sin() * 127.0 + 128.0;
280            let g = (diag * 0.12 + t * 0.3).cos() * 127.0 + 128.0;
281            let b = (diag * 0.07 + t * 0.4).sin() * 127.0 + 128.0;
282            Color32::from_rgba_unmultiplied(r as u8, g as u8, b as u8, a)
283        }
284        11 => {
285            let v = (fx * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
286            Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
287        }
288        12 => {
289            let v = (fy * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
290            Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
291        }
292        13 => {
293            let v = (dist * 0.25 + t * 0.25).sin() * 127.0 + 128.0;
294            let w = (dist * 0.12 + t * 0.1).cos() * 127.0 + 128.0;
295            Color32::from_rgba_unmultiplied(v as u8, 255 - w as u8, (w as u8) / 2, a)
296        }
297        14 => {
298            let v = (diag * 0.2 + t * 0.25).sin() * 127.0 + 128.0;
299            Color32::from_rgba_unmultiplied(v as u8, 255 - v as u8, (v as u8) / 2, a)
300        }
301        15 => {
302            let seed =
303                (x as u32).wrapping_mul(73856093) ^ (zebra_offset as u32).wrapping_mul(83492791);
304            let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
305            Color32::from_rgba_unmultiplied(val, val.wrapping_mul(3), val.wrapping_mul(7), a)
306        }
307        16 => {
308            let seed =
309                (y as u32).wrapping_mul(19349663) ^ (zebra_offset as u32).wrapping_mul(83492791);
310            let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
311            Color32::from_rgba_unmultiplied(val, val.wrapping_mul(3), val.wrapping_mul(7), a)
312        }
313        17 => {
314            let theta = (fy - cy).atan2(fx - cx);
315            let seed = (dist as u32).wrapping_mul(1234567)
316                ^ (theta.to_bits().wrapping_mul(314159))
317                ^ (zebra_offset as u32).wrapping_mul(83492791);
318            let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
319            Color32::from_rgba_unmultiplied(val, val.rotate_left(2), val.rotate_left(4), a)
320        }
321        18 => {
322            let seed =
323                (diag as u32).wrapping_mul(9876543) ^ (zebra_offset as u32).wrapping_mul(83492791);
324            let val = ((seed >> 3) ^ (seed << 7)).wrapping_add(seed) as u8;
325            Color32::from_rgba_unmultiplied(val, val.wrapping_mul(3), val.wrapping_mul(7), a)
326        }
327        _ => Color32::from_rgba_unmultiplied(r, g, b, a),
328    }
329}
330
331/// Blend effect and original pixel using various blend modes
332#[derive(Clone, Copy, PartialEq)]
333pub enum BlendMode {
334    None,
335    FullMode,
336    FullDyn,
337    BlendMode,
338    Random,
339    Diagonal,
340    Checker,
341    Threshold,
342    Intensity,
343    AnyLit,
344    Optimal,
345    OptimalPerFrame,
346    RandomMode,
347    RandomModeMode,
348}
349
350#[allow(clippy::too_many_arguments)]
351pub fn apply_effect_blend(
352    index: usize,
353    r: u8,
354    g: u8,
355    b: u8,
356    a: u8,
357    x: usize,
358    y: usize,
359    w: usize,
360    h: usize,
361    t: f32,
362    zebra_offset: usize,
363    mode: BlendMode,
364) -> Color32 {
365    use getrandom::fill;
366
367    /// Returns a random f32 in [0, 1)
368    pub fn random_f32() -> f32 {
369        let mut bytes = [0u8; 4];
370        fill(&mut bytes).expect("getrandom failed");
371        let val = u32::from_le_bytes(bytes);
372        (val as f32) / (u32::MAX as f32)
373    }
374
375    /// Returns a random usize in [start, end)
376    pub fn random_range(start: usize, end: usize) -> usize {
377        let mut bytes = [0u8; 8];
378        fill(&mut bytes).expect("getrandom failed");
379        let val = u64::from_le_bytes(bytes);
380        let range = end - start;
381        start + (val as usize % range)
382    }
383    // For FullDyn, vary t over time for dynamic effect
384    let effect = match mode {
385        BlendMode::FullDyn => {
386            // Multiply t by 10000 for dynamic animation
387            apply_effect(index, r, g, b, a, x, y, w, h, t * 10000.0, zebra_offset)
388        }
389        _ => apply_effect(index, r, g, b, a, x, y, w, h, t, zebra_offset),
390    };
391    let intensity = (r as u16 + g as u16 + b as u16) as f32 / (3.0 * 255.0);
392    let mut alpha = match mode {
393        BlendMode::None => 0.0,
394        BlendMode::FullMode => 1.0, // Always apply effect color
395        BlendMode::FullDyn => 1.0,
396        BlendMode::BlendMode => intensity.clamp(0.0, 1.0),
397        BlendMode::Random => random_f32(),
398        BlendMode::Diagonal => ((x as f32 + y as f32) / (w as f32 + h as f32)).clamp(0.0, 1.0),
399        BlendMode::Checker => {
400            if (x / 8 + y / 8).is_multiple_of(2) {
401                1.0
402            } else {
403                0.0
404            }
405        }
406        BlendMode::Threshold => {
407            if intensity > 0.5 {
408                1.0
409            } else {
410                0.0
411            }
412        }
413        BlendMode::Intensity => intensity.clamp(0.0, 1.0),
414        BlendMode::AnyLit => {
415            if intensity > 0.0 {
416                1.0
417            } else {
418                0.0
419            }
420        }
421        BlendMode::Optimal | BlendMode::OptimalPerFrame => 1.0, // fallback, should not be used directly
422        BlendMode::RandomMode => match random_range(0, 5) {
423            0 => random_f32(),
424            1 => ((x as f32 + y as f32) / (w as f32 + h as f32)).clamp(0.0, 1.0),
425            2 => {
426                if (x / 8 + y / 8).is_multiple_of(2) {
427                    1.0
428                } else {
429                    0.0
430                }
431            }
432            3 => {
433                if intensity > 0.5 {
434                    1.0
435                } else {
436                    0.0
437                }
438            }
439            _ => intensity.clamp(0.0, 1.0),
440        },
441        BlendMode::RandomModeMode => {
442            // Randomly select any blend mode (including RandomMode) for this pixel
443            let blend_modes = [
444                BlendMode::FullMode,
445                BlendMode::FullDyn,
446                BlendMode::BlendMode,
447                BlendMode::Random,
448                BlendMode::Diagonal,
449                BlendMode::Checker,
450                BlendMode::Threshold,
451                BlendMode::Intensity,
452                BlendMode::AnyLit,
453                BlendMode::RandomMode,
454                // Exclude RandomModeMode to prevent deep recursion
455            ];
456            let selected = blend_modes[random_range(0, blend_modes.len())];
457            // Recurse with selected mode
458            return apply_effect_blend(index, r, g, b, a, x, y, w, h, t, zebra_offset, selected);
459        }
460    };
461    // For any invert effect, if pixel is black, use full effect color (white)
462    if (index == 1 || index == 20) && r == 0 && g == 0 && b == 0 {
463        alpha = 1.0;
464    }
465    let blend = |orig: u8, eff: u8| {
466        ((eff as f32) * alpha + (orig as f32) * (1.0 - alpha))
467            .round()
468            .clamp(0.0, 255.0) as u8
469    };
470    Color32::from_rgba_unmultiplied(
471        blend(r, effect.r()),
472        blend(g, effect.g()),
473        blend(b, effect.b()),
474        a,
475    )
476}