1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::Path as SkiaPath;
4
5use super::animation::{Animation, AnimationPreset, EasingType, PresetConfig, SpringConfig};
6use super::style::{FontWeight, TextAlign, VerticalAlign};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
20#[serde(tag = "name", rename_all = "snake_case")]
21pub enum AnimationEffect {
22 FadeIn(AnimationTiming),
24 FadeInUp(AnimationTiming),
25 FadeInDown(AnimationTiming),
26 FadeInLeft(AnimationTiming),
27 FadeInRight(AnimationTiming),
28 SlideInLeft(AnimationTiming),
29 SlideInRight(AnimationTiming),
30 SlideInUp(AnimationTiming),
31 SlideInDown(AnimationTiming),
32 ScaleIn(AnimationTiming),
33 BounceIn(AnimationTiming),
34 BlurIn(AnimationTiming),
35 RotateIn(AnimationTiming),
36 ElasticIn(AnimationTiming),
37 PopIn(AnimationTiming),
43 FadeOut(AnimationTiming),
45 FadeOutUp(AnimationTiming),
46 FadeOutDown(AnimationTiming),
47 SlideOutLeft(AnimationTiming),
48 SlideOutRight(AnimationTiming),
49 SlideOutUp(AnimationTiming),
50 SlideOutDown(AnimationTiming),
51 ScaleOut(AnimationTiming),
52 BounceOut(AnimationTiming),
53 BlurOut(AnimationTiming),
54 RotateOut(AnimationTiming),
55 Pulse(AnimationTiming),
57 Float(AnimationTiming),
58 Shake(AnimationTiming),
59 Spin(AnimationTiming),
60 FlipInX(AnimationTiming),
62 FlipInY(AnimationTiming),
63 FlipOutX(AnimationTiming),
64 FlipOutY(AnimationTiming),
65 TiltIn(TiltInConfig),
66 DrawIn(AnimationTiming),
68 StrokeReveal(AnimationTiming),
69 Typewriter(AnimationTiming),
71 WipeLeft(AnimationTiming),
72 WipeRight(AnimationTiming),
73 #[serde(alias = "float_3d")]
75 Float3d(AnimationTiming),
76 CharScaleIn(CharAnimationTiming),
78 CharFadeIn(CharAnimationTiming),
79 CharWave(CharAnimationTiming),
80 CharBounce(CharAnimationTiming),
81 CharRotateIn(CharAnimationTiming),
82 CharSlideUp(CharAnimationTiming),
83 CharBlurIn(CharAnimationTiming),
97 Glow(GlowConfig),
99 Shimmer(ShimmerConfig),
102 Wiggle(WiggleConfig),
103 Orbit(OrbitConfig),
104 Keyframes(KeyframesConfig),
105 MotionBlur(MotionBlurConfig),
106 Trail(TrailConfig),
109 MotionPath(MotionPathConfig),
115}
116
117impl AnimationEffect {
118 pub fn shift_delay(&mut self, by: f64) {
123 use AnimationEffect::*;
124 match self {
125 FadeIn(t) | FadeInUp(t) | FadeInDown(t) | FadeInLeft(t) | FadeInRight(t)
126 | SlideInLeft(t) | SlideInRight(t) | SlideInUp(t) | SlideInDown(t) | ScaleIn(t)
127 | BounceIn(t) | BlurIn(t) | RotateIn(t) | ElasticIn(t) | PopIn(t) | FadeOut(t)
128 | FadeOutUp(t) | FadeOutDown(t) | SlideOutLeft(t) | SlideOutRight(t)
129 | SlideOutUp(t) | SlideOutDown(t) | ScaleOut(t) | BounceOut(t) | BlurOut(t)
130 | RotateOut(t) | Pulse(t) | Float(t) | Shake(t) | Spin(t) | FlipInX(t) | FlipInY(t)
131 | FlipOutX(t) | FlipOutY(t) | DrawIn(t) | StrokeReveal(t) | Typewriter(t)
132 | WipeLeft(t) | WipeRight(t) | Float3d(t) => t.delay += by,
133 TiltIn(c) => c.delay += by,
134 CharScaleIn(c) | CharFadeIn(c) | CharWave(c) | CharBounce(c) | CharRotateIn(c)
135 | CharSlideUp(c) | CharBlurIn(c) => c.delay += by,
136 Keyframes(c) => c.delay += by,
137 MotionPath(c) => c.delay += by,
138 Shimmer(c) => c.delay += by,
139 Glow(_) | Wiggle(_) | Orbit(_) | MotionBlur(_) | Trail(_) => {}
140 }
141 }
142
143 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, Serialize, Deserialize, JsonSchema, PartialEq)]
203#[serde(deny_unknown_fields)]
204pub struct AnimationTiming {
205 #[serde(default)]
207 pub delay: f64,
208 #[serde(default = "default_animation_duration")]
210 pub duration: f64,
211 #[serde(default, rename = "loop")]
213 pub repeat: bool,
214 #[serde(default)]
216 pub overshoot: Option<f64>,
217 #[serde(default)]
222 pub spring: Option<SpringConfig>,
223 #[serde(default)]
229 pub amplitude: Option<f64>,
230}
231
232fn default_animation_duration() -> f64 {
233 0.8
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct TiltInConfig {
240 #[serde(default)]
241 pub delay: f64,
242 #[serde(default = "default_animation_duration")]
243 pub duration: f64,
244 #[serde(default, rename = "loop")]
245 pub repeat: bool,
246 #[serde(default)]
248 pub rotate_x: Option<f64>,
249 #[serde(default)]
251 pub rotate_y: Option<f64>,
252 #[serde(default)]
254 pub perspective: Option<f64>,
255 #[serde(default)]
257 pub scale_from: Option<f64>,
258}
259
260impl Default for AnimationTiming {
261 fn default() -> Self {
262 Self {
263 delay: 0.0,
264 duration: 0.8,
265 repeat: false,
266 overshoot: None,
267 spring: None,
268 amplitude: None,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
275#[serde(deny_unknown_fields)]
276pub struct CharAnimationTiming {
277 #[serde(default)]
279 pub delay: f64,
280 #[serde(default = "default_char_duration_f64")]
282 pub duration: f64,
283 #[serde(default = "default_char_stagger_f64")]
285 pub stagger: f64,
286 #[serde(default)]
288 pub granularity: TextAnimGranularity,
289 #[serde(default)]
291 pub easing: EasingType,
292 #[serde(default)]
294 pub overshoot: Option<f64>,
295 #[serde(default)]
302 pub blur: Option<f64>,
303 #[serde(default)]
307 pub direction: TextAnimDirection,
308 #[serde(default)]
312 pub distance: Option<f64>,
313 #[serde(default)]
320 pub scale_from: Option<f64>,
321 #[serde(default)]
331 pub jitter: Option<f64>,
332 #[serde(default)]
335 pub seed: Option<u32>,
336 #[serde(default)]
340 pub ink_from: Option<String>,
341}
342
343impl Default for CharAnimationTiming {
344 fn default() -> Self {
347 Self {
348 delay: 0.0,
349 duration: default_char_duration_f64(),
350 stagger: default_char_stagger_f64(),
351 granularity: TextAnimGranularity::default(),
352 easing: EasingType::default(),
353 overshoot: None,
354 blur: None,
355 direction: TextAnimDirection::default(),
356 distance: None,
357 scale_from: None,
358 jitter: None,
359 seed: None,
360 ink_from: None,
361 }
362 }
363}
364
365fn default_char_stagger_f64() -> f64 {
366 0.03
367}
368
369fn default_char_duration_f64() -> f64 {
370 0.4
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
375pub struct CharAnimation {
376 #[serde(default = "default_char_preset")]
378 pub preset: CharAnimPreset,
379 #[serde(default)]
381 pub granularity: TextAnimGranularity,
382 #[serde(default = "default_char_stagger")]
384 pub stagger: f32,
385 #[serde(default = "default_char_duration")]
387 pub duration: f32,
388 #[serde(default)]
390 pub easing: EasingType,
391 #[serde(default)]
393 pub delay: f32,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
398#[serde(rename_all = "snake_case")]
399#[derive(PartialEq)]
400pub enum TextAnimGranularity {
401 #[default]
403 Char,
404 Word,
406}
407
408#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
419#[serde(rename_all = "snake_case")]
420pub enum TextAnimDirection {
421 #[default]
423 Up,
424 Down,
426 Left,
428 Right,
430}
431
432impl TextAnimDirection {
433 pub fn offset(self, travel: f32) -> (f32, f32) {
436 match self {
437 Self::Up => (0.0, travel),
438 Self::Down => (0.0, -travel),
439 Self::Left => (travel, 0.0),
440 Self::Right => (-travel, 0.0),
441 }
442 }
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
446#[serde(rename_all = "snake_case")]
447#[derive(PartialEq)]
448pub enum CharAnimPreset {
449 #[default]
451 ScaleIn,
452 FadeIn,
454 Wave,
456 Bounce,
458 RotateIn,
460 SlideUp,
462 BlurIn,
466}
467
468fn default_char_preset() -> CharAnimPreset {
469 CharAnimPreset::ScaleIn
470}
471
472fn default_char_stagger() -> f32 {
473 0.03
474}
475
476fn default_char_duration() -> f32 {
477 0.4
478}
479
480impl AnimationTiming {
481 pub fn to_preset_config(&self) -> PresetConfig {
483 PresetConfig {
484 amplitude: self.amplitude,
485 delay: self.delay,
486 duration: self.duration,
487 repeat: self.repeat,
488 overshoot: self.overshoot,
489 spring: self.spring.clone(),
490 }
491 }
492}
493
494const KNOWN_MOTION_PROPERTIES: &[&str] = &[
513 "opacity",
514 "position.x",
515 "translate_x",
516 "position.y",
517 "translate_y",
518 "scale",
519 "scale.x",
520 "scale.y",
521 "rotation",
522 "rotate_x",
523 "rotate_y",
524 "blur",
525 "visible_chars",
526 "visible_chars_progress",
527 "border_radius",
528 "font_size",
529 "width",
530 "height",
531 "gap",
532 "padding",
533 "stroke_width",
534 "shadow_blur",
535 "glow_radius",
536 "glow_intensity",
537 "perspective",
538 "draw_progress",
539 "motion_progress",
540 "color",
541];
542
543fn validate_motion_property<E: serde::de::Error>(value: &str) -> Result<(), E> {
551 if KNOWN_MOTION_PROPERTIES.contains(&value) {
552 return Ok(());
553 }
554 let normalize = |s: &str| s.replace(['-', ' '], "_").to_lowercase();
555 let normalized = normalize(value);
556 if let Some(suggestion) = KNOWN_MOTION_PROPERTIES
557 .iter()
558 .find(|known| normalize(known) == normalized)
559 {
560 Err(E::custom(format!(
561 "unknown animation property '{value}' — did you mean '{suggestion}'?"
562 )))
563 } else {
564 Err(E::custom(format!(
565 "unknown animation property '{value}': expected one of {}",
566 KNOWN_MOTION_PROPERTIES.join(", ")
567 )))
568 }
569}
570
571fn deserialize_motion_property<'de, D>(deserializer: D) -> Result<String, D::Error>
572where
573 D: serde::Deserializer<'de>,
574{
575 let s = String::deserialize(deserializer)?;
576 validate_motion_property::<D::Error>(&s)?;
577 Ok(s)
578}
579
580fn deserialize_validated_keyframes<'de, D>(deserializer: D) -> Result<Vec<Animation>, D::Error>
586where
587 D: serde::Deserializer<'de>,
588{
589 let animations = Vec::<Animation>::deserialize(deserializer)?;
590 for anim in &animations {
591 validate_motion_property::<D::Error>(&anim.property)?;
592 }
593 Ok(animations)
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
598#[serde(deny_unknown_fields)]
599pub struct KeyframesConfig {
600 #[serde(deserialize_with = "deserialize_validated_keyframes")]
601 pub keyframes: Vec<Animation>,
602 #[serde(default)]
603 pub delay: f64,
604 #[serde(default = "default_animation_duration")]
605 pub duration: f64,
606 #[serde(default, rename = "loop")]
607 pub repeat: bool,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
620#[serde(deny_unknown_fields)]
621pub struct MotionBlurConfig {
622 #[serde(default)]
626 pub intensity: f32,
627 #[serde(default = "default_motion_blur_samples")]
630 pub samples: u32,
631 #[serde(default = "default_motion_blur_shutter")]
634 pub shutter: f64,
635}
636
637fn default_motion_blur_samples() -> u32 {
638 6
639}
640fn default_motion_blur_shutter() -> f64 {
641 0.5
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
651#[serde(deny_unknown_fields)]
652pub struct TrailConfig {
653 #[serde(default = "default_trail_copies")]
655 pub copies: u32,
656 #[serde(default = "default_trail_spacing")]
658 pub spacing: f64,
659 #[serde(default = "default_trail_falloff")]
662 pub falloff: f32,
663}
664
665fn default_trail_copies() -> u32 {
666 4
667}
668fn default_trail_spacing() -> f64 {
669 0.05
670}
671fn default_trail_falloff() -> f32 {
672 0.6
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
680#[serde(deny_unknown_fields)]
681pub struct OrbitConfig {
682 #[serde(default = "default_orbit_radius")]
684 pub radius_x: f64,
685 #[serde(default = "default_orbit_radius")]
687 pub radius_y: f64,
688 #[serde(default = "default_orbit_speed")]
690 pub speed: f64,
691 #[serde(default)]
693 pub start_angle: f64,
694 #[serde(default = "default_orbit_depth")]
696 pub depth: f64,
697 #[serde(default)]
699 pub opacity_depth: f64,
700 #[serde(default)]
702 pub tilt: f64,
703 #[serde(default)]
705 pub phase: f64,
706}
707
708fn default_orbit_radius() -> f64 {
709 30.0
710}
711fn default_orbit_speed() -> f64 {
712 0.5
713}
714fn default_orbit_depth() -> f64 {
715 0.15
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
721#[serde(deny_unknown_fields)]
722pub struct WiggleConfig {
723 #[serde(deserialize_with = "deserialize_motion_property")]
724 pub property: String,
725 pub amplitude: f64,
726 pub frequency: f64,
727 #[serde(default)]
728 pub seed: u64,
729 #[serde(default)]
731 pub octaves: Option<u32>,
732 #[serde(default)]
734 pub phase: Option<f64>,
735 #[serde(default)]
737 pub decay: Option<f64>,
738 #[serde(default)]
740 pub easing: Option<EasingType>,
741 #[serde(default)]
743 pub mode: Option<String>,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
795#[serde(deny_unknown_fields)]
796pub struct MotionPathConfig {
797 #[serde(deserialize_with = "deserialize_motion_path_data")]
801 pub path: String,
802 #[serde(default)]
807 pub delay: f64,
808 #[serde(default = "default_animation_duration")]
810 pub duration: f64,
811 #[serde(default, rename = "loop")]
814 pub repeat: bool,
815 #[serde(default)]
819 pub orient: bool,
820 #[serde(default)]
824 pub orient_offset: f64,
825 #[serde(default)]
829 pub easing: EasingType,
830}
831
832fn deserialize_motion_path_data<'de, D>(deserializer: D) -> Result<String, D::Error>
846where
847 D: serde::Deserializer<'de>,
848{
849 let s = String::deserialize(deserializer)?;
850 match SkiaPath::from_svg(&s) {
851 Some(path) if path.count_points() > 0 => Ok(s),
852 Some(_) => Err(serde::de::Error::custom(format!(
853 "motion_path.path '{s}' has no drawable point (an empty path has nothing to travel \
854 to) — provide at least one command, e.g. \"M0,0 L100,0\""
855 ))),
856 None => Err(serde::de::Error::custom(format!(
857 "motion_path.path '{s}' is not valid SVG path data (the same 'd'-attribute \
858 mini-language shape's type: \"path\" accepts), e.g. \"M0,0 C50,-100 150,-100 200,0\""
859 ))),
860 }
861}
862
863#[derive(Debug, Serialize, Deserialize, JsonSchema)]
866#[serde(rename_all = "snake_case")]
867pub enum ShapeType {
868 Rect,
869 Circle,
870 RoundedRect,
871 Ellipse,
872 Triangle,
873 Star {
874 #[serde(default = "default_star_points")]
875 points: u32,
876 },
877 Polygon {
878 #[serde(default = "default_polygon_sides")]
879 sides: u32,
880 },
881 Path {
882 data: String,
883 },
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
887#[serde(untagged)]
888pub enum Fill {
889 Solid(String),
890 Gradient(Gradient),
891}
892
893#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
894pub struct Gradient {
895 #[serde(rename = "type")]
896 pub gradient_type: GradientType,
897 pub colors: Vec<String>,
898 #[serde(default)]
899 pub stops: Option<Vec<f32>>,
900 #[serde(default)]
901 pub angle: Option<f32>,
902}
903
904#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
905#[serde(rename_all = "snake_case")]
906pub enum GradientType {
907 Linear,
908 Radial,
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
912pub struct Stroke {
913 pub color: String,
914 #[serde(default = "default_stroke_width")]
915 pub width: f32,
916}
917
918#[derive(Debug, Serialize, Deserialize, JsonSchema)]
919#[serde(rename_all = "snake_case")]
920#[derive(Default)]
921pub enum ImageFit {
922 Cover,
923 #[default]
924 Contain,
925 Fill,
926}
927
928#[derive(Debug, Serialize, Deserialize, JsonSchema)]
931pub struct ShapeText {
932 pub content: String,
933 #[serde(default = "default_font_size")]
934 pub font_size: f32,
935 #[serde(default = "default_color")]
936 pub color: String,
937 #[serde(default = "default_font_family")]
938 pub font_family: String,
939 #[serde(default)]
940 pub font_weight: FontWeight,
941 #[serde(default)]
942 pub align: TextAlign,
943 #[serde(default)]
944 pub vertical_align: VerticalAlign,
945 #[serde(default)]
946 pub line_height: Option<f32>,
947 #[serde(default)]
948 pub letter_spacing: Option<f32>,
949 #[serde(default)]
950 pub padding: Option<f32>,
951}
952
953#[derive(Debug, Serialize, Deserialize, JsonSchema)]
954pub struct CaptionWord {
955 pub text: String,
956 pub start: f64,
957 pub end: f64,
958}
959
960#[derive(Debug, Serialize, Deserialize, JsonSchema)]
961#[serde(rename_all = "snake_case")]
962#[derive(Default)]
963pub enum CaptionStyle {
964 #[default]
965 Highlight,
966 Karaoke,
967 WordByWord,
968 WordPop,
971 KaraokePop,
974}
975
976#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
977pub struct GradientBorder {
978 pub colors: Vec<String>,
979 #[serde(default = "default_gradient_border_width")]
980 pub width: f32,
981 #[serde(default)]
982 pub angle: f32,
983}
984
985fn default_gradient_border_width() -> f32 {
986 2.0
987}
988
989#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
991pub struct InnerShadow {
992 pub color: String,
993 #[serde(default)]
994 pub offset_x: f32,
995 #[serde(default)]
996 pub offset_y: f32,
997 #[serde(default = "default_inner_shadow_blur")]
998 pub blur: f32,
999}
1000
1001fn default_inner_shadow_blur() -> f32 {
1002 10.0
1003}
1004
1005#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1006pub struct TextShadow {
1007 #[serde(default = "default_shadow_color")]
1008 pub color: String,
1009 #[serde(default = "default_shadow_offset")]
1010 pub offset_x: f32,
1011 #[serde(default = "default_shadow_offset")]
1012 pub offset_y: f32,
1013 #[serde(default = "default_shadow_blur")]
1014 pub blur: f32,
1015}
1016
1017#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1018pub struct TextBackground {
1019 pub color: String,
1020 #[serde(default = "default_text_bg_padding")]
1021 pub padding: f32,
1022 #[serde(default)]
1023 pub corner_radius: f32,
1024}
1025
1026#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1030#[serde(deny_unknown_fields)]
1031pub struct GlowConfig {
1032 #[serde(default = "default_glow_color")]
1034 pub color: String,
1035 #[serde(default = "default_glow_radius")]
1037 pub radius: f32,
1038 #[serde(default = "default_glow_intensity")]
1040 pub intensity: f32,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1045#[serde(deny_unknown_fields)]
1046pub struct TextState {
1047 pub at: f64,
1049 pub content: String,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1060#[serde(deny_unknown_fields)]
1061pub struct TextSwapConfig {
1062 #[serde(default = "default_swap_duration")]
1064 pub duration: f64,
1065 #[serde(default = "default_swap_distance")]
1067 pub distance: f32,
1068 #[serde(default = "default_swap_blur")]
1071 pub blur: f32,
1072}
1073
1074impl Default for TextSwapConfig {
1075 fn default() -> Self {
1076 Self {
1077 duration: default_swap_duration(),
1078 distance: default_swap_distance(),
1079 blur: default_swap_blur(),
1080 }
1081 }
1082}
1083
1084fn default_swap_duration() -> f64 {
1085 0.45
1086}
1087
1088fn default_swap_distance() -> f32 {
1089 18.0
1090}
1091
1092fn default_swap_blur() -> f32 {
1093 8.0
1094}
1095
1096#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
1098#[serde(rename_all = "snake_case")]
1099pub enum CaretShape {
1100 #[default]
1102 Line,
1103 Block,
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1115#[serde(deny_unknown_fields)]
1116pub struct CaretConfig {
1117 #[serde(default)]
1119 pub shape: CaretShape,
1120 #[serde(default)]
1123 pub color: Option<String>,
1124 #[serde(default = "default_caret_blink")]
1127 pub blink: f32,
1128 #[serde(default)]
1131 pub hide_when_done: bool,
1132}
1133
1134impl Default for CaretConfig {
1135 fn default() -> Self {
1136 Self {
1137 shape: CaretShape::default(),
1138 color: None,
1139 blink: default_caret_blink(),
1140 hide_when_done: false,
1141 }
1142 }
1143}
1144
1145fn default_caret_blink() -> f32 {
1146 1.0
1147}
1148
1149#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
1158#[serde(deny_unknown_fields)]
1159pub struct ShimmerConfig {
1160 #[serde(default)]
1162 pub delay: f64,
1163 #[serde(default = "default_shimmer_duration")]
1165 pub duration: f64,
1166 #[serde(default = "default_shimmer_color")]
1168 pub color: String,
1169 #[serde(default = "default_shimmer_width")]
1173 pub width: f32,
1174 #[serde(default = "default_shimmer_intensity")]
1176 pub intensity: f32,
1177 #[serde(default = "default_shimmer_angle")]
1182 pub angle: f32,
1183 #[serde(default, rename = "loop")]
1185 pub repeat: bool,
1186}
1187
1188fn default_shimmer_duration() -> f64 {
1189 0.9
1190}
1191
1192fn default_shimmer_color() -> String {
1193 "#FFFFFF".to_string()
1194}
1195
1196fn default_shimmer_width() -> f32 {
1197 0.35
1198}
1199
1200fn default_shimmer_intensity() -> f32 {
1201 0.75
1202}
1203
1204fn default_shimmer_angle() -> f32 {
1205 20.0
1206}
1207
1208fn default_glow_color() -> String {
1209 "#FFFFFF80".to_string()
1210}
1211
1212fn default_glow_radius() -> f32 {
1213 10.0
1214}
1215
1216fn default_glow_intensity() -> f32 {
1217 1.0
1218}
1219
1220fn default_font_size() -> f32 {
1223 48.0
1224}
1225
1226fn default_color() -> String {
1227 "#FFFFFF".to_string()
1228}
1229
1230fn default_font_family() -> String {
1231 "Inter".to_string()
1232}
1233
1234fn default_stroke_width() -> f32 {
1235 2.0
1236}
1237
1238fn default_star_points() -> u32 {
1239 5
1240}
1241
1242fn default_polygon_sides() -> u32 {
1243 6
1244}
1245
1246fn default_shadow_color() -> String {
1247 "#00000080".to_string()
1248}
1249
1250fn default_shadow_offset() -> f32 {
1251 2.0
1252}
1253
1254fn default_shadow_blur() -> f32 {
1255 4.0
1256}
1257
1258fn default_text_bg_padding() -> f32 {
1259 8.0
1260}
1261
1262#[cfg(test)]
1263mod motion_property_tests {
1264 use super::*;
1265 use serde_json::json;
1266
1267 #[test]
1272 fn wiggle_known_property_still_works() {
1273 let json = json!({
1274 "name": "wiggle",
1275 "property": "translate_x",
1276 "amplitude": 10.0,
1277 "frequency": 1.0
1278 });
1279 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1280 match effect {
1281 AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "translate_x"),
1282 other => panic!("expected Wiggle, got {other:?}"),
1283 }
1284 }
1285
1286 #[test]
1287 fn wiggle_unknown_property_is_a_named_error_not_a_silent_no_op() {
1288 let json = json!({
1292 "name": "wiggle",
1293 "property": "skew",
1294 "amplitude": 10.0,
1295 "frequency": 1.0
1296 });
1297 let err = serde_json::from_value::<AnimationEffect>(json)
1298 .expect_err("an unrecognised wiggle property must be rejected, not silently inert");
1299 assert!(err.to_string().contains("skew"), "got: {err}");
1300 }
1301
1302 #[test]
1303 fn wiggle_kebab_case_property_gets_a_did_you_mean() {
1304 let json = json!({
1310 "name": "wiggle",
1311 "property": "translate-x",
1312 "amplitude": 10.0,
1313 "frequency": 1.0
1314 });
1315 let err = serde_json::from_value::<AnimationEffect>(json)
1316 .expect_err("kebab-case must not silently resolve to a snake_case no-op");
1317 let msg = err.to_string();
1318 assert!(msg.contains("translate-x"), "got: {msg}");
1319 assert!(
1320 msg.contains("translate_x"),
1321 "expected a did-you-mean nudge toward the correct spelling, got: {msg}"
1322 );
1323 }
1324
1325 #[test]
1326 fn keyframes_animation_known_property_still_works() {
1327 let json = json!({
1328 "name": "keyframes",
1329 "keyframes": [
1330 { "property": "opacity", "keyframes": [
1331 { "time": 0.0, "value": 0.0 },
1332 { "time": 1.0, "value": 1.0 }
1333 ]}
1334 ]
1335 });
1336 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1337 match effect {
1338 AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "opacity"),
1339 other => panic!("expected Keyframes, got {other:?}"),
1340 }
1341 }
1342
1343 #[test]
1344 fn keyframes_animation_unknown_property_is_a_named_error() {
1345 let json = json!({
1346 "name": "keyframes",
1347 "keyframes": [
1348 { "property": "positionX", "keyframes": [
1349 { "time": 0.0, "value": 0.0 },
1350 { "time": 1.0, "value": 1.0 }
1351 ]}
1352 ]
1353 });
1354 let err = serde_json::from_value::<AnimationEffect>(json).expect_err(
1355 "an unrecognised keyframe animation property must be rejected, not silently inert",
1356 );
1357 assert!(err.to_string().contains("positionX"), "got: {err}");
1358 }
1359
1360 #[test]
1361 fn keyframes_animation_color_property_still_works() {
1362 let json = json!({
1366 "name": "keyframes",
1367 "keyframes": [
1368 { "property": "color", "keyframes": [
1369 { "time": 0.0, "value": "#000000" },
1370 { "time": 1.0, "value": "#ffffff" }
1371 ]}
1372 ]
1373 });
1374 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1375 assert!(matches!(effect, AnimationEffect::Keyframes(_)));
1376 }
1377}
1378
1379#[cfg(test)]
1380mod motion_path_schema_tests {
1381 use super::*;
1382 use serde_json::json;
1383
1384 #[test]
1385 fn motion_path_deserializes_with_known_fields() {
1386 let json = json!({
1387 "name": "motion_path",
1388 "path": "M0,0 L100,0 L100,100",
1389 "delay": 0.2,
1390 "duration": 1.5,
1391 "loop": true,
1392 "orient": true,
1393 "orient_offset": 90.0,
1394 "easing": "ease_out"
1395 });
1396 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1397 match effect {
1398 AnimationEffect::MotionPath(cfg) => {
1399 assert_eq!(cfg.path, "M0,0 L100,0 L100,100");
1400 assert_eq!(cfg.delay, 0.2);
1401 assert_eq!(cfg.duration, 1.5);
1402 assert!(cfg.repeat);
1403 assert!(cfg.orient);
1404 assert_eq!(cfg.orient_offset, 90.0);
1405 assert_eq!(cfg.easing, EasingType::EaseOut);
1406 }
1407 other => panic!("expected MotionPath, got {other:?}"),
1408 }
1409 }
1410
1411 #[test]
1412 fn motion_path_defaults_match_documented_values() {
1413 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0" });
1414 let effect: AnimationEffect = serde_json::from_value(json).unwrap();
1415 match effect {
1416 AnimationEffect::MotionPath(cfg) => {
1417 assert_eq!(cfg.delay, 0.0);
1418 assert_eq!(cfg.duration, default_animation_duration());
1419 assert!(!cfg.repeat);
1420 assert!(!cfg.orient);
1421 assert_eq!(cfg.orient_offset, 0.0);
1422 assert_eq!(cfg.easing, EasingType::Linear);
1423 }
1424 other => panic!("expected MotionPath, got {other:?}"),
1425 }
1426 }
1427
1428 #[test]
1432 fn motion_path_rejects_an_empty_path_string() {
1433 let json = json!({ "name": "motion_path", "path": "" });
1434 let err = serde_json::from_value::<AnimationEffect>(json)
1435 .expect_err("an empty motion_path.path must be rejected, not silently accepted");
1436 assert!(err.to_string().contains("no drawable point"), "got: {err}");
1437 }
1438
1439 #[test]
1440 fn motion_path_rejects_unparsable_svg_path_data() {
1441 let json = json!({ "name": "motion_path", "path": "this is not svg path data !!" });
1442 let err = serde_json::from_value::<AnimationEffect>(json)
1443 .expect_err("garbage path data must be rejected, not silently accepted");
1444 assert!(
1445 err.to_string().contains("not valid SVG path data"),
1446 "got: {err}"
1447 );
1448 }
1449
1450 #[test]
1456 fn motion_path_accepts_a_single_point_path() {
1457 let json = json!({ "name": "motion_path", "path": "M50,50" });
1458 let effect: AnimationEffect = serde_json::from_value(json)
1459 .expect("a syntactically valid single-point path must be accepted");
1460 assert!(matches!(effect, AnimationEffect::MotionPath(_)));
1461 }
1462
1463 #[test]
1464 fn motion_path_rejects_unknown_fields() {
1465 let json = json!({ "name": "motion_path", "path": "M0,0 L10,0", "detla": 0.2 });
1466 let err = serde_json::from_value::<AnimationEffect>(json)
1467 .expect_err("a typo'd field on motion_path must be rejected, not silently ignored");
1468 assert!(err.to_string().contains("detla"), "got: {err}");
1469 }
1470
1471 #[test]
1472 fn motion_path_shift_delay_shifts_the_configs_own_delay() {
1473 let mut effect = AnimationEffect::MotionPath(MotionPathConfig {
1474 path: "M0,0 L10,0".to_string(),
1475 delay: 0.5,
1476 duration: 1.0,
1477 repeat: false,
1478 orient: false,
1479 orient_offset: 0.0,
1480 easing: EasingType::Linear,
1481 });
1482 effect.shift_delay(0.25);
1483 match effect {
1484 AnimationEffect::MotionPath(cfg) => assert!((cfg.delay - 0.75).abs() < 1e-9),
1485 other => panic!("expected MotionPath, got {other:?}"),
1486 }
1487 }
1488}