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