1use oximedia_core::Rational;
6
7use crate::clip::ClipId;
8use crate::error::{EditError, EditResult};
9
10#[derive(Clone, Debug)]
12pub struct Transition {
13 pub id: u64,
15 pub transition_type: TransitionType,
17 pub track: usize,
19 pub start: i64,
21 pub duration: i64,
23 pub timebase: Rational,
25 pub clip_a: ClipId,
27 pub clip_b: ClipId,
29 pub parameters: TransitionParameters,
31}
32
33impl Transition {
34 #[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 #[must_use]
60 pub fn end(&self) -> i64 {
61 self.start + self.duration
62 }
63
64 #[must_use]
66 pub fn is_active_at(&self, time: i64) -> bool {
67 time >= self.start && time < self.end()
68 }
69
70 #[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 self.parameters.apply_easing(progress)
88 }
89
90 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#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum TransitionType {
111 Dissolve,
113 CrossFade,
115 WipeLeft,
117 WipeRight,
119 WipeDown,
121 WipeUp,
123 Slide,
125 Push,
127 ZoomIn,
129 ZoomOut,
131 FadeThrough,
133 FadeThroughWhite,
135 DipToColor,
137 Custom(String),
139}
140
141impl TransitionType {
142 #[must_use]
144 pub fn is_video(&self) -> bool {
145 !matches!(self, Self::CrossFade)
146 }
147
148 #[must_use]
150 pub fn is_audio(&self) -> bool {
151 matches!(self, Self::CrossFade)
152 }
153}
154
155#[derive(Clone, Debug)]
157pub struct TransitionParameters {
158 pub easing: EasingFunction,
160 pub reverse: bool,
162 pub color: Option<[f32; 4]>,
164 pub softness: f32,
166 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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum EasingFunction {
194 Linear,
196 EaseIn,
198 EaseOut,
200 EaseInOut,
202 ExpoIn,
204 ExpoOut,
206 CubicIn,
208 CubicOut,
210 SineIn,
212 SineOut,
214}
215
216impl EasingFunction {
217 #[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#[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 #[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 #[must_use]
293 pub fn easing(mut self, easing: EasingFunction) -> Self {
294 self.parameters.easing = easing;
295 self
296 }
297
298 #[must_use]
300 pub fn reverse(mut self, reverse: bool) -> Self {
301 self.parameters.reverse = reverse;
302 self
303 }
304
305 #[must_use]
307 pub fn color(mut self, color: [f32; 4]) -> Self {
308 self.parameters.color = Some(color);
309 self
310 }
311
312 #[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 #[must_use]
321 pub fn angle(mut self, angle: f32) -> Self {
322 self.parameters.angle = angle;
323 self
324 }
325
326 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
345pub struct TransitionPresets;
347
348impl TransitionPresets {
349 #[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 #[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 #[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 #[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#[derive(Debug, Default)]
440pub struct TransitionManager {
441 transitions: Vec<Transition>,
443 next_id: u64,
445}
446
447impl TransitionManager {
448 #[must_use]
450 pub fn new() -> Self {
451 Self {
452 transitions: Vec::new(),
453 next_id: 1,
454 }
455 }
456
457 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 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 #[must_use]
477 pub fn get(&self, id: u64) -> Option<&Transition> {
478 self.transitions.iter().find(|t| t.id == id)
479 }
480
481 pub fn get_mut(&mut self, id: u64) -> Option<&mut Transition> {
483 self.transitions.iter_mut().find(|t| t.id == id)
484 }
485
486 #[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 #[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 pub fn clear(&mut self) {
506 self.transitions.clear();
507 }
508
509 #[must_use]
511 pub fn len(&self) -> usize {
512 self.transitions.len()
513 }
514
515 #[must_use]
517 pub fn is_empty(&self) -> bool {
518 self.transitions.is_empty()
519 }
520}