Skip to main content

wallr_common/
effect.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, PartialEq)]
4#[serde(rename_all = "snake_case")]
5pub enum Effect {
6    Fade(FadeParams),
7    Wipe(WipeParams),
8    Slide(SlideParams),
9    Wave(WaveParams),
10    Grow(GrowParams),
11    Outer(OuterParams),
12}
13
14impl<'de> Deserialize<'de> for Effect {
15    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
16    where
17        D: serde::Deserializer<'de>,
18    {
19        struct EffectVisitor;
20
21        impl<'de> serde::de::Visitor<'de> for EffectVisitor {
22            type Value = Effect;
23
24            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
25                formatter.write_str("a string or a map representing an effect")
26            }
27
28            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
29            where
30                E: serde::de::Error,
31            {
32                match value {
33                    "fade" => Ok(Effect::Fade(FadeParams::default())),
34                    "wipe" => Ok(Effect::Wipe(WipeParams::default())),
35                    "slide" => Ok(Effect::Slide(SlideParams::default())),
36                    "wave" => Ok(Effect::Wave(WaveParams::default())),
37                    "grow" => Ok(Effect::Grow(GrowParams::default())),
38                    "outer" => Ok(Effect::Outer(OuterParams::default())),
39                    _ => Err(E::custom(format!("unknown effect type: {}", value))),
40                }
41            }
42
43            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
44            where
45                A: serde::de::MapAccess<'de>,
46            {
47                let key: String = map
48                    .next_key()?
49                    .ok_or_else(|| serde::de::Error::custom("expected a key in Effect map"))?;
50
51                match key.as_str() {
52                    "fade" => Ok(Effect::Fade(map.next_value()?)),
53                    "wipe" => Ok(Effect::Wipe(map.next_value()?)),
54                    "slide" => Ok(Effect::Slide(map.next_value()?)),
55                    "wave" => Ok(Effect::Wave(map.next_value()?)),
56                    "grow" => Ok(Effect::Grow(map.next_value()?)),
57                    "outer" => Ok(Effect::Outer(map.next_value()?)),
58                    _ => Err(serde::de::Error::custom(format!(
59                        "unknown effect type: {}",
60                        key
61                    ))),
62                }
63            }
64        }
65
66        deserializer.deserialize_any(EffectVisitor)
67    }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct FadeParams {
72    #[serde(default)]
73    pub from: f32,
74    #[serde(default = "default_one")]
75    pub to: f32,
76    #[serde(default)]
77    pub easing: Easing,
78}
79
80impl Default for FadeParams {
81    fn default() -> Self {
82        Self {
83            from: 0.0,
84            to: 1.0,
85            easing: Easing::Bezier,
86        }
87    }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct WipeParams {
92    #[serde(default)]
93    pub direction: WipeDirection,
94    #[serde(default = "default_wipe_softness")]
95    pub softness: f32,
96    #[serde(default)]
97    pub angle: Option<f32>,
98    #[serde(default)]
99    pub easing: Easing,
100}
101
102impl Default for WipeParams {
103    fn default() -> Self {
104        Self {
105            direction: WipeDirection::Left,
106            softness: 0.006,
107            angle: None,
108            easing: Easing::Bezier,
109        }
110    }
111}
112
113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
114#[serde(rename_all = "snake_case")]
115pub enum WipeDirection {
116    #[default]
117    Left,
118    Right,
119    Up,
120    Down,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct SlideParams {
125    #[serde(default)]
126    pub direction: SlideDirection,
127    #[serde(default)]
128    pub easing: Easing,
129}
130
131impl Default for SlideParams {
132    fn default() -> Self {
133        Self {
134            direction: SlideDirection::Left,
135            easing: Easing::Bezier,
136        }
137    }
138}
139
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
141#[serde(rename_all = "snake_case")]
142pub enum SlideDirection {
143    #[default]
144    Left,
145    Right,
146    Up,
147    Down,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
151#[serde(rename_all = "snake_case")]
152pub enum Origin {
153    #[default]
154    Center,
155    Cursor,
156    Custom(f32, f32),
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
160pub struct WaveParams {
161    #[serde(default = "default_wave_frequency")]
162    pub frequency: f32,
163    #[serde(default = "default_wave_amplitude")]
164    pub amplitude: f32,
165    #[serde(default)]
166    pub angle: Option<f32>,
167    /// Wave center. An explicit `--origin` wins when no angle is given;
168    /// an angle derives the center from the sweep direction.
169    #[serde(default)]
170    pub origin: Origin,
171    #[serde(default)]
172    pub easing: Easing,
173}
174
175impl Default for WaveParams {
176    fn default() -> Self {
177        Self {
178            frequency: 3.0,
179            amplitude: 0.05,
180            angle: None,
181            origin: Origin::Center,
182            easing: Easing::Bezier,
183        }
184    }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
188pub struct GrowParams {
189    #[serde(default)]
190    pub origin: Origin,
191    #[serde(default)]
192    pub easing: Easing,
193}
194
195impl Default for GrowParams {
196    fn default() -> Self {
197        Self {
198            origin: Origin::Center,
199            easing: Easing::Bezier,
200        }
201    }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
205pub struct OuterParams {
206    #[serde(default)]
207    pub origin: Origin,
208    #[serde(default)]
209    pub easing: Easing,
210}
211
212impl Default for OuterParams {
213    fn default() -> Self {
214        Self {
215            origin: Origin::Center,
216            easing: Easing::Bezier,
217        }
218    }
219}
220
221#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, clap::ValueEnum)]
222#[serde(rename_all = "snake_case")]
223#[clap(rename_all = "snake_case")]
224pub enum Easing {
225    Linear,
226    #[serde(alias = "ease-in")]
227    EaseIn,
228    #[serde(alias = "ease-out")]
229    EaseOut,
230    #[serde(alias = "ease-in-out")]
231    #[default]
232    EaseInOut,
233    /// Cubic-bezier(.54, 0, .34, .99): awww's default transition curve.
234    /// Evaluated on the CPU once per frame and sent with linear passthrough,
235    /// since the shader only implements fixed curves.
236    Bezier,
237}
238
239fn default_one() -> f32 {
240    1.0
241}
242
243fn default_wipe_softness() -> f32 {
244    0.006
245}
246fn default_wave_frequency() -> f32 {
247    3.0
248}
249fn default_wave_amplitude() -> f32 {
250    0.05
251}
252
253/// Cubic-bezier y(x) with awww's default control points, in the style of
254/// gre/bezier-easing (the same family awww ports): Newton-Raphson with a
255/// bisection fallback.
256fn cubic_bezier_y(x: f32) -> f32 {
257    const X1: f32 = 0.54;
258    const Y1: f32 = 0.0;
259    const X2: f32 = 0.34;
260    const Y2: f32 = 0.99;
261    fn sample_curve_x(t: f32) -> f32 {
262        ((1.0 - 3.0 * X2 + 3.0 * X1) * t + (3.0 * X2 - 6.0 * X1)) * t * t + 3.0 * X1 * t
263    }
264    fn sample_curve_y(t: f32) -> f32 {
265        ((1.0 - 3.0 * Y2 + 3.0 * Y1) * t + (3.0 * Y2 - 6.0 * Y1)) * t * t + 3.0 * Y1 * t
266    }
267    fn sample_slope_x(t: f32) -> f32 {
268        3.0 * (1.0 - 3.0 * X2 + 3.0 * X1) * t * t + 2.0 * (3.0 * X2 - 6.0 * X1) * t + 3.0 * X1
269    }
270    let mut t = x.clamp(0.0, 1.0);
271    for _ in 0..5 {
272        let slope = sample_slope_x(t);
273        if slope.abs() < 1e-4 {
274            break;
275        }
276        t -= (sample_curve_x(t) - x) / slope;
277    }
278    if !(0.0..=1.0).contains(&t) {
279        let (mut lo, mut hi) = (0.0f32, 1.0f32);
280        t = x;
281        while hi - lo > 1e-4 {
282            if sample_curve_x(t) < x {
283                lo = t;
284            } else {
285                hi = t;
286            }
287            t = (lo + hi) / 2.0;
288        }
289    }
290    sample_curve_y(t.clamp(0.0, 1.0))
291}
292
293#[repr(C)]
294#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
295pub struct EffectUniforms {
296    pub effect_type: u32,
297    pub progress: f32,
298    pub param_a: f32,
299    pub param_b: f32,
300    pub param_c: f32,
301    pub param_d: f32,
302    pub origin: [f32; 2],
303    pub direction: [f32; 2],
304    pub easing: u32,
305}
306
307pub fn compute_effect_uniforms(effect: &Effect, progress: f32) -> EffectUniforms {
308    let progress = progress.clamp(0.0, 1.0);
309    let easing_index = |e: &Easing| match e {
310        Easing::Linear | Easing::Bezier => 0,
311        Easing::EaseIn => 1,
312        Easing::EaseOut => 2,
313        Easing::EaseInOut => 3,
314    };
315    // Bezier is pre-evaluated here (once per frame) and sent with linear
316    // passthrough; the shader only implements fixed curves.
317    let eased = |easing: &Easing| match easing {
318        Easing::Bezier => cubic_bezier_y(progress),
319        _ => progress,
320    };
321    match effect {
322        Effect::Fade(params) => EffectUniforms {
323            effect_type: 0,
324            progress: eased(&params.easing),
325            param_a: params.from,
326            param_b: params.to,
327            param_c: 0.0,
328            param_d: 0.0,
329            origin: [0.5, 0.5],
330            direction: [0.0, 0.0],
331            easing: easing_index(&params.easing),
332        },
333        Effect::Wipe(params) => {
334            let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
335                // awww angle convention: 0 = right-to-left, 90 = top-to-bottom,
336                // 270 = bottom-to-top. -sin keeps UV y (top-left origin) aligned.
337                let rad = angle_deg.to_radians();
338                (
339                    [rad.cos(), -rad.sin()],
340                    [0.5 - 0.5 * rad.cos(), 0.5 + 0.5 * rad.sin()],
341                )
342            } else {
343                match params.direction {
344                    WipeDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
345                    WipeDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
346                    WipeDirection::Up => ([0.0, -1.0], [0.5, 0.0]),
347                    WipeDirection::Down => ([0.0, 1.0], [0.5, 1.0]),
348                }
349            };
350            EffectUniforms {
351                effect_type: 1,
352                progress: eased(&params.easing),
353                param_a: params.softness,
354                param_b: 0.0,
355                param_c: 0.0,
356                param_d: 0.0,
357                origin,
358                direction: dir_vec,
359                easing: easing_index(&params.easing),
360            }
361        }
362        Effect::Slide(params) => {
363            let (dir_vec, origin) = match params.direction {
364                SlideDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
365                SlideDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
366                SlideDirection::Up => ([0.0, -1.0], [0.5, 0.0]),
367                SlideDirection::Down => ([0.0, 1.0], [0.5, 1.0]),
368            };
369            EffectUniforms {
370                effect_type: 2,
371                progress: eased(&params.easing),
372                param_a: 0.0,
373                param_b: 0.0,
374                param_c: 0.0,
375                param_d: 0.0,
376                origin,
377                direction: dir_vec,
378                easing: easing_index(&params.easing),
379            }
380        }
381        Effect::Wave(params) => {
382            let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
383                let rad = angle_deg.to_radians();
384                (
385                    [rad.cos(), rad.sin()],
386                    [0.5 + 0.5 * rad.cos(), 0.5 - 0.5 * rad.sin()],
387                )
388            } else {
389                let origin = match params.origin {
390                    Origin::Center | Origin::Cursor => [0.5, 0.5],
391                    Origin::Custom(x, y) => [x, y],
392                };
393                ([0.0, 0.0], origin)
394            };
395            EffectUniforms {
396                effect_type: 3,
397                progress: eased(&params.easing),
398                param_a: params.frequency,
399                param_b: params.amplitude,
400                param_c: 0.0,
401                param_d: 0.0,
402                origin,
403                direction: dir_vec,
404                easing: easing_index(&params.easing),
405            }
406        }
407        Effect::Grow(params) => {
408            let orig = match params.origin {
409                Origin::Center | Origin::Cursor => [0.5, 0.5],
410                Origin::Custom(x, y) => [x, y],
411            };
412            EffectUniforms {
413                effect_type: 4,
414                progress: eased(&params.easing),
415                param_a: 0.0,
416                param_b: 0.0,
417                param_c: 0.0,
418                param_d: 0.0,
419                origin: orig,
420                direction: [0.0, 0.0],
421                easing: easing_index(&params.easing),
422            }
423        }
424        Effect::Outer(params) => {
425            let orig = match params.origin {
426                Origin::Center | Origin::Cursor => [0.5, 0.5],
427                Origin::Custom(x, y) => [x, y],
428            };
429            EffectUniforms {
430                effect_type: 5,
431                progress: eased(&params.easing),
432                param_a: 0.0,
433                param_b: 0.0,
434                param_c: 0.0,
435                param_d: 0.0,
436                origin: orig,
437                direction: [0.0, 0.0],
438                easing: easing_index(&params.easing),
439            }
440        }
441    }
442}
443
444/// Parse an effect name into its default `Effect`.
445/// Accepts Wallr names plus the directional aliases familiar from awww:
446/// `simple`, `left`, `right`, `top`, `bottom`, `center`, `any`, and `random`.
447pub fn effect_from_name(name: &str) -> Option<Effect> {
448    let seed = || {
449        std::time::SystemTime::now()
450            .duration_since(std::time::UNIX_EPOCH)
451            .unwrap_or_default()
452            .subsec_nanos()
453    };
454    Some(match name {
455        // `simple` is a plain linear crossfade, matching awww's constant
456        // byte-step feel; `fade` keeps the eased (bezier-like) polish.
457        "simple" => Effect::Fade(FadeParams {
458            easing: Easing::Linear,
459            ..FadeParams::default()
460        }),
461        "fade" => Effect::Fade(FadeParams::default()),
462        "wipe" => Effect::Wipe(WipeParams::default()),
463        "slide" => Effect::Slide(SlideParams::default()),
464        "left" => Effect::Slide(SlideParams {
465            direction: SlideDirection::Left,
466            ..SlideParams::default()
467        }),
468        "right" => Effect::Slide(SlideParams {
469            direction: SlideDirection::Right,
470            ..SlideParams::default()
471        }),
472        "top" => Effect::Slide(SlideParams {
473            direction: SlideDirection::Up,
474            ..SlideParams::default()
475        }),
476        "bottom" => Effect::Slide(SlideParams {
477            direction: SlideDirection::Down,
478            ..SlideParams::default()
479        }),
480        "wave" => Effect::Wave(WaveParams::default()),
481        "grow" => Effect::Grow(GrowParams::default()),
482        "center" => Effect::Grow(GrowParams::default()),
483        "outer" => Effect::Outer(OuterParams::default()),
484        "any" => {
485            let value = seed();
486            let origin = Origin::Custom(
487                (value % 1000) as f32 / 1000.0,
488                ((value / 1000) % 1000) as f32 / 1000.0,
489            );
490            if value % 2 == 0 {
491                Effect::Grow(GrowParams {
492                    origin,
493                    ..GrowParams::default()
494                })
495            } else {
496                Effect::Outer(OuterParams {
497                    origin,
498                    ..OuterParams::default()
499                })
500            }
501        }
502        "random" => match seed() % 5 {
503            0 => Effect::Fade(FadeParams::default()),
504            1 => Effect::Slide(SlideParams::default()),
505            2 => Effect::Wave(WaveParams::default()),
506            3 => Effect::Grow(GrowParams::default()),
507            _ => Effect::Outer(OuterParams::default()),
508        },
509        _ => return None,
510    })
511}
512
513/// Canonical effect names. Directional and convenience aliases (`simple`,
514/// `left`, `right`, `top`, `bottom`, `center`, `any`, `random`) still map
515/// through `effect_from_name`; this list drives CLI help and errors so
516/// users see the six real transitions rather than every alias.
517pub fn effect_names() -> &'static [&'static str] {
518    &["fade", "wipe", "slide", "wave", "grow", "outer"]
519}
520
521#[derive(Debug, Clone, Default)]
522pub struct EffectOverrides {
523    pub origin: Option<(f32, f32)>,
524    pub origin_preset: Option<String>,
525    pub direction: Option<[f32; 2]>,
526    pub angle: Option<f32>,
527    pub easing: Option<Easing>,
528    pub from: Option<f32>,
529    pub to: Option<f32>,
530    pub frequency: Option<f32>,
531    pub amplitude: Option<f32>,
532    pub softness: Option<f32>,
533}
534
535fn origin_from_preset(preset: &str) -> (f32, f32) {
536    match preset {
537        "top_left" => (0.0, 0.0),
538        "top" => (0.5, 0.0),
539        "top_right" => (1.0, 0.0),
540        "left" => (0.0, 0.5),
541        "center" => (0.5, 0.5),
542        "right" => (1.0, 0.5),
543        "bottom_left" => (0.0, 1.0),
544        "bottom" => (0.5, 1.0),
545        "bottom_right" => (1.0, 1.0),
546        _ => (0.5, 0.5),
547    }
548}
549
550/// Apply CLI-style overrides to an effect, mutating it in place.
551pub fn apply_effect_overrides(effect: &mut Effect, o: &EffectOverrides) {
552    let origin = o
553        .origin
554        .or_else(|| o.origin_preset.as_ref().map(|p| origin_from_preset(p)));
555
556    match effect {
557        Effect::Fade(p) => {
558            if let Some(v) = o.from {
559                p.from = v;
560            }
561            if let Some(v) = o.to {
562                p.to = v;
563            }
564            if let Some(e) = o.easing {
565                p.easing = e;
566            }
567        }
568        Effect::Wipe(p) => {
569            if let Some(s) = o.softness {
570                p.softness = s;
571            }
572            if let Some(a) = o.angle {
573                p.angle = Some(a);
574            }
575            if let Some(e) = o.easing {
576                p.easing = e;
577            }
578            if let Some(d) = o.direction {
579                p.direction = match d {
580                    [-1.0, 0.0] => WipeDirection::Left,
581                    [1.0, 0.0] => WipeDirection::Right,
582                    [0.0, 1.0] => WipeDirection::Up,
583                    [0.0, -1.0] => WipeDirection::Down,
584                    _ => p.direction,
585                };
586            }
587        }
588        Effect::Slide(p) => {
589            if let Some(e) = o.easing {
590                p.easing = e;
591            }
592            if let Some(d) = o.direction {
593                p.direction = match d {
594                    [-1.0, 0.0] => SlideDirection::Left,
595                    [1.0, 0.0] => SlideDirection::Right,
596                    [0.0, 1.0] => SlideDirection::Up,
597                    [0.0, -1.0] => SlideDirection::Down,
598                    _ => p.direction,
599                };
600            }
601        }
602        Effect::Wave(p) => {
603            if let Some(v) = o.frequency {
604                p.frequency = v;
605            }
606            if let Some(v) = o.amplitude {
607                p.amplitude = v;
608            }
609            if let Some(a) = o.angle {
610                p.angle = Some(a);
611            }
612            if let Some((x, y)) = origin {
613                p.origin = Origin::Custom(x, y);
614            }
615            if let Some(e) = o.easing {
616                p.easing = e;
617            }
618        }
619        Effect::Grow(p) => {
620            if let Some((x, y)) = origin {
621                p.origin = Origin::Custom(x, y);
622            }
623            if let Some(e) = o.easing {
624                p.easing = e;
625            }
626        }
627        Effect::Outer(p) => {
628            if let Some((x, y)) = origin {
629                p.origin = Origin::Custom(x, y);
630            }
631            if let Some(e) = o.easing {
632                p.easing = e;
633            }
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    #[test]
643    fn ranged_effects_keep_both_endpoints_for_the_shader() {
644        let fade = Effect::Fade(FadeParams {
645            from: 0.2,
646            to: 0.9,
647            easing: Easing::EaseOut,
648        });
649        let start = compute_effect_uniforms(&fade, 0.0);
650        let end = compute_effect_uniforms(&fade, 1.0);
651        assert_eq!((start.param_a, start.param_b), (0.2, 0.9));
652        assert_eq!((end.param_a, end.param_b), (0.2, 0.9));
653    }
654
655    #[test]
656    fn awww_direction_aliases_resolve_to_typed_effects() {
657        assert!(matches!(effect_from_name("simple"), Some(Effect::Fade(_))));
658        assert!(matches!(effect_from_name("left"), Some(Effect::Slide(_))));
659        assert!(matches!(effect_from_name("right"), Some(Effect::Slide(_))));
660        assert!(matches!(effect_from_name("center"), Some(Effect::Grow(_))));
661        assert!(matches!(
662            effect_from_name("any"),
663            Some(Effect::Grow(_)) | Some(Effect::Outer(_))
664        ));
665        assert!(effect_from_name("random").is_some());
666    }
667
668    #[test]
669    fn effect_types_match_shader_arms() {
670        // Must track effects.wgsl: 0 fade, 1 wipe, 2 slide, 3 wave, 4 grow, 5 outer.
671        let cases = [
672            (Effect::Fade(FadeParams::default()), 0),
673            (Effect::Wipe(WipeParams::default()), 1),
674            (Effect::Slide(SlideParams::default()), 2),
675            (Effect::Wave(WaveParams::default()), 3),
676            (Effect::Grow(GrowParams::default()), 4),
677            (Effect::Outer(OuterParams::default()), 5),
678        ];
679        for (effect, expected) in cases {
680            assert_eq!(compute_effect_uniforms(&effect, 0.5).effect_type, expected);
681        }
682    }
683
684    #[test]
685    fn removed_effects_no_longer_resolve() {
686        for name in ["blur", "zoom", "pixelate", "ripple", "dissolve", "shader"] {
687            assert_eq!(effect_from_name(name), None);
688        }
689    }
690
691    #[test]
692    fn bezier_easing_matches_awww_curve() {
693        assert_eq!(cubic_bezier_y(0.0), 0.0);
694        assert_eq!(cubic_bezier_y(1.0), 1.0);
695        let mut prev = 0.0;
696        let mut i = 1;
697        while i <= 20 {
698            let y = cubic_bezier_y(i as f32 / 20.0);
699            assert!(y >= prev, "bezier must not decrease");
700            prev = y;
701            i += 1;
702        }
703        // awww's (.54, 0, .34, .99): slow start, fast middle, soft landing.
704        let mid = cubic_bezier_y(0.5);
705        assert!((0.4..0.7).contains(&mid), "mid: {mid}");
706        assert!(cubic_bezier_y(0.25) < 0.25);
707    }
708
709    #[test]
710    fn wave_honors_explicit_origin_without_angle() {
711        let mut wave = Effect::Wave(WaveParams::default());
712        apply_effect_overrides(
713            &mut wave,
714            &EffectOverrides {
715                origin: Some((1.0, 1.0)),
716                ..EffectOverrides::default()
717            },
718        );
719        let uniforms = compute_effect_uniforms(&wave, 0.5);
720        assert_eq!(uniforms.origin, [1.0, 1.0]);
721
722        // An angle still derives the center from the sweep direction.
723        let mut angled = Effect::Wave(WaveParams::default());
724        apply_effect_overrides(
725            &mut angled,
726            &EffectOverrides {
727                origin: Some((1.0, 1.0)),
728                angle: Some(90.0),
729                ..EffectOverrides::default()
730            },
731        );
732        let uniforms = compute_effect_uniforms(&angled, 0.5);
733        assert_ne!(uniforms.origin, [1.0, 1.0]);
734    }
735}