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 = "default_easing")]
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 = "default_easing")]
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 = "default_easing")]
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 #[serde(default)]
170 pub origin: Origin,
171 #[serde(default = "default_easing")]
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 = "default_easing")]
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 = "default_easing")]
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 Bezier,
237}
238
239fn default_one() -> f32 {
240 1.0
241}
242
243fn default_easing() -> Easing {
244 Easing::Bezier
245}
246
247fn default_wipe_softness() -> f32 {
248 0.006
249}
250fn default_wave_frequency() -> f32 {
251 3.0
252}
253fn default_wave_amplitude() -> f32 {
254 0.05
255}
256
257fn cubic_bezier_y(x: f32) -> f32 {
261 const X1: f32 = 0.54;
262 const Y1: f32 = 0.0;
263 const X2: f32 = 0.34;
264 const Y2: f32 = 0.99;
265 fn sample_curve_x(t: f32) -> f32 {
266 ((1.0 - 3.0 * X2 + 3.0 * X1) * t + (3.0 * X2 - 6.0 * X1)) * t * t + 3.0 * X1 * t
267 }
268 fn sample_curve_y(t: f32) -> f32 {
269 ((1.0 - 3.0 * Y2 + 3.0 * Y1) * t + (3.0 * Y2 - 6.0 * Y1)) * t * t + 3.0 * Y1 * t
270 }
271 fn sample_slope_x(t: f32) -> f32 {
272 3.0 * (1.0 - 3.0 * X2 + 3.0 * X1) * t * t + 2.0 * (3.0 * X2 - 6.0 * X1) * t + 3.0 * X1
273 }
274 let mut t = x.clamp(0.0, 1.0);
275 for _ in 0..5 {
276 let slope = sample_slope_x(t);
277 if slope.abs() < 1e-4 {
278 break;
279 }
280 t -= (sample_curve_x(t) - x) / slope;
281 }
282 if !(0.0..=1.0).contains(&t) {
283 let (mut lo, mut hi) = (0.0f32, 1.0f32);
284 t = x;
285 while hi - lo > 1e-4 {
286 if sample_curve_x(t) < x {
287 lo = t;
288 } else {
289 hi = t;
290 }
291 t = (lo + hi) / 2.0;
292 }
293 }
294 sample_curve_y(t.clamp(0.0, 1.0))
295}
296
297#[repr(C)]
298#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
299pub struct EffectUniforms {
300 pub effect_type: u32,
301 pub progress: f32,
302 pub param_a: f32,
303 pub param_b: f32,
304 pub param_c: f32,
305 pub param_d: f32,
306 pub origin: [f32; 2],
307 pub direction: [f32; 2],
308 pub easing: u32,
309}
310
311pub fn compute_effect_uniforms(effect: &Effect, progress: f32) -> EffectUniforms {
312 let progress = progress.clamp(0.0, 1.0);
313 let easing_index = |e: &Easing| match e {
314 Easing::Linear | Easing::Bezier => 0,
315 Easing::EaseIn => 1,
316 Easing::EaseOut => 2,
317 Easing::EaseInOut => 3,
318 };
319 let eased = |easing: &Easing| match easing {
322 Easing::Bezier => cubic_bezier_y(progress),
323 _ => progress,
324 };
325 match effect {
326 Effect::Fade(params) => EffectUniforms {
327 effect_type: 0,
328 progress: eased(¶ms.easing),
329 param_a: params.from,
330 param_b: params.to,
331 param_c: 0.0,
332 param_d: 0.0,
333 origin: [0.5, 0.5],
334 direction: [0.0, 0.0],
335 easing: easing_index(¶ms.easing),
336 },
337 Effect::Wipe(params) => {
338 let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
339 let rad = angle_deg.to_radians();
342 (
343 [rad.cos(), -rad.sin()],
344 [0.5 - 0.5 * rad.cos(), 0.5 + 0.5 * rad.sin()],
345 )
346 } else {
347 match params.direction {
348 WipeDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
349 WipeDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
350 WipeDirection::Up => ([0.0, -1.0], [0.5, 0.0]),
351 WipeDirection::Down => ([0.0, 1.0], [0.5, 1.0]),
352 }
353 };
354 EffectUniforms {
355 effect_type: 1,
356 progress: eased(¶ms.easing),
357 param_a: params.softness,
358 param_b: 0.0,
359 param_c: 0.0,
360 param_d: 0.0,
361 origin,
362 direction: dir_vec,
363 easing: easing_index(¶ms.easing),
364 }
365 }
366 Effect::Slide(params) => {
367 let (dir_vec, origin) = match params.direction {
368 SlideDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
369 SlideDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
370 SlideDirection::Up => ([0.0, -1.0], [0.5, 0.0]),
371 SlideDirection::Down => ([0.0, 1.0], [0.5, 1.0]),
372 };
373 EffectUniforms {
374 effect_type: 2,
375 progress: eased(¶ms.easing),
376 param_a: 0.0,
377 param_b: 0.0,
378 param_c: 0.0,
379 param_d: 0.0,
380 origin,
381 direction: dir_vec,
382 easing: easing_index(¶ms.easing),
383 }
384 }
385 Effect::Wave(params) => {
386 let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
387 let rad = angle_deg.to_radians();
388 (
389 [rad.cos(), rad.sin()],
390 [0.5 + 0.5 * rad.cos(), 0.5 - 0.5 * rad.sin()],
391 )
392 } else {
393 let origin = match params.origin {
394 Origin::Center | Origin::Cursor => [0.5, 0.5],
395 Origin::Custom(x, y) => [x, y],
396 };
397 ([0.0, 0.0], origin)
398 };
399 EffectUniforms {
400 effect_type: 3,
401 progress: eased(¶ms.easing),
402 param_a: params.frequency,
403 param_b: params.amplitude,
404 param_c: 0.0,
405 param_d: 0.0,
406 origin,
407 direction: dir_vec,
408 easing: easing_index(¶ms.easing),
409 }
410 }
411 Effect::Grow(params) => {
412 let orig = match params.origin {
413 Origin::Center | Origin::Cursor => [0.5, 0.5],
414 Origin::Custom(x, y) => [x, y],
415 };
416 EffectUniforms {
417 effect_type: 4,
418 progress: eased(¶ms.easing),
419 param_a: 0.0,
420 param_b: 0.0,
421 param_c: 0.0,
422 param_d: 0.0,
423 origin: orig,
424 direction: [0.0, 0.0],
425 easing: easing_index(¶ms.easing),
426 }
427 }
428 Effect::Outer(params) => {
429 let orig = match params.origin {
430 Origin::Center | Origin::Cursor => [0.5, 0.5],
431 Origin::Custom(x, y) => [x, y],
432 };
433 EffectUniforms {
434 effect_type: 5,
435 progress: eased(¶ms.easing),
436 param_a: 0.0,
437 param_b: 0.0,
438 param_c: 0.0,
439 param_d: 0.0,
440 origin: orig,
441 direction: [0.0, 0.0],
442 easing: easing_index(¶ms.easing),
443 }
444 }
445 }
446}
447
448pub fn effect_from_name(name: &str) -> Option<Effect> {
452 let seed = || {
453 std::time::SystemTime::now()
454 .duration_since(std::time::UNIX_EPOCH)
455 .unwrap_or_default()
456 .subsec_nanos()
457 };
458 Some(match name {
459 "simple" => Effect::Fade(FadeParams {
462 easing: Easing::Linear,
463 ..FadeParams::default()
464 }),
465 "fade" => Effect::Fade(FadeParams::default()),
466 "wipe" => Effect::Wipe(WipeParams::default()),
467 "slide" => Effect::Slide(SlideParams::default()),
468 "left" => Effect::Slide(SlideParams {
469 direction: SlideDirection::Left,
470 ..SlideParams::default()
471 }),
472 "right" => Effect::Slide(SlideParams {
473 direction: SlideDirection::Right,
474 ..SlideParams::default()
475 }),
476 "top" => Effect::Slide(SlideParams {
477 direction: SlideDirection::Up,
478 ..SlideParams::default()
479 }),
480 "bottom" => Effect::Slide(SlideParams {
481 direction: SlideDirection::Down,
482 ..SlideParams::default()
483 }),
484 "wave" => Effect::Wave(WaveParams::default()),
485 "grow" => Effect::Grow(GrowParams::default()),
486 "center" => Effect::Grow(GrowParams::default()),
487 "outer" => Effect::Outer(OuterParams::default()),
488 "any" => {
489 let value = seed();
490 let origin = Origin::Custom(
491 (value % 1000) as f32 / 1000.0,
492 ((value / 1000) % 1000) as f32 / 1000.0,
493 );
494 if value % 2 == 0 {
495 Effect::Grow(GrowParams {
496 origin,
497 ..GrowParams::default()
498 })
499 } else {
500 Effect::Outer(OuterParams {
501 origin,
502 ..OuterParams::default()
503 })
504 }
505 }
506 "random" => match seed() % 5 {
507 0 => Effect::Fade(FadeParams::default()),
508 1 => Effect::Slide(SlideParams::default()),
509 2 => Effect::Wave(WaveParams::default()),
510 3 => Effect::Grow(GrowParams::default()),
511 _ => Effect::Outer(OuterParams::default()),
512 },
513 _ => return None,
514 })
515}
516
517pub fn effect_names() -> &'static [&'static str] {
522 &["fade", "wipe", "slide", "wave", "grow", "outer"]
523}
524
525#[derive(Debug, Clone, Default)]
526pub struct EffectOverrides {
527 pub origin: Option<(f32, f32)>,
528 pub origin_preset: Option<String>,
529 pub direction: Option<[f32; 2]>,
530 pub angle: Option<f32>,
531 pub easing: Option<Easing>,
532 pub from: Option<f32>,
533 pub to: Option<f32>,
534 pub frequency: Option<f32>,
535 pub amplitude: Option<f32>,
536 pub softness: Option<f32>,
537}
538
539fn origin_from_preset(preset: &str) -> (f32, f32) {
540 match preset {
541 "top_left" => (0.0, 0.0),
542 "top" => (0.5, 0.0),
543 "top_right" => (1.0, 0.0),
544 "left" => (0.0, 0.5),
545 "center" => (0.5, 0.5),
546 "right" => (1.0, 0.5),
547 "bottom_left" => (0.0, 1.0),
548 "bottom" => (0.5, 1.0),
549 "bottom_right" => (1.0, 1.0),
550 _ => (0.5, 0.5),
551 }
552}
553
554pub fn apply_effect_overrides(effect: &mut Effect, o: &EffectOverrides) {
556 let origin = o
557 .origin
558 .or_else(|| o.origin_preset.as_ref().map(|p| origin_from_preset(p)));
559
560 match effect {
561 Effect::Fade(p) => {
562 if let Some(v) = o.from {
563 p.from = v;
564 }
565 if let Some(v) = o.to {
566 p.to = v;
567 }
568 if let Some(e) = o.easing {
569 p.easing = e;
570 }
571 }
572 Effect::Wipe(p) => {
573 if let Some(s) = o.softness {
574 p.softness = s;
575 }
576 if let Some(a) = o.angle {
577 p.angle = Some(a);
578 }
579 if let Some(e) = o.easing {
580 p.easing = e;
581 }
582 if let Some(d) = o.direction {
583 p.direction = match d {
584 [-1.0, 0.0] => WipeDirection::Left,
585 [1.0, 0.0] => WipeDirection::Right,
586 [0.0, 1.0] => WipeDirection::Up,
587 [0.0, -1.0] => WipeDirection::Down,
588 _ => p.direction,
589 };
590 }
591 }
592 Effect::Slide(p) => {
593 if let Some(e) = o.easing {
594 p.easing = e;
595 }
596 if let Some(d) = o.direction {
597 p.direction = match d {
598 [-1.0, 0.0] => SlideDirection::Left,
599 [1.0, 0.0] => SlideDirection::Right,
600 [0.0, 1.0] => SlideDirection::Up,
601 [0.0, -1.0] => SlideDirection::Down,
602 _ => p.direction,
603 };
604 }
605 }
606 Effect::Wave(p) => {
607 if let Some(v) = o.frequency {
608 p.frequency = v;
609 }
610 if let Some(v) = o.amplitude {
611 p.amplitude = v;
612 }
613 if let Some(a) = o.angle {
614 p.angle = Some(a);
615 }
616 if let Some((x, y)) = origin {
617 p.origin = Origin::Custom(x, y);
618 }
619 if let Some(e) = o.easing {
620 p.easing = e;
621 }
622 }
623 Effect::Grow(p) => {
624 if let Some((x, y)) = origin {
625 p.origin = Origin::Custom(x, y);
626 }
627 if let Some(e) = o.easing {
628 p.easing = e;
629 }
630 }
631 Effect::Outer(p) => {
632 if let Some((x, y)) = origin {
633 p.origin = Origin::Custom(x, y);
634 }
635 if let Some(e) = o.easing {
636 p.easing = e;
637 }
638 }
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn ranged_effects_keep_both_endpoints_for_the_shader() {
648 let fade = Effect::Fade(FadeParams {
649 from: 0.2,
650 to: 0.9,
651 easing: Easing::EaseOut,
652 });
653 let start = compute_effect_uniforms(&fade, 0.0);
654 let end = compute_effect_uniforms(&fade, 1.0);
655 assert_eq!((start.param_a, start.param_b), (0.2, 0.9));
656 assert_eq!((end.param_a, end.param_b), (0.2, 0.9));
657 }
658
659 #[test]
660 fn awww_direction_aliases_resolve_to_typed_effects() {
661 assert!(matches!(effect_from_name("simple"), Some(Effect::Fade(_))));
662 assert!(matches!(effect_from_name("left"), Some(Effect::Slide(_))));
663 assert!(matches!(effect_from_name("right"), Some(Effect::Slide(_))));
664 assert!(matches!(effect_from_name("center"), Some(Effect::Grow(_))));
665 assert!(matches!(
666 effect_from_name("any"),
667 Some(Effect::Grow(_)) | Some(Effect::Outer(_))
668 ));
669 assert!(effect_from_name("random").is_some());
670 }
671
672 #[test]
673 fn effect_types_match_shader_arms() {
674 let cases = [
676 (Effect::Fade(FadeParams::default()), 0),
677 (Effect::Wipe(WipeParams::default()), 1),
678 (Effect::Slide(SlideParams::default()), 2),
679 (Effect::Wave(WaveParams::default()), 3),
680 (Effect::Grow(GrowParams::default()), 4),
681 (Effect::Outer(OuterParams::default()), 5),
682 ];
683 for (effect, expected) in cases {
684 assert_eq!(compute_effect_uniforms(&effect, 0.5).effect_type, expected);
685 }
686 }
687
688 #[test]
689 fn removed_effects_no_longer_resolve() {
690 for name in ["blur", "zoom", "pixelate", "ripple", "dissolve", "shader"] {
691 assert_eq!(effect_from_name(name), None);
692 }
693 }
694
695 #[test]
696 fn bezier_easing_matches_awww_curve() {
697 assert_eq!(cubic_bezier_y(0.0), 0.0);
698 assert_eq!(cubic_bezier_y(1.0), 1.0);
699 let mut prev = 0.0;
700 let mut i = 1;
701 while i <= 20 {
702 let y = cubic_bezier_y(i as f32 / 20.0);
703 assert!(y >= prev, "bezier must not decrease");
704 prev = y;
705 i += 1;
706 }
707 let mid = cubic_bezier_y(0.5);
709 assert!((0.4..0.7).contains(&mid), "mid: {mid}");
710 assert!(cubic_bezier_y(0.25) < 0.25);
711 }
712
713 #[test]
714 fn wave_honors_explicit_origin_without_angle() {
715 let mut wave = Effect::Wave(WaveParams::default());
716 apply_effect_overrides(
717 &mut wave,
718 &EffectOverrides {
719 origin: Some((1.0, 1.0)),
720 ..EffectOverrides::default()
721 },
722 );
723 let uniforms = compute_effect_uniforms(&wave, 0.5);
724 assert_eq!(uniforms.origin, [1.0, 1.0]);
725
726 let mut angled = Effect::Wave(WaveParams::default());
728 apply_effect_overrides(
729 &mut angled,
730 &EffectOverrides {
731 origin: Some((1.0, 1.0)),
732 angle: Some(90.0),
733 ..EffectOverrides::default()
734 },
735 );
736 let uniforms = compute_effect_uniforms(&angled, 0.5);
737 assert_ne!(uniforms.origin, [1.0, 1.0]);
738 }
739}