use crate::error::{EdlError, EdlResult};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub struct MotionEffect {
pub speed: f64,
pub reverse: bool,
pub freeze_frames: Option<u32>,
pub interpolation: InterpolationMethod,
}
impl MotionEffect {
#[must_use]
pub fn new(speed: f64) -> Self {
Self {
speed,
reverse: false,
freeze_frames: None,
interpolation: InterpolationMethod::Blend,
}
}
#[must_use]
pub const fn freeze(frames: u32) -> Self {
Self {
speed: 0.0,
reverse: false,
freeze_frames: Some(frames),
interpolation: InterpolationMethod::Nearest,
}
}
#[must_use]
pub fn reverse() -> Self {
Self {
speed: -1.0,
reverse: true,
freeze_frames: None,
interpolation: InterpolationMethod::Blend,
}
}
#[must_use]
pub fn slow_motion(factor: f64) -> Self {
Self::new(factor)
}
#[must_use]
pub fn fast_motion(factor: f64) -> Self {
Self::new(factor)
}
pub fn set_interpolation(&mut self, method: InterpolationMethod) {
self.interpolation = method;
}
#[must_use]
pub const fn is_freeze(&self) -> bool {
self.freeze_frames.is_some()
}
#[must_use]
pub const fn is_reverse(&self) -> bool {
self.reverse
}
#[must_use]
pub fn is_speed_change(&self) -> bool {
(self.speed - 1.0).abs() > f64::EPSILON
}
#[must_use]
pub fn effective_speed(&self) -> f64 {
if self.reverse {
-self.speed.abs()
} else {
self.speed
}
}
pub fn validate(&self) -> EdlResult<()> {
if self.speed < 0.0 && !self.reverse {
return Err(EdlError::InvalidMotionEffect(
"Negative speed requires reverse flag".to_string(),
));
}
if self.freeze_frames.is_some() && self.speed.abs() > f64::EPSILON {
return Err(EdlError::InvalidMotionEffect(
"Freeze frame must have speed 0.0".to_string(),
));
}
Ok(())
}
pub fn from_m2_comment(comment: &str) -> EdlResult<Self> {
let parts: Vec<&str> = comment.split_whitespace().collect();
if parts.len() < 4 || parts[0] != "M2" {
return Err(EdlError::InvalidMotionEffect(format!(
"Invalid M2 comment: {comment}"
)));
}
let speed = parts[2].parse::<f64>().map_err(|_| {
EdlError::InvalidMotionEffect(format!("Invalid speed value: {}", parts[2]))
})?;
Ok(Self::new(speed))
}
#[must_use]
pub fn to_m2_comment(&self, reel: &str, entry_frame: u32) -> String {
format!("M2 {} {:.2} {}", reel, self.speed, entry_frame)
}
}
impl Default for MotionEffect {
fn default() -> Self {
Self::new(1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum InterpolationMethod {
Nearest,
Linear,
Blend,
OpticalFlow,
}
impl InterpolationMethod {
#[must_use]
pub const fn quality_level(&self) -> u8 {
match self {
Self::Nearest => 0,
Self::Linear => 1,
Self::Blend => 2,
Self::OpticalFlow => 3,
}
}
}
impl FromStr for InterpolationMethod {
type Err = EdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"NEAREST" | "NONE" => Ok(Self::Nearest),
"LINEAR" => Ok(Self::Linear),
"BLEND" | "MOTION_BLUR" => Ok(Self::Blend),
"OPTICAL_FLOW" | "FLOW" => Ok(Self::OpticalFlow),
_ => Err(EdlError::InvalidMotionEffect(s.to_string())),
}
}
}
impl fmt::Display for InterpolationMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Nearest => "Nearest",
Self::Linear => "Linear",
Self::Blend => "Blend",
Self::OpticalFlow => "Optical Flow",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone)]
pub struct MotionEffectBuilder {
speed: f64,
reverse: bool,
freeze_frames: Option<u32>,
interpolation: InterpolationMethod,
}
impl MotionEffectBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
speed: 1.0,
reverse: false,
freeze_frames: None,
interpolation: InterpolationMethod::Blend,
}
}
#[must_use]
pub const fn speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
#[must_use]
pub const fn reverse(mut self) -> Self {
self.reverse = true;
self
}
#[must_use]
pub const fn freeze(mut self, frames: u32) -> Self {
self.freeze_frames = Some(frames);
self.speed = 0.0;
self
}
#[must_use]
pub const fn interpolation(mut self, method: InterpolationMethod) -> Self {
self.interpolation = method;
self
}
#[must_use]
pub const fn build(self) -> MotionEffect {
MotionEffect {
speed: self.speed,
reverse: self.reverse,
freeze_frames: self.freeze_frames,
interpolation: self.interpolation,
}
}
}
impl Default for MotionEffectBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_speed() {
let effect = MotionEffect::new(1.0);
assert!((effect.speed - 1.0).abs() < f64::EPSILON);
assert!(!effect.is_reverse());
assert!(!effect.is_freeze());
assert!(!effect.is_speed_change());
}
#[test]
fn test_slow_motion() {
let effect = MotionEffect::slow_motion(0.5);
assert!((effect.speed - 0.5).abs() < f64::EPSILON);
assert!(effect.is_speed_change());
}
#[test]
fn test_fast_motion() {
let effect = MotionEffect::fast_motion(2.0);
assert!((effect.speed - 2.0).abs() < f64::EPSILON);
assert!(effect.is_speed_change());
}
#[test]
fn test_freeze_frame() {
let effect = MotionEffect::freeze(100);
assert!(effect.is_freeze());
assert_eq!(effect.freeze_frames, Some(100));
assert!((effect.speed).abs() < f64::EPSILON);
}
#[test]
fn test_reverse() {
let effect = MotionEffect::reverse();
assert!(effect.is_reverse());
assert!((effect.speed + 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_effective_speed() {
let effect = MotionEffect::reverse();
assert!((effect.effective_speed() + 1.0).abs() < f64::EPSILON);
let effect = MotionEffect::new(2.0);
assert!((effect.effective_speed() - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_builder() {
let effect = MotionEffectBuilder::new()
.speed(0.5)
.interpolation(InterpolationMethod::Linear)
.build();
assert!((effect.speed - 0.5).abs() < f64::EPSILON);
assert_eq!(effect.interpolation, InterpolationMethod::Linear);
}
#[test]
fn test_m2_comment_parsing() {
let effect =
MotionEffect::from_m2_comment("M2 REEL1 0.50 100").expect("operation should succeed");
assert!((effect.speed - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_m2_comment_formatting() {
let effect = MotionEffect::new(0.5);
let comment = effect.to_m2_comment("REEL1", 100);
assert_eq!(comment, "M2 REEL1 0.50 100");
}
#[test]
fn test_interpolation_parsing() {
assert_eq!(
"NEAREST"
.parse::<InterpolationMethod>()
.expect("operation should succeed"),
InterpolationMethod::Nearest
);
assert_eq!(
"BLEND"
.parse::<InterpolationMethod>()
.expect("operation should succeed"),
InterpolationMethod::Blend
);
}
#[test]
fn test_validation() {
let effect = MotionEffect::new(1.0);
assert!(effect.validate().is_ok());
let mut bad_effect = MotionEffect::freeze(100);
bad_effect.speed = 1.0;
assert!(bad_effect.validate().is_err());
}
}