1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::Path;
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6pub struct AnimationSpec {
7 pub name: String,
8 #[serde(default)]
10 pub duration: Option<String>,
11 #[serde(default)]
12 pub effects: Vec<Effect>,
13 #[serde(default)]
14 pub timeline: Option<Vec<TimelineEntry>>,
15 #[serde(default)]
16 pub variables: HashMap<String, f64>,
17 #[serde(default)]
19 pub extends: Vec<String>,
20 #[serde(default)]
22 pub custom_effects: HashMap<String, crate::custom_effects::CustomEffect>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct TimelineEntry {
27 pub at: String,
28 #[serde(default)]
29 pub duration: Option<String>,
30 #[serde(flatten)]
31 pub effect: Effect,
32}
33
34#[derive(Debug, Clone, Serialize, PartialEq)]
35#[serde(rename_all = "snake_case")]
36pub enum Effect {
37 Fade(FadeParams),
38 Blur(BlurParams),
39 Wipe(WipeParams),
40 Slide(SlideParams),
41 Zoom(ZoomParams),
42 Pixelate(PixelateParams),
43 Ripple(RippleParams),
44 Dissolve(DissolveParams),
45 Wave(WaveParams),
46 Grow(GrowParams),
47 Outer(OuterParams),
48 Shader(ShaderParams),
49}
50
51impl<'de> Deserialize<'de> for Effect {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: serde::Deserializer<'de>,
55 {
56 struct EffectVisitor;
57
58 impl<'de> serde::de::Visitor<'de> for EffectVisitor {
59 type Value = Effect;
60
61 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
62 formatter.write_str("a string or a map representing an effect")
63 }
64
65 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
66 where
67 E: serde::de::Error,
68 {
69 match value {
70 "fade" => Ok(Effect::Fade(FadeParams::default())),
71 "blur" => Ok(Effect::Blur(BlurParams::default())),
72 "wipe" => Ok(Effect::Wipe(WipeParams::default())),
73 "slide" => Ok(Effect::Slide(SlideParams::default())),
74 "zoom" => Ok(Effect::Zoom(ZoomParams::default())),
75 "pixelate" => Ok(Effect::Pixelate(PixelateParams::default())),
76 "ripple" => Ok(Effect::Ripple(RippleParams::default())),
77 "dissolve" => Ok(Effect::Dissolve(DissolveParams::default())),
78 "wave" => Ok(Effect::Wave(WaveParams::default())),
79 "grow" => Ok(Effect::Grow(GrowParams::default())),
80 "outer" => Ok(Effect::Outer(OuterParams::default())),
81 _ => Err(E::custom(format!("unknown effect type: {}", value))),
82 }
83 }
84
85 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
86 where
87 A: serde::de::MapAccess<'de>,
88 {
89 let key: String = map
90 .next_key()?
91 .ok_or_else(|| serde::de::Error::custom("expected a key in Effect map"))?;
92
93 match key.as_str() {
94 "fade" => Ok(Effect::Fade(map.next_value()?)),
95 "blur" => Ok(Effect::Blur(map.next_value()?)),
96 "wipe" => Ok(Effect::Wipe(map.next_value()?)),
97 "slide" => Ok(Effect::Slide(map.next_value()?)),
98 "zoom" => Ok(Effect::Zoom(map.next_value()?)),
99 "pixelate" => Ok(Effect::Pixelate(map.next_value()?)),
100 "ripple" => Ok(Effect::Ripple(map.next_value()?)),
101 "dissolve" => Ok(Effect::Dissolve(map.next_value()?)),
102 "wave" => Ok(Effect::Wave(map.next_value()?)),
103 "grow" => Ok(Effect::Grow(map.next_value()?)),
104 "outer" => Ok(Effect::Outer(map.next_value()?)),
105 "shader" => Ok(Effect::Shader(map.next_value()?)),
106 _ => Err(serde::de::Error::custom(format!(
107 "unknown effect type: {}",
108 key
109 ))),
110 }
111 }
112 }
113
114 deserializer.deserialize_any(EffectVisitor)
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct FadeParams {
120 #[serde(default)]
121 pub from: f32,
122 #[serde(default = "default_one")]
123 pub to: f32,
124 #[serde(default)]
125 pub easing: Easing,
126}
127
128impl Default for FadeParams {
129 fn default() -> Self {
130 Self {
131 from: 0.0,
132 to: 1.0,
133 easing: Easing::EaseInOut,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
139pub struct BlurParams {
140 #[serde(default = "default_blur_from")]
141 pub from: f32,
142 #[serde(default)]
143 pub to: f32,
144 #[serde(default)]
145 pub easing: Easing,
146}
147
148impl Default for BlurParams {
149 fn default() -> Self {
150 Self {
151 from: 20.0,
152 to: 0.0,
153 easing: Easing::EaseInOut,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159pub struct WipeParams {
160 #[serde(default)]
161 pub direction: WipeDirection,
162 #[serde(default = "default_wipe_softness")]
163 pub softness: f32,
164 #[serde(default)]
165 pub angle: Option<f32>,
166 #[serde(default)]
167 pub easing: Easing,
168}
169
170impl Default for WipeParams {
171 fn default() -> Self {
172 Self {
173 direction: WipeDirection::Left,
174 softness: 0.12,
175 angle: None,
176 easing: Easing::EaseInOut,
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
182#[serde(rename_all = "snake_case")]
183pub enum WipeDirection {
184 #[default]
185 Left,
186 Right,
187 Up,
188 Down,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct SlideParams {
193 #[serde(default)]
194 pub direction: SlideDirection,
195 #[serde(default)]
196 pub easing: Easing,
197}
198
199impl Default for SlideParams {
200 fn default() -> Self {
201 Self {
202 direction: SlideDirection::Left,
203 easing: Easing::EaseInOut,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
209#[serde(rename_all = "snake_case")]
210pub enum SlideDirection {
211 #[default]
212 Left,
213 Right,
214 Up,
215 Down,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219pub struct ZoomParams {
220 #[serde(default = "default_zoom_from")]
221 pub from: f32,
222 #[serde(default = "default_one")]
223 pub to: f32,
224 #[serde(default)]
225 pub origin: Origin,
226 #[serde(default)]
227 pub easing: Easing,
228}
229
230impl Default for ZoomParams {
231 fn default() -> Self {
232 Self {
233 from: 1.08,
234 to: 1.0,
235 origin: Origin::Center,
236 easing: Easing::EaseInOut,
237 }
238 }
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
242#[serde(rename_all = "snake_case")]
243pub enum Origin {
244 #[default]
245 Center,
246 Cursor,
247 Custom(f32, f32),
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
251pub struct PixelateParams {
252 #[serde(default = "default_pixelate_from")]
253 pub from: f32,
254 #[serde(default = "default_one")]
255 pub to: f32,
256 #[serde(default)]
257 pub easing: Easing,
258}
259
260impl Default for PixelateParams {
261 fn default() -> Self {
262 Self {
263 from: 64.0,
264 to: 1.0,
265 easing: Easing::EaseInOut,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
271pub struct RippleParams {
272 #[serde(default)]
273 pub origin: Origin,
274 #[serde(default = "default_frequency")]
275 pub frequency: f32,
276 #[serde(default = "default_amplitude")]
277 pub amplitude: f32,
278 #[serde(default = "default_speed")]
279 pub speed: f32,
280 #[serde(default)]
281 pub easing: Easing,
282}
283
284impl Default for RippleParams {
285 fn default() -> Self {
286 Self {
287 origin: Origin::Center,
288 frequency: 12.0,
289 amplitude: 0.03,
290 speed: 5.0,
291 easing: Easing::EaseInOut,
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
297pub struct DissolveParams {
298 #[serde(default = "default_dissolve_scale")]
299 pub scale: f32,
300 #[serde(default = "default_softness")]
301 pub softness: f32,
302 #[serde(default)]
303 pub easing: Easing,
304}
305
306impl Default for DissolveParams {
307 fn default() -> Self {
308 Self {
309 scale: 4.0,
310 softness: 0.05,
311 easing: Easing::EaseInOut,
312 }
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct WaveParams {
318 #[serde(default = "default_wave_frequency")]
319 pub frequency: f32,
320 #[serde(default = "default_wave_amplitude")]
321 pub amplitude: f32,
322 #[serde(default)]
323 pub angle: Option<f32>,
324 #[serde(default)]
325 pub easing: Easing,
326}
327
328impl Default for WaveParams {
329 fn default() -> Self {
330 Self {
331 frequency: 3.0,
332 amplitude: 0.05,
333 angle: None,
334 easing: Easing::EaseInOut,
335 }
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340pub struct GrowParams {
341 #[serde(default)]
342 pub origin: Origin,
343 #[serde(default)]
344 pub easing: Easing,
345}
346
347impl Default for GrowParams {
348 fn default() -> Self {
349 Self {
350 origin: Origin::Center,
351 easing: Easing::EaseInOut,
352 }
353 }
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
357pub struct OuterParams {
358 #[serde(default)]
359 pub origin: Origin,
360 #[serde(default)]
361 pub easing: Easing,
362}
363
364impl Default for OuterParams {
365 fn default() -> Self {
366 Self {
367 origin: Origin::Center,
368 easing: Easing::EaseInOut,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
374pub struct ShaderParams {
375 pub file: String,
376 #[serde(default)]
377 pub uniforms: HashMap<String, f64>,
378}
379
380#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, clap::ValueEnum)]
381#[serde(rename_all = "snake_case")]
382#[clap(rename_all = "snake_case")]
383pub enum Easing {
384 Linear,
385 #[serde(alias = "ease-in")]
386 EaseIn,
387 #[serde(alias = "ease-out")]
388 EaseOut,
389 #[serde(alias = "ease-in-out")]
390 #[default]
391 EaseInOut,
392 Emphatic,
393 Spring,
394}
395
396fn default_one() -> f32 {
397 1.0
398}
399fn default_zoom_from() -> f32 {
400 1.08
401}
402fn default_blur_from() -> f32 {
403 20.0
404}
405fn default_softness() -> f32 {
406 0.05
407}
408
409fn default_wipe_softness() -> f32 {
410 0.12
411}
412fn default_pixelate_from() -> f32 {
413 64.0
414}
415fn default_frequency() -> f32 {
416 12.0
417}
418fn default_amplitude() -> f32 {
419 0.03
420}
421fn default_speed() -> f32 {
422 5.0
423}
424fn default_dissolve_scale() -> f32 {
425 4.0
426}
427fn default_wave_frequency() -> f32 {
428 3.0
429}
430fn default_wave_amplitude() -> f32 {
431 0.05
432}
433
434#[repr(C)]
435#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
436pub struct EffectUniforms {
437 pub effect_type: u32,
438 pub progress: f32,
439 pub param_a: f32,
440 pub param_b: f32,
441 pub param_c: f32,
442 pub param_d: f32,
443 pub origin: [f32; 2],
444 pub direction: [f32; 2],
445 pub easing: u32,
446}
447
448#[derive(Debug, thiserror::Error)]
449pub enum AnimationError {
450 #[error("failed to read animation file: {0}")]
451 ReadError(#[from] std::io::Error),
452 #[error("failed to parse animation YAML: {0}")]
453 ParseError(#[from] serde_yaml::Error),
454 #[error("invalid effect: {0}")]
455 InvalidEffect(String),
456 #[error("invalid timeline: {0}")]
457 InvalidTimeline(String),
458 #[error("unresolved variable: {0}")]
459 UnresolvedVariable(String),
460 #[error("invalid duration: {0}")]
461 InvalidDuration(String),
462 #[error("shader error: {0}")]
463 ShaderError(String),
464}
465
466pub fn load_animation(path: &Path) -> Result<AnimationSpec, AnimationError> {
467 let content = std::fs::read_to_string(path)?;
468 parse_animation_yaml(&content)
469}
470
471pub fn parse_animation_yaml(content: &str) -> Result<AnimationSpec, AnimationError> {
473 let mut value: serde_yaml::Value = serde_yaml::from_str(content)?;
474 let variables = value
475 .get("variables")
476 .and_then(serde_yaml::Value::as_mapping)
477 .cloned()
478 .unwrap_or_default();
479 fn expand(value: &mut serde_yaml::Value, variables: &serde_yaml::Mapping) {
480 match value {
481 serde_yaml::Value::Mapping(map) => {
482 for child in map.values_mut() {
483 expand(child, variables);
484 }
485 }
486 serde_yaml::Value::Sequence(items) => {
487 for child in items {
488 expand(child, variables);
489 }
490 }
491 serde_yaml::Value::String(text) if text.starts_with("${") && text.ends_with('}') => {
492 let key = &text[2..text.len() - 1];
493 if let Some(replacement) = variables.get(serde_yaml::Value::String(key.to_string()))
494 {
495 *value = replacement.clone();
496 }
497 }
498 _ => {}
499 }
500 }
501 expand(&mut value, &variables);
502 serde_yaml::from_value(value).map_err(AnimationError::from)
503}
504
505pub fn validate_animation(spec: &AnimationSpec) -> Result<(), Vec<AnimationError>> {
506 let mut errors = Vec::new();
507 if spec.name.is_empty() {
508 errors.push(AnimationError::InvalidEffect(
509 "Animation name cannot be empty".to_string(),
510 ));
511 }
512 if spec.duration.is_none() {
513 errors.push(AnimationError::InvalidDuration(
514 "duration is required; add e.g. duration: 800ms".to_string(),
515 ));
516 } else if let Some(duration) = &spec.duration
517 && crate::config::parse_duration(duration).is_err()
518 {
519 errors.push(AnimationError::InvalidDuration(duration.clone()));
520 }
521 if let Some(timeline) = &spec.timeline {
522 for entry in timeline {
523 if crate::config::parse_duration(&entry.at).is_err() {
524 errors.push(AnimationError::InvalidTimeline(format!(
525 "invalid at: {}",
526 entry.at
527 )));
528 }
529 if let Some(duration) = &entry.duration
530 && crate::config::parse_duration(duration).is_err()
531 {
532 errors.push(AnimationError::InvalidTimeline(format!(
533 "invalid duration: {duration}"
534 )));
535 }
536 }
537 }
538 for (name, custom) in &spec.custom_effects {
539 if let Err(error) = crate::custom_effects::transpile(name, custom) {
540 errors.push(AnimationError::ShaderError(format!(
541 "custom effect {name}: {error}"
542 )));
543 }
544 }
545 if spec.effects.is_empty() && spec.timeline.is_none() {
546 errors.push(AnimationError::InvalidTimeline(
547 "Animation must contain at least one effect or timeline entry".to_string(),
548 ));
549 }
550 if errors.is_empty() {
551 Ok(())
552 } else {
553 Err(errors)
554 }
555}
556
557pub fn compute_effect_uniforms(effect: &Effect, progress: f32) -> EffectUniforms {
558 let progress = progress.clamp(0.0, 1.0);
559 let easing_index = |e: &Easing| match e {
560 Easing::Linear => 0,
561 Easing::EaseIn => 1,
562 Easing::EaseOut => 2,
563 Easing::EaseInOut => 3,
564 Easing::Emphatic => 4,
565 Easing::Spring => 5,
566 };
567 match effect {
568 Effect::Fade(params) => EffectUniforms {
569 effect_type: 0,
570 progress,
571 param_a: params.from,
572 param_b: params.to,
573 param_c: 0.0,
574 param_d: 0.0,
575 origin: [0.5, 0.5],
576 direction: [0.0, 0.0],
577 easing: easing_index(¶ms.easing),
578 },
579 Effect::Blur(params) => EffectUniforms {
580 effect_type: 1,
581 progress,
582 param_a: params.from,
583 param_b: params.to,
584 param_c: 0.0,
585 param_d: 0.0,
586 origin: [0.5, 0.5],
587 direction: [0.0, 0.0],
588 easing: easing_index(¶ms.easing),
589 },
590 Effect::Wipe(params) => {
591 let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
592 let rad = angle_deg.to_radians();
593 (
594 [rad.cos(), rad.sin()],
595 [0.5 + 0.5 * rad.cos(), 0.5 - 0.5 * rad.sin()],
596 )
597 } else {
598 match params.direction {
599 WipeDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
600 WipeDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
601 WipeDirection::Up => ([0.0, 1.0], [0.5, 0.0]),
602 WipeDirection::Down => ([0.0, -1.0], [0.5, 1.0]),
603 }
604 };
605 EffectUniforms {
606 effect_type: 2,
607 progress,
608 param_a: params.softness,
609 param_b: 0.0,
610 param_c: 0.0,
611 param_d: 0.0,
612 origin,
613 direction: dir_vec,
614 easing: easing_index(¶ms.easing),
615 }
616 }
617 Effect::Slide(params) => {
618 let (dir_vec, origin) = match params.direction {
619 SlideDirection::Left => ([-1.0, 0.0], [0.0, 0.5]),
620 SlideDirection::Right => ([1.0, 0.0], [1.0, 0.5]),
621 SlideDirection::Up => ([0.0, 1.0], [0.5, 0.0]),
622 SlideDirection::Down => ([0.0, -1.0], [0.5, 1.0]),
623 };
624 EffectUniforms {
625 effect_type: 3,
626 progress,
627 param_a: 0.0,
628 param_b: 0.0,
629 param_c: 0.0,
630 param_d: 0.0,
631 origin,
632 direction: dir_vec,
633 easing: easing_index(¶ms.easing),
634 }
635 }
636 Effect::Zoom(params) => {
637 let orig = match params.origin {
638 Origin::Center | Origin::Cursor => [0.5, 0.5],
639 Origin::Custom(x, y) => [x, y],
640 };
641 EffectUniforms {
642 effect_type: 4,
643 progress,
644 param_a: params.from,
645 param_b: params.to,
646 param_c: 0.0,
647 param_d: 0.0,
648 origin: orig,
649 direction: [0.0, 0.0],
650 easing: easing_index(¶ms.easing),
651 }
652 }
653 Effect::Pixelate(params) => EffectUniforms {
654 effect_type: 5,
655 progress,
656 param_a: params.from,
657 param_b: params.to,
658 param_c: 0.0,
659 param_d: 0.0,
660 origin: [0.5, 0.5],
661 direction: [0.0, 0.0],
662 easing: easing_index(¶ms.easing),
663 },
664 Effect::Ripple(params) => {
665 let orig = match params.origin {
666 Origin::Center | Origin::Cursor => [0.5, 0.5],
667 Origin::Custom(x, y) => [x, y],
668 };
669 EffectUniforms {
670 effect_type: 6,
671 progress,
672 param_a: params.frequency,
673 param_b: params.amplitude,
674 param_c: params.speed,
675 param_d: 0.0,
676 origin: orig,
677 direction: [0.0, 0.0],
678 easing: easing_index(¶ms.easing),
679 }
680 }
681 Effect::Dissolve(params) => EffectUniforms {
682 effect_type: 7,
683 progress,
684 param_a: params.scale,
685 param_b: params.softness,
686 param_c: 0.0,
687 param_d: 0.0,
688 origin: [0.5, 0.5],
689 direction: [0.0, 0.0],
690 easing: easing_index(¶ms.easing),
691 },
692 Effect::Wave(params) => {
693 let (dir_vec, origin) = if let Some(angle_deg) = params.angle {
694 let rad = angle_deg.to_radians();
695 (
696 [rad.cos(), rad.sin()],
697 [0.5 + 0.5 * rad.cos(), 0.5 - 0.5 * rad.sin()],
698 )
699 } else {
700 ([0.0, 0.0], [0.5, 0.5])
701 };
702 EffectUniforms {
703 effect_type: 9,
704 progress,
705 param_a: params.frequency,
706 param_b: params.amplitude,
707 param_c: 0.0,
708 param_d: 0.0,
709 origin,
710 direction: dir_vec,
711 easing: easing_index(¶ms.easing),
712 }
713 }
714 Effect::Grow(params) => {
715 let orig = match params.origin {
716 Origin::Center | Origin::Cursor => [0.5, 0.5],
717 Origin::Custom(x, y) => [x, y],
718 };
719 EffectUniforms {
720 effect_type: 10,
721 progress,
722 param_a: 0.0,
723 param_b: 0.0,
724 param_c: 0.0,
725 param_d: 0.0,
726 origin: orig,
727 direction: [0.0, 0.0],
728 easing: easing_index(¶ms.easing),
729 }
730 }
731 Effect::Outer(params) => {
732 let orig = match params.origin {
733 Origin::Center | Origin::Cursor => [0.5, 0.5],
734 Origin::Custom(x, y) => [x, y],
735 };
736 EffectUniforms {
737 effect_type: 11,
738 progress,
739 param_a: 0.0,
740 param_b: 0.0,
741 param_c: 0.0,
742 param_d: 0.0,
743 origin: orig,
744 direction: [0.0, 0.0],
745 easing: easing_index(¶ms.easing),
746 }
747 }
748 Effect::Shader(params) => EffectUniforms {
749 effect_type: 8,
750 progress,
751 param_a: params.uniforms.get("strength").copied().unwrap_or(0.0) as f32,
752 param_b: 0.0,
753 param_c: 0.0,
754 param_d: 0.0,
755 origin: [0.5, 0.5],
756 direction: [0.0, 0.0],
757 easing: 3,
758 },
759 }
760}
761
762pub fn effect_from_name(name: &str) -> Option<Effect> {
766 let seed = || {
767 std::time::SystemTime::now()
768 .duration_since(std::time::UNIX_EPOCH)
769 .unwrap_or_default()
770 .subsec_nanos()
771 };
772 Some(match name {
773 "simple" => Effect::Fade(FadeParams::default()),
774 "fade" => Effect::Fade(FadeParams::default()),
775 "blur" => Effect::Blur(BlurParams::default()),
776 "wipe" => Effect::Wipe(WipeParams::default()),
777 "slide" => Effect::Slide(SlideParams::default()),
778 "left" => Effect::Slide(SlideParams {
779 direction: SlideDirection::Left,
780 ..SlideParams::default()
781 }),
782 "right" => Effect::Slide(SlideParams {
783 direction: SlideDirection::Right,
784 ..SlideParams::default()
785 }),
786 "top" => Effect::Slide(SlideParams {
787 direction: SlideDirection::Up,
788 ..SlideParams::default()
789 }),
790 "bottom" => Effect::Slide(SlideParams {
791 direction: SlideDirection::Down,
792 ..SlideParams::default()
793 }),
794 "zoom" => Effect::Zoom(ZoomParams::default()),
795 "pixelate" => Effect::Pixelate(PixelateParams::default()),
796 "ripple" => Effect::Ripple(RippleParams::default()),
797 "dissolve" => Effect::Dissolve(DissolveParams::default()),
798 "wave" => Effect::Wave(WaveParams::default()),
799 "grow" => Effect::Grow(GrowParams::default()),
800 "center" => Effect::Grow(GrowParams::default()),
801 "outer" => Effect::Outer(OuterParams::default()),
802 "any" => {
803 let value = seed();
804 let origin = Origin::Custom(
805 (value % 1000) as f32 / 1000.0,
806 ((value / 1000) % 1000) as f32 / 1000.0,
807 );
808 if value % 2 == 0 {
809 Effect::Grow(GrowParams {
810 origin,
811 ..GrowParams::default()
812 })
813 } else {
814 Effect::Outer(OuterParams {
815 origin,
816 ..OuterParams::default()
817 })
818 }
819 }
820 "random" => match seed() % 5 {
821 0 => Effect::Fade(FadeParams::default()),
822 1 => Effect::Slide(SlideParams::default()),
823 2 => Effect::Wave(WaveParams::default()),
824 3 => Effect::Grow(GrowParams::default()),
825 _ => Effect::Outer(OuterParams::default()),
826 },
827 _ => return None,
828 })
829}
830
831pub fn effect_names() -> &'static [&'static str] {
833 &[
834 "simple", "fade", "blur", "wipe", "slide", "left", "right", "top", "bottom", "zoom",
835 "pixelate", "ripple", "dissolve", "wave", "grow", "center", "outer", "any", "random",
836 ]
837}
838
839#[derive(Debug, Clone, Default)]
841pub struct EffectOverrides {
842 pub origin: Option<(f32, f32)>,
843 pub origin_preset: Option<String>,
844 pub direction: Option<[f32; 2]>,
845 pub angle: Option<f32>,
846 pub easing: Option<Easing>,
847 pub from: Option<f32>,
848 pub to: Option<f32>,
849 pub frequency: Option<f32>,
850 pub amplitude: Option<f32>,
851 pub speed: Option<f32>,
852 pub softness: Option<f32>,
853 pub scale: Option<f32>,
854}
855
856fn origin_from_preset(preset: &str) -> (f32, f32) {
857 match preset {
858 "top_left" => (0.0, 0.0),
859 "top" => (0.5, 0.0),
860 "top_right" => (1.0, 0.0),
861 "left" => (0.0, 0.5),
862 "center" => (0.5, 0.5),
863 "right" => (1.0, 0.5),
864 "bottom_left" => (0.0, 1.0),
865 "bottom" => (0.5, 1.0),
866 "bottom_right" => (1.0, 1.0),
867 _ => (0.5, 0.5),
868 }
869}
870
871pub fn apply_effect_overrides(effect: &mut Effect, o: &EffectOverrides) {
873 let origin = o
874 .origin
875 .or_else(|| o.origin_preset.as_ref().map(|p| origin_from_preset(p)));
876
877 match effect {
878 Effect::Fade(p) => {
879 if let Some(v) = o.from {
880 p.from = v;
881 }
882 if let Some(v) = o.to {
883 p.to = v;
884 }
885 if let Some(e) = o.easing {
886 p.easing = e;
887 }
888 }
889 Effect::Blur(p) => {
890 if let Some(v) = o.from {
891 p.from = v;
892 }
893 if let Some(v) = o.to {
894 p.to = v;
895 }
896 if let Some(e) = o.easing {
897 p.easing = e;
898 }
899 }
900 Effect::Wipe(p) => {
901 if let Some(s) = o.softness {
902 p.softness = s;
903 }
904 if let Some(a) = o.angle {
905 p.angle = Some(a);
906 }
907 if let Some(e) = o.easing {
908 p.easing = e;
909 }
910 if let Some(d) = o.direction {
911 p.direction = match d {
912 [-1.0, 0.0] => WipeDirection::Left,
913 [1.0, 0.0] => WipeDirection::Right,
914 [0.0, 1.0] => WipeDirection::Up,
915 [0.0, -1.0] => WipeDirection::Down,
916 _ => p.direction,
917 };
918 }
919 }
920 Effect::Slide(p) => {
921 if let Some(e) = o.easing {
922 p.easing = e;
923 }
924 if let Some(d) = o.direction {
925 p.direction = match d {
926 [-1.0, 0.0] => SlideDirection::Left,
927 [1.0, 0.0] => SlideDirection::Right,
928 [0.0, 1.0] => SlideDirection::Up,
929 [0.0, -1.0] => SlideDirection::Down,
930 _ => p.direction,
931 };
932 }
933 }
934 Effect::Zoom(p) => {
935 if let Some(v) = o.from {
936 p.from = v;
937 }
938 if let Some(v) = o.to {
939 p.to = v;
940 }
941 if let Some((x, y)) = origin {
942 p.origin = Origin::Custom(x, y);
943 }
944 if let Some(e) = o.easing {
945 p.easing = e;
946 }
947 }
948 Effect::Pixelate(p) => {
949 if let Some(v) = o.from {
950 p.from = v;
951 }
952 if let Some(v) = o.to {
953 p.to = v;
954 }
955 if let Some(e) = o.easing {
956 p.easing = e;
957 }
958 }
959 Effect::Ripple(p) => {
960 if let Some(v) = o.frequency {
961 p.frequency = v;
962 }
963 if let Some(v) = o.amplitude {
964 p.amplitude = v;
965 }
966 if let Some(v) = o.speed {
967 p.speed = v;
968 }
969 if let Some((x, y)) = origin {
970 p.origin = Origin::Custom(x, y);
971 }
972 if let Some(e) = o.easing {
973 p.easing = e;
974 }
975 }
976 Effect::Dissolve(p) => {
977 if let Some(v) = o.scale {
978 p.scale = v;
979 }
980 if let Some(v) = o.softness {
981 p.softness = v;
982 }
983 if let Some(e) = o.easing {
984 p.easing = e;
985 }
986 }
987 Effect::Wave(p) => {
988 if let Some(v) = o.frequency {
989 p.frequency = v;
990 }
991 if let Some(v) = o.amplitude {
992 p.amplitude = v;
993 }
994 if let Some(a) = o.angle {
995 p.angle = Some(a);
996 }
997 if let Some(e) = o.easing {
998 p.easing = e;
999 }
1000 }
1001 Effect::Grow(p) => {
1002 if let Some((x, y)) = origin {
1003 p.origin = Origin::Custom(x, y);
1004 }
1005 if let Some(e) = o.easing {
1006 p.easing = e;
1007 }
1008 }
1009 Effect::Outer(p) => {
1010 if let Some((x, y)) = origin {
1011 p.origin = Origin::Custom(x, y);
1012 }
1013 if let Some(e) = o.easing {
1014 p.easing = e;
1015 }
1016 }
1017 Effect::Shader(_) => {}
1018 }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 #[test]
1026 fn package_duration_is_required() {
1027 let spec = AnimationSpec {
1028 name: "missing-duration".into(),
1029 effects: vec![Effect::Fade(FadeParams::default())],
1030 ..Default::default()
1031 };
1032 assert!(validate_animation(&spec).is_err());
1033 }
1034
1035 #[test]
1036 fn numeric_variables_are_expanded_before_deserialization() {
1037 let spec = parse_animation_yaml("name: vars\nduration: 1s\nvariables: {amount: 12}\neffects:\n - blur: {from: \"${amount}\", to: 0}\n").expect("variable package should parse");
1038 match &spec.effects[0] {
1039 Effect::Blur(params) => assert_eq!(params.from, 12.0),
1040 _ => panic!("expected blur"),
1041 }
1042 }
1043
1044 #[test]
1045 fn ranged_effects_keep_both_endpoints_for_the_shader() {
1046 let fade = Effect::Fade(FadeParams {
1047 from: 0.2,
1048 to: 0.9,
1049 easing: Easing::EaseOut,
1050 });
1051 let start = compute_effect_uniforms(&fade, 0.0);
1052 let end = compute_effect_uniforms(&fade, 1.0);
1053 assert_eq!((start.param_a, start.param_b), (0.2, 0.9));
1054 assert_eq!((end.param_a, end.param_b), (0.2, 0.9));
1055 }
1056
1057 #[test]
1058 fn awww_direction_aliases_resolve_to_typed_effects() {
1059 assert!(matches!(effect_from_name("simple"), Some(Effect::Fade(_))));
1060 assert!(matches!(effect_from_name("left"), Some(Effect::Slide(_))));
1061 assert!(matches!(effect_from_name("right"), Some(Effect::Slide(_))));
1062 assert!(matches!(effect_from_name("center"), Some(Effect::Grow(_))));
1063 assert!(matches!(
1064 effect_from_name("any"),
1065 Some(Effect::Grow(_)) | Some(Effect::Outer(_))
1066 ));
1067 assert!(effect_from_name("random").is_some());
1068 }
1069}