Skip to main content

oximedia_edit/
transition.rs

1//! Transition effects between clips.
2//!
3//! Transitions provide smooth blending between adjacent clips on the timeline.
4
5use oximedia_core::Rational;
6
7use crate::clip::ClipId;
8use crate::error::{EditError, EditResult};
9
10/// A transition between two clips.
11#[derive(Clone, Debug)]
12pub struct Transition {
13    /// Unique transition identifier.
14    pub id: u64,
15    /// Transition type.
16    pub transition_type: TransitionType,
17    /// Track index.
18    pub track: usize,
19    /// Timeline position where transition starts.
20    pub start: i64,
21    /// Transition duration.
22    pub duration: i64,
23    /// Timebase.
24    pub timebase: Rational,
25    /// Clip before transition.
26    pub clip_a: ClipId,
27    /// Clip after transition.
28    pub clip_b: ClipId,
29    /// Transition-specific parameters.
30    pub parameters: TransitionParameters,
31}
32
33impl Transition {
34    /// Create a new transition.
35    #[must_use]
36    pub fn new(
37        id: u64,
38        transition_type: TransitionType,
39        track: usize,
40        start: i64,
41        duration: i64,
42        clip_a: ClipId,
43        clip_b: ClipId,
44    ) -> Self {
45        Self {
46            id,
47            transition_type,
48            track,
49            start,
50            duration,
51            timebase: Rational::new(1, 1000),
52            clip_a,
53            clip_b,
54            parameters: TransitionParameters::default(),
55        }
56    }
57
58    /// Get the end position of the transition.
59    #[must_use]
60    pub fn end(&self) -> i64 {
61        self.start + self.duration
62    }
63
64    /// Check if this transition is active at a given time.
65    #[must_use]
66    pub fn is_active_at(&self, time: i64) -> bool {
67        time >= self.start && time < self.end()
68    }
69
70    /// Calculate transition progress (0.0 to 1.0) at a given time.
71    #[must_use]
72    pub fn progress_at(&self, time: i64) -> f64 {
73        if time <= self.start {
74            return 0.0;
75        }
76        if time >= self.end() {
77            return 1.0;
78        }
79        if self.duration == 0 {
80            return 1.0;
81        }
82
83        #[allow(clippy::cast_precision_loss)]
84        let progress = (time - self.start) as f64 / self.duration as f64;
85
86        // Apply easing based on parameters
87        self.parameters.apply_easing(progress)
88    }
89
90    /// Validate transition parameters.
91    pub fn validate(&self) -> EditResult<()> {
92        if self.duration <= 0 {
93            return Err(EditError::InvalidTransition(
94                "Duration must be positive".to_string(),
95            ));
96        }
97
98        if self.clip_a == self.clip_b {
99            return Err(EditError::InvalidTransition(
100                "Cannot transition between same clip".to_string(),
101            ));
102        }
103
104        Ok(())
105    }
106}
107
108/// Type of transition effect.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum TransitionType {
111    /// Video cross-dissolve.
112    Dissolve,
113    /// Audio cross-fade.
114    CrossFade,
115    /// Wipe transition (left to right).
116    WipeLeft,
117    /// Wipe transition (right to left).
118    WipeRight,
119    /// Wipe transition (top to bottom).
120    WipeDown,
121    /// Wipe transition (bottom to top).
122    WipeUp,
123    /// Slide transition.
124    Slide,
125    /// Push transition.
126    Push,
127    /// Zoom in transition.
128    ZoomIn,
129    /// Zoom out transition.
130    ZoomOut,
131    /// Fade through black.
132    FadeThrough,
133    /// Fade through white.
134    FadeThroughWhite,
135    /// Dip to color.
136    DipToColor,
137    /// Custom transition.
138    Custom(String),
139}
140
141impl TransitionType {
142    /// Check if this is a video transition.
143    #[must_use]
144    pub fn is_video(&self) -> bool {
145        !matches!(self, Self::CrossFade)
146    }
147
148    /// Check if this is an audio transition.
149    #[must_use]
150    pub fn is_audio(&self) -> bool {
151        matches!(self, Self::CrossFade)
152    }
153}
154
155/// Parameters for transition effects.
156#[derive(Clone, Debug)]
157pub struct TransitionParameters {
158    /// Easing function.
159    pub easing: EasingFunction,
160    /// Reverse the transition direction.
161    pub reverse: bool,
162    /// Transition color (for fade-through effects).
163    pub color: Option<[f32; 4]>,
164    /// Softness/feathering amount (0.0-1.0).
165    pub softness: f32,
166    /// Angle for directional transitions (in degrees).
167    pub angle: f32,
168}
169
170impl Default for TransitionParameters {
171    fn default() -> Self {
172        Self {
173            easing: EasingFunction::Linear,
174            reverse: false,
175            color: None,
176            softness: 0.0,
177            angle: 0.0,
178        }
179    }
180}
181
182impl TransitionParameters {
183    /// Apply easing function to transition progress.
184    #[must_use]
185    pub fn apply_easing(&self, t: f64) -> f64 {
186        let t = if self.reverse { 1.0 - t } else { t };
187        self.easing.apply(t)
188    }
189}
190
191/// Easing function for transition timing.
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum EasingFunction {
194    /// Linear (no easing).
195    Linear,
196    /// Ease in (slow start).
197    EaseIn,
198    /// Ease out (slow end).
199    EaseOut,
200    /// Ease in and out (slow start and end).
201    EaseInOut,
202    /// Exponential ease in.
203    ExpoIn,
204    /// Exponential ease out.
205    ExpoOut,
206    /// Cubic ease in.
207    CubicIn,
208    /// Cubic ease out.
209    CubicOut,
210    /// Sine ease in.
211    SineIn,
212    /// Sine ease out.
213    SineOut,
214}
215
216impl EasingFunction {
217    /// Apply the easing function to a value (0.0 to 1.0).
218    #[must_use]
219    #[allow(clippy::excessive_precision)]
220    pub fn apply(&self, t: f64) -> f64 {
221        match self {
222            Self::Linear => t,
223            Self::EaseIn => t * t,
224            Self::EaseOut => t * (2.0 - t),
225            Self::EaseInOut => {
226                if t < 0.5 {
227                    2.0 * t * t
228                } else {
229                    -1.0 + (4.0 - 2.0 * t) * t
230                }
231            }
232            Self::ExpoIn => {
233                if t == 0.0 {
234                    0.0
235                } else {
236                    2.0_f64.powf(10.0 * (t - 1.0))
237                }
238            }
239            Self::ExpoOut => {
240                if t == 1.0 {
241                    1.0
242                } else {
243                    1.0 - 2.0_f64.powf(-10.0 * t)
244                }
245            }
246            Self::CubicIn => t * t * t,
247            Self::CubicOut => {
248                let t1 = t - 1.0;
249                t1 * t1 * t1 + 1.0
250            }
251            Self::SineIn => 1.0 - (t * std::f64::consts::FRAC_PI_2).cos(),
252            Self::SineOut => (t * std::f64::consts::FRAC_PI_2).sin(),
253        }
254    }
255}
256
257/// Transition builder for creating transitions with validation.
258#[derive(Debug)]
259pub struct TransitionBuilder {
260    transition_type: TransitionType,
261    track: usize,
262    start: i64,
263    duration: i64,
264    clip_a: ClipId,
265    clip_b: ClipId,
266    parameters: TransitionParameters,
267}
268
269impl TransitionBuilder {
270    /// Create a new transition builder.
271    #[must_use]
272    pub fn new(
273        transition_type: TransitionType,
274        track: usize,
275        start: i64,
276        duration: i64,
277        clip_a: ClipId,
278        clip_b: ClipId,
279    ) -> Self {
280        Self {
281            transition_type,
282            track,
283            start,
284            duration,
285            clip_a,
286            clip_b,
287            parameters: TransitionParameters::default(),
288        }
289    }
290
291    /// Set easing function.
292    #[must_use]
293    pub fn easing(mut self, easing: EasingFunction) -> Self {
294        self.parameters.easing = easing;
295        self
296    }
297
298    /// Set reverse flag.
299    #[must_use]
300    pub fn reverse(mut self, reverse: bool) -> Self {
301        self.parameters.reverse = reverse;
302        self
303    }
304
305    /// Set transition color.
306    #[must_use]
307    pub fn color(mut self, color: [f32; 4]) -> Self {
308        self.parameters.color = Some(color);
309        self
310    }
311
312    /// Set softness amount.
313    #[must_use]
314    pub fn softness(mut self, softness: f32) -> Self {
315        self.parameters.softness = softness.clamp(0.0, 1.0);
316        self
317    }
318
319    /// Set angle.
320    #[must_use]
321    pub fn angle(mut self, angle: f32) -> Self {
322        self.parameters.angle = angle;
323        self
324    }
325
326    /// Build the transition.
327    pub fn build(self, id: u64) -> EditResult<Transition> {
328        let transition = Transition {
329            id,
330            transition_type: self.transition_type,
331            track: self.track,
332            start: self.start,
333            duration: self.duration,
334            timebase: Rational::new(1, 1000),
335            clip_a: self.clip_a,
336            clip_b: self.clip_b,
337            parameters: self.parameters,
338        };
339
340        transition.validate()?;
341        Ok(transition)
342    }
343}
344
345/// Preset transitions for common use cases.
346pub struct TransitionPresets;
347
348impl TransitionPresets {
349    /// Create a standard cross-dissolve transition.
350    #[must_use]
351    pub fn dissolve(
352        id: u64,
353        track: usize,
354        start: i64,
355        duration: i64,
356        clip_a: ClipId,
357        clip_b: ClipId,
358    ) -> Transition {
359        Transition::new(
360            id,
361            TransitionType::Dissolve,
362            track,
363            start,
364            duration,
365            clip_a,
366            clip_b,
367        )
368    }
369
370    /// Create an audio cross-fade transition.
371    #[must_use]
372    pub fn crossfade(
373        id: u64,
374        track: usize,
375        start: i64,
376        duration: i64,
377        clip_a: ClipId,
378        clip_b: ClipId,
379    ) -> Transition {
380        Transition::new(
381            id,
382            TransitionType::CrossFade,
383            track,
384            start,
385            duration,
386            clip_a,
387            clip_b,
388        )
389    }
390
391    /// Create a fade through black transition.
392    #[must_use]
393    pub fn fade_through_black(
394        id: u64,
395        track: usize,
396        start: i64,
397        duration: i64,
398        clip_a: ClipId,
399        clip_b: ClipId,
400    ) -> Transition {
401        let mut transition = Transition::new(
402            id,
403            TransitionType::FadeThrough,
404            track,
405            start,
406            duration,
407            clip_a,
408            clip_b,
409        );
410        transition.parameters.color = Some([0.0, 0.0, 0.0, 1.0]);
411        transition
412    }
413
414    /// Create a smooth dissolve with ease in/out.
415    #[must_use]
416    pub fn smooth_dissolve(
417        id: u64,
418        track: usize,
419        start: i64,
420        duration: i64,
421        clip_a: ClipId,
422        clip_b: ClipId,
423    ) -> Transition {
424        let mut transition = Transition::new(
425            id,
426            TransitionType::Dissolve,
427            track,
428            start,
429            duration,
430            clip_a,
431            clip_b,
432        );
433        transition.parameters.easing = EasingFunction::EaseInOut;
434        transition
435    }
436}
437
438/// Transition manager for a timeline.
439#[derive(Debug, Default)]
440pub struct TransitionManager {
441    /// All transitions in the timeline.
442    transitions: Vec<Transition>,
443    /// Next transition ID.
444    next_id: u64,
445}
446
447impl TransitionManager {
448    /// Create a new transition manager.
449    #[must_use]
450    pub fn new() -> Self {
451        Self {
452            transitions: Vec::new(),
453            next_id: 1,
454        }
455    }
456
457    /// Add a transition.
458    pub fn add(&mut self, mut transition: Transition) -> u64 {
459        let id = self.next_id;
460        self.next_id += 1;
461        transition.id = id;
462        self.transitions.push(transition);
463        id
464    }
465
466    /// Remove a transition by ID.
467    pub fn remove(&mut self, id: u64) -> Option<Transition> {
468        if let Some(pos) = self.transitions.iter().position(|t| t.id == id) {
469            Some(self.transitions.remove(pos))
470        } else {
471            None
472        }
473    }
474
475    /// Get a transition by ID.
476    #[must_use]
477    pub fn get(&self, id: u64) -> Option<&Transition> {
478        self.transitions.iter().find(|t| t.id == id)
479    }
480
481    /// Get mutable transition by ID.
482    pub fn get_mut(&mut self, id: u64) -> Option<&mut Transition> {
483        self.transitions.iter_mut().find(|t| t.id == id)
484    }
485
486    /// Get all transitions on a track.
487    #[must_use]
488    pub fn get_track_transitions(&self, track: usize) -> Vec<&Transition> {
489        self.transitions
490            .iter()
491            .filter(|t| t.track == track)
492            .collect()
493    }
494
495    /// Get active transitions at a specific time on a track.
496    #[must_use]
497    pub fn get_active_at(&self, track: usize, time: i64) -> Vec<&Transition> {
498        self.transitions
499            .iter()
500            .filter(|t| t.track == track && t.is_active_at(time))
501            .collect()
502    }
503
504    /// Clear all transitions.
505    pub fn clear(&mut self) {
506        self.transitions.clear();
507    }
508
509    /// Get total number of transitions.
510    #[must_use]
511    pub fn len(&self) -> usize {
512        self.transitions.len()
513    }
514
515    /// Check if there are no transitions.
516    #[must_use]
517    pub fn is_empty(&self) -> bool {
518        self.transitions.is_empty()
519    }
520}