1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::Path as SkiaPath;
4
5use super::animation::{
6 Animation, AnimationPreset, ChromaticAberrationConfig, EasingType, PresetConfig, SpringConfig,
7};
8use super::style::{FontWeight, TextAlign, VerticalAlign};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
20#[serde(tag = "name", rename_all = "snake_case")]
21pub enum AnimationEffect {
22 FadeIn(AnimationTiming),
23 FadeInUp(AnimationTiming),
24 FadeInDown(AnimationTiming),
25 FadeInLeft(AnimationTiming),
26 FadeInRight(AnimationTiming),
27 SlideInLeft(AnimationTiming),
28 SlideInRight(AnimationTiming),
29 SlideInUp(AnimationTiming),
30 SlideInDown(AnimationTiming),
31 ScaleIn(AnimationTiming),
32 BounceIn(AnimationTiming),
33 BlurIn(AnimationTiming),
34 RotateIn(AnimationTiming),
35 ElasticIn(AnimationTiming),
36 PopIn(AnimationTiming),
42 FadeOut(AnimationTiming),
43 FadeOutUp(AnimationTiming),
44 FadeOutDown(AnimationTiming),
45 SlideOutLeft(AnimationTiming),
46 SlideOutRight(AnimationTiming),
47 SlideOutUp(AnimationTiming),
48 SlideOutDown(AnimationTiming),
49 ScaleOut(AnimationTiming),
50 BounceOut(AnimationTiming),
51 BlurOut(AnimationTiming),
52 RotateOut(AnimationTiming),
53 Pulse(AnimationTiming),
54 Float(AnimationTiming),
55 Shake(AnimationTiming),
56 Spin(AnimationTiming),
57 FlipInX(AnimationTiming),
58 FlipInY(AnimationTiming),
59 FlipOutX(AnimationTiming),
60 FlipOutY(AnimationTiming),
61 TiltIn(TiltInConfig),
62 DrawIn(AnimationTiming),
63 StrokeReveal(AnimationTiming),
64 Typewriter(AnimationTiming),
65 WipeLeft(AnimationTiming),
66 WipeRight(AnimationTiming),
67 #[serde(alias = "float_3d")]
68 Float3d(AnimationTiming),
69 CharScaleIn(CharAnimationTiming),
70 CharFadeIn(CharAnimationTiming),
71 CharWave(CharAnimationTiming),
72 CharBounce(CharAnimationTiming),
73 CharRotateIn(CharAnimationTiming),
74 CharSlideUp(CharAnimationTiming),
75 CharBlurIn(CharAnimationTiming),
89 Glow(GlowConfig),
90 Shimmer(ShimmerConfig),
93 Wiggle(WiggleConfig),
94 Orbit(OrbitConfig),
95 Keyframes(KeyframesConfig),
96 MotionBlur(MotionBlurConfig),
97 Trail(TrailConfig),
100 MotionPath(MotionPathConfig),
106 ChromaticAberration(ChromaticAberrationConfig),
110 Shatter(ShatterConfig),
114 Burst(BurstConfig),
117}
118
119impl AnimationEffect {
120 pub fn shift_delay(&mut self, by: f64) {
121 use AnimationEffect::*;
122 match self {
123 FadeIn(t) | FadeInUp(t) | FadeInDown(t) | FadeInLeft(t) | FadeInRight(t)
124 | SlideInLeft(t) | SlideInRight(t) | SlideInUp(t) | SlideInDown(t) | ScaleIn(t)
125 | BounceIn(t) | BlurIn(t) | RotateIn(t) | ElasticIn(t) | PopIn(t) | FadeOut(t)
126 | FadeOutUp(t) | FadeOutDown(t) | SlideOutLeft(t) | SlideOutRight(t)
127 | SlideOutUp(t) | SlideOutDown(t) | ScaleOut(t) | BounceOut(t) | BlurOut(t)
128 | RotateOut(t) | Pulse(t) | Float(t) | Shake(t) | Spin(t) | FlipInX(t) | FlipInY(t)
129 | FlipOutX(t) | FlipOutY(t) | DrawIn(t) | StrokeReveal(t) | Typewriter(t)
130 | WipeLeft(t) | WipeRight(t) | Float3d(t) => t.delay += by,
131 TiltIn(c) => c.delay += by,
132 CharScaleIn(c) | CharFadeIn(c) | CharWave(c) | CharBounce(c) | CharRotateIn(c)
133 | CharSlideUp(c) | CharBlurIn(c) => c.delay += by,
134 Keyframes(c) => c.delay += by,
135 MotionPath(c) => c.delay += by,
136 Shimmer(c) => c.delay += by,
137 ChromaticAberration(c) => c.delay += by,
138 Shatter(c) => c.delay += by,
139 Burst(c) => c.delay += by,
140 Glow(_) | Wiggle(_) | Orbit(_) | MotionBlur(_) | Trail(_) => {}
141 }
142 }
143
144 pub fn as_preset(&self) -> Option<(AnimationPreset, &AnimationTiming)> {
145 match self {
146 Self::FadeIn(t) => Some((AnimationPreset::FadeIn, t)),
147 Self::FadeInUp(t) => Some((AnimationPreset::FadeInUp, t)),
148 Self::FadeInDown(t) => Some((AnimationPreset::FadeInDown, t)),
149 Self::FadeInLeft(t) => Some((AnimationPreset::FadeInLeft, t)),
150 Self::FadeInRight(t) => Some((AnimationPreset::FadeInRight, t)),
151 Self::SlideInLeft(t) => Some((AnimationPreset::SlideInLeft, t)),
152 Self::SlideInRight(t) => Some((AnimationPreset::SlideInRight, t)),
153 Self::SlideInUp(t) => Some((AnimationPreset::SlideInUp, t)),
154 Self::SlideInDown(t) => Some((AnimationPreset::SlideInDown, t)),
155 Self::ScaleIn(t) => Some((AnimationPreset::ScaleIn, t)),
156 Self::BounceIn(t) => Some((AnimationPreset::BounceIn, t)),
157 Self::BlurIn(t) => Some((AnimationPreset::BlurIn, t)),
158 Self::RotateIn(t) => Some((AnimationPreset::RotateIn, t)),
159 Self::ElasticIn(t) => Some((AnimationPreset::ElasticIn, t)),
160 Self::PopIn(t) => Some((AnimationPreset::PopIn, t)),
161 Self::FadeOut(t) => Some((AnimationPreset::FadeOut, t)),
162 Self::FadeOutUp(t) => Some((AnimationPreset::FadeOutUp, t)),
163 Self::FadeOutDown(t) => Some((AnimationPreset::FadeOutDown, t)),
164 Self::SlideOutLeft(t) => Some((AnimationPreset::SlideOutLeft, t)),
165 Self::SlideOutRight(t) => Some((AnimationPreset::SlideOutRight, t)),
166 Self::SlideOutUp(t) => Some((AnimationPreset::SlideOutUp, t)),
167 Self::SlideOutDown(t) => Some((AnimationPreset::SlideOutDown, t)),
168 Self::ScaleOut(t) => Some((AnimationPreset::ScaleOut, t)),
169 Self::BounceOut(t) => Some((AnimationPreset::BounceOut, t)),
170 Self::BlurOut(t) => Some((AnimationPreset::BlurOut, t)),
171 Self::RotateOut(t) => Some((AnimationPreset::RotateOut, t)),
172 Self::Pulse(t) => Some((AnimationPreset::Pulse, t)),
173 Self::Float(t) => Some((AnimationPreset::Float, t)),
174 Self::Shake(t) => Some((AnimationPreset::Shake, t)),
175 Self::Spin(t) => Some((AnimationPreset::Spin, t)),
176 Self::FlipInX(t) => Some((AnimationPreset::FlipInX, t)),
177 Self::FlipInY(t) => Some((AnimationPreset::FlipInY, t)),
178 Self::FlipOutX(t) => Some((AnimationPreset::FlipOutX, t)),
179 Self::FlipOutY(t) => Some((AnimationPreset::FlipOutY, t)),
180 Self::TiltIn(_) => None,
181 Self::DrawIn(t) => Some((AnimationPreset::DrawIn, t)),
182 Self::StrokeReveal(t) => Some((AnimationPreset::StrokeReveal, t)),
183 Self::Float3d(t) => Some((AnimationPreset::Float3d, t)),
184 Self::Typewriter(t) => Some((AnimationPreset::Typewriter, t)),
185 Self::WipeLeft(t) => Some((AnimationPreset::WipeLeft, t)),
186 Self::WipeRight(t) => Some((AnimationPreset::WipeRight, t)),
187 _ => None,
188 }
189 }
190}
191
192#[derive(Debug, Clone, JsonSchema, PartialEq)]
194pub struct AnimationTiming {
195 #[serde(default)]
197 pub delay: f64,
198 #[serde(default = "default_animation_duration")]
200 pub duration: f64,
201 #[serde(default, rename = "loop")]
211 #[schemars(with = "RepeatSpec")]
212 pub repeat: bool,
213 #[serde(default, skip)]
224 #[schemars(skip)]
225 pub repeat_count: Option<u32>,
226 #[serde(default)]
233 pub yoyo: bool,
234 #[serde(default)]
238 pub repeat_delay: f64,
239 #[serde(default)]
241 pub overshoot: Option<f64>,
242 #[serde(default)]
247 pub spring: Option<SpringConfig>,
248 #[serde(default)]
254 pub amplitude: Option<f64>,
255}
256
257fn default_animation_duration() -> f64 {
258 0.8
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
265#[serde(untagged)]
266enum RepeatSpec {
267 Loop(bool),
268 Count(u32),
269}
270
271impl Default for RepeatSpec {
272 fn default() -> Self {
273 RepeatSpec::Loop(false)
274 }
275}
276
277impl RepeatSpec {
278 fn into_parts(self) -> (bool, Option<u32>) {
279 match self {
280 RepeatSpec::Loop(b) => (b, None),
281 RepeatSpec::Count(0) | RepeatSpec::Count(1) => (false, None),
282 RepeatSpec::Count(n) => (true, Some(n)),
283 }
284 }
285
286 fn from_parts(repeat: bool, repeat_count: Option<u32>) -> Self {
287 match repeat_count {
288 Some(n) => RepeatSpec::Count(n),
289 None => RepeatSpec::Loop(repeat),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(deny_unknown_fields)]
296struct AnimationTimingWire {
297 #[serde(default)]
298 delay: f64,
299 #[serde(default = "default_animation_duration")]
300 duration: f64,
301 #[serde(default, rename = "loop")]
302 repeat: RepeatSpec,
303 #[serde(default)]
304 yoyo: bool,
305 #[serde(default)]
306 repeat_delay: f64,
307 #[serde(default)]
308 overshoot: Option<f64>,
309 #[serde(default)]
310 spring: Option<SpringConfig>,
311 #[serde(default)]
312 amplitude: Option<f64>,
313}
314
315impl From<AnimationTimingWire> for AnimationTiming {
316 fn from(wire: AnimationTimingWire) -> Self {
317 let (repeat, repeat_count) = wire.repeat.into_parts();
318 AnimationTiming {
319 delay: wire.delay,
320 duration: wire.duration,
321 repeat,
322 repeat_count,
323 yoyo: wire.yoyo,
324 repeat_delay: wire.repeat_delay,
325 overshoot: wire.overshoot,
326 spring: wire.spring,
327 amplitude: wire.amplitude,
328 }
329 }
330}
331
332impl From<&AnimationTiming> for AnimationTimingWire {
333 fn from(t: &AnimationTiming) -> Self {
334 AnimationTimingWire {
335 delay: t.delay,
336 duration: t.duration,
337 repeat: RepeatSpec::from_parts(t.repeat, t.repeat_count),
338 yoyo: t.yoyo,
339 repeat_delay: t.repeat_delay,
340 overshoot: t.overshoot,
341 spring: t.spring.clone(),
342 amplitude: t.amplitude,
343 }
344 }
345}
346
347impl<'de> Deserialize<'de> for AnimationTiming {
348 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
349 where
350 D: serde::Deserializer<'de>,
351 {
352 AnimationTimingWire::deserialize(deserializer).map(AnimationTiming::from)
353 }
354}
355
356impl Serialize for AnimationTiming {
357 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
358 where
359 S: serde::Serializer,
360 {
361 AnimationTimingWire::from(self).serialize(serializer)
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
367#[serde(deny_unknown_fields)]
368pub struct TiltInConfig {
369 #[serde(default)]
370 pub delay: f64,
371 #[serde(default = "default_animation_duration")]
372 pub duration: f64,
373 #[serde(default, rename = "loop")]
374 pub repeat: bool,
375 #[serde(default)]
377 pub rotate_x: Option<f64>,
378 #[serde(default)]
380 pub rotate_y: Option<f64>,
381 #[serde(default)]
383 pub perspective: Option<f64>,
384 #[serde(default)]
386 pub scale_from: Option<f64>,
387}
388
389impl Default for AnimationTiming {
390 fn default() -> Self {
391 Self {
392 delay: 0.0,
393 duration: 0.8,
394 repeat: false,
395 repeat_count: None,
396 yoyo: false,
397 repeat_delay: 0.0,
398 overshoot: None,
399 spring: None,
400 amplitude: None,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
407#[serde(deny_unknown_fields)]
408pub struct CharAnimationTiming {
409 #[serde(default)]
411 pub delay: f64,
412 #[serde(default = "default_char_duration_f64")]
414 pub duration: f64,
415 #[serde(default = "default_char_stagger_f64")]
417 pub stagger: f64,
418 #[serde(default)]
420 pub granularity: TextAnimGranularity,
421 #[serde(default)]
423 pub easing: EasingType,
424 #[serde(default)]
426 pub overshoot: Option<f64>,
427 #[serde(default)]
434 pub blur: Option<f64>,
435 #[serde(default)]
439 pub direction: TextAnimDirection,
440 #[serde(default)]
444 pub distance: Option<f64>,
445 #[serde(default)]
452 pub scale_from: Option<f64>,
453 #[serde(default)]
463 pub jitter: Option<f64>,
464 #[serde(default)]
467 pub seed: Option<u32>,
468 #[serde(default)]
472 pub ink_from: Option<String>,
473 #[serde(default)]
480 pub rotate_from: Option<f64>,
481 #[serde(default)]
484 pub rotate_origin: RotateOrigin,
485 #[serde(default)]
492 pub scale_jitter: Option<f64>,
493 #[serde(default)]
500 pub baseline_jitter: Option<f64>,
501 #[serde(default)]
511 pub reflow: bool,
512 #[serde(default)]
516 pub reflow_easing: EasingType,
517}
518
519impl Default for CharAnimationTiming {
520 fn default() -> Self {
521 Self {
522 delay: 0.0,
523 duration: default_char_duration_f64(),
524 stagger: default_char_stagger_f64(),
525 granularity: TextAnimGranularity::default(),
526 easing: EasingType::default(),
527 overshoot: None,
528 blur: None,
529 direction: TextAnimDirection::default(),
530 distance: None,
531 scale_from: None,
532 jitter: None,
533 seed: None,
534 ink_from: None,
535 rotate_from: None,
536 rotate_origin: RotateOrigin::default(),
537 scale_jitter: None,
538 baseline_jitter: None,
539 reflow: false,
540 reflow_easing: EasingType::default(),
541 }
542 }
543}
544
545fn default_char_stagger_f64() -> f64 {
546 0.03
547}
548
549fn default_char_duration_f64() -> f64 {
550 0.4
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
555pub struct CharAnimation {
556 #[serde(default = "default_char_preset")]
558 pub preset: CharAnimPreset,
559 #[serde(default)]
561 pub granularity: TextAnimGranularity,
562 #[serde(default = "default_char_stagger")]
564 pub stagger: f32,
565 #[serde(default = "default_char_duration")]
567 pub duration: f32,
568 #[serde(default)]
570 pub easing: EasingType,
571 #[serde(default)]
573 pub delay: f32,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
578#[serde(rename_all = "snake_case")]
579#[derive(PartialEq)]
580pub enum TextAnimGranularity {
581 #[default]
583 Char,
584 Word,
586}
587
588#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
599#[serde(rename_all = "snake_case")]
600pub enum TextAnimDirection {
601 #[default]
603 Up,
604 Down,
606 Left,
608 Right,
610}
611
612impl TextAnimDirection {
613 pub fn offset(self, travel: f32) -> (f32, f32) {
614 match self {
615 Self::Up => (0.0, travel),
616 Self::Down => (0.0, -travel),
617 Self::Left => (travel, 0.0),
618 Self::Right => (-travel, 0.0),
619 }
620 }
621}
622
623#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
626#[serde(rename_all = "snake_case")]
627pub enum RotateOrigin {
628 #[default]
631 Center,
632 Edge,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
640#[serde(rename_all = "snake_case")]
641#[derive(PartialEq)]
642pub enum CharAnimPreset {
643 #[default]
645 ScaleIn,
646 FadeIn,
648 Wave,
650 Bounce,
652 RotateIn,
654 SlideUp,
656 BlurIn,
660}
661
662fn default_char_preset() -> CharAnimPreset {
663 CharAnimPreset::ScaleIn
664}
665
666fn default_char_stagger() -> f32 {
667 0.03
668}
669
670fn default_char_duration() -> f32 {
671 0.4
672}
673
674impl AnimationTiming {
675 pub fn to_preset_config(&self) -> PresetConfig {
676 PresetConfig {
677 amplitude: self.amplitude,
678 delay: self.delay,
679 duration: self.duration,
680 repeat: self.repeat,
681 repeat_count: self.repeat_count,
682 yoyo: self.yoyo,
683 repeat_delay: self.repeat_delay,
684 overshoot: self.overshoot,
685 spring: self.spring.clone(),
686 }
687 }
688}
689
690const KNOWN_MOTION_PROPERTIES: &[&str] = &[
691 "opacity",
692 "position.x",
693 "translate_x",
694 "position.y",
695 "translate_y",
696 "scale",
697 "scale.x",
698 "scale.y",
699 "rotation",
700 "rotate_x",
701 "rotate_y",
702 "blur",
703 "blur_x",
704 "blur_y",
705 "visible_chars",
706 "visible_chars_progress",
707 "border_radius",
708 "font_size",
709 "width",
710 "height",
711 "gap",
712 "padding",
713 "stroke_width",
714 "shadow_blur",
715 "glow_radius",
716 "glow_intensity",
717 "perspective",
718 "draw_progress",
719 "motion_progress",
720 "color",
721 "clip_path_progress",
722 "letter_spacing",
723 "draw_start",
724 "draw_offset",
725];
726
727fn validate_motion_property<E: serde::de::Error>(value: &str) -> Result<(), E> {
728 if KNOWN_MOTION_PROPERTIES.contains(&value) {
729 return Ok(());
730 }
731 let normalize = |s: &str| s.replace(['-', ' '], "_").to_lowercase();
732 let normalized = normalize(value);
733 if let Some(suggestion) = KNOWN_MOTION_PROPERTIES
734 .iter()
735 .find(|known| normalize(known) == normalized)
736 {
737 Err(E::custom(format!(
738 "unknown animation property '{value}' — did you mean '{suggestion}'?"
739 )))
740 } else {
741 Err(E::custom(format!(
742 "unknown animation property '{value}': expected one of {}",
743 KNOWN_MOTION_PROPERTIES.join(", ")
744 )))
745 }
746}
747
748fn deserialize_motion_property<'de, D>(deserializer: D) -> Result<String, D::Error>
749where
750 D: serde::Deserializer<'de>,
751{
752 let s = String::deserialize(deserializer)?;
753 validate_motion_property::<D::Error>(&s)?;
754 Ok(s)
755}
756
757fn deserialize_validated_keyframes<'de, D>(deserializer: D) -> Result<Vec<Animation>, D::Error>
758where
759 D: serde::Deserializer<'de>,
760{
761 let animations = Vec::<Animation>::deserialize(deserializer)?;
762 for anim in &animations {
763 validate_motion_property::<D::Error>(&anim.property)?;
764 }
765 Ok(animations)
766}
767
768#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
770#[serde(deny_unknown_fields)]
771pub struct KeyframesConfig {
772 #[serde(deserialize_with = "deserialize_validated_keyframes")]
773 pub keyframes: Vec<Animation>,
774 #[serde(default)]
775 pub delay: f64,
776 #[serde(default = "default_animation_duration")]
777 pub duration: f64,
778 #[serde(default, rename = "loop")]
779 pub repeat: bool,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
788#[serde(deny_unknown_fields)]
789pub struct SvgPathMorph {
790 pub target: String,
794 pub keyframes: Vec<SvgPathMorphKeyframe>,
796 #[serde(default)]
798 pub easing: EasingType,
799 #[serde(default)]
802 pub repeat: bool,
803 #[serde(default)]
806 pub yoyo: bool,
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
811#[serde(deny_unknown_fields)]
812pub struct SvgPathMorphKeyframe {
813 pub time: f64,
816 pub value: String,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
837#[serde(deny_unknown_fields)]
838pub struct MotionBlurConfig {
839 #[serde(default)]
843 pub intensity: f32,
844 #[serde(default = "default_motion_blur_samples")]
848 pub samples: u32,
849 #[serde(default = "default_motion_blur_shutter")]
853 pub shutter: f64,
854 #[serde(default)]
857 pub mode: MotionBlurMode,
858}
859
860#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
862#[serde(rename_all = "snake_case")]
863pub enum MotionBlurMode {
864 #[default]
866 Stack,
867 Smear,
870}
871
872fn default_motion_blur_samples() -> u32 {
873 6
874}
875fn default_motion_blur_shutter() -> f64 {
876 0.5
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
886#[serde(deny_unknown_fields)]
887pub struct TrailConfig {
888 #[serde(default = "default_trail_copies")]
890 pub copies: u32,
891 #[serde(default = "default_trail_spacing")]
893 pub spacing: f64,
894 #[serde(default = "default_trail_falloff")]
897 pub falloff: f32,
898}
899
900fn default_trail_copies() -> u32 {
901 4
902}
903fn default_trail_spacing() -> f64 {
904 0.05
905}
906fn default_trail_falloff() -> f32 {
907 0.6
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
913#[serde(deny_unknown_fields)]
914pub struct OrbitConfig {
915 #[serde(default = "default_orbit_radius")]
917 pub radius_x: f64,
918 #[serde(default = "default_orbit_radius")]
920 pub radius_y: f64,
921 #[serde(default = "default_orbit_speed")]
923 pub speed: f64,
924 #[serde(default)]
926 pub start_angle: f64,
927 #[serde(default = "default_orbit_depth")]
929 pub depth: f64,
930 #[serde(default)]
932 pub opacity_depth: f64,
933 #[serde(default)]
935 pub tilt: f64,
936 #[serde(default)]
938 pub phase: f64,
939}
940
941fn default_orbit_radius() -> f64 {
942 30.0
943}
944fn default_orbit_speed() -> f64 {
945 0.5
946}
947fn default_orbit_depth() -> f64 {
948 0.15
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
952#[serde(deny_unknown_fields)]
953pub struct WiggleConfig {
954 #[serde(deserialize_with = "deserialize_motion_property")]
955 pub property: String,
956 pub amplitude: f64,
957 pub frequency: f64,
958 #[serde(default)]
959 pub seed: u64,
960 #[serde(default)]
962 pub octaves: Option<u32>,
963 #[serde(default)]
965 pub phase: Option<f64>,
966 #[serde(default)]
968 pub decay: Option<f64>,
969 #[serde(default)]
971 pub easing: Option<EasingType>,
972 #[serde(default)]
974 pub mode: Option<String>,
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1024#[serde(deny_unknown_fields)]
1025pub struct MotionPathConfig {
1026 #[serde(deserialize_with = "deserialize_motion_path_data")]
1030 pub path: String,
1031 #[serde(default)]
1036 pub delay: f64,
1037 #[serde(default = "default_animation_duration")]
1039 pub duration: f64,
1040 #[serde(default, rename = "loop")]
1043 pub repeat: bool,
1044 #[serde(default)]
1048 pub orient: bool,
1049 #[serde(default)]
1053 pub orient_offset: f64,
1054 #[serde(default)]
1058 pub easing: EasingType,
1059}
1060
1061fn deserialize_motion_path_data<'de, D>(deserializer: D) -> Result<String, D::Error>
1062where
1063 D: serde::Deserializer<'de>,
1064{
1065 let s = String::deserialize(deserializer)?;
1066 match SkiaPath::from_svg(&s) {
1067 Some(path) if path.count_points() > 0 => Ok(s),
1068 Some(_) => Err(serde::de::Error::custom(format!(
1069 "motion_path.path '{s}' has no drawable point (an empty path has nothing to travel \
1070 to) — provide at least one command, e.g. \"M0,0 L100,0\""
1071 ))),
1072 None => Err(serde::de::Error::custom(format!(
1073 "motion_path.path '{s}' is not valid SVG path data (the same 'd'-attribute \
1074 mini-language shape's type: \"path\" accepts), e.g. \"M0,0 C50,-100 150,-100 200,0\""
1075 ))),
1076 }
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1106#[serde(deny_unknown_fields)]
1107pub struct ShatterConfig {
1108 #[serde(default)]
1110 pub delay: f64,
1111 #[serde(default = "default_shatter_duration")]
1114 pub duration: f64,
1115 #[serde(default)]
1118 pub mode: ShatterMode,
1119 #[serde(default = "default_shatter_pieces")]
1121 pub pieces: u32,
1122 #[serde(default)]
1125 pub seed: u32,
1126 #[serde(default)]
1130 pub origin: ShatterOrigin,
1131 #[serde(default = "default_shatter_spread")]
1135 pub spread: f32,
1136 #[serde(default = "default_shatter_spin")]
1140 pub spin: f32,
1141 #[serde(default = "default_shatter_depth")]
1146 pub depth: f32,
1147 #[serde(default = "default_true")]
1151 pub fade: bool,
1152}
1153
1154fn default_shatter_duration() -> f64 {
1155 0.6
1156}
1157fn default_shatter_pieces() -> u32 {
1158 24
1159}
1160fn default_shatter_spread() -> f32 {
1161 1.0
1162}
1163fn default_shatter_spin() -> f32 {
1164 90.0
1165}
1166fn default_shatter_depth() -> f32 {
1167 0.4
1168}
1169fn default_true() -> bool {
1170 true
1171}
1172
1173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1176#[serde(rename_all = "snake_case")]
1177pub enum ShatterMode {
1178 #[default]
1181 Out,
1182 In,
1186 Hold,
1189}
1190
1191#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
1195#[serde(deny_unknown_fields)]
1196pub struct ShatterOrigin {
1197 #[serde(default = "default_shatter_origin_component")]
1198 pub x: f32,
1199 #[serde(default = "default_shatter_origin_component")]
1200 pub y: f32,
1201}
1202
1203impl Default for ShatterOrigin {
1204 fn default() -> Self {
1205 Self {
1206 x: default_shatter_origin_component(),
1207 y: default_shatter_origin_component(),
1208 }
1209 }
1210}
1211
1212fn default_shatter_origin_component() -> f32 {
1213 0.5
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1230#[serde(deny_unknown_fields)]
1231pub struct BurstConfig {
1232 #[serde(default)]
1234 pub delay: f64,
1235 #[serde(default = "default_burst_duration")]
1238 pub duration: f64,
1239 #[serde(default = "default_burst_count")]
1241 pub count: u32,
1242 #[serde(default = "default_burst_length")]
1245 pub length: f32,
1246 #[serde(default = "default_burst_gap")]
1249 pub gap: f32,
1250 #[serde(default = "default_burst_width")]
1252 pub width: f32,
1253 #[serde(default = "default_burst_color")]
1255 pub color: String,
1256 #[serde(default)]
1258 pub seed: u32,
1259 #[serde(default = "default_burst_jitter")]
1264 pub jitter: f32,
1265}
1266
1267fn default_burst_duration() -> f64 {
1268 0.4
1269}
1270fn default_burst_count() -> u32 {
1271 8
1272}
1273fn default_burst_length() -> f32 {
1274 40.0
1275}
1276fn default_burst_gap() -> f32 {
1277 12.0
1278}
1279fn default_burst_width() -> f32 {
1280 4.0
1281}
1282fn default_burst_color() -> String {
1283 "#FFB020".to_string()
1284}
1285fn default_burst_jitter() -> f32 {
1286 0.2
1287}
1288
1289#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1290#[serde(rename_all = "snake_case")]
1291pub enum ShapeType {
1292 Rect,
1293 Circle,
1294 RoundedRect,
1295 Ellipse,
1296 Triangle,
1297 Star {
1298 #[serde(default = "default_star_points")]
1299 points: u32,
1300 },
1301 Polygon {
1302 #[serde(default = "default_polygon_sides")]
1303 sides: u32,
1304 },
1305 Path {
1306 data: String,
1307 },
1308}
1309
1310#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1311#[serde(untagged)]
1312pub enum Fill {
1313 Solid(String),
1314 Gradient(Gradient),
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1318pub struct Gradient {
1319 #[serde(rename = "type")]
1320 pub gradient_type: GradientType,
1321 pub colors: Vec<String>,
1322 #[serde(default)]
1323 pub stops: Option<Vec<f32>>,
1324 #[serde(default)]
1325 pub angle: Option<f32>,
1326}
1327
1328#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1329#[serde(rename_all = "snake_case")]
1330pub enum GradientType {
1331 Linear,
1332 Radial,
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1336pub struct Stroke {
1337 pub color: String,
1338 #[serde(default = "default_stroke_width")]
1339 pub width: f32,
1340 #[serde(default)]
1346 pub dashed: Option<Vec<f32>>,
1347 #[serde(default)]
1358 pub dash_offset: Option<crate::expr::Computed<f32>>,
1359 #[serde(default)]
1363 pub line_cap: LineCap,
1364 #[serde(default)]
1367 pub line_join: LineJoin,
1368}
1369
1370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1373#[serde(rename_all = "snake_case")]
1374pub enum LineCap {
1375 #[default]
1376 Butt,
1377 Round,
1378 Square,
1379}
1380
1381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1384#[serde(rename_all = "snake_case")]
1385pub enum LineJoin {
1386 #[default]
1387 Miter,
1388 Round,
1389 Bevel,
1390}
1391
1392#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1393#[serde(rename_all = "snake_case")]
1394#[derive(Default)]
1395pub enum ImageFit {
1396 Cover,
1397 #[default]
1398 Contain,
1399 Fill,
1400}
1401
1402#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1403pub struct ShapeText {
1404 pub content: String,
1405 #[serde(default = "default_font_size")]
1406 pub font_size: f32,
1407 #[serde(default = "default_color")]
1408 pub color: String,
1409 #[serde(default = "default_font_family")]
1410 pub font_family: String,
1411 #[serde(default)]
1412 pub font_weight: FontWeight,
1413 #[serde(default)]
1414 pub align: TextAlign,
1415 #[serde(default)]
1416 pub vertical_align: VerticalAlign,
1417 #[serde(default)]
1418 pub line_height: Option<f32>,
1419 #[serde(default)]
1420 pub letter_spacing: Option<f32>,
1421 #[serde(default)]
1422 pub padding: Option<f32>,
1423}
1424
1425#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1426pub struct CaptionWord {
1427 pub text: String,
1428 pub start: f64,
1429 pub end: f64,
1430}
1431
1432#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1433#[serde(rename_all = "snake_case")]
1434#[derive(Default)]
1435pub enum CaptionStyle {
1436 #[default]
1437 Highlight,
1438 Karaoke,
1439 WordByWord,
1440 WordPop,
1443 KaraokePop,
1446}
1447
1448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1449pub struct GradientBorder {
1450 pub colors: Vec<String>,
1451 #[serde(default = "default_gradient_border_width")]
1452 pub width: f32,
1453 #[serde(default)]
1454 pub angle: f32,
1455}
1456
1457fn default_gradient_border_width() -> f32 {
1458 2.0
1459}
1460
1461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1463pub struct InnerShadow {
1464 pub color: String,
1465 #[serde(default)]
1466 pub offset_x: f32,
1467 #[serde(default)]
1468 pub offset_y: f32,
1469 #[serde(default = "default_inner_shadow_blur")]
1470 pub blur: f32,
1471}
1472
1473fn default_inner_shadow_blur() -> f32 {
1474 10.0
1475}
1476
1477#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1478pub struct TextShadow {
1479 #[serde(default = "default_shadow_color")]
1480 pub color: String,
1481 #[serde(default = "default_shadow_offset")]
1482 pub offset_x: f32,
1483 #[serde(default = "default_shadow_offset")]
1484 pub offset_y: f32,
1485 #[serde(default = "default_shadow_blur")]
1486 pub blur: f32,
1487}
1488
1489#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1490pub struct TextBackground {
1491 pub color: String,
1492 #[serde(default = "default_text_bg_padding")]
1493 pub padding: f32,
1494 #[serde(default)]
1495 pub corner_radius: f32,
1496}
1497
1498#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1500#[serde(deny_unknown_fields)]
1501pub struct GlowConfig {
1502 #[serde(default = "default_glow_color")]
1504 pub color: String,
1505 #[serde(default = "default_glow_radius")]
1507 pub radius: f32,
1508 #[serde(default = "default_glow_intensity")]
1510 pub intensity: f32,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1515#[serde(deny_unknown_fields)]
1516pub struct TextState {
1517 pub at: f64,
1519 pub content: String,
1521}
1522
1523#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1530#[serde(deny_unknown_fields)]
1531pub struct TextSwapConfig {
1532 #[serde(default = "default_swap_duration")]
1534 pub duration: f64,
1535 #[serde(default = "default_swap_distance")]
1537 pub distance: f32,
1538 #[serde(default = "default_swap_blur")]
1541 pub blur: f32,
1542}
1543
1544impl Default for TextSwapConfig {
1545 fn default() -> Self {
1546 Self {
1547 duration: default_swap_duration(),
1548 distance: default_swap_distance(),
1549 blur: default_swap_blur(),
1550 }
1551 }
1552}
1553
1554fn default_swap_duration() -> f64 {
1555 0.45
1556}
1557
1558fn default_swap_distance() -> f32 {
1559 18.0
1560}
1561
1562fn default_swap_blur() -> f32 {
1563 8.0
1564}
1565
1566#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
1568#[serde(rename_all = "snake_case")]
1569pub enum CaretShape {
1570 #[default]
1572 Line,
1573 Block,
1575}
1576
1577#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1585#[serde(deny_unknown_fields)]
1586pub struct CaretConfig {
1587 #[serde(default)]
1589 pub shape: CaretShape,
1590 #[serde(default)]
1593 pub color: Option<String>,
1594 #[serde(default = "default_caret_blink")]
1597 pub blink: f32,
1598 #[serde(default)]
1601 pub hide_when_done: bool,
1602}
1603
1604impl Default for CaretConfig {
1605 fn default() -> Self {
1606 Self {
1607 shape: CaretShape::default(),
1608 color: None,
1609 blink: default_caret_blink(),
1610 hide_when_done: false,
1611 }
1612 }
1613}
1614
1615fn default_caret_blink() -> f32 {
1616 1.0
1617}
1618
1619#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1628#[serde(deny_unknown_fields)]
1629pub struct ShimmerConfig {
1630 #[serde(default)]
1632 pub delay: f64,
1633 #[serde(default = "default_shimmer_duration")]
1635 pub duration: f64,
1636 #[serde(default = "default_shimmer_color")]
1638 pub color: String,
1639 #[serde(default = "default_shimmer_width")]
1643 pub width: f32,
1644 #[serde(default = "default_shimmer_intensity")]
1646 pub intensity: f32,
1647 #[serde(default = "default_shimmer_angle")]
1652 pub angle: f32,
1653 #[serde(default, rename = "loop")]
1655 pub repeat: bool,
1656}
1657
1658fn default_shimmer_duration() -> f64 {
1659 0.9
1660}
1661
1662fn default_shimmer_color() -> String {
1663 "#FFFFFF".to_string()
1664}
1665
1666fn default_shimmer_width() -> f32 {
1667 0.35
1668}
1669
1670fn default_shimmer_intensity() -> f32 {
1671 0.75
1672}
1673
1674fn default_shimmer_angle() -> f32 {
1675 20.0
1676}
1677
1678fn default_glow_color() -> String {
1679 "#FFFFFF80".to_string()
1680}
1681
1682fn default_glow_radius() -> f32 {
1683 10.0
1684}
1685
1686fn default_glow_intensity() -> f32 {
1687 1.0
1688}
1689
1690fn default_font_size() -> f32 {
1691 48.0
1692}
1693
1694fn default_color() -> String {
1695 "#FFFFFF".to_string()
1696}
1697
1698fn default_font_family() -> String {
1699 "Inter".to_string()
1700}
1701
1702fn default_stroke_width() -> f32 {
1703 2.0
1704}
1705
1706fn default_star_points() -> u32 {
1707 5
1708}
1709
1710fn default_polygon_sides() -> u32 {
1711 6
1712}
1713
1714fn default_shadow_color() -> String {
1715 "#00000080".to_string()
1716}
1717
1718fn default_shadow_offset() -> f32 {
1719 2.0
1720}
1721
1722fn default_shadow_blur() -> f32 {
1723 4.0
1724}
1725
1726fn default_text_bg_padding() -> f32 {
1727 8.0
1728}
1729
1730#[cfg(test)]
1731mod motion_property_tests {
1732 use super::*;
1733 use serde_json::json;
1734
1735 #[test]
1736 fn wiggle_known_property_still_works() {
1737 let json = json!({
1738 "name": "wiggle",
1739 "property": "translate_x",
1740 "amplitude": 10.0,
1741 "frequency": 1.0
1742 });
1743 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1744 match effect {
1745 AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "translate_x"),
1746 other => panic!("expected Wiggle, got {other:?}"),
1747 }
1748 }
1749
1750 #[test]
1751 fn wiggle_unknown_property_is_a_named_error_not_a_silent_no_op() {
1752 let json = json!({
1753 "name": "wiggle",
1754 "property": "skew",
1755 "amplitude": 10.0,
1756 "frequency": 1.0
1757 });
1758 let err = serde_json::from_value::<AnimationEffect>(json)
1759 .expect_err("an unrecognised wiggle property must be rejected, not silently inert");
1760 assert!(err.to_string().contains("skew"), "got: {err}");
1761 }
1762
1763 #[test]
1764 fn wiggle_kebab_case_property_gets_a_did_you_mean() {
1765 let json = json!({
1766 "name": "wiggle",
1767 "property": "translate-x",
1768 "amplitude": 10.0,
1769 "frequency": 1.0
1770 });
1771 let err = serde_json::from_value::<AnimationEffect>(json)
1772 .expect_err("kebab-case must not silently resolve to a snake_case no-op");
1773 let msg = err.to_string();
1774 assert!(msg.contains("translate-x"), "got: {msg}");
1775 assert!(
1776 msg.contains("translate_x"),
1777 "expected a did-you-mean nudge toward the correct spelling, got: {msg}"
1778 );
1779 }
1780
1781 #[test]
1782 fn keyframes_animation_known_property_still_works() {
1783 let json = json!({
1784 "name": "keyframes",
1785 "keyframes": [
1786 { "property": "opacity", "keyframes": [
1787 { "time": 0.0, "value": 0.0 },
1788 { "time": 1.0, "value": 1.0 }
1789 ]}
1790 ]
1791 });
1792 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1793 match effect {
1794 AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "opacity"),
1795 other => panic!("expected Keyframes, got {other:?}"),
1796 }
1797 }
1798
1799 #[test]
1800 fn keyframes_animation_unknown_property_is_a_named_error() {
1801 let json = json!({
1802 "name": "keyframes",
1803 "keyframes": [
1804 { "property": "positionX", "keyframes": [
1805 { "time": 0.0, "value": 0.0 },
1806 { "time": 1.0, "value": 1.0 }
1807 ]}
1808 ]
1809 });
1810 let err = serde_json::from_value::<AnimationEffect>(json).expect_err(
1811 "an unrecognised keyframe animation property must be rejected, not silently inert",
1812 );
1813 assert!(err.to_string().contains("positionX"), "got: {err}");
1814 }
1815
1816 #[test]
1817 fn keyframes_animation_color_property_still_works() {
1818 let json = json!({
1819 "name": "keyframes",
1820 "keyframes": [
1821 { "property": "color", "keyframes": [
1822 { "time": 0.0, "value": "#000000" },
1823 { "time": 1.0, "value": "#ffffff" }
1824 ]}
1825 ]
1826 });
1827 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1828 assert!(matches!(effect, AnimationEffect::Keyframes(_)));
1829 }
1830
1831 #[test]
1832 fn draw_offset_is_a_recognized_keyframe_animation_property() {
1833 let json = json!({
1834 "name": "keyframes",
1835 "keyframes": [
1836 { "property": "draw_offset", "keyframes": [
1837 { "time": 0.0, "value": 0.0 },
1838 { "time": 1.0, "value": 1.0 }
1839 ]}
1840 ]
1841 });
1842 let effect: AnimationEffect = serde_json::from_value(json)
1843 .expect("draw_offset must be a known animation property, not rejected as unknown");
1844 match effect {
1845 AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "draw_offset"),
1846 other => panic!("expected Keyframes, got {other:?}"),
1847 }
1848 }
1849
1850 #[test]
1851 fn draw_offset_is_a_recognized_wiggle_property() {
1852 let json = json!({
1853 "name": "wiggle",
1854 "property": "draw_offset",
1855 "amplitude": 0.1,
1856 "frequency": 1.0
1857 });
1858 let effect: AnimationEffect = serde_json::from_value(json)
1859 .expect("draw_offset must be usable as a wiggle property, not rejected as unknown");
1860 match effect {
1861 AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "draw_offset"),
1862 other => panic!("expected Wiggle, got {other:?}"),
1863 }
1864 }
1865}
1866
1867#[cfg(test)]
1868mod svg_path_morph_schema_tests {
1869 use super::*;
1870 use serde_json::json;
1871
1872 #[test]
1873 fn a_target_and_at_least_one_keyframe_parse() {
1874 let json = json!({
1875 "target": "#outline",
1876 "keyframes": [
1877 { "time": 0.0, "value": "M0 0 L10 0" },
1878 { "time": 1.0, "value": "M0 0 L20 0" }
1879 ]
1880 });
1881 let morph: SvgPathMorph = serde_json::from_value(json).expect("valid SvgPathMorph JSON");
1882 assert_eq!(morph.target, "#outline");
1883 assert_eq!(morph.keyframes.len(), 2);
1884 }
1885
1886 #[test]
1887 fn a_missing_target_is_rejected_rather_than_silently_matching_nothing() {
1888 let json = json!({
1889 "keyframes": [
1890 { "time": 0.0, "value": "M0 0 L10 0" }
1891 ]
1892 });
1893 serde_json::from_value::<SvgPathMorph>(json).expect_err(
1894 "target is required — without it there is no path to select inside the document",
1895 );
1896 }
1897
1898 #[test]
1899 fn an_unknown_field_is_rejected_by_name() {
1900 let json = json!({
1901 "target": "#outline",
1902 "keyframes": [{ "time": 0.0, "value": "M0 0 L10 0" }],
1903 "duratoin": 1.0
1904 });
1905 let err = serde_json::from_value::<SvgPathMorph>(json)
1906 .expect_err("a typo'd field must be rejected, not silently ignored");
1907 assert!(err.to_string().contains("duratoin"), "got: {err}");
1908 }
1909}
1910
1911#[cfg(test)]
1912mod motion_path_schema_tests {
1913 use super::*;
1914 use serde_json::json;
1915
1916 #[test]
1917 fn motion_path_deserializes_with_known_fields() {
1918 let json = json!({
1919 "name": "motion_path",
1920 "path": "M0,0 L100,0 L100,100",
1921 "delay": 0.2,
1922 "duration": 1.5,
1923 "loop": true,
1924 "orient": true,
1925 "orient_offset": 90.0,
1926 "easing": "ease_out"
1927 });
1928 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1929 match effect {
1930 AnimationEffect::MotionPath(cfg) => {
1931 assert_eq!(cfg.path, "M0,0 L100,0 L100,100");
1932 assert_eq!(cfg.delay, 0.2);
1933 assert_eq!(cfg.duration, 1.5);
1934 assert!(cfg.repeat);
1935 assert!(cfg.orient);
1936 assert_eq!(cfg.orient_offset, 90.0);
1937 assert_eq!(cfg.easing, EasingType::EaseOut);
1938 }
1939 other => panic!("expected MotionPath, got {other:?}"),
1940 }
1941 }
1942
1943 #[test]
1944 fn motion_path_defaults_match_documented_values() {
1945 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0" });
1946 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1947 match effect {
1948 AnimationEffect::MotionPath(cfg) => {
1949 assert_eq!(cfg.delay, 0.0);
1950 assert_eq!(cfg.duration, default_animation_duration());
1951 assert!(!cfg.repeat);
1952 assert!(!cfg.orient);
1953 assert_eq!(cfg.orient_offset, 0.0);
1954 assert_eq!(cfg.easing, EasingType::Linear);
1955 }
1956 other => panic!("expected MotionPath, got {other:?}"),
1957 }
1958 }
1959
1960 #[test]
1961 fn motion_path_rejects_an_empty_path_string() {
1962 let json = json!({ "name": "motion_path", "path": "" });
1963 let err = serde_json::from_value::<AnimationEffect>(json)
1964 .expect_err("an empty motion_path.path must be rejected, not silently accepted");
1965 assert!(err.to_string().contains("no drawable point"), "got: {err}");
1966 }
1967
1968 #[test]
1969 fn motion_path_rejects_unparsable_svg_path_data() {
1970 let json = json!({ "name": "motion_path", "path": "this is not svg path data !!" });
1971 let err = serde_json::from_value::<AnimationEffect>(json)
1972 .expect_err("garbage path data must be rejected, not silently accepted");
1973 assert!(
1974 err.to_string().contains("not valid SVG path data"),
1975 "got: {err}"
1976 );
1977 }
1978
1979 #[test]
1980 fn motion_path_accepts_a_single_point_path() {
1981 let json = json!({ "name": "motion_path", "path": "M50,50" });
1982 let effect: AnimationEffect = serde_json::from_value(json)
1983 .expect("a syntactically valid single-point path must be accepted");
1984 assert!(matches!(effect, AnimationEffect::MotionPath(_)));
1985 }
1986
1987 #[test]
1988 fn motion_path_rejects_unknown_fields() {
1989 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0", "detla": 0.2 });
1990 let err = serde_json::from_value::<AnimationEffect>(json)
1991 .expect_err("a typo'd field on motion_path must be rejected, not silently ignored");
1992 assert!(err.to_string().contains("detla"), "got: {err}");
1993 }
1994
1995 #[test]
1996 fn motion_path_shift_delay_shifts_the_configs_own_delay() {
1997 let mut effect = AnimationEffect::MotionPath(MotionPathConfig {
1998 path: "M0,0 L10,0".to_string(),
1999 delay: 0.5,
2000 duration: 1.0,
2001 repeat: false,
2002 orient: false,
2003 orient_offset: 0.0,
2004 easing: EasingType::Linear,
2005 });
2006 effect.shift_delay(0.25);
2007 match effect {
2008 AnimationEffect::MotionPath(cfg) => assert!((cfg.delay - 0.75).abs() < 1e-9),
2009 other => panic!("expected MotionPath, got {other:?}"),
2010 }
2011 }
2012}
2013
2014#[cfg(test)]
2015mod animation_timing_repeat_widening_tests {
2016 use super::*;
2017 use serde_json::json;
2018
2019 #[test]
2020 fn loop_true_is_unchanged_infinite_repeat() {
2021 let json = json!({ "name": "pulse", "loop": true });
2022 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
2023 match effect {
2024 AnimationEffect::Pulse(t) => {
2025 assert!(t.repeat, "\"loop\": true must still set repeat = true");
2026 assert_eq!(
2027 t.repeat_count, None,
2028 "a bare `true` carries no count — infinite, exactly as before"
2029 );
2030 assert!(!t.yoyo);
2031 assert_eq!(t.repeat_delay, 0.0);
2032 }
2033 other => panic!("expected Pulse, got {other:?}"),
2034 }
2035 }
2036
2037 #[test]
2038 fn loop_false_and_omitted_are_unchanged_and_identical() {
2039 let explicit: AnimationTiming = serde_json::from_value(json!({ "loop": false })).unwrap();
2040 let omitted: AnimationTiming = serde_json::from_value(json!({})).unwrap();
2041 assert_eq!(
2042 explicit, omitted,
2043 "an explicit `false` and an omitted `loop` must resolve identically"
2044 );
2045 assert!(!explicit.repeat);
2046 assert_eq!(explicit.repeat_count, None);
2047 assert_eq!(explicit, AnimationTiming::default());
2048 }
2049
2050 #[test]
2051 fn loop_as_a_positive_integer_sets_repeat_and_the_count() {
2052 let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
2053 assert!(t.repeat, "a finite count still loops — see the field doc");
2054 assert_eq!(t.repeat_count, Some(12));
2055 }
2056
2057 #[test]
2058 fn loop_as_zero_or_one_folds_back_to_the_boolean_form() {
2059 for n in [0, 1] {
2060 let t: AnimationTiming = serde_json::from_value(json!({ "loop": n })).unwrap();
2061 assert!(!t.repeat, "loop: {n} must not set repeat");
2062 assert_eq!(t.repeat_count, None, "loop: {n} must not carry a count");
2063 }
2064 }
2065
2066 #[test]
2067 fn yoyo_and_repeat_delay_default_to_off_and_zero() {
2068 let t: AnimationTiming = serde_json::from_value(json!({})).unwrap();
2069 assert!(!t.yoyo);
2070 assert_eq!(t.repeat_delay, 0.0);
2071 }
2072
2073 #[test]
2074 fn yoyo_and_repeat_delay_are_accepted_on_any_animation_timing() {
2075 let json = json!({
2076 "name": "shake",
2077 "duration": 0.035,
2078 "loop": 12,
2079 "yoyo": true,
2080 "repeat_delay": 0.01
2081 });
2082 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
2083 match effect {
2084 AnimationEffect::Shake(t) => {
2085 assert!(t.yoyo);
2086 assert_eq!(t.repeat_delay, 0.01);
2087 assert_eq!(t.repeat_count, Some(12));
2088 }
2089 other => panic!("expected Shake, got {other:?}"),
2090 }
2091 }
2092
2093 #[test]
2094 fn a_typo_is_still_rejected_through_the_wire_type() {
2095 let json = json!({ "name": "pulse", "duratoin": 1.0 });
2096 let err = serde_json::from_value::<AnimationEffect>(json)
2097 .expect_err("a typo'd field must still be rejected, not silently ignored");
2098 assert!(err.to_string().contains("duratoin"), "got: {err}");
2099 }
2100
2101 #[test]
2102 fn loop_as_a_negative_number_is_rejected_not_silently_coerced() {
2103 let json = json!({ "name": "pulse", "loop": -1 });
2104 assert!(
2105 serde_json::from_value::<AnimationEffect>(json).is_err(),
2106 "a negative \"loop\" is neither a bool nor a valid play count"
2107 );
2108 }
2109
2110 #[test]
2111 fn a_finite_repeat_count_round_trips_through_a_single_loop_key() {
2112 let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
2113 let json = serde_json::to_value(&t).unwrap();
2114 assert!(
2115 json.get("repeat_count").is_none(),
2116 "must not emit a separate repeat_count key: {json}"
2117 );
2118 assert_eq!(json["loop"], serde_json::json!(12));
2119 let back: AnimationTiming = serde_json::from_value(json).unwrap();
2120 assert_eq!(t, back);
2121 }
2122
2123 #[test]
2124 fn easing_steps_round_trips_and_is_distinct_from_cubic_bezier() {
2125 let json = json!({ "steps": 4 });
2126 let easing: EasingType = serde_json::from_value(json).unwrap();
2127 assert_eq!(easing, EasingType::Steps(4));
2128 let back = serde_json::to_value(&easing).unwrap();
2129 assert_eq!(back, json!({ "steps": 4 }));
2130 }
2131}