Skip to main content

motion_core/time/
frame_rate.rs

1//! Rational frame rates with nonzero components, reduced to lowest terms.
2
3use std::num::NonZeroU32;
4
5use super::Duration;
6
7/// Builds a [`FrameRate`] of whole frames per second from an integer constant.
8///
9/// A zero value fails compilation. Use [`FrameRate::try_new`] for runtime values.
10///
11/// ```
12/// use motion_core::{FrameRate, fps};
13/// assert_eq!(fps!(30), FrameRate::try_new(60, 2).unwrap());
14/// ```
15#[macro_export]
16macro_rules! fps {
17    ($frames:expr) => {
18        $crate::FrameRate::per_second(
19            const {
20                match ::std::num::NonZeroU32::new($frames) {
21                    Some(frames) => frames,
22                    None => panic!("frames per second must be nonzero"),
23                }
24            },
25        )
26    };
27}
28
29/// A positive rational number of frames per second, stored in reduced form.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct FrameRate {
32    numerator: NonZeroU32,
33    denominator: NonZeroU32,
34}
35
36impl FrameRate {
37    /// Film rate of exactly 24000/1001 frames per second.
38    pub const NTSC_FILM: Self = Self::new(non_zero(24_000), non_zero(1_001));
39
40    /// NTSC rate of exactly 30000/1001 frames per second.
41    pub const NTSC: Self = Self::new(non_zero(30_000), non_zero(1_001));
42
43    /// Double NTSC rate of exactly 60000/1001 frames per second.
44    pub const NTSC_DOUBLE: Self = Self::new(non_zero(60_000), non_zero(1_001));
45
46    /// Constructs and reduces a rational frame rate from runtime components.
47    ///
48    /// # Errors
49    /// Returns an error when either component is zero.
50    pub fn try_new(numerator: u32, denominator: u32) -> Result<Self, crate::ValidationError> {
51        match (NonZeroU32::new(numerator), NonZeroU32::new(denominator)) {
52            (Some(numerator), Some(denominator)) => Ok(Self::new(numerator, denominator)),
53            _ => Err(crate::ValidationError::invalid(
54                "frame_rate",
55                "ratio",
56                "frame-rate numerator and denominator must be nonzero",
57            )),
58        }
59    }
60
61    /// Constructs and reduces a rational frame rate.
62    pub const fn new(numerator: NonZeroU32, denominator: NonZeroU32) -> Self {
63        let mut common = numerator.get();
64        let mut remainder = denominator.get();
65        while remainder != 0 {
66            let next = common % remainder;
67            common = remainder;
68            remainder = next;
69        }
70        Self {
71            numerator: non_zero(numerator.get() / common),
72            denominator: non_zero(denominator.get() / common),
73        }
74    }
75
76    /// Constructs a rate of whole frames per second; see also [`fps!`](crate::fps).
77    pub const fn per_second(frames: NonZeroU32) -> Self {
78        Self {
79            numerator: frames,
80            denominator: NonZeroU32::MIN,
81        }
82    }
83
84    /// Returns the numerator of the reduced rate.
85    pub const fn numerator(self) -> u32 {
86        self.numerator.get()
87    }
88
89    /// Returns the denominator of the reduced rate.
90    pub const fn denominator(self) -> u32 {
91        self.denominator.get()
92    }
93
94    /// Returns the first frame's duration, rounded up if not tick-exact.
95    /// Use [`Time::from_frame`](super::Time::from_frame) for later boundaries to avoid accumulated rounding.
96    pub fn frame_duration(self) -> Duration {
97        Duration::from_frame(1, self)
98    }
99}
100
101impl From<NonZeroU32> for FrameRate {
102    fn from(frames_per_second: NonZeroU32) -> Self {
103        Self::per_second(frames_per_second)
104    }
105}
106
107/// Wraps a component that is nonzero by construction; zero fails constant evaluation.
108const fn non_zero(value: u32) -> NonZeroU32 {
109    match NonZeroU32::new(value) {
110        Some(value) => value,
111        None => panic!("frame-rate component must be nonzero"),
112    }
113}