Skip to main content

ass_renderer/animation/
controller.rs

1//! Animation controller that drives multiple tracks and `\t` tag parsing
2
3#[cfg(feature = "nostd")]
4use alloc::{string::ToString, vec::Vec};
5#[cfg(not(feature = "nostd"))]
6use std::vec::Vec;
7
8use crate::utils::RenderError;
9use smallvec::SmallVec;
10
11use super::state::AnimationState;
12use super::timing::{AnimationInterpolation, AnimationTag, AnimationTiming};
13use super::track::AnimationTrack;
14use super::value::AnimatedValue;
15
16/// Animation controller for managing multiple tracks
17pub struct AnimationController {
18    tracks: SmallVec<[AnimationTrack; 8]>,
19}
20
21impl AnimationController {
22    /// Create a new animation controller
23    pub fn new() -> Self {
24        Self {
25            tracks: SmallVec::new(),
26        }
27    }
28
29    /// Add an animation track
30    pub fn add_track(&mut self, track: AnimationTrack) {
31        self.tracks.push(track);
32    }
33
34    /// Parse and add \t tag animations
35    pub fn add_from_tag(
36        &mut self,
37        tag: &AnimationTag,
38        event_start: u32,
39        event_end: u32,
40    ) -> Result<(), RenderError> {
41        let timing = AnimationTiming::new(
42            event_start + tag.t1.unwrap_or(0),
43            event_start + tag.t2.unwrap_or(event_end - event_start),
44            tag.accel.unwrap_or(1.0),
45        );
46
47        // Parse animated properties from tag modifiers
48        for modifier in &tag.modifiers {
49            let track = self.parse_modifier_animation(modifier, timing.clone())?;
50            if let Some(track) = track {
51                self.tracks.push(track);
52            }
53        }
54
55        Ok(())
56    }
57
58    /// Parse a modifier into an animation track
59    fn parse_modifier_animation(
60        &self,
61        modifier: &str,
62        timing: AnimationTiming,
63    ) -> Result<Option<AnimationTrack>, RenderError> {
64        // Parse modifier format: \property(from,to) or \property(to)
65        // Examples: \fscx(100,200), \c(&H0000FF&,&HFF0000&), \pos(0,0,100,100)
66
67        // Simplified parsing - in production would use proper parser
68        if modifier.starts_with("\\fscx") {
69            // Font scale X animation
70            if let Some(values) = extract_values(modifier) {
71                if values.len() == 2 {
72                    let from = values[0].parse::<f32>().unwrap_or(100.0);
73                    let to = values[1].parse::<f32>().unwrap_or(100.0);
74                    return Ok(Some(AnimationTrack::new(
75                        "fscx".to_string(),
76                        timing,
77                        AnimatedValue::Float { from, to },
78                        AnimationInterpolation::Linear,
79                    )));
80                }
81            }
82        } else if modifier.starts_with("\\fscy") {
83            // Font scale Y animation
84            if let Some(values) = extract_values(modifier) {
85                if values.len() == 2 {
86                    let from = values[0].parse::<f32>().unwrap_or(100.0);
87                    let to = values[1].parse::<f32>().unwrap_or(100.0);
88                    return Ok(Some(AnimationTrack::new(
89                        "fscy".to_string(),
90                        timing,
91                        AnimatedValue::Float { from, to },
92                        AnimationInterpolation::Linear,
93                    )));
94                }
95            }
96        } else if modifier.starts_with("\\fs") {
97            // Font size animation
98            if let Some(values) = extract_values(modifier) {
99                if values.len() == 2 {
100                    let from = values[0].parse::<f32>().unwrap_or(20.0);
101                    let to = values[1].parse::<f32>().unwrap_or(20.0);
102                    return Ok(Some(AnimationTrack::new(
103                        "fs".to_string(),
104                        timing,
105                        AnimatedValue::Float { from, to },
106                        AnimationInterpolation::Linear,
107                    )));
108                }
109            }
110        } else if modifier.starts_with("\\frz") || modifier.starts_with("\\fr") {
111            // Rotation animation
112            if let Some(values) = extract_values(modifier) {
113                if values.len() == 2 {
114                    let from = values[0].parse::<f32>().unwrap_or(0.0);
115                    let to = values[1].parse::<f32>().unwrap_or(0.0);
116                    return Ok(Some(AnimationTrack::new(
117                        "frz".to_string(),
118                        timing,
119                        AnimatedValue::Float { from, to },
120                        AnimationInterpolation::Linear,
121                    )));
122                }
123            }
124        } else if modifier.starts_with("\\c") || modifier.starts_with("\\1c") {
125            // Color animation
126            if let Some(values) = extract_values(modifier) {
127                if values.len() == 2 {
128                    let from = parse_color(values[0]).unwrap_or([255, 255, 255, 255]);
129                    let to = parse_color(values[1]).unwrap_or([255, 255, 255, 255]);
130                    return Ok(Some(AnimationTrack::new(
131                        "c".to_string(),
132                        timing,
133                        AnimatedValue::Color { from, to },
134                        AnimationInterpolation::Linear,
135                    )));
136                }
137            }
138        } else if modifier.starts_with("\\alpha") {
139            // Alpha animation
140            if let Some(values) = extract_values(modifier) {
141                if values.len() == 2 {
142                    let from = parse_alpha(values[0]).unwrap_or(255) as i32;
143                    let to = parse_alpha(values[1]).unwrap_or(255) as i32;
144                    return Ok(Some(AnimationTrack::new(
145                        "alpha".to_string(),
146                        timing,
147                        AnimatedValue::Integer { from, to },
148                        AnimationInterpolation::Linear,
149                    )));
150                }
151            }
152        }
153
154        Ok(None)
155    }
156
157    /// Evaluate all animations at given time
158    pub fn evaluate(&self, time_cs: u32) -> AnimationState {
159        let mut state = AnimationState::new();
160
161        for track in &self.tracks {
162            let result = track.evaluate(time_cs);
163            state.set_property(&track.property, result);
164        }
165
166        state
167    }
168
169    /// Check if any animations are active at given time
170    pub fn is_active(&self, time_cs: u32) -> bool {
171        self.tracks
172            .iter()
173            .any(|track| time_cs >= track.timing.start_cs && time_cs <= track.timing.end_cs)
174    }
175
176    /// Get all active tracks at given time
177    pub fn active_tracks(&self, time_cs: u32) -> SmallVec<[&AnimationTrack; 8]> {
178        self.tracks
179            .iter()
180            .filter(|track| time_cs >= track.timing.start_cs && time_cs <= track.timing.end_cs)
181            .collect()
182    }
183}
184
185impl Default for AnimationController {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191// Helper functions
192
193/// Extract values from a modifier string
194fn extract_values(modifier: &str) -> Option<Vec<&str>> {
195    // Find content between parentheses
196    let start = modifier.find('(')?;
197    let end = modifier.find(')')?;
198    let content = &modifier[start + 1..end];
199    Some(content.split(',').collect())
200}
201
202/// Parse ASS color format
203fn parse_color(color_str: &str) -> Option<[u8; 4]> {
204    // Parse &HBBGGRR& or &HAABBGGRR& format
205    let cleaned = color_str.trim_start_matches("&H").trim_end_matches('&');
206
207    if cleaned.len() == 6 {
208        // BGR format
209        let bgr = u32::from_str_radix(cleaned, 16).ok()?;
210        let b = ((bgr >> 16) & 0xFF) as u8;
211        let g = ((bgr >> 8) & 0xFF) as u8;
212        let r = (bgr & 0xFF) as u8;
213        Some([r, g, b, 255])
214    } else if cleaned.len() == 8 {
215        // ABGR format
216        let abgr = u32::from_str_radix(cleaned, 16).ok()?;
217        let a = ((abgr >> 24) & 0xFF) as u8;
218        let b = ((abgr >> 16) & 0xFF) as u8;
219        let g = ((abgr >> 8) & 0xFF) as u8;
220        let r = (abgr & 0xFF) as u8;
221        Some([r, g, b, 255 - a]) // ASS uses inverse alpha
222    } else {
223        None
224    }
225}
226
227/// Parse ASS alpha format
228fn parse_alpha(alpha_str: &str) -> Option<u8> {
229    // Parse &HXX& format
230    let cleaned = alpha_str.trim_start_matches("&H").trim_end_matches('&');
231    let alpha = u8::from_str_radix(cleaned, 16).ok()?;
232    Some(255 - alpha) // ASS uses inverse alpha
233}