use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::Path as SkiaPath;
use super::animation::{
Animation, AnimationPreset, ChromaticAberrationConfig, EasingType, PresetConfig, SpringConfig,
};
use super::style::{FontWeight, TextAlign, VerticalAlign};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AnimationEffect {
FadeIn(AnimationTiming),
FadeInUp(AnimationTiming),
FadeInDown(AnimationTiming),
FadeInLeft(AnimationTiming),
FadeInRight(AnimationTiming),
SlideInLeft(AnimationTiming),
SlideInRight(AnimationTiming),
SlideInUp(AnimationTiming),
SlideInDown(AnimationTiming),
ScaleIn(AnimationTiming),
BounceIn(AnimationTiming),
BlurIn(AnimationTiming),
RotateIn(AnimationTiming),
ElasticIn(AnimationTiming),
PopIn(AnimationTiming),
FadeOut(AnimationTiming),
FadeOutUp(AnimationTiming),
FadeOutDown(AnimationTiming),
SlideOutLeft(AnimationTiming),
SlideOutRight(AnimationTiming),
SlideOutUp(AnimationTiming),
SlideOutDown(AnimationTiming),
ScaleOut(AnimationTiming),
BounceOut(AnimationTiming),
BlurOut(AnimationTiming),
RotateOut(AnimationTiming),
Pulse(AnimationTiming),
Float(AnimationTiming),
Shake(AnimationTiming),
Spin(AnimationTiming),
FlipInX(AnimationTiming),
FlipInY(AnimationTiming),
FlipOutX(AnimationTiming),
FlipOutY(AnimationTiming),
TiltIn(TiltInConfig),
DrawIn(AnimationTiming),
StrokeReveal(AnimationTiming),
Typewriter(AnimationTiming),
WipeLeft(AnimationTiming),
WipeRight(AnimationTiming),
#[serde(alias = "float_3d")]
Float3d(AnimationTiming),
CharScaleIn(CharAnimationTiming),
CharFadeIn(CharAnimationTiming),
CharWave(CharAnimationTiming),
CharBounce(CharAnimationTiming),
CharRotateIn(CharAnimationTiming),
CharSlideUp(CharAnimationTiming),
CharBlurIn(CharAnimationTiming),
Glow(GlowConfig),
Shimmer(ShimmerConfig),
Wiggle(WiggleConfig),
Orbit(OrbitConfig),
Keyframes(KeyframesConfig),
MotionBlur(MotionBlurConfig),
Trail(TrailConfig),
MotionPath(MotionPathConfig),
ChromaticAberration(ChromaticAberrationConfig),
Shatter(ShatterConfig),
Burst(BurstConfig),
}
impl AnimationEffect {
pub fn shift_delay(&mut self, by: f64) {
use AnimationEffect::*;
match self {
FadeIn(t) | FadeInUp(t) | FadeInDown(t) | FadeInLeft(t) | FadeInRight(t)
| SlideInLeft(t) | SlideInRight(t) | SlideInUp(t) | SlideInDown(t) | ScaleIn(t)
| BounceIn(t) | BlurIn(t) | RotateIn(t) | ElasticIn(t) | PopIn(t) | FadeOut(t)
| FadeOutUp(t) | FadeOutDown(t) | SlideOutLeft(t) | SlideOutRight(t)
| SlideOutUp(t) | SlideOutDown(t) | ScaleOut(t) | BounceOut(t) | BlurOut(t)
| RotateOut(t) | Pulse(t) | Float(t) | Shake(t) | Spin(t) | FlipInX(t) | FlipInY(t)
| FlipOutX(t) | FlipOutY(t) | DrawIn(t) | StrokeReveal(t) | Typewriter(t)
| WipeLeft(t) | WipeRight(t) | Float3d(t) => t.delay += by,
TiltIn(c) => c.delay += by,
CharScaleIn(c) | CharFadeIn(c) | CharWave(c) | CharBounce(c) | CharRotateIn(c)
| CharSlideUp(c) | CharBlurIn(c) => c.delay += by,
Keyframes(c) => c.delay += by,
MotionPath(c) => c.delay += by,
Shimmer(c) => c.delay += by,
ChromaticAberration(c) => c.delay += by,
Shatter(c) => c.delay += by,
Burst(c) => c.delay += by,
Glow(_) | Wiggle(_) | Orbit(_) | MotionBlur(_) | Trail(_) => {}
}
}
pub fn as_preset(&self) -> Option<(AnimationPreset, &AnimationTiming)> {
match self {
Self::FadeIn(t) => Some((AnimationPreset::FadeIn, t)),
Self::FadeInUp(t) => Some((AnimationPreset::FadeInUp, t)),
Self::FadeInDown(t) => Some((AnimationPreset::FadeInDown, t)),
Self::FadeInLeft(t) => Some((AnimationPreset::FadeInLeft, t)),
Self::FadeInRight(t) => Some((AnimationPreset::FadeInRight, t)),
Self::SlideInLeft(t) => Some((AnimationPreset::SlideInLeft, t)),
Self::SlideInRight(t) => Some((AnimationPreset::SlideInRight, t)),
Self::SlideInUp(t) => Some((AnimationPreset::SlideInUp, t)),
Self::SlideInDown(t) => Some((AnimationPreset::SlideInDown, t)),
Self::ScaleIn(t) => Some((AnimationPreset::ScaleIn, t)),
Self::BounceIn(t) => Some((AnimationPreset::BounceIn, t)),
Self::BlurIn(t) => Some((AnimationPreset::BlurIn, t)),
Self::RotateIn(t) => Some((AnimationPreset::RotateIn, t)),
Self::ElasticIn(t) => Some((AnimationPreset::ElasticIn, t)),
Self::PopIn(t) => Some((AnimationPreset::PopIn, t)),
Self::FadeOut(t) => Some((AnimationPreset::FadeOut, t)),
Self::FadeOutUp(t) => Some((AnimationPreset::FadeOutUp, t)),
Self::FadeOutDown(t) => Some((AnimationPreset::FadeOutDown, t)),
Self::SlideOutLeft(t) => Some((AnimationPreset::SlideOutLeft, t)),
Self::SlideOutRight(t) => Some((AnimationPreset::SlideOutRight, t)),
Self::SlideOutUp(t) => Some((AnimationPreset::SlideOutUp, t)),
Self::SlideOutDown(t) => Some((AnimationPreset::SlideOutDown, t)),
Self::ScaleOut(t) => Some((AnimationPreset::ScaleOut, t)),
Self::BounceOut(t) => Some((AnimationPreset::BounceOut, t)),
Self::BlurOut(t) => Some((AnimationPreset::BlurOut, t)),
Self::RotateOut(t) => Some((AnimationPreset::RotateOut, t)),
Self::Pulse(t) => Some((AnimationPreset::Pulse, t)),
Self::Float(t) => Some((AnimationPreset::Float, t)),
Self::Shake(t) => Some((AnimationPreset::Shake, t)),
Self::Spin(t) => Some((AnimationPreset::Spin, t)),
Self::FlipInX(t) => Some((AnimationPreset::FlipInX, t)),
Self::FlipInY(t) => Some((AnimationPreset::FlipInY, t)),
Self::FlipOutX(t) => Some((AnimationPreset::FlipOutX, t)),
Self::FlipOutY(t) => Some((AnimationPreset::FlipOutY, t)),
Self::TiltIn(_) => None,
Self::DrawIn(t) => Some((AnimationPreset::DrawIn, t)),
Self::StrokeReveal(t) => Some((AnimationPreset::StrokeReveal, t)),
Self::Float3d(t) => Some((AnimationPreset::Float3d, t)),
Self::Typewriter(t) => Some((AnimationPreset::Typewriter, t)),
Self::WipeLeft(t) => Some((AnimationPreset::WipeLeft, t)),
Self::WipeRight(t) => Some((AnimationPreset::WipeRight, t)),
_ => None,
}
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq)]
pub struct AnimationTiming {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_animation_duration")]
pub duration: f64,
#[serde(rename = "loop")]
pub repeat: bool,
#[serde(default)]
pub repeat_count: Option<u32>,
#[serde(default)]
pub yoyo: bool,
#[serde(default)]
pub repeat_delay: f64,
#[serde(default)]
pub overshoot: Option<f64>,
#[serde(default)]
pub spring: Option<SpringConfig>,
#[serde(default)]
pub amplitude: Option<f64>,
}
fn default_animation_duration() -> f64 {
0.8
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
enum RepeatSpec {
Loop(bool),
Count(u32),
}
impl Default for RepeatSpec {
fn default() -> Self {
RepeatSpec::Loop(false)
}
}
impl RepeatSpec {
fn into_parts(self) -> (bool, Option<u32>) {
match self {
RepeatSpec::Loop(b) => (b, None),
RepeatSpec::Count(0) | RepeatSpec::Count(1) => (false, None),
RepeatSpec::Count(n) => (true, Some(n)),
}
}
fn from_parts(repeat: bool, repeat_count: Option<u32>) -> Self {
match repeat_count {
Some(n) => RepeatSpec::Count(n),
None => RepeatSpec::Loop(repeat),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct AnimationTimingWire {
#[serde(default)]
delay: f64,
#[serde(default = "default_animation_duration")]
duration: f64,
#[serde(default, rename = "loop")]
repeat: RepeatSpec,
#[serde(default)]
yoyo: bool,
#[serde(default)]
repeat_delay: f64,
#[serde(default)]
overshoot: Option<f64>,
#[serde(default)]
spring: Option<SpringConfig>,
#[serde(default)]
amplitude: Option<f64>,
}
impl From<AnimationTimingWire> for AnimationTiming {
fn from(wire: AnimationTimingWire) -> Self {
let (repeat, repeat_count) = wire.repeat.into_parts();
AnimationTiming {
delay: wire.delay,
duration: wire.duration,
repeat,
repeat_count,
yoyo: wire.yoyo,
repeat_delay: wire.repeat_delay,
overshoot: wire.overshoot,
spring: wire.spring,
amplitude: wire.amplitude,
}
}
}
impl From<&AnimationTiming> for AnimationTimingWire {
fn from(t: &AnimationTiming) -> Self {
AnimationTimingWire {
delay: t.delay,
duration: t.duration,
repeat: RepeatSpec::from_parts(t.repeat, t.repeat_count),
yoyo: t.yoyo,
repeat_delay: t.repeat_delay,
overshoot: t.overshoot,
spring: t.spring.clone(),
amplitude: t.amplitude,
}
}
}
impl<'de> Deserialize<'de> for AnimationTiming {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
AnimationTimingWire::deserialize(deserializer).map(AnimationTiming::from)
}
}
impl Serialize for AnimationTiming {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
AnimationTimingWire::from(self).serialize(serializer)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TiltInConfig {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_animation_duration")]
pub duration: f64,
#[serde(default, rename = "loop")]
pub repeat: bool,
#[serde(default)]
pub rotate_x: Option<f64>,
#[serde(default)]
pub rotate_y: Option<f64>,
#[serde(default)]
pub perspective: Option<f64>,
#[serde(default)]
pub scale_from: Option<f64>,
}
impl Default for AnimationTiming {
fn default() -> Self {
Self {
delay: 0.0,
duration: 0.8,
repeat: false,
repeat_count: None,
yoyo: false,
repeat_delay: 0.0,
overshoot: None,
spring: None,
amplitude: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct CharAnimationTiming {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_char_duration_f64")]
pub duration: f64,
#[serde(default = "default_char_stagger_f64")]
pub stagger: f64,
#[serde(default)]
pub granularity: TextAnimGranularity,
#[serde(default)]
pub easing: EasingType,
#[serde(default)]
pub overshoot: Option<f64>,
#[serde(default)]
pub blur: Option<f64>,
#[serde(default)]
pub direction: TextAnimDirection,
#[serde(default)]
pub distance: Option<f64>,
#[serde(default)]
pub scale_from: Option<f64>,
#[serde(default)]
pub jitter: Option<f64>,
#[serde(default)]
pub seed: Option<u32>,
#[serde(default)]
pub ink_from: Option<String>,
#[serde(default)]
pub rotate_from: Option<f64>,
#[serde(default)]
pub rotate_origin: RotateOrigin,
#[serde(default)]
pub scale_jitter: Option<f64>,
#[serde(default)]
pub baseline_jitter: Option<f64>,
#[serde(default)]
pub reflow: bool,
#[serde(default)]
pub reflow_easing: EasingType,
}
impl Default for CharAnimationTiming {
fn default() -> Self {
Self {
delay: 0.0,
duration: default_char_duration_f64(),
stagger: default_char_stagger_f64(),
granularity: TextAnimGranularity::default(),
easing: EasingType::default(),
overshoot: None,
blur: None,
direction: TextAnimDirection::default(),
distance: None,
scale_from: None,
jitter: None,
seed: None,
ink_from: None,
rotate_from: None,
rotate_origin: RotateOrigin::default(),
scale_jitter: None,
baseline_jitter: None,
reflow: false,
reflow_easing: EasingType::default(),
}
}
}
fn default_char_stagger_f64() -> f64 {
0.03
}
fn default_char_duration_f64() -> f64 {
0.4
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CharAnimation {
#[serde(default = "default_char_preset")]
pub preset: CharAnimPreset,
#[serde(default)]
pub granularity: TextAnimGranularity,
#[serde(default = "default_char_stagger")]
pub stagger: f32,
#[serde(default = "default_char_duration")]
pub duration: f32,
#[serde(default)]
pub easing: EasingType,
#[serde(default)]
pub delay: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
#[derive(PartialEq)]
pub enum TextAnimGranularity {
#[default]
Char,
Word,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TextAnimDirection {
#[default]
Up,
Down,
Left,
Right,
}
impl TextAnimDirection {
pub fn offset(self, travel: f32) -> (f32, f32) {
match self {
Self::Up => (0.0, travel),
Self::Down => (0.0, -travel),
Self::Left => (travel, 0.0),
Self::Right => (-travel, 0.0),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum RotateOrigin {
#[default]
Center,
Edge,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
#[derive(PartialEq)]
pub enum CharAnimPreset {
#[default]
ScaleIn,
FadeIn,
Wave,
Bounce,
RotateIn,
SlideUp,
BlurIn,
}
fn default_char_preset() -> CharAnimPreset {
CharAnimPreset::ScaleIn
}
fn default_char_stagger() -> f32 {
0.03
}
fn default_char_duration() -> f32 {
0.4
}
impl AnimationTiming {
pub fn to_preset_config(&self) -> PresetConfig {
PresetConfig {
amplitude: self.amplitude,
delay: self.delay,
duration: self.duration,
repeat: self.repeat,
repeat_count: self.repeat_count,
yoyo: self.yoyo,
repeat_delay: self.repeat_delay,
overshoot: self.overshoot,
spring: self.spring.clone(),
}
}
}
const KNOWN_MOTION_PROPERTIES: &[&str] = &[
"opacity",
"position.x",
"translate_x",
"position.y",
"translate_y",
"scale",
"scale.x",
"scale.y",
"rotation",
"rotate_x",
"rotate_y",
"blur",
"blur_x",
"blur_y",
"visible_chars",
"visible_chars_progress",
"border_radius",
"font_size",
"width",
"height",
"gap",
"padding",
"stroke_width",
"shadow_blur",
"glow_radius",
"glow_intensity",
"perspective",
"draw_progress",
"motion_progress",
"color",
"clip_path_progress",
"letter_spacing",
"draw_start",
"draw_offset",
];
fn validate_motion_property<E: serde::de::Error>(value: &str) -> Result<(), E> {
if KNOWN_MOTION_PROPERTIES.contains(&value) {
return Ok(());
}
let normalize = |s: &str| s.replace(['-', ' '], "_").to_lowercase();
let normalized = normalize(value);
if let Some(suggestion) = KNOWN_MOTION_PROPERTIES
.iter()
.find(|known| normalize(known) == normalized)
{
Err(E::custom(format!(
"unknown animation property '{value}' — did you mean '{suggestion}'?"
)))
} else {
Err(E::custom(format!(
"unknown animation property '{value}': expected one of {}",
KNOWN_MOTION_PROPERTIES.join(", ")
)))
}
}
fn deserialize_motion_property<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
validate_motion_property::<D::Error>(&s)?;
Ok(s)
}
fn deserialize_validated_keyframes<'de, D>(deserializer: D) -> Result<Vec<Animation>, D::Error>
where
D: serde::Deserializer<'de>,
{
let animations = Vec::<Animation>::deserialize(deserializer)?;
for anim in &animations {
validate_motion_property::<D::Error>(&anim.property)?;
}
Ok(animations)
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct KeyframesConfig {
#[serde(deserialize_with = "deserialize_validated_keyframes")]
pub keyframes: Vec<Animation>,
#[serde(default)]
pub delay: f64,
#[serde(default = "default_animation_duration")]
pub duration: f64,
#[serde(default, rename = "loop")]
pub repeat: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SvgPathMorph {
pub target: String,
pub keyframes: Vec<SvgPathMorphKeyframe>,
#[serde(default)]
pub easing: EasingType,
#[serde(default)]
pub repeat: bool,
#[serde(default)]
pub yoyo: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SvgPathMorphKeyframe {
pub time: f64,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MotionBlurConfig {
#[serde(default)]
pub intensity: f32,
#[serde(default = "default_motion_blur_samples")]
pub samples: u32,
#[serde(default = "default_motion_blur_shutter")]
pub shutter: f64,
#[serde(default)]
pub mode: MotionBlurMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MotionBlurMode {
#[default]
Stack,
Smear,
}
fn default_motion_blur_samples() -> u32 {
6
}
fn default_motion_blur_shutter() -> f64 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct TrailConfig {
#[serde(default = "default_trail_copies")]
pub copies: u32,
#[serde(default = "default_trail_spacing")]
pub spacing: f64,
#[serde(default = "default_trail_falloff")]
pub falloff: f32,
}
fn default_trail_copies() -> u32 {
4
}
fn default_trail_spacing() -> f64 {
0.05
}
fn default_trail_falloff() -> f32 {
0.6
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct OrbitConfig {
#[serde(default = "default_orbit_radius")]
pub radius_x: f64,
#[serde(default = "default_orbit_radius")]
pub radius_y: f64,
#[serde(default = "default_orbit_speed")]
pub speed: f64,
#[serde(default)]
pub start_angle: f64,
#[serde(default = "default_orbit_depth")]
pub depth: f64,
#[serde(default)]
pub opacity_depth: f64,
#[serde(default)]
pub tilt: f64,
#[serde(default)]
pub phase: f64,
}
fn default_orbit_radius() -> f64 {
30.0
}
fn default_orbit_speed() -> f64 {
0.5
}
fn default_orbit_depth() -> f64 {
0.15
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct WiggleConfig {
#[serde(deserialize_with = "deserialize_motion_property")]
pub property: String,
pub amplitude: f64,
pub frequency: f64,
#[serde(default)]
pub seed: u64,
#[serde(default)]
pub octaves: Option<u32>,
#[serde(default)]
pub phase: Option<f64>,
#[serde(default)]
pub decay: Option<f64>,
#[serde(default)]
pub easing: Option<EasingType>,
#[serde(default)]
pub mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MotionPathConfig {
#[serde(deserialize_with = "deserialize_motion_path_data")]
pub path: String,
#[serde(default)]
pub delay: f64,
#[serde(default = "default_animation_duration")]
pub duration: f64,
#[serde(default, rename = "loop")]
pub repeat: bool,
#[serde(default)]
pub orient: bool,
#[serde(default)]
pub orient_offset: f64,
#[serde(default)]
pub easing: EasingType,
}
fn deserialize_motion_path_data<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match SkiaPath::from_svg(&s) {
Some(path) if path.count_points() > 0 => Ok(s),
Some(_) => Err(serde::de::Error::custom(format!(
"motion_path.path '{s}' has no drawable point (an empty path has nothing to travel \
to) — provide at least one command, e.g. \"M0,0 L100,0\""
))),
None => Err(serde::de::Error::custom(format!(
"motion_path.path '{s}' is not valid SVG path data (the same 'd'-attribute \
mini-language shape's type: \"path\" accepts), e.g. \"M0,0 C50,-100 150,-100 200,0\""
))),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ShatterConfig {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_shatter_duration")]
pub duration: f64,
#[serde(default)]
pub mode: ShatterMode,
#[serde(default = "default_shatter_pieces")]
pub pieces: u32,
#[serde(default)]
pub seed: u32,
#[serde(default)]
pub origin: ShatterOrigin,
#[serde(default = "default_shatter_spread")]
pub spread: f32,
#[serde(default = "default_shatter_spin")]
pub spin: f32,
#[serde(default = "default_shatter_depth")]
pub depth: f32,
#[serde(default = "default_true")]
pub fade: bool,
}
fn default_shatter_duration() -> f64 {
0.6
}
fn default_shatter_pieces() -> u32 {
24
}
fn default_shatter_spread() -> f32 {
1.0
}
fn default_shatter_spin() -> f32 {
90.0
}
fn default_shatter_depth() -> f32 {
0.4
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ShatterMode {
#[default]
Out,
In,
Hold,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ShatterOrigin {
#[serde(default = "default_shatter_origin_component")]
pub x: f32,
#[serde(default = "default_shatter_origin_component")]
pub y: f32,
}
impl Default for ShatterOrigin {
fn default() -> Self {
Self {
x: default_shatter_origin_component(),
y: default_shatter_origin_component(),
}
}
}
fn default_shatter_origin_component() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct BurstConfig {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_burst_duration")]
pub duration: f64,
#[serde(default = "default_burst_count")]
pub count: u32,
#[serde(default = "default_burst_length")]
pub length: f32,
#[serde(default = "default_burst_gap")]
pub gap: f32,
#[serde(default = "default_burst_width")]
pub width: f32,
#[serde(default = "default_burst_color")]
pub color: String,
#[serde(default)]
pub seed: u32,
#[serde(default = "default_burst_jitter")]
pub jitter: f32,
}
fn default_burst_duration() -> f64 {
0.4
}
fn default_burst_count() -> u32 {
8
}
fn default_burst_length() -> f32 {
40.0
}
fn default_burst_gap() -> f32 {
12.0
}
fn default_burst_width() -> f32 {
4.0
}
fn default_burst_color() -> String {
"#FFB020".to_string()
}
fn default_burst_jitter() -> f32 {
0.2
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ShapeType {
Rect,
Circle,
RoundedRect,
Ellipse,
Triangle,
Star {
#[serde(default = "default_star_points")]
points: u32,
},
Polygon {
#[serde(default = "default_polygon_sides")]
sides: u32,
},
Path {
data: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Fill {
Solid(String),
Gradient(Gradient),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Gradient {
#[serde(rename = "type")]
pub gradient_type: GradientType,
pub colors: Vec<String>,
#[serde(default)]
pub stops: Option<Vec<f32>>,
#[serde(default)]
pub angle: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GradientType {
Linear,
Radial,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Stroke {
pub color: String,
#[serde(default = "default_stroke_width")]
pub width: f32,
#[serde(default)]
pub dashed: Option<Vec<f32>>,
#[serde(default)]
pub dash_offset: Option<crate::expr::Computed<f32>>,
#[serde(default)]
pub line_cap: LineCap,
#[serde(default)]
pub line_join: LineJoin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum ImageFit {
Cover,
#[default]
Contain,
Fill,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ShapeText {
pub content: String,
#[serde(default = "default_font_size")]
pub font_size: f32,
#[serde(default = "default_color")]
pub color: String,
#[serde(default = "default_font_family")]
pub font_family: String,
#[serde(default)]
pub font_weight: FontWeight,
#[serde(default)]
pub align: TextAlign,
#[serde(default)]
pub vertical_align: VerticalAlign,
#[serde(default)]
pub line_height: Option<f32>,
#[serde(default)]
pub letter_spacing: Option<f32>,
#[serde(default)]
pub padding: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CaptionWord {
pub text: String,
pub start: f64,
pub end: f64,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum CaptionStyle {
#[default]
Highlight,
Karaoke,
WordByWord,
WordPop,
KaraokePop,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct GradientBorder {
pub colors: Vec<String>,
#[serde(default = "default_gradient_border_width")]
pub width: f32,
#[serde(default)]
pub angle: f32,
}
fn default_gradient_border_width() -> f32 {
2.0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct InnerShadow {
pub color: String,
#[serde(default)]
pub offset_x: f32,
#[serde(default)]
pub offset_y: f32,
#[serde(default = "default_inner_shadow_blur")]
pub blur: f32,
}
fn default_inner_shadow_blur() -> f32 {
10.0
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TextShadow {
#[serde(default = "default_shadow_color")]
pub color: String,
#[serde(default = "default_shadow_offset")]
pub offset_x: f32,
#[serde(default = "default_shadow_offset")]
pub offset_y: f32,
#[serde(default = "default_shadow_blur")]
pub blur: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TextBackground {
pub color: String,
#[serde(default = "default_text_bg_padding")]
pub padding: f32,
#[serde(default)]
pub corner_radius: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct GlowConfig {
#[serde(default = "default_glow_color")]
pub color: String,
#[serde(default = "default_glow_radius")]
pub radius: f32,
#[serde(default = "default_glow_intensity")]
pub intensity: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct TextState {
pub at: f64,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct TextSwapConfig {
#[serde(default = "default_swap_duration")]
pub duration: f64,
#[serde(default = "default_swap_distance")]
pub distance: f32,
#[serde(default = "default_swap_blur")]
pub blur: f32,
}
impl Default for TextSwapConfig {
fn default() -> Self {
Self {
duration: default_swap_duration(),
distance: default_swap_distance(),
blur: default_swap_blur(),
}
}
}
fn default_swap_duration() -> f64 {
0.45
}
fn default_swap_distance() -> f32 {
18.0
}
fn default_swap_blur() -> f32 {
8.0
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CaretShape {
#[default]
Line,
Block,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct CaretConfig {
#[serde(default)]
pub shape: CaretShape,
#[serde(default)]
pub color: Option<String>,
#[serde(default = "default_caret_blink")]
pub blink: f32,
#[serde(default)]
pub hide_when_done: bool,
}
impl Default for CaretConfig {
fn default() -> Self {
Self {
shape: CaretShape::default(),
color: None,
blink: default_caret_blink(),
hide_when_done: false,
}
}
}
fn default_caret_blink() -> f32 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ShimmerConfig {
#[serde(default)]
pub delay: f64,
#[serde(default = "default_shimmer_duration")]
pub duration: f64,
#[serde(default = "default_shimmer_color")]
pub color: String,
#[serde(default = "default_shimmer_width")]
pub width: f32,
#[serde(default = "default_shimmer_intensity")]
pub intensity: f32,
#[serde(default = "default_shimmer_angle")]
pub angle: f32,
#[serde(default, rename = "loop")]
pub repeat: bool,
}
fn default_shimmer_duration() -> f64 {
0.9
}
fn default_shimmer_color() -> String {
"#FFFFFF".to_string()
}
fn default_shimmer_width() -> f32 {
0.35
}
fn default_shimmer_intensity() -> f32 {
0.75
}
fn default_shimmer_angle() -> f32 {
20.0
}
fn default_glow_color() -> String {
"#FFFFFF80".to_string()
}
fn default_glow_radius() -> f32 {
10.0
}
fn default_glow_intensity() -> f32 {
1.0
}
fn default_font_size() -> f32 {
48.0
}
fn default_color() -> String {
"#FFFFFF".to_string()
}
fn default_font_family() -> String {
"Inter".to_string()
}
fn default_stroke_width() -> f32 {
2.0
}
fn default_star_points() -> u32 {
5
}
fn default_polygon_sides() -> u32 {
6
}
fn default_shadow_color() -> String {
"#00000080".to_string()
}
fn default_shadow_offset() -> f32 {
2.0
}
fn default_shadow_blur() -> f32 {
4.0
}
fn default_text_bg_padding() -> f32 {
8.0
}
#[cfg(test)]
mod motion_property_tests {
use super::*;
use serde_json::json;
#[test]
fn wiggle_known_property_still_works() {
let json = json!({
"name": "wiggle",
"property": "translate_x",
"amplitude": 10.0,
"frequency": 1.0
});
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "translate_x"),
other => panic!("expected Wiggle, got {other:?}"),
}
}
#[test]
fn wiggle_unknown_property_is_a_named_error_not_a_silent_no_op() {
let json = json!({
"name": "wiggle",
"property": "skew",
"amplitude": 10.0,
"frequency": 1.0
});
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("an unrecognised wiggle property must be rejected, not silently inert");
assert!(err.to_string().contains("skew"), "got: {err}");
}
#[test]
fn wiggle_kebab_case_property_gets_a_did_you_mean() {
let json = json!({
"name": "wiggle",
"property": "translate-x",
"amplitude": 10.0,
"frequency": 1.0
});
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("kebab-case must not silently resolve to a snake_case no-op");
let msg = err.to_string();
assert!(msg.contains("translate-x"), "got: {msg}");
assert!(
msg.contains("translate_x"),
"expected a did-you-mean nudge toward the correct spelling, got: {msg}"
);
}
#[test]
fn keyframes_animation_known_property_still_works() {
let json = json!({
"name": "keyframes",
"keyframes": [
{ "property": "opacity", "keyframes": [
{ "time": 0.0, "value": 0.0 },
{ "time": 1.0, "value": 1.0 }
]}
]
});
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "opacity"),
other => panic!("expected Keyframes, got {other:?}"),
}
}
#[test]
fn keyframes_animation_unknown_property_is_a_named_error() {
let json = json!({
"name": "keyframes",
"keyframes": [
{ "property": "positionX", "keyframes": [
{ "time": 0.0, "value": 0.0 },
{ "time": 1.0, "value": 1.0 }
]}
]
});
let err = serde_json::from_value::<AnimationEffect>(json).expect_err(
"an unrecognised keyframe animation property must be rejected, not silently inert",
);
assert!(err.to_string().contains("positionX"), "got: {err}");
}
#[test]
fn keyframes_animation_color_property_still_works() {
let json = json!({
"name": "keyframes",
"keyframes": [
{ "property": "color", "keyframes": [
{ "time": 0.0, "value": "#000000" },
{ "time": 1.0, "value": "#ffffff" }
]}
]
});
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
assert!(matches!(effect, AnimationEffect::Keyframes(_)));
}
#[test]
fn draw_offset_is_a_recognized_keyframe_animation_property() {
let json = json!({
"name": "keyframes",
"keyframes": [
{ "property": "draw_offset", "keyframes": [
{ "time": 0.0, "value": 0.0 },
{ "time": 1.0, "value": 1.0 }
]}
]
});
let effect: AnimationEffect = serde_json::from_value(json)
.expect("draw_offset must be a known animation property, not rejected as unknown");
match effect {
AnimationEffect::Keyframes(cfg) => assert_eq!(cfg.keyframes[0].property, "draw_offset"),
other => panic!("expected Keyframes, got {other:?}"),
}
}
#[test]
fn draw_offset_is_a_recognized_wiggle_property() {
let json = json!({
"name": "wiggle",
"property": "draw_offset",
"amplitude": 0.1,
"frequency": 1.0
});
let effect: AnimationEffect = serde_json::from_value(json)
.expect("draw_offset must be usable as a wiggle property, not rejected as unknown");
match effect {
AnimationEffect::Wiggle(cfg) => assert_eq!(cfg.property, "draw_offset"),
other => panic!("expected Wiggle, got {other:?}"),
}
}
}
#[cfg(test)]
mod svg_path_morph_schema_tests {
use super::*;
use serde_json::json;
#[test]
fn a_target_and_at_least_one_keyframe_parse() {
let json = json!({
"target": "#outline",
"keyframes": [
{ "time": 0.0, "value": "M0 0 L10 0" },
{ "time": 1.0, "value": "M0 0 L20 0" }
]
});
let morph: SvgPathMorph = serde_json::from_value(json).expect("valid SvgPathMorph JSON");
assert_eq!(morph.target, "#outline");
assert_eq!(morph.keyframes.len(), 2);
}
#[test]
fn a_missing_target_is_rejected_rather_than_silently_matching_nothing() {
let json = json!({
"keyframes": [
{ "time": 0.0, "value": "M0 0 L10 0" }
]
});
serde_json::from_value::<SvgPathMorph>(json).expect_err(
"target is required — without it there is no path to select inside the document",
);
}
#[test]
fn an_unknown_field_is_rejected_by_name() {
let json = json!({
"target": "#outline",
"keyframes": [{ "time": 0.0, "value": "M0 0 L10 0" }],
"duratoin": 1.0
});
let err = serde_json::from_value::<SvgPathMorph>(json)
.expect_err("a typo'd field must be rejected, not silently ignored");
assert!(err.to_string().contains("duratoin"), "got: {err}");
}
}
#[cfg(test)]
mod motion_path_schema_tests {
use super::*;
use serde_json::json;
#[test]
fn motion_path_deserializes_with_known_fields() {
let json = json!({
"name": "motion_path",
"path": "M0,0 L100,0 L100,100",
"delay": 0.2,
"duration": 1.5,
"loop": true,
"orient": true,
"orient_offset": 90.0,
"easing": "ease_out"
});
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::MotionPath(cfg) => {
assert_eq!(cfg.path, "M0,0 L100,0 L100,100");
assert_eq!(cfg.delay, 0.2);
assert_eq!(cfg.duration, 1.5);
assert!(cfg.repeat);
assert!(cfg.orient);
assert_eq!(cfg.orient_offset, 90.0);
assert_eq!(cfg.easing, EasingType::EaseOut);
}
other => panic!("expected MotionPath, got {other:?}"),
}
}
#[test]
fn motion_path_defaults_match_documented_values() {
let json = json!({ "name": "motion_path", "path": "M0,0 L10,0" });
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::MotionPath(cfg) => {
assert_eq!(cfg.delay, 0.0);
assert_eq!(cfg.duration, default_animation_duration());
assert!(!cfg.repeat);
assert!(!cfg.orient);
assert_eq!(cfg.orient_offset, 0.0);
assert_eq!(cfg.easing, EasingType::Linear);
}
other => panic!("expected MotionPath, got {other:?}"),
}
}
#[test]
fn motion_path_rejects_an_empty_path_string() {
let json = json!({ "name": "motion_path", "path": "" });
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("an empty motion_path.path must be rejected, not silently accepted");
assert!(err.to_string().contains("no drawable point"), "got: {err}");
}
#[test]
fn motion_path_rejects_unparsable_svg_path_data() {
let json = json!({ "name": "motion_path", "path": "this is not svg path data !!" });
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("garbage path data must be rejected, not silently accepted");
assert!(
err.to_string().contains("not valid SVG path data"),
"got: {err}"
);
}
#[test]
fn motion_path_accepts_a_single_point_path() {
let json = json!({ "name": "motion_path", "path": "M50,50" });
let effect: AnimationEffect = serde_json::from_value(json)
.expect("a syntactically valid single-point path must be accepted");
assert!(matches!(effect, AnimationEffect::MotionPath(_)));
}
#[test]
fn motion_path_rejects_unknown_fields() {
let json = json!({ "name": "motion_path", "path": "M0,0 L10,0", "detla": 0.2 });
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("a typo'd field on motion_path must be rejected, not silently ignored");
assert!(err.to_string().contains("detla"), "got: {err}");
}
#[test]
fn motion_path_shift_delay_shifts_the_configs_own_delay() {
let mut effect = AnimationEffect::MotionPath(MotionPathConfig {
path: "M0,0 L10,0".to_string(),
delay: 0.5,
duration: 1.0,
repeat: false,
orient: false,
orient_offset: 0.0,
easing: EasingType::Linear,
});
effect.shift_delay(0.25);
match effect {
AnimationEffect::MotionPath(cfg) => assert!((cfg.delay - 0.75).abs() < 1e-9),
other => panic!("expected MotionPath, got {other:?}"),
}
}
}
#[cfg(test)]
mod animation_timing_repeat_widening_tests {
use super::*;
use serde_json::json;
#[test]
fn loop_true_is_unchanged_infinite_repeat() {
let json = json!({ "name": "pulse", "loop": true });
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::Pulse(t) => {
assert!(t.repeat, "\"loop\": true must still set repeat = true");
assert_eq!(
t.repeat_count, None,
"a bare `true` carries no count — infinite, exactly as before"
);
assert!(!t.yoyo);
assert_eq!(t.repeat_delay, 0.0);
}
other => panic!("expected Pulse, got {other:?}"),
}
}
#[test]
fn loop_false_and_omitted_are_unchanged_and_identical() {
let explicit: AnimationTiming = serde_json::from_value(json!({ "loop": false })).unwrap();
let omitted: AnimationTiming = serde_json::from_value(json!({})).unwrap();
assert_eq!(
explicit, omitted,
"an explicit `false` and an omitted `loop` must resolve identically"
);
assert!(!explicit.repeat);
assert_eq!(explicit.repeat_count, None);
assert_eq!(explicit, AnimationTiming::default());
}
#[test]
fn loop_as_a_positive_integer_sets_repeat_and_the_count() {
let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
assert!(t.repeat, "a finite count still loops — see the field doc");
assert_eq!(t.repeat_count, Some(12));
}
#[test]
fn loop_as_zero_or_one_folds_back_to_the_boolean_form() {
for n in [0, 1] {
let t: AnimationTiming = serde_json::from_value(json!({ "loop": n })).unwrap();
assert!(!t.repeat, "loop: {n} must not set repeat");
assert_eq!(t.repeat_count, None, "loop: {n} must not carry a count");
}
}
#[test]
fn yoyo_and_repeat_delay_default_to_off_and_zero() {
let t: AnimationTiming = serde_json::from_value(json!({})).unwrap();
assert!(!t.yoyo);
assert_eq!(t.repeat_delay, 0.0);
}
#[test]
fn yoyo_and_repeat_delay_are_accepted_on_any_animation_timing() {
let json = json!({
"name": "shake",
"duration": 0.035,
"loop": 12,
"yoyo": true,
"repeat_delay": 0.01
});
let effect: AnimationEffect = serde_json::from_value(json).unwrap();
match effect {
AnimationEffect::Shake(t) => {
assert!(t.yoyo);
assert_eq!(t.repeat_delay, 0.01);
assert_eq!(t.repeat_count, Some(12));
}
other => panic!("expected Shake, got {other:?}"),
}
}
#[test]
fn a_typo_is_still_rejected_through_the_wire_type() {
let json = json!({ "name": "pulse", "duratoin": 1.0 });
let err = serde_json::from_value::<AnimationEffect>(json)
.expect_err("a typo'd field must still be rejected, not silently ignored");
assert!(err.to_string().contains("duratoin"), "got: {err}");
}
#[test]
fn loop_as_a_negative_number_is_rejected_not_silently_coerced() {
let json = json!({ "name": "pulse", "loop": -1 });
assert!(
serde_json::from_value::<AnimationEffect>(json).is_err(),
"a negative \"loop\" is neither a bool nor a valid play count"
);
}
#[test]
fn a_finite_repeat_count_round_trips_through_a_single_loop_key() {
let t: AnimationTiming = serde_json::from_value(json!({ "loop": 12 })).unwrap();
let json = serde_json::to_value(&t).unwrap();
assert!(
json.get("repeat_count").is_none(),
"must not emit a separate repeat_count key: {json}"
);
assert_eq!(json["loop"], serde_json::json!(12));
let back: AnimationTiming = serde_json::from_value(json).unwrap();
assert_eq!(t, back);
}
#[test]
fn easing_steps_round_trips_and_is_distinct_from_cubic_bezier() {
let json = json!({ "steps": 4 });
let easing: EasingType = serde_json::from_value(json).unwrap();
assert_eq!(easing, EasingType::Steps(4));
let back = serde_json::to_value(&easing).unwrap();
assert_eq!(back, json!({ "steps": 4 }));
}
}