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 #[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 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
253fn 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 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(¶ms.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(¶ms.easing),
332 },
333 Effect::Wipe(params) => {
334 let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
335 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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.easing),
439 }
440 }
441 }
442}
443
444pub 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" => 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
513pub 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
550pub 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 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 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 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}