1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct AnimationTimeline {
14 pub video_id: String,
16 pub events: Vec<AnimationEvent>,
18}
19
20impl AnimationTimeline {
21 #[must_use]
23 pub fn event_count(&self) -> usize {
24 self.events.len()
25 }
26
27 #[must_use]
29 pub fn has_events(&self) -> bool {
30 !self.events.is_empty()
31 }
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
36pub struct AnimationEvent {
37 pub name: String,
39 pub event_type: AnimationEventType,
41 pub expected_secs: f64,
43 pub duration_secs: Option<f64>,
45 pub easing: Option<String>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51pub enum AnimationEventType {
52 Enter,
54 Exit,
56 TransitionStart,
58 TransitionEnd,
60 Keyframe,
62 PhysicsEvent,
64}
65
66impl std::fmt::Display for AnimationEventType {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 Self::Enter => write!(f, "enter"),
70 Self::Exit => write!(f, "exit"),
71 Self::TransitionStart => write!(f, "transition_start"),
72 Self::TransitionEnd => write!(f, "transition_end"),
73 Self::Keyframe => write!(f, "keyframe"),
74 Self::PhysicsEvent => write!(f, "physics_event"),
75 }
76 }
77}
78
79#[derive(Clone, Debug, Serialize)]
81pub struct AnimationReport {
82 pub video_id: String,
84 pub verdict: AnimationVerdict,
86 pub events: Vec<EventResult>,
88 pub total_events: usize,
90 pub verified_events: usize,
92 pub max_delta_ms: f64,
94 pub mean_delta_ms: f64,
96}
97
98#[derive(Clone, Debug, Serialize)]
100pub struct EventResult {
101 pub name: String,
103 pub event_type: AnimationEventType,
105 pub expected_secs: f64,
107 pub actual_secs: Option<f64>,
109 pub delta_ms: Option<f64>,
111 pub passed: bool,
113}
114
115#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
117pub enum AnimationVerdict {
118 Pass,
120 Fail,
122 NoEvents,
124}
125
126impl std::fmt::Display for AnimationVerdict {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 Self::Pass => write!(f, "PASS"),
130 Self::Fail => write!(f, "FAIL"),
131 Self::NoEvents => write!(f, "NO EVENTS"),
132 }
133 }
134}
135
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
138pub enum EasingFunction {
139 Linear,
141 EaseIn,
143 EaseOut,
145 EaseInOut,
147 CubicIn,
149 CubicOut,
151 CubicInOut,
153 Bounce,
155 CubicBezier(f64, f64, f64, f64),
157}
158
159impl EasingFunction {
160 #[must_use]
164 pub fn evaluate(&self, t: f64) -> f64 {
165 let t = t.clamp(0.0, 1.0);
166 match self {
167 Self::Linear => t,
168 Self::EaseIn => t * t,
169 Self::EaseOut => t * (2.0 - t),
170 Self::EaseInOut => {
171 if t < 0.5 {
172 2.0 * t * t
173 } else {
174 -1.0 + (4.0 - 2.0 * t) * t
175 }
176 }
177 Self::CubicIn => t * t * t,
178 Self::CubicOut => {
179 let t1 = t - 1.0;
180 t1 * t1 * t1 + 1.0
181 }
182 Self::CubicInOut => {
183 if t < 0.5 {
184 4.0 * t * t * t
185 } else {
186 let t1 = 2.0 * t - 2.0;
187 0.5 * t1 * t1 * t1 + 1.0
188 }
189 }
190 Self::Bounce => bounce_ease_out(t),
191 Self::CubicBezier(x1, y1, x2, y2) => cubic_bezier_approx(t, *x1, *y1, *x2, *y2),
192 }
193 }
194}
195
196fn bounce_ease_out(t: f64) -> f64 {
198 if t < 1.0 / 2.75 {
199 7.5625 * t * t
200 } else if t < 2.0 / 2.75 {
201 let t = t - 1.5 / 2.75;
202 7.5625 * t * t + 0.75
203 } else if t < 2.5 / 2.75 {
204 let t = t - 2.25 / 2.75;
205 7.5625 * t * t + 0.9375
206 } else {
207 let t = t - 2.625 / 2.75;
208 7.5625 * t * t + 0.984_375
209 }
210}
211
212fn cubic_bezier_approx(t: f64, _x1: f64, y1: f64, _x2: f64, y2: f64) -> f64 {
214 let mt = 1.0 - t;
217 let mt2 = mt * mt;
218 let t2 = t * t;
219 mt2 * mt * 0.0 + 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t2 * t * 1.0
220}
221
222#[cfg(test)]
223#[allow(clippy::unwrap_used)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_animation_verdict_display() {
229 assert_eq!(AnimationVerdict::Pass.to_string(), "PASS");
230 assert_eq!(AnimationVerdict::Fail.to_string(), "FAIL");
231 assert_eq!(AnimationVerdict::NoEvents.to_string(), "NO EVENTS");
232 }
233
234 #[test]
235 fn test_animation_event_type_display() {
236 assert_eq!(AnimationEventType::Enter.to_string(), "enter");
237 assert_eq!(AnimationEventType::PhysicsEvent.to_string(), "physics_event");
238 }
239
240 #[test]
241 fn test_timeline_event_count() {
242 let timeline = AnimationTimeline {
243 video_id: "test".to_string(),
244 events: vec![
245 AnimationEvent {
246 name: "event1".to_string(),
247 event_type: AnimationEventType::Enter,
248 expected_secs: 1.0,
249 duration_secs: None,
250 easing: None,
251 },
252 AnimationEvent {
253 name: "event2".to_string(),
254 event_type: AnimationEventType::Exit,
255 expected_secs: 2.0,
256 duration_secs: None,
257 easing: None,
258 },
259 ],
260 };
261 assert_eq!(timeline.event_count(), 2);
262 assert!(timeline.has_events());
263 }
264
265 #[test]
266 fn test_timeline_empty() {
267 let timeline = AnimationTimeline {
268 video_id: "empty".to_string(),
269 events: vec![],
270 };
271 assert_eq!(timeline.event_count(), 0);
272 assert!(!timeline.has_events());
273 }
274
275 #[test]
276 fn test_timeline_json_roundtrip() {
277 let timeline = AnimationTimeline {
278 video_id: "test".to_string(),
279 events: vec![AnimationEvent {
280 name: "bullet_land".to_string(),
281 event_type: AnimationEventType::PhysicsEvent,
282 expected_secs: 1.7,
283 duration_secs: Some(0.05),
284 easing: Some("bounce".to_string()),
285 }],
286 };
287 let json = serde_json::to_string(&timeline).unwrap();
288 let parsed: AnimationTimeline = serde_json::from_str(&json).unwrap();
289 assert_eq!(parsed.video_id, "test");
290 assert_eq!(parsed.events.len(), 1);
291 assert_eq!(parsed.events[0].event_type, AnimationEventType::PhysicsEvent);
292 }
293
294 #[test]
295 fn test_easing_linear() {
296 let f = EasingFunction::Linear;
297 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
298 assert!((f.evaluate(0.5) - 0.5).abs() < f64::EPSILON);
299 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
300 }
301
302 #[test]
303 fn test_easing_ease_in() {
304 let f = EasingFunction::EaseIn;
305 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
306 assert!((f.evaluate(0.5) - 0.25).abs() < f64::EPSILON); assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
308 }
309
310 #[test]
311 fn test_easing_ease_out() {
312 let f = EasingFunction::EaseOut;
313 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
314 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
315 assert!(f.evaluate(0.5) > 0.5);
317 }
318
319 #[test]
320 fn test_easing_ease_in_out() {
321 let f = EasingFunction::EaseInOut;
322 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
323 assert!((f.evaluate(0.5) - 0.5).abs() < f64::EPSILON);
324 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
325 }
326
327 #[test]
328 fn test_easing_cubic_in() {
329 let f = EasingFunction::CubicIn;
330 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
331 assert!((f.evaluate(0.5) - 0.125).abs() < f64::EPSILON); assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
333 }
334
335 #[test]
336 fn test_easing_cubic_out() {
337 let f = EasingFunction::CubicOut;
338 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
339 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
340 }
341
342 #[test]
343 fn test_easing_cubic_in_out() {
344 let f = EasingFunction::CubicInOut;
345 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
346 assert!((f.evaluate(0.5) - 0.5).abs() < f64::EPSILON);
347 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
348 }
349
350 #[test]
351 fn test_easing_bounce() {
352 let f = EasingFunction::Bounce;
353 assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
354 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
355 assert!(f.evaluate(0.5) > 0.0);
357 }
358
359 #[test]
360 fn test_easing_cubic_bezier() {
361 let f = EasingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0); assert!((f.evaluate(0.0)).abs() < f64::EPSILON);
363 assert!((f.evaluate(1.0) - 1.0).abs() < f64::EPSILON);
364 }
365
366 #[test]
367 fn test_easing_clamp() {
368 let f = EasingFunction::Linear;
369 assert!((f.evaluate(-0.5)).abs() < f64::EPSILON);
370 assert!((f.evaluate(1.5) - 1.0).abs() < f64::EPSILON);
371 }
372
373 #[test]
374 fn test_event_result() {
375 let result = EventResult {
376 name: "bullet_land".to_string(),
377 event_type: AnimationEventType::PhysicsEvent,
378 expected_secs: 1.7,
379 actual_secs: Some(1.71),
380 delta_ms: Some(10.0),
381 passed: true,
382 };
383 assert!(result.passed);
384 }
385
386 #[test]
387 fn test_animation_report_serialization() {
388 let report = AnimationReport {
389 video_id: "test".to_string(),
390 verdict: AnimationVerdict::Pass,
391 events: vec![],
392 total_events: 0,
393 verified_events: 0,
394 max_delta_ms: 0.0,
395 mean_delta_ms: 0.0,
396 };
397 let json = serde_json::to_string(&report).unwrap();
398 assert!(json.contains("\"verdict\":\"Pass\""));
399 }
400}