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(rename = "loop")]
211 pub repeat: bool,
212 #[serde(default)]
223 pub repeat_count: Option<u32>,
224 #[serde(default)]
231 pub yoyo: bool,
232 #[serde(default)]
236 pub repeat_delay: f64,
237 #[serde(default)]
239 pub overshoot: Option<f64>,
240 #[serde(default)]
245 pub spring: Option<SpringConfig>,
246 #[serde(default)]
252 pub amplitude: Option<f64>,
253}
254
255fn default_animation_duration() -> f64 {
256 0.8
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
260#[serde(untagged)]
261enum RepeatSpec {
262 Loop(bool),
263 Count(u32),
264}
265
266impl Default for RepeatSpec {
267 fn default() -> Self {
268 RepeatSpec::Loop(false)
269 }
270}
271
272impl RepeatSpec {
273 fn into_parts(self) -> (bool, Option<u32>) {
274 match self {
275 RepeatSpec::Loop(b) => (b, None),
276 RepeatSpec::Count(0) | RepeatSpec::Count(1) => (false, None),
277 RepeatSpec::Count(n) => (true, Some(n)),
278 }
279 }
280
281 fn from_parts(repeat: bool, repeat_count: Option<u32>) -> Self {
282 match repeat_count {
283 Some(n) => RepeatSpec::Count(n),
284 None => RepeatSpec::Loop(repeat),
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(deny_unknown_fields)]
291struct AnimationTimingWire {
292 #[serde(default)]
293 delay: f64,
294 #[serde(default = "default_animation_duration")]
295 duration: f64,
296 #[serde(default, rename = "loop")]
297 repeat: RepeatSpec,
298 #[serde(default)]
299 yoyo: bool,
300 #[serde(default)]
301 repeat_delay: f64,
302 #[serde(default)]
303 overshoot: Option<f64>,
304 #[serde(default)]
305 spring: Option<SpringConfig>,
306 #[serde(default)]
307 amplitude: Option<f64>,
308}
309
310impl From<AnimationTimingWire> for AnimationTiming {
311 fn from(wire: AnimationTimingWire) -> Self {
312 let (repeat, repeat_count) = wire.repeat.into_parts();
313 AnimationTiming {
314 delay: wire.delay,
315 duration: wire.duration,
316 repeat,
317 repeat_count,
318 yoyo: wire.yoyo,
319 repeat_delay: wire.repeat_delay,
320 overshoot: wire.overshoot,
321 spring: wire.spring,
322 amplitude: wire.amplitude,
323 }
324 }
325}
326
327impl From<&AnimationTiming> for AnimationTimingWire {
328 fn from(t: &AnimationTiming) -> Self {
329 AnimationTimingWire {
330 delay: t.delay,
331 duration: t.duration,
332 repeat: RepeatSpec::from_parts(t.repeat, t.repeat_count),
333 yoyo: t.yoyo,
334 repeat_delay: t.repeat_delay,
335 overshoot: t.overshoot,
336 spring: t.spring.clone(),
337 amplitude: t.amplitude,
338 }
339 }
340}
341
342impl<'de> Deserialize<'de> for AnimationTiming {
343 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
344 where
345 D: serde::Deserializer<'de>,
346 {
347 AnimationTimingWire::deserialize(deserializer).map(AnimationTiming::from)
348 }
349}
350
351impl Serialize for AnimationTiming {
352 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
353 where
354 S: serde::Serializer,
355 {
356 AnimationTimingWire::from(self).serialize(serializer)
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
362#[serde(deny_unknown_fields)]
363pub struct TiltInConfig {
364 #[serde(default)]
365 pub delay: f64,
366 #[serde(default = "default_animation_duration")]
367 pub duration: f64,
368 #[serde(default, rename = "loop")]
369 pub repeat: bool,
370 #[serde(default)]
372 pub rotate_x: Option<f64>,
373 #[serde(default)]
375 pub rotate_y: Option<f64>,
376 #[serde(default)]
378 pub perspective: Option<f64>,
379 #[serde(default)]
381 pub scale_from: Option<f64>,
382}
383
384impl Default for AnimationTiming {
385 fn default() -> Self {
386 Self {
387 delay: 0.0,
388 duration: 0.8,
389 repeat: false,
390 repeat_count: None,
391 yoyo: false,
392 repeat_delay: 0.0,
393 overshoot: None,
394 spring: None,
395 amplitude: None,
396 }
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
402#[serde(deny_unknown_fields)]
403pub struct CharAnimationTiming {
404 #[serde(default)]
406 pub delay: f64,
407 #[serde(default = "default_char_duration_f64")]
409 pub duration: f64,
410 #[serde(default = "default_char_stagger_f64")]
412 pub stagger: f64,
413 #[serde(default)]
415 pub granularity: TextAnimGranularity,
416 #[serde(default)]
418 pub easing: EasingType,
419 #[serde(default)]
421 pub overshoot: Option<f64>,
422 #[serde(default)]
429 pub blur: Option<f64>,
430 #[serde(default)]
434 pub direction: TextAnimDirection,
435 #[serde(default)]
439 pub distance: Option<f64>,
440 #[serde(default)]
447 pub scale_from: Option<f64>,
448 #[serde(default)]
458 pub jitter: Option<f64>,
459 #[serde(default)]
462 pub seed: Option<u32>,
463 #[serde(default)]
467 pub ink_from: Option<String>,
468 #[serde(default)]
475 pub rotate_from: Option<f64>,
476 #[serde(default)]
479 pub rotate_origin: RotateOrigin,
480 #[serde(default)]
487 pub scale_jitter: Option<f64>,
488 #[serde(default)]
495 pub baseline_jitter: Option<f64>,
496 #[serde(default)]
506 pub reflow: bool,
507 #[serde(default)]
511 pub reflow_easing: EasingType,
512}
513
514impl Default for CharAnimationTiming {
515 fn default() -> Self {
516 Self {
517 delay: 0.0,
518 duration: default_char_duration_f64(),
519 stagger: default_char_stagger_f64(),
520 granularity: TextAnimGranularity::default(),
521 easing: EasingType::default(),
522 overshoot: None,
523 blur: None,
524 direction: TextAnimDirection::default(),
525 distance: None,
526 scale_from: None,
527 jitter: None,
528 seed: None,
529 ink_from: None,
530 rotate_from: None,
531 rotate_origin: RotateOrigin::default(),
532 scale_jitter: None,
533 baseline_jitter: None,
534 reflow: false,
535 reflow_easing: EasingType::default(),
536 }
537 }
538}
539
540fn default_char_stagger_f64() -> f64 {
541 0.03
542}
543
544fn default_char_duration_f64() -> f64 {
545 0.4
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
550pub struct CharAnimation {
551 #[serde(default = "default_char_preset")]
553 pub preset: CharAnimPreset,
554 #[serde(default)]
556 pub granularity: TextAnimGranularity,
557 #[serde(default = "default_char_stagger")]
559 pub stagger: f32,
560 #[serde(default = "default_char_duration")]
562 pub duration: f32,
563 #[serde(default)]
565 pub easing: EasingType,
566 #[serde(default)]
568 pub delay: f32,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
573#[serde(rename_all = "snake_case")]
574#[derive(PartialEq)]
575pub enum TextAnimGranularity {
576 #[default]
578 Char,
579 Word,
581}
582
583#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
594#[serde(rename_all = "snake_case")]
595pub enum TextAnimDirection {
596 #[default]
598 Up,
599 Down,
601 Left,
603 Right,
605}
606
607impl TextAnimDirection {
608 pub fn offset(self, travel: f32) -> (f32, f32) {
609 match self {
610 Self::Up => (0.0, travel),
611 Self::Down => (0.0, -travel),
612 Self::Left => (travel, 0.0),
613 Self::Right => (-travel, 0.0),
614 }
615 }
616}
617
618#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
621#[serde(rename_all = "snake_case")]
622pub enum RotateOrigin {
623 #[default]
626 Center,
627 Edge,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
635#[serde(rename_all = "snake_case")]
636#[derive(PartialEq)]
637pub enum CharAnimPreset {
638 #[default]
640 ScaleIn,
641 FadeIn,
643 Wave,
645 Bounce,
647 RotateIn,
649 SlideUp,
651 BlurIn,
655}
656
657fn default_char_preset() -> CharAnimPreset {
658 CharAnimPreset::ScaleIn
659}
660
661fn default_char_stagger() -> f32 {
662 0.03
663}
664
665fn default_char_duration() -> f32 {
666 0.4
667}
668
669impl AnimationTiming {
670 pub fn to_preset_config(&self) -> PresetConfig {
671 PresetConfig {
672 amplitude: self.amplitude,
673 delay: self.delay,
674 duration: self.duration,
675 repeat: self.repeat,
676 repeat_count: self.repeat_count,
677 yoyo: self.yoyo,
678 repeat_delay: self.repeat_delay,
679 overshoot: self.overshoot,
680 spring: self.spring.clone(),
681 }
682 }
683}
684
685const KNOWN_MOTION_PROPERTIES: &[&str] = &[
686 "opacity",
687 "position.x",
688 "translate_x",
689 "position.y",
690 "translate_y",
691 "scale",
692 "scale.x",
693 "scale.y",
694 "rotation",
695 "rotate_x",
696 "rotate_y",
697 "blur",
698 "blur_x",
699 "blur_y",
700 "visible_chars",
701 "visible_chars_progress",
702 "border_radius",
703 "font_size",
704 "width",
705 "height",
706 "gap",
707 "padding",
708 "stroke_width",
709 "shadow_blur",
710 "glow_radius",
711 "glow_intensity",
712 "perspective",
713 "draw_progress",
714 "motion_progress",
715 "color",
716 "clip_path_progress",
717 "letter_spacing",
718 "draw_start",
719 "draw_offset",
720];
721
722fn validate_motion_property<E: serde::de::Error>(value: &str) -> Result<(), E> {
723 if KNOWN_MOTION_PROPERTIES.contains(&value) {
724 return Ok(());
725 }
726 let normalize = |s: &str| s.replace(['-', ' '], "_").to_lowercase();
727 let normalized = normalize(value);
728 if let Some(suggestion) = KNOWN_MOTION_PROPERTIES
729 .iter()
730 .find(|known| normalize(known) == normalized)
731 {
732 Err(E::custom(format!(
733 "unknown animation property '{value}' — did you mean '{suggestion}'?"
734 )))
735 } else {
736 Err(E::custom(format!(
737 "unknown animation property '{value}': expected one of {}",
738 KNOWN_MOTION_PROPERTIES.join(", ")
739 )))
740 }
741}
742
743fn deserialize_motion_property<'de, D>(deserializer: D) -> Result<String, D::Error>
744where
745 D: serde::Deserializer<'de>,
746{
747 let s = String::deserialize(deserializer)?;
748 validate_motion_property::<D::Error>(&s)?;
749 Ok(s)
750}
751
752fn deserialize_validated_keyframes<'de, D>(deserializer: D) -> Result<Vec<Animation>, D::Error>
753where
754 D: serde::Deserializer<'de>,
755{
756 let animations = Vec::<Animation>::deserialize(deserializer)?;
757 for anim in &animations {
758 validate_motion_property::<D::Error>(&anim.property)?;
759 }
760 Ok(animations)
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
765#[serde(deny_unknown_fields)]
766pub struct KeyframesConfig {
767 #[serde(deserialize_with = "deserialize_validated_keyframes")]
768 pub keyframes: Vec<Animation>,
769 #[serde(default)]
770 pub delay: f64,
771 #[serde(default = "default_animation_duration")]
772 pub duration: f64,
773 #[serde(default, rename = "loop")]
774 pub repeat: bool,
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
783#[serde(deny_unknown_fields)]
784pub struct SvgPathMorph {
785 pub target: String,
789 pub keyframes: Vec<SvgPathMorphKeyframe>,
791 #[serde(default)]
793 pub easing: EasingType,
794 #[serde(default)]
797 pub repeat: bool,
798 #[serde(default)]
801 pub yoyo: bool,
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
806#[serde(deny_unknown_fields)]
807pub struct SvgPathMorphKeyframe {
808 pub time: f64,
811 pub value: String,
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
832#[serde(deny_unknown_fields)]
833pub struct MotionBlurConfig {
834 #[serde(default)]
838 pub intensity: f32,
839 #[serde(default = "default_motion_blur_samples")]
843 pub samples: u32,
844 #[serde(default = "default_motion_blur_shutter")]
848 pub shutter: f64,
849 #[serde(default)]
852 pub mode: MotionBlurMode,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
857#[serde(rename_all = "snake_case")]
858pub enum MotionBlurMode {
859 #[default]
861 Stack,
862 Smear,
865}
866
867fn default_motion_blur_samples() -> u32 {
868 6
869}
870fn default_motion_blur_shutter() -> f64 {
871 0.5
872}
873
874#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
881#[serde(deny_unknown_fields)]
882pub struct TrailConfig {
883 #[serde(default = "default_trail_copies")]
885 pub copies: u32,
886 #[serde(default = "default_trail_spacing")]
888 pub spacing: f64,
889 #[serde(default = "default_trail_falloff")]
892 pub falloff: f32,
893}
894
895fn default_trail_copies() -> u32 {
896 4
897}
898fn default_trail_spacing() -> f64 {
899 0.05
900}
901fn default_trail_falloff() -> f32 {
902 0.6
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
908#[serde(deny_unknown_fields)]
909pub struct OrbitConfig {
910 #[serde(default = "default_orbit_radius")]
912 pub radius_x: f64,
913 #[serde(default = "default_orbit_radius")]
915 pub radius_y: f64,
916 #[serde(default = "default_orbit_speed")]
918 pub speed: f64,
919 #[serde(default)]
921 pub start_angle: f64,
922 #[serde(default = "default_orbit_depth")]
924 pub depth: f64,
925 #[serde(default)]
927 pub opacity_depth: f64,
928 #[serde(default)]
930 pub tilt: f64,
931 #[serde(default)]
933 pub phase: f64,
934}
935
936fn default_orbit_radius() -> f64 {
937 30.0
938}
939fn default_orbit_speed() -> f64 {
940 0.5
941}
942fn default_orbit_depth() -> f64 {
943 0.15
944}
945
946#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
947#[serde(deny_unknown_fields)]
948pub struct WiggleConfig {
949 #[serde(deserialize_with = "deserialize_motion_property")]
950 pub property: String,
951 pub amplitude: f64,
952 pub frequency: f64,
953 #[serde(default)]
954 pub seed: u64,
955 #[serde(default)]
957 pub octaves: Option<u32>,
958 #[serde(default)]
960 pub phase: Option<f64>,
961 #[serde(default)]
963 pub decay: Option<f64>,
964 #[serde(default)]
966 pub easing: Option<EasingType>,
967 #[serde(default)]
969 pub mode: Option<String>,
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1019#[serde(deny_unknown_fields)]
1020pub struct MotionPathConfig {
1021 #[serde(deserialize_with = "deserialize_motion_path_data")]
1025 pub path: String,
1026 #[serde(default)]
1031 pub delay: f64,
1032 #[serde(default = "default_animation_duration")]
1034 pub duration: f64,
1035 #[serde(default, rename = "loop")]
1038 pub repeat: bool,
1039 #[serde(default)]
1043 pub orient: bool,
1044 #[serde(default)]
1048 pub orient_offset: f64,
1049 #[serde(default)]
1053 pub easing: EasingType,
1054}
1055
1056fn deserialize_motion_path_data<'de, D>(deserializer: D) -> Result<String, D::Error>
1057where
1058 D: serde::Deserializer<'de>,
1059{
1060 let s = String::deserialize(deserializer)?;
1061 match SkiaPath::from_svg(&s) {
1062 Some(path) if path.count_points() > 0 => Ok(s),
1063 Some(_) => Err(serde::de::Error::custom(format!(
1064 "motion_path.path '{s}' has no drawable point (an empty path has nothing to travel \
1065 to) — provide at least one command, e.g. \"M0,0 L100,0\""
1066 ))),
1067 None => Err(serde::de::Error::custom(format!(
1068 "motion_path.path '{s}' is not valid SVG path data (the same 'd'-attribute \
1069 mini-language shape's type: \"path\" accepts), e.g. \"M0,0 C50,-100 150,-100 200,0\""
1070 ))),
1071 }
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1101#[serde(deny_unknown_fields)]
1102pub struct ShatterConfig {
1103 #[serde(default)]
1105 pub delay: f64,
1106 #[serde(default = "default_shatter_duration")]
1109 pub duration: f64,
1110 #[serde(default)]
1113 pub mode: ShatterMode,
1114 #[serde(default = "default_shatter_pieces")]
1116 pub pieces: u32,
1117 #[serde(default)]
1120 pub seed: u32,
1121 #[serde(default)]
1125 pub origin: ShatterOrigin,
1126 #[serde(default = "default_shatter_spread")]
1130 pub spread: f32,
1131 #[serde(default = "default_shatter_spin")]
1135 pub spin: f32,
1136 #[serde(default = "default_shatter_depth")]
1141 pub depth: f32,
1142 #[serde(default = "default_true")]
1146 pub fade: bool,
1147}
1148
1149fn default_shatter_duration() -> f64 {
1150 0.6
1151}
1152fn default_shatter_pieces() -> u32 {
1153 24
1154}
1155fn default_shatter_spread() -> f32 {
1156 1.0
1157}
1158fn default_shatter_spin() -> f32 {
1159 90.0
1160}
1161fn default_shatter_depth() -> f32 {
1162 0.4
1163}
1164fn default_true() -> bool {
1165 true
1166}
1167
1168#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1171#[serde(rename_all = "snake_case")]
1172pub enum ShatterMode {
1173 #[default]
1176 Out,
1177 In,
1181 Hold,
1184}
1185
1186#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
1190#[serde(deny_unknown_fields)]
1191pub struct ShatterOrigin {
1192 #[serde(default = "default_shatter_origin_component")]
1193 pub x: f32,
1194 #[serde(default = "default_shatter_origin_component")]
1195 pub y: f32,
1196}
1197
1198impl Default for ShatterOrigin {
1199 fn default() -> Self {
1200 Self {
1201 x: default_shatter_origin_component(),
1202 y: default_shatter_origin_component(),
1203 }
1204 }
1205}
1206
1207fn default_shatter_origin_component() -> f32 {
1208 0.5
1209}
1210
1211#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1225#[serde(deny_unknown_fields)]
1226pub struct BurstConfig {
1227 #[serde(default)]
1229 pub delay: f64,
1230 #[serde(default = "default_burst_duration")]
1233 pub duration: f64,
1234 #[serde(default = "default_burst_count")]
1236 pub count: u32,
1237 #[serde(default = "default_burst_length")]
1240 pub length: f32,
1241 #[serde(default = "default_burst_gap")]
1244 pub gap: f32,
1245 #[serde(default = "default_burst_width")]
1247 pub width: f32,
1248 #[serde(default = "default_burst_color")]
1250 pub color: String,
1251 #[serde(default)]
1253 pub seed: u32,
1254 #[serde(default = "default_burst_jitter")]
1259 pub jitter: f32,
1260}
1261
1262fn default_burst_duration() -> f64 {
1263 0.4
1264}
1265fn default_burst_count() -> u32 {
1266 8
1267}
1268fn default_burst_length() -> f32 {
1269 40.0
1270}
1271fn default_burst_gap() -> f32 {
1272 12.0
1273}
1274fn default_burst_width() -> f32 {
1275 4.0
1276}
1277fn default_burst_color() -> String {
1278 "#FFB020".to_string()
1279}
1280fn default_burst_jitter() -> f32 {
1281 0.2
1282}
1283
1284#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1285#[serde(rename_all = "snake_case")]
1286pub enum ShapeType {
1287 Rect,
1288 Circle,
1289 RoundedRect,
1290 Ellipse,
1291 Triangle,
1292 Star {
1293 #[serde(default = "default_star_points")]
1294 points: u32,
1295 },
1296 Polygon {
1297 #[serde(default = "default_polygon_sides")]
1298 sides: u32,
1299 },
1300 Path {
1301 data: String,
1302 },
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1306#[serde(untagged)]
1307pub enum Fill {
1308 Solid(String),
1309 Gradient(Gradient),
1310}
1311
1312#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1313pub struct Gradient {
1314 #[serde(rename = "type")]
1315 pub gradient_type: GradientType,
1316 pub colors: Vec<String>,
1317 #[serde(default)]
1318 pub stops: Option<Vec<f32>>,
1319 #[serde(default)]
1320 pub angle: Option<f32>,
1321}
1322
1323#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1324#[serde(rename_all = "snake_case")]
1325pub enum GradientType {
1326 Linear,
1327 Radial,
1328}
1329
1330#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1331pub struct Stroke {
1332 pub color: String,
1333 #[serde(default = "default_stroke_width")]
1334 pub width: f32,
1335 #[serde(default)]
1341 pub dashed: Option<Vec<f32>>,
1342 #[serde(default)]
1353 pub dash_offset: Option<crate::expr::Computed<f32>>,
1354 #[serde(default)]
1358 pub line_cap: LineCap,
1359 #[serde(default)]
1362 pub line_join: LineJoin,
1363}
1364
1365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1368#[serde(rename_all = "snake_case")]
1369pub enum LineCap {
1370 #[default]
1371 Butt,
1372 Round,
1373 Square,
1374}
1375
1376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1379#[serde(rename_all = "snake_case")]
1380pub enum LineJoin {
1381 #[default]
1382 Miter,
1383 Round,
1384 Bevel,
1385}
1386
1387#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1388#[serde(rename_all = "snake_case")]
1389#[derive(Default)]
1390pub enum ImageFit {
1391 Cover,
1392 #[default]
1393 Contain,
1394 Fill,
1395}
1396
1397#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1398pub struct ShapeText {
1399 pub content: String,
1400 #[serde(default = "default_font_size")]
1401 pub font_size: f32,
1402 #[serde(default = "default_color")]
1403 pub color: String,
1404 #[serde(default = "default_font_family")]
1405 pub font_family: String,
1406 #[serde(default)]
1407 pub font_weight: FontWeight,
1408 #[serde(default)]
1409 pub align: TextAlign,
1410 #[serde(default)]
1411 pub vertical_align: VerticalAlign,
1412 #[serde(default)]
1413 pub line_height: Option<f32>,
1414 #[serde(default)]
1415 pub letter_spacing: Option<f32>,
1416 #[serde(default)]
1417 pub padding: Option<f32>,
1418}
1419
1420#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1421pub struct CaptionWord {
1422 pub text: String,
1423 pub start: f64,
1424 pub end: f64,
1425}
1426
1427#[derive(Debug, Serialize, Deserialize, JsonSchema)]
1428#[serde(rename_all = "snake_case")]
1429#[derive(Default)]
1430pub enum CaptionStyle {
1431 #[default]
1432 Highlight,
1433 Karaoke,
1434 WordByWord,
1435 WordPop,
1438 KaraokePop,
1441}
1442
1443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1444pub struct GradientBorder {
1445 pub colors: Vec<String>,
1446 #[serde(default = "default_gradient_border_width")]
1447 pub width: f32,
1448 #[serde(default)]
1449 pub angle: f32,
1450}
1451
1452fn default_gradient_border_width() -> f32 {
1453 2.0
1454}
1455
1456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1458pub struct InnerShadow {
1459 pub color: String,
1460 #[serde(default)]
1461 pub offset_x: f32,
1462 #[serde(default)]
1463 pub offset_y: f32,
1464 #[serde(default = "default_inner_shadow_blur")]
1465 pub blur: f32,
1466}
1467
1468fn default_inner_shadow_blur() -> f32 {
1469 10.0
1470}
1471
1472#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1473pub struct TextShadow {
1474 #[serde(default = "default_shadow_color")]
1475 pub color: String,
1476 #[serde(default = "default_shadow_offset")]
1477 pub offset_x: f32,
1478 #[serde(default = "default_shadow_offset")]
1479 pub offset_y: f32,
1480 #[serde(default = "default_shadow_blur")]
1481 pub blur: f32,
1482}
1483
1484#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1485pub struct TextBackground {
1486 pub color: String,
1487 #[serde(default = "default_text_bg_padding")]
1488 pub padding: f32,
1489 #[serde(default)]
1490 pub corner_radius: f32,
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1495#[serde(deny_unknown_fields)]
1496pub struct GlowConfig {
1497 #[serde(default = "default_glow_color")]
1499 pub color: String,
1500 #[serde(default = "default_glow_radius")]
1502 pub radius: f32,
1503 #[serde(default = "default_glow_intensity")]
1505 pub intensity: f32,
1506}
1507
1508#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1510#[serde(deny_unknown_fields)]
1511pub struct TextState {
1512 pub at: f64,
1514 pub content: String,
1516}
1517
1518#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1525#[serde(deny_unknown_fields)]
1526pub struct TextSwapConfig {
1527 #[serde(default = "default_swap_duration")]
1529 pub duration: f64,
1530 #[serde(default = "default_swap_distance")]
1532 pub distance: f32,
1533 #[serde(default = "default_swap_blur")]
1536 pub blur: f32,
1537}
1538
1539impl Default for TextSwapConfig {
1540 fn default() -> Self {
1541 Self {
1542 duration: default_swap_duration(),
1543 distance: default_swap_distance(),
1544 blur: default_swap_blur(),
1545 }
1546 }
1547}
1548
1549fn default_swap_duration() -> f64 {
1550 0.45
1551}
1552
1553fn default_swap_distance() -> f32 {
1554 18.0
1555}
1556
1557fn default_swap_blur() -> f32 {
1558 8.0
1559}
1560
1561#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
1563#[serde(rename_all = "snake_case")]
1564pub enum CaretShape {
1565 #[default]
1567 Line,
1568 Block,
1570}
1571
1572#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1580#[serde(deny_unknown_fields)]
1581pub struct CaretConfig {
1582 #[serde(default)]
1584 pub shape: CaretShape,
1585 #[serde(default)]
1588 pub color: Option<String>,
1589 #[serde(default = "default_caret_blink")]
1592 pub blink: f32,
1593 #[serde(default)]
1596 pub hide_when_done: bool,
1597}
1598
1599impl Default for CaretConfig {
1600 fn default() -> Self {
1601 Self {
1602 shape: CaretShape::default(),
1603 color: None,
1604 blink: default_caret_blink(),
1605 hide_when_done: false,
1606 }
1607 }
1608}
1609
1610fn default_caret_blink() -> f32 {
1611 1.0
1612}
1613
1614#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1623#[serde(deny_unknown_fields)]
1624pub struct ShimmerConfig {
1625 #[serde(default)]
1627 pub delay: f64,
1628 #[serde(default = "default_shimmer_duration")]
1630 pub duration: f64,
1631 #[serde(default = "default_shimmer_color")]
1633 pub color: String,
1634 #[serde(default = "default_shimmer_width")]
1638 pub width: f32,
1639 #[serde(default = "default_shimmer_intensity")]
1641 pub intensity: f32,
1642 #[serde(default = "default_shimmer_angle")]
1647 pub angle: f32,
1648 #[serde(default, rename = "loop")]
1650 pub repeat: bool,
1651}
1652
1653fn default_shimmer_duration() -> f64 {
1654 0.9
1655}
1656
1657fn default_shimmer_color() -> String {
1658 "#FFFFFF".to_string()
1659}
1660
1661fn default_shimmer_width() -> f32 {
1662 0.35
1663}
1664
1665fn default_shimmer_intensity() -> f32 {
1666 0.75
1667}
1668
1669fn default_shimmer_angle() -> f32 {
1670 20.0
1671}
1672
1673fn default_glow_color() -> String {
1674 "#FFFFFF80".to_string()
1675}
1676
1677fn default_glow_radius() -> f32 {
1678 10.0
1679}
1680
1681fn default_glow_intensity() -> f32 {
1682 1.0
1683}
1684
1685fn default_font_size() -> f32 {
1686 48.0
1687}
1688
1689fn default_color() -> String {
1690 "#FFFFFF".to_string()
1691}
1692
1693fn default_font_family() -> String {
1694 "Inter".to_string()
1695}
1696
1697fn default_stroke_width() -> f32 {
1698 2.0
1699}
1700
1701fn default_star_points() -> u32 {
1702 5
1703}
1704
1705fn default_polygon_sides() -> u32 {
1706 6
1707}
1708
1709fn default_shadow_color() -> String {
1710 "#00000080".to_string()
1711}
1712
1713fn default_shadow_offset() -> f32 {
1714 2.0
1715}
1716
1717fn default_shadow_blur() -> f32 {
1718 4.0
1719}
1720
1721fn default_text_bg_padding() -> f32 {
1722 8.0
1723}
1724
1725#[cfg(test)]
1726mod motion_property_tests {
1727 use super::*;
1728 use serde_json::json;
1729
1730 #[test]
1731 fn wiggle_known_property_still_works() {
1732 let json = json!({
1733 "name": "wiggle",
1734 "property": "translate_x",
1735 "amplitude": 10.0,
1736 "frequency": 1.0
1737 });
1738 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1739 match effect {
1740 AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "translate_x"),
1741 other => panic!("expected Wiggle, got {other:?}"),
1742 }
1743 }
1744
1745 #[test]
1746 fn wiggle_unknown_property_is_a_named_error_not_a_silent_no_op() {
1747 let json = json!({
1748 "name": "wiggle",
1749 "property": "skew",
1750 "amplitude": 10.0,
1751 "frequency": 1.0
1752 });
1753 let err = serde_json::from_value::<AnimationEffect>(json)
1754 .expect_err("an unrecognised wiggle property must be rejected, not silently inert");
1755 assert!(err.to_string().contains("skew"), "got: {err}");
1756 }
1757
1758 #[test]
1759 fn wiggle_kebab_case_property_gets_a_did_you_mean() {
1760 let json = json!({
1761 "name": "wiggle",
1762 "property": "translate-x",
1763 "amplitude": 10.0,
1764 "frequency": 1.0
1765 });
1766 let err = serde_json::from_value::<AnimationEffect>(json)
1767 .expect_err("kebab-case must not silently resolve to a snake_case no-op");
1768 let msg = err.to_string();
1769 assert!(msg.contains("translate-x"), "got: {msg}");
1770 assert!(
1771 msg.contains("translate_x"),
1772 "expected a did-you-mean nudge toward the correct spelling, got: {msg}"
1773 );
1774 }
1775
1776 #[test]
1777 fn keyframes_animation_known_property_still_works() {
1778 let json = json!({
1779 "name": "keyframes",
1780 "keyframes": [
1781 { "property": "opacity", "keyframes": [
1782 { "time": 0.0, "value": 0.0 },
1783 { "time": 1.0, "value": 1.0 }
1784 ]}
1785 ]
1786 });
1787 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1788 match effect {
1789 AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "opacity"),
1790 other => panic!("expected Keyframes, got {other:?}"),
1791 }
1792 }
1793
1794 #[test]
1795 fn keyframes_animation_unknown_property_is_a_named_error() {
1796 let json = json!({
1797 "name": "keyframes",
1798 "keyframes": [
1799 { "property": "positionX", "keyframes": [
1800 { "time": 0.0, "value": 0.0 },
1801 { "time": 1.0, "value": 1.0 }
1802 ]}
1803 ]
1804 });
1805 let err = serde_json::from_value::<AnimationEffect>(json).expect_err(
1806 "an unrecognised keyframe animation property must be rejected, not silently inert",
1807 );
1808 assert!(err.to_string().contains("positionX"), "got: {err}");
1809 }
1810
1811 #[test]
1812 fn keyframes_animation_color_property_still_works() {
1813 let json = json!({
1814 "name": "keyframes",
1815 "keyframes": [
1816 { "property": "color", "keyframes": [
1817 { "time": 0.0, "value": "#000000" },
1818 { "time": 1.0, "value": "#ffffff" }
1819 ]}
1820 ]
1821 });
1822 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1823 assert!(matches!(effect, AnimationEffect::Keyframes(_)));
1824 }
1825
1826 #[test]
1827 fn draw_offset_is_a_recognized_keyframe_animation_property() {
1828 let json = json!({
1829 "name": "keyframes",
1830 "keyframes": [
1831 { "property": "draw_offset", "keyframes": [
1832 { "time": 0.0, "value": 0.0 },
1833 { "time": 1.0, "value": 1.0 }
1834 ]}
1835 ]
1836 });
1837 let effect: AnimationEffect = serde_json::from_value(json)
1838 .expect("draw_offset must be a known animation property, not rejected as unknown");
1839 match effect {
1840 AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "draw_offset"),
1841 other => panic!("expected Keyframes, got {other:?}"),
1842 }
1843 }
1844
1845 #[test]
1846 fn draw_offset_is_a_recognized_wiggle_property() {
1847 let json = json!({
1848 "name": "wiggle",
1849 "property": "draw_offset",
1850 "amplitude": 0.1,
1851 "frequency": 1.0
1852 });
1853 let effect: AnimationEffect = serde_json::from_value(json)
1854 .expect("draw_offset must be usable as a wiggle property, not rejected as unknown");
1855 match effect {
1856 AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "draw_offset"),
1857 other => panic!("expected Wiggle, got {other:?}"),
1858 }
1859 }
1860}
1861
1862#[cfg(test)]
1863mod svg_path_morph_schema_tests {
1864 use super::*;
1865 use serde_json::json;
1866
1867 #[test]
1868 fn a_target_and_at_least_one_keyframe_parse() {
1869 let json = json!({
1870 "target": "#outline",
1871 "keyframes": [
1872 { "time": 0.0, "value": "M0 0 L10 0" },
1873 { "time": 1.0, "value": "M0 0 L20 0" }
1874 ]
1875 });
1876 let morph: SvgPathMorph = serde_json::from_value(json).expect("valid SvgPathMorph JSON");
1877 assert_eq!(morph.target, "#outline");
1878 assert_eq!(morph.keyframes.len(), 2);
1879 }
1880
1881 #[test]
1882 fn a_missing_target_is_rejected_rather_than_silently_matching_nothing() {
1883 let json = json!({
1884 "keyframes": [
1885 { "time": 0.0, "value": "M0 0 L10 0" }
1886 ]
1887 });
1888 serde_json::from_value::<SvgPathMorph>(json).expect_err(
1889 "target is required — without it there is no path to select inside the document",
1890 );
1891 }
1892
1893 #[test]
1894 fn an_unknown_field_is_rejected_by_name() {
1895 let json = json!({
1896 "target": "#outline",
1897 "keyframes": [{ "time": 0.0, "value": "M0 0 L10 0" }],
1898 "duratoin": 1.0
1899 });
1900 let err = serde_json::from_value::<SvgPathMorph>(json)
1901 .expect_err("a typo'd field must be rejected, not silently ignored");
1902 assert!(err.to_string().contains("duratoin"), "got: {err}");
1903 }
1904}
1905
1906#[cfg(test)]
1907mod motion_path_schema_tests {
1908 use super::*;
1909 use serde_json::json;
1910
1911 #[test]
1912 fn motion_path_deserializes_with_known_fields() {
1913 let json = json!({
1914 "name": "motion_path",
1915 "path": "M0,0 L100,0 L100,100",
1916 "delay": 0.2,
1917 "duration": 1.5,
1918 "loop": true,
1919 "orient": true,
1920 "orient_offset": 90.0,
1921 "easing": "ease_out"
1922 });
1923 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1924 match effect {
1925 AnimationEffect::MotionPath(cfg) => {
1926 assert_eq!(cfg.path, "M0,0 L100,0 L100,100");
1927 assert_eq!(cfg.delay, 0.2);
1928 assert_eq!(cfg.duration, 1.5);
1929 assert!(cfg.repeat);
1930 assert!(cfg.orient);
1931 assert_eq!(cfg.orient_offset, 90.0);
1932 assert_eq!(cfg.easing, EasingType::EaseOut);
1933 }
1934 other => panic!("expected MotionPath, got {other:?}"),
1935 }
1936 }
1937
1938 #[test]
1939 fn motion_path_defaults_match_documented_values() {
1940 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0" });
1941 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1942 match effect {
1943 AnimationEffect::MotionPath(cfg) => {
1944 assert_eq!(cfg.delay, 0.0);
1945 assert_eq!(cfg.duration, default_animation_duration());
1946 assert!(!cfg.repeat);
1947 assert!(!cfg.orient);
1948 assert_eq!(cfg.orient_offset, 0.0);
1949 assert_eq!(cfg.easing, EasingType::Linear);
1950 }
1951 other => panic!("expected MotionPath, got {other:?}"),
1952 }
1953 }
1954
1955 #[test]
1956 fn motion_path_rejects_an_empty_path_string() {
1957 let json = json!({ "name": "motion_path", "path": "" });
1958 let err = serde_json::from_value::<AnimationEffect>(json)
1959 .expect_err("an empty motion_path.path must be rejected, not silently accepted");
1960 assert!(err.to_string().contains("no drawable point"), "got: {err}");
1961 }
1962
1963 #[test]
1964 fn motion_path_rejects_unparsable_svg_path_data() {
1965 let json = json!({ "name": "motion_path", "path": "this is not svg path data !!" });
1966 let err = serde_json::from_value::<AnimationEffect>(json)
1967 .expect_err("garbage path data must be rejected, not silently accepted");
1968 assert!(
1969 err.to_string().contains("not valid SVG path data"),
1970 "got: {err}"
1971 );
1972 }
1973
1974 #[test]
1975 fn motion_path_accepts_a_single_point_path() {
1976 let json = json!({ "name": "motion_path", "path": "M50,50" });
1977 let effect: AnimationEffect = serde_json::from_value(json)
1978 .expect("a syntactically valid single-point path must be accepted");
1979 assert!(matches!(effect, AnimationEffect::MotionPath(_)));
1980 }
1981
1982 #[test]
1983 fn motion_path_rejects_unknown_fields() {
1984 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0", "detla": 0.2 });
1985 let err = serde_json::from_value::<AnimationEffect>(json)
1986 .expect_err("a typo'd field on motion_path must be rejected, not silently ignored");
1987 assert!(err.to_string().contains("detla"), "got: {err}");
1988 }
1989
1990 #[test]
1991 fn motion_path_shift_delay_shifts_the_configs_own_delay() {
1992 let mut effect = AnimationEffect::MotionPath(MotionPathConfig {
1993 path: "M0,0 L10,0".to_string(),
1994 delay: 0.5,
1995 duration: 1.0,
1996 repeat: false,
1997 orient: false,
1998 orient_offset: 0.0,
1999 easing: EasingType::Linear,
2000 });
2001 effect.shift_delay(0.25);
2002 match effect {
2003 AnimationEffect::MotionPath(cfg) => assert!((cfg.delay - 0.75).abs() < 1e-9),
2004 other => panic!("expected MotionPath, got {other:?}"),
2005 }
2006 }
2007}
2008
2009#[cfg(test)]
2010mod animation_timing_repeat_widening_tests {
2011 use super::*;
2012 use serde_json::json;
2013
2014 #[test]
2015 fn loop_true_is_unchanged_infinite_repeat() {
2016 let json = json!({ "name": "pulse", "loop": true });
2017 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
2018 match effect {
2019 AnimationEffect::Pulse(t) => {
2020 assert!(t.repeat, "\"loop\": true must still set repeat = true");
2021 assert_eq!(
2022 t.repeat_count, None,
2023 "a bare `true` carries no count — infinite, exactly as before"
2024 );
2025 assert!(!t.yoyo);
2026 assert_eq!(t.repeat_delay, 0.0);
2027 }
2028 other => panic!("expected Pulse, got {other:?}"),
2029 }
2030 }
2031
2032 #[test]
2033 fn loop_false_and_omitted_are_unchanged_and_identical() {
2034 let explicit: AnimationTiming = serde_json::from_value(json!({ "loop": false })).unwrap();
2035 let omitted: AnimationTiming = serde_json::from_value(json!({})).unwrap();
2036 assert_eq!(
2037 explicit, omitted,
2038 "an explicit `false` and an omitted `loop` must resolve identically"
2039 );
2040 assert!(!explicit.repeat);
2041 assert_eq!(explicit.repeat_count, None);
2042 assert_eq!(explicit, AnimationTiming::default());
2043 }
2044
2045 #[test]
2046 fn loop_as_a_positive_integer_sets_repeat_and_the_count() {
2047 let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
2048 assert!(t.repeat, "a finite count still loops — see the field doc");
2049 assert_eq!(t.repeat_count, Some(12));
2050 }
2051
2052 #[test]
2053 fn loop_as_zero_or_one_folds_back_to_the_boolean_form() {
2054 for n in [0, 1] {
2055 let t: AnimationTiming = serde_json::from_value(json!({ "loop": n })).unwrap();
2056 assert!(!t.repeat, "loop: {n} must not set repeat");
2057 assert_eq!(t.repeat_count, None, "loop: {n} must not carry a count");
2058 }
2059 }
2060
2061 #[test]
2062 fn yoyo_and_repeat_delay_default_to_off_and_zero() {
2063 let t: AnimationTiming = serde_json::from_value(json!({})).unwrap();
2064 assert!(!t.yoyo);
2065 assert_eq!(t.repeat_delay, 0.0);
2066 }
2067
2068 #[test]
2069 fn yoyo_and_repeat_delay_are_accepted_on_any_animation_timing() {
2070 let json = json!({
2071 "name": "shake",
2072 "duration": 0.035,
2073 "loop": 12,
2074 "yoyo": true,
2075 "repeat_delay": 0.01
2076 });
2077 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
2078 match effect {
2079 AnimationEffect::Shake(t) => {
2080 assert!(t.yoyo);
2081 assert_eq!(t.repeat_delay, 0.01);
2082 assert_eq!(t.repeat_count, Some(12));
2083 }
2084 other => panic!("expected Shake, got {other:?}"),
2085 }
2086 }
2087
2088 #[test]
2089 fn a_typo_is_still_rejected_through_the_wire_type() {
2090 let json = json!({ "name": "pulse", "duratoin": 1.0 });
2091 let err = serde_json::from_value::<AnimationEffect>(json)
2092 .expect_err("a typo'd field must still be rejected, not silently ignored");
2093 assert!(err.to_string().contains("duratoin"), "got: {err}");
2094 }
2095
2096 #[test]
2097 fn loop_as_a_negative_number_is_rejected_not_silently_coerced() {
2098 let json = json!({ "name": "pulse", "loop": -1 });
2099 assert!(
2100 serde_json::from_value::<AnimationEffect>(json).is_err(),
2101 "a negative \"loop\" is neither a bool nor a valid play count"
2102 );
2103 }
2104
2105 #[test]
2106 fn a_finite_repeat_count_round_trips_through_a_single_loop_key() {
2107 let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
2108 let json = serde_json::to_value(&t).unwrap();
2109 assert!(
2110 json.get("repeat_count").is_none(),
2111 "must not emit a separate repeat_count key: {json}"
2112 );
2113 assert_eq!(json["loop"], serde_json::json!(12));
2114 let back: AnimationTiming = serde_json::from_value(json).unwrap();
2115 assert_eq!(t, back);
2116 }
2117
2118 #[test]
2119 fn easing_steps_round_trips_and_is_distinct_from_cubic_bezier() {
2120 let json = json!({ "steps": 4 });
2121 let easing: EasingType = serde_json::from_value(json).unwrap();
2122 assert_eq!(easing, EasingType::Steps(4));
2123 let back = serde_json::to_value(&easing).unwrap();
2124 assert_eq!(back, json!({ "steps": 4 }));
2125 }
2126}