Skip to main content

oximedia_edit/
effect.rs

1//! Effect system with keyframe animation.
2//!
3//! Effects can be applied to clips and animated using keyframes.
4
5#![allow(missing_docs)]
6
7use oximedia_core::Rational;
8use oximedia_graph::FilterGraph;
9use std::collections::BTreeMap;
10
11use crate::error::{EditError, EditResult};
12
13/// Stack of effects applied to a clip.
14#[derive(Clone, Debug, Default)]
15pub struct EffectStack {
16    /// Effects in the stack (applied in order).
17    pub effects: Vec<Effect>,
18}
19
20impl EffectStack {
21    /// Create a new empty effect stack.
22    #[must_use]
23    pub fn new() -> Self {
24        Self {
25            effects: Vec::new(),
26        }
27    }
28
29    /// Add an effect to the stack.
30    pub fn add(&mut self, effect: Effect) {
31        self.effects.push(effect);
32    }
33
34    /// Remove an effect by index.
35    pub fn remove(&mut self, index: usize) -> Option<Effect> {
36        if index < self.effects.len() {
37            Some(self.effects.remove(index))
38        } else {
39            None
40        }
41    }
42
43    /// Get an effect by index.
44    #[must_use]
45    pub fn get(&self, index: usize) -> Option<&Effect> {
46        self.effects.get(index)
47    }
48
49    /// Get mutable effect by index.
50    pub fn get_mut(&mut self, index: usize) -> Option<&mut Effect> {
51        self.effects.get_mut(index)
52    }
53
54    /// Get number of effects.
55    #[must_use]
56    pub fn len(&self) -> usize {
57        self.effects.len()
58    }
59
60    /// Check if stack is empty.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.effects.is_empty()
64    }
65
66    /// Clear all effects.
67    pub fn clear(&mut self) {
68        self.effects.clear();
69    }
70
71    /// Evaluate all effects at a given time.
72    pub fn evaluate_at(&self, time: i64, timebase: Rational) -> EditResult<Vec<EffectInstance>> {
73        self.effects
74            .iter()
75            .map(|effect| effect.evaluate_at(time, timebase))
76            .collect()
77    }
78}
79
80/// An effect that can be applied to a clip.
81#[derive(Clone, Debug)]
82pub struct Effect {
83    /// Effect type.
84    pub effect_type: EffectType,
85    /// Effect parameters with keyframe support.
86    pub parameters: BTreeMap<String, Parameter>,
87    /// Effect is enabled.
88    pub enabled: bool,
89    /// Effect name (user-defined).
90    pub name: Option<String>,
91}
92
93impl Effect {
94    /// Create a new effect.
95    #[must_use]
96    pub fn new(effect_type: EffectType) -> Self {
97        Self {
98            effect_type,
99            parameters: BTreeMap::new(),
100            enabled: true,
101            name: None,
102        }
103    }
104
105    /// Set a parameter value.
106    pub fn set_parameter(&mut self, name: String, parameter: Parameter) {
107        self.parameters.insert(name, parameter);
108    }
109
110    /// Get a parameter value.
111    #[must_use]
112    pub fn get_parameter(&self, name: &str) -> Option<&Parameter> {
113        self.parameters.get(name)
114    }
115
116    /// Evaluate effect at a specific time.
117    pub fn evaluate_at(&self, time: i64, timebase: Rational) -> EditResult<EffectInstance> {
118        let mut values = BTreeMap::new();
119
120        for (name, param) in &self.parameters {
121            let value = param.evaluate_at(time, timebase)?;
122            values.insert(name.clone(), value);
123        }
124
125        Ok(EffectInstance {
126            effect_type: self.effect_type.clone(),
127            values,
128        })
129    }
130}
131
132/// Effect type.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum EffectType {
135    /// Brightness adjustment.
136    Brightness,
137    /// Contrast adjustment.
138    Contrast,
139    /// Saturation adjustment.
140    Saturation,
141    /// Hue rotation.
142    Hue,
143    /// Blur effect.
144    Blur,
145    /// Sharpen effect.
146    Sharpen,
147    /// Color correction (curves).
148    ColorCurve,
149    /// Crop.
150    Crop,
151    /// Scale/resize.
152    Scale,
153    /// Rotate.
154    Rotate,
155    /// Position/translate.
156    Position,
157    /// Opacity/transparency.
158    Opacity,
159    /// Audio gain.
160    AudioGain,
161    /// Audio pan.
162    AudioPan,
163    /// Audio equalizer.
164    AudioEq,
165    /// Audio reverb.
166    AudioReverb,
167    /// Chroma key (green screen).
168    ChromaKey,
169    /// Custom filter graph.
170    Custom(String),
171}
172
173/// Effect parameter with keyframe support.
174#[derive(Clone, Debug)]
175pub struct Parameter {
176    /// Parameter keyframes.
177    pub keyframes: BTreeMap<i64, ParameterValue>,
178    /// Interpolation mode.
179    pub interpolation: InterpolationMode,
180}
181
182impl Parameter {
183    /// Create a new parameter with a constant value.
184    #[must_use]
185    pub fn constant(value: ParameterValue) -> Self {
186        let mut keyframes = BTreeMap::new();
187        keyframes.insert(0, value);
188        Self {
189            keyframes,
190            interpolation: InterpolationMode::Linear,
191        }
192    }
193
194    /// Add a keyframe.
195    pub fn add_keyframe(&mut self, time: i64, value: ParameterValue) {
196        self.keyframes.insert(time, value);
197    }
198
199    /// Remove a keyframe.
200    pub fn remove_keyframe(&mut self, time: i64) -> Option<ParameterValue> {
201        self.keyframes.remove(&time)
202    }
203
204    /// Evaluate parameter at a specific time.
205    pub fn evaluate_at(&self, time: i64, _timebase: Rational) -> EditResult<ParameterValue> {
206        if self.keyframes.is_empty() {
207            return Err(EditError::KeyframeError("No keyframes defined".to_string()));
208        }
209
210        // If there's an exact keyframe, return it
211        if let Some(value) = self.keyframes.get(&time) {
212            return Ok(value.clone());
213        }
214
215        // Find surrounding keyframes for interpolation
216        let before = self
217            .keyframes
218            .range(..time)
219            .next_back()
220            .map(|(t, v)| (*t, v.clone()));
221        let after = self
222            .keyframes
223            .range(time..)
224            .next()
225            .map(|(t, v)| (*t, v.clone()));
226
227        match (before, after) {
228            (Some((t1, v1)), Some((t2, v2))) => {
229                // Interpolate between keyframes
230                let factor = if t2 == t1 {
231                    0.0
232                } else {
233                    #[allow(clippy::cast_precision_loss)]
234                    let result = (time - t1) as f64 / (t2 - t1) as f64;
235                    result
236                };
237                Ok(self.interpolation.interpolate(&v1, &v2, factor))
238            }
239            (Some((_, v)), None) | (None, Some((_, v))) => {
240                // Use the closest keyframe
241                Ok(v)
242            }
243            (None, None) => Err(EditError::KeyframeError("No keyframes found".to_string())),
244        }
245    }
246
247    /// Get all keyframe times.
248    #[must_use]
249    pub fn keyframe_times(&self) -> Vec<i64> {
250        self.keyframes.keys().copied().collect()
251    }
252}
253
254/// Parameter value type.
255#[derive(Clone, Debug)]
256pub enum ParameterValue {
257    /// Floating point value.
258    Float(f64),
259    /// Integer value.
260    Int(i64),
261    /// Boolean value.
262    Bool(bool),
263    /// String value.
264    String(String),
265    /// 2D point.
266    Point2D { x: f64, y: f64 },
267    /// 3D point.
268    Point3D { x: f64, y: f64, z: f64 },
269    /// Color (RGBA).
270    Color { r: f32, g: f32, b: f32, a: f32 },
271}
272
273/// Keyframe interpolation mode.
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub enum InterpolationMode {
276    /// No interpolation (step/hold).
277    Constant,
278    /// Linear interpolation.
279    Linear,
280    /// Smooth interpolation (ease in/out).
281    Smooth,
282    /// Bezier curve interpolation.
283    Bezier,
284}
285
286impl InterpolationMode {
287    /// Interpolate between two values.
288    #[must_use]
289    #[allow(clippy::needless_pass_by_value)]
290    pub fn interpolate(
291        &self,
292        v1: &ParameterValue,
293        v2: &ParameterValue,
294        factor: f64,
295    ) -> ParameterValue {
296        let t = match self {
297            Self::Constant => 0.0,
298            Self::Linear => factor,
299            Self::Smooth => {
300                // Smooth step (ease in/out)
301                factor * factor * (3.0 - 2.0 * factor)
302            }
303            Self::Bezier => {
304                // Simple bezier approximation
305                let t = factor;
306                t * t * (3.0 - 2.0 * t)
307            }
308        };
309
310        match (v1, v2) {
311            (ParameterValue::Float(a), ParameterValue::Float(b)) => {
312                ParameterValue::Float(a + (b - a) * t)
313            }
314            (ParameterValue::Int(a), ParameterValue::Int(b)) => {
315                #[allow(clippy::cast_possible_truncation)]
316                let result = a + ((b - a) as f64 * t) as i64;
317                ParameterValue::Int(result)
318            }
319            (ParameterValue::Bool(a), ParameterValue::Bool(_)) => {
320                // No interpolation for booleans
321                ParameterValue::Bool(*a)
322            }
323            (ParameterValue::String(s), _) => {
324                // No interpolation for strings
325                ParameterValue::String(s.clone())
326            }
327            (
328                ParameterValue::Point2D { x: x1, y: y1 },
329                ParameterValue::Point2D { x: x2, y: y2 },
330            ) => ParameterValue::Point2D {
331                x: x1 + (x2 - x1) * t,
332                y: y1 + (y2 - y1) * t,
333            },
334            (
335                ParameterValue::Point3D {
336                    x: x1,
337                    y: y1,
338                    z: z1,
339                },
340                ParameterValue::Point3D {
341                    x: x2,
342                    y: y2,
343                    z: z2,
344                },
345            ) => ParameterValue::Point3D {
346                x: x1 + (x2 - x1) * t,
347                y: y1 + (y2 - y1) * t,
348                z: z1 + (z2 - z1) * t,
349            },
350            (
351                ParameterValue::Color {
352                    r: r1,
353                    g: g1,
354                    b: b1,
355                    a: a1,
356                },
357                ParameterValue::Color {
358                    r: r2,
359                    g: g2,
360                    b: b2,
361                    a: a2,
362                },
363            ) => {
364                #[allow(clippy::cast_possible_truncation)]
365                let result = ParameterValue::Color {
366                    r: r1 + (r2 - r1) * t as f32,
367                    g: g1 + (g2 - g1) * t as f32,
368                    b: b1 + (b2 - b1) * t as f32,
369                    a: a1 + (a2 - a1) * t as f32,
370                };
371                result
372            }
373            _ => {
374                // Type mismatch, return first value
375                v1.clone()
376            }
377        }
378    }
379}
380
381/// Evaluated effect instance at a specific time.
382#[derive(Clone, Debug)]
383pub struct EffectInstance {
384    /// Effect type.
385    pub effect_type: EffectType,
386    /// Evaluated parameter values.
387    pub values: BTreeMap<String, ParameterValue>,
388}
389
390impl EffectInstance {
391    /// Get a float parameter value.
392    #[must_use]
393    pub fn get_float(&self, name: &str) -> Option<f64> {
394        match self.values.get(name) {
395            Some(ParameterValue::Float(v)) => Some(*v),
396            _ => None,
397        }
398    }
399
400    /// Get an integer parameter value.
401    #[must_use]
402    pub fn get_int(&self, name: &str) -> Option<i64> {
403        match self.values.get(name) {
404            Some(ParameterValue::Int(v)) => Some(*v),
405            _ => None,
406        }
407    }
408
409    /// Get a boolean parameter value.
410    #[must_use]
411    pub fn get_bool(&self, name: &str) -> Option<bool> {
412        match self.values.get(name) {
413            Some(ParameterValue::Bool(v)) => Some(*v),
414            _ => None,
415        }
416    }
417
418    /// Get a 2D point parameter value.
419    #[must_use]
420    pub fn get_point2d(&self, name: &str) -> Option<(f64, f64)> {
421        match self.values.get(name) {
422            Some(ParameterValue::Point2D { x, y }) => Some((*x, *y)),
423            _ => None,
424        }
425    }
426}
427
428/// Effect preset for common effect configurations.
429#[derive(Clone, Debug)]
430pub struct EffectPreset {
431    /// Preset name.
432    pub name: String,
433    /// Preset description.
434    pub description: String,
435    /// Effect configuration.
436    pub effect: Effect,
437}
438
439impl EffectPreset {
440    /// Create a new preset.
441    #[must_use]
442    pub fn new(name: String, description: String, effect: Effect) -> Self {
443        Self {
444            name,
445            description,
446            effect,
447        }
448    }
449
450    /// Create a brightness preset.
451    #[must_use]
452    pub fn brightness(value: f64) -> Self {
453        let mut effect = Effect::new(EffectType::Brightness);
454        effect.set_parameter(
455            "brightness".to_string(),
456            Parameter::constant(ParameterValue::Float(value)),
457        );
458        Self::new(
459            "Brightness".to_string(),
460            format!("Adjust brightness to {value}"),
461            effect,
462        )
463    }
464
465    /// Create a blur preset.
466    #[must_use]
467    pub fn blur(radius: f64) -> Self {
468        let mut effect = Effect::new(EffectType::Blur);
469        effect.set_parameter(
470            "radius".to_string(),
471            Parameter::constant(ParameterValue::Float(radius)),
472        );
473        Self::new(
474            "Blur".to_string(),
475            format!("Apply blur with radius {radius}"),
476            effect,
477        )
478    }
479
480    /// Create a fade in preset.
481    #[must_use]
482    pub fn fade_in(duration: i64) -> Self {
483        let mut effect = Effect::new(EffectType::Opacity);
484        let mut parameter = Parameter::constant(ParameterValue::Float(0.0));
485        parameter.add_keyframe(0, ParameterValue::Float(0.0));
486        parameter.add_keyframe(duration, ParameterValue::Float(1.0));
487        parameter.interpolation = InterpolationMode::Linear;
488        effect.set_parameter("opacity".to_string(), parameter);
489        Self::new(
490            "Fade In".to_string(),
491            format!("Fade in over {duration} frames"),
492            effect,
493        )
494    }
495
496    /// Create a fade out preset.
497    #[must_use]
498    pub fn fade_out(duration: i64) -> Self {
499        let mut effect = Effect::new(EffectType::Opacity);
500        let mut parameter = Parameter::constant(ParameterValue::Float(1.0));
501        parameter.add_keyframe(0, ParameterValue::Float(1.0));
502        parameter.add_keyframe(duration, ParameterValue::Float(0.0));
503        parameter.interpolation = InterpolationMode::Linear;
504        effect.set_parameter("opacity".to_string(), parameter);
505        Self::new(
506            "Fade Out".to_string(),
507            format!("Fade out over {duration} frames"),
508            effect,
509        )
510    }
511}
512
513/// Builder for creating filter graphs from effects.
514#[derive(Debug, Default)]
515pub struct EffectGraphBuilder;
516
517impl EffectGraphBuilder {
518    /// Create a new effect graph builder.
519    #[must_use]
520    pub fn new() -> Self {
521        Self
522    }
523
524    /// Build a filter graph from an effect stack.
525    pub fn build(&self, _effects: &EffectStack) -> EditResult<FilterGraph> {
526        // This would build an actual filter graph from the effect stack
527        // For now, return a default graph
528        Ok(FilterGraph::new())
529    }
530}