Skip to main content

frame_tick/
lib.rs

1//! Fixed-point representation of time where each second is divided into
2//! 3,603,600 `Tick`s (or 25,200, if the `cargo` feature `low_res` is set).
3//!
4//! This crate was inspired by [this article](https://iquilezles.org/articles/ticks/)
5//! from [Inigo Quilez](https://iquilezles.org/). Please refer to this for a
6//! more detailed explanation.
7//! > *Note that the default for `TICKS_PER_SECOND`, 3,603,600, is the Least
8//! > Common Multiple of all numbers in the list given in the article as well as
9//! > 11 and 13, which are needed for NTSC.*
10//!
11//! This makes it 'compatible' with lots of frame- and refresh rates without
12//! ever mapping outside of or repeating a frame. That is: without strobing.
13//!
14//! In particular, a `Tick` can represent exactly:
15//!
16//! - 24hz and 48hz, great for movie playback.
17//!
18//! - 6hz, 8hz and 12hz, great for animating on 4s, 3s and 2s.
19//!
20//! - 29.97hz, 59.94hz NTSC found in Japan, South Korea and the USA.
21//!
22//! - 30hz, 60hz, for internet video and TV in the USA.
23//!
24//! - 25hz and 50hz, for TV in the EU.
25//!
26//! - 72hz, for Oculus Quest 1.
27//!
28//! - 90hz for Quest 2, Rift and other headsets.
29//!
30//! - 120hz, 144hz and 240hz, for newer VR headesets and high frequency
31//!   monitors.
32//!
33//! - And many more.
34//!
35//! # Examples
36//!
37//! ```
38//! # use core::num::NonZeroU32;
39//! use frame_tick::{FrameRateConversion, FramesPerSec, Tick};
40//!
41//! let tick = Tick::from_secs(1.0);
42//!
43//! /// A round trip is lossless.
44//! assert_eq!(1.0, tick.to_secs());
45//! /// One second at 120hz == frame № 120.
46//! assert_eq!(120, tick.to_frames(FramesPerSec::new(120).unwrap()));
47//! ```
48//!
49//! # Cargo features
50#![doc = document_features::document_features!()]
51#![cfg_attr(not(feature = "std"), no_std)]
52
53use core::{
54    convert::{AsMut, AsRef},
55    num::{NonZeroU32, ParseIntError},
56    ops::{Add, Div, Mul, Sub},
57    str::FromStr,
58};
59#[cfg(feature = "facet")]
60use facet::Facet;
61#[cfg(all(feature = "std", doc))]
62use std::time::Duration;
63
64#[cfg(feature = "std")]
65pub mod std_traits;
66
67#[cfg(test)]
68mod tests;
69
70#[cfg(feature = "float_frame_rate")]
71pub type FramesPerSecF32 = typed_floats::StrictlyPositiveFinite<f32>;
72#[cfg(feature = "float_frame_rate")]
73pub type FramesPerSecF64 = typed_floats::StrictlyPositiveFinite<f64>;
74
75pub type FramesPerSec = NonZeroU32;
76
77/// A frame rate represented as a fraction (numerator/denominator).
78///
79/// This allows exact representation of fractional frame rates like NTSC
80/// (30000/1001 ≈ 29.97 fps).
81#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
82#[cfg_attr(feature = "facet", derive(Facet))]
83#[cfg_attr(feature = "facet", facet(opaque))]
84#[cfg_attr(
85    feature = "rkyv",
86    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
87)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct FrameRate {
90    /// Numerator (frames).
91    num: NonZeroU32,
92    /// Denominator (per seconds).
93    den: NonZeroU32,
94}
95
96impl FrameRate {
97    /// 24 fps - Film.
98    pub const FILM: Self = Self {
99        num: NonZeroU32::new(24).unwrap(),
100        den: NonZeroU32::new(1).unwrap(),
101    };
102    /// 30 fps.
103    pub const FPS_30: Self = Self {
104        num: NonZeroU32::new(30).unwrap(),
105        den: NonZeroU32::new(1).unwrap(),
106    };
107    /// 60 fps.
108    pub const FPS_60: Self = Self {
109        num: NonZeroU32::new(60).unwrap(),
110        den: NonZeroU32::new(1).unwrap(),
111    };
112    /// 29.97 fps (30000/1001) - NTSC.
113    pub const NTSC: Self = Self {
114        num: NonZeroU32::new(30000).unwrap(),
115        den: NonZeroU32::new(1001).unwrap(),
116    };
117    /// 23.976 fps (24000/1001) - Film on NTSC video.
118    pub const NTSC_FILM: Self = Self {
119        num: NonZeroU32::new(24000).unwrap(),
120        den: NonZeroU32::new(1001).unwrap(),
121    };
122    /// 59.94 fps (60000/1001) - NTSC high frame rate.
123    pub const NTSC_HIGH: Self = Self {
124        num: NonZeroU32::new(60000).unwrap(),
125        den: NonZeroU32::new(1001).unwrap(),
126    };
127    /// 25 fps - PAL.
128    pub const PAL: Self = Self {
129        num: NonZeroU32::new(25).unwrap(),
130        den: NonZeroU32::new(1).unwrap(),
131    };
132    /// 50 fps - PAL high frame rate.
133    pub const PAL_HIGH: Self = Self {
134        num: NonZeroU32::new(50).unwrap(),
135        den: NonZeroU32::new(1).unwrap(),
136    };
137
138    /// Create a new frame rate from numerator and denominator.
139    ///
140    /// # Example
141    /// ```
142    /// use frame_tick::FrameRate;
143    ///
144    /// // 29.97 fps (NTSC)
145    /// let ntsc = FrameRate::new(30000, 1001).unwrap();
146    /// ```
147    #[inline]
148    pub fn new(num: u32, den: u32) -> Option<Self> {
149        Some(Self {
150            num: NonZeroU32::new(num)?,
151            den: NonZeroU32::new(den)?,
152        })
153    }
154
155    /// Create an integer frame rate (e.g., 24 fps = 24/1).
156    #[inline]
157    pub fn from_int(fps: u32) -> Option<Self> {
158        Self::new(fps, 1)
159    }
160
161    /// Get the numerator.
162    #[inline]
163    pub fn num(&self) -> u32 {
164        self.num.get()
165    }
166
167    /// Get the denominator.
168    #[inline]
169    pub fn den(&self) -> u32 {
170        self.den.get()
171    }
172}
173
174impl From<NonZeroU32> for FrameRate {
175    fn from(fps: NonZeroU32) -> Self {
176        Self {
177            num: fps,
178            den: unsafe { NonZeroU32::new_unchecked(1) },
179        }
180    }
181}
182
183/// The number of ticks per second.
184///
185/// Use the `low_res` feature to configure this.
186#[cfg(not(feature = "low_res"))]
187pub const TICKS_PER_SECOND: i64 = 3_603_600;
188/// The number of ticks per second.
189///
190/// Use the `low_res` feature to configure this.
191#[cfg(feature = "low_res")]
192pub const TICKS_PER_SECOND: i64 = 25_200;
193
194/// Fixed-point representation of time where each second is divided into
195/// [`TICKS_PER_SECOND`].
196///
197/// This type can also represent negative time as this is common in DCCs like a
198/// video editor or animation system where this type would typically be used.
199#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
200#[cfg_attr(feature = "facet", derive(Facet))]
201#[cfg_attr(
202    feature = "rkyv",
203    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize),
204    rkyv(attr(derive(
205        Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash
206    )))
207)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub struct Tick(i64);
210
211impl IntoIterator for Tick {
212    type IntoIter = TickIter;
213    type Item = i64;
214
215    fn into_iter(self) -> Self::IntoIter {
216        TickIter(self.0)
217    }
218}
219
220/// An iterator over [`Tick`]s.
221#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
222#[cfg_attr(feature = "facet", derive(Facet))]
223pub struct TickIter(i64);
224
225impl Iterator for TickIter {
226    type Item = i64;
227
228    fn next(&mut self) -> Option<Self::Item> {
229        if i64::MAX == self.0 {
230            None
231        } else {
232            let value = self.0;
233            self.0 += 1;
234
235            Some(value)
236        }
237    }
238}
239
240impl DoubleEndedIterator for TickIter {
241    fn next_back(&mut self) -> Option<Self::Item> {
242        if i64::MIN == self.0 {
243            None
244        } else {
245            self.0 -= 1;
246            Some(self.0)
247        }
248    }
249}
250
251/// An iterator over [`Tick`]s in reverse order.
252#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
253#[cfg_attr(feature = "facet", derive(Facet))]
254pub struct TickRevIter(i64);
255
256impl Iterator for TickRevIter {
257    type Item = i64;
258
259    fn next(&mut self) -> Option<Self::Item> {
260        let value = self.0;
261        self.0 -= 1;
262        if i64::MIN == self.0 {
263            None
264        } else {
265            Some(value)
266        }
267    }
268}
269
270impl AsRef<i64> for Tick {
271    fn as_ref(&self) -> &i64 {
272        &self.0
273    }
274}
275
276impl AsMut<i64> for Tick {
277    fn as_mut(&mut self) -> &mut i64 {
278        &mut self.0
279    }
280}
281
282macro_rules! impl_tick_from {
283    ($ty:ty) => {
284        impl From<$ty> for Tick {
285            fn from(value: $ty) -> Self {
286                Self(value as _)
287            }
288        }
289    };
290}
291
292macro_rules! impl_from_tick {
293    ($ty:ty) => {
294        impl From<Tick> for $ty {
295            fn from(tick: Tick) -> Self {
296                tick.0 as _
297            }
298        }
299
300        impl From<&Tick> for $ty {
301            fn from(tick: &Tick) -> Self {
302                tick.0 as _
303            }
304        }
305    };
306}
307
308impl_from_tick!(u64);
309impl_from_tick!(u128);
310impl_from_tick!(usize);
311impl_from_tick!(i64);
312impl_from_tick!(i128);
313impl_from_tick!(isize);
314impl_from_tick!(f32);
315impl_from_tick!(f64);
316
317impl_tick_from!(u8);
318impl_tick_from!(u16);
319impl_tick_from!(u32);
320impl_tick_from!(i8);
321impl_tick_from!(i16);
322impl_tick_from!(i32);
323impl_tick_from!(i64);
324
325macro_rules! round {
326    ($ty:ty, $value:expr) => {{
327        let value = $value;
328        #[cfg(feature = "std")]
329        let value = value.round();
330        #[cfg(not(feature = "std"))]
331        let value = if value >= 0.0 {
332            value + 0.5
333        } else {
334            value - 0.5
335        };
336
337        value as i64
338    }};
339}
340
341impl From<f32> for Tick {
342    fn from(value: f32) -> Self {
343        Self(round!(f32, value))
344    }
345}
346
347impl From<f64> for Tick {
348    fn from(value: f64) -> Self {
349        Self(round!(f64, value))
350    }
351}
352
353impl FromStr for Tick {
354    type Err = ParseIntError;
355
356    fn from_str(s: &str) -> Result<Self, Self::Err> {
357        s.parse::<i64>().map(Tick)
358    }
359}
360
361impl Add for Tick {
362    type Output = Tick;
363
364    fn add(self, rhs: Self) -> Self::Output {
365        Tick(self.0 + rhs.0)
366    }
367}
368
369impl Sub for Tick {
370    type Output = Tick;
371
372    fn sub(self, rhs: Self) -> Self::Output {
373        Tick(self.0 - rhs.0)
374    }
375}
376
377macro_rules! impl_mul_div_float {
378    ($ty:ty) => {
379        impl Mul<$ty> for Tick {
380            type Output = Self;
381
382            fn mul(self, rhs: $ty) -> Self::Output {
383                let value = self.0 as $ty * rhs;
384                Self(round!($ty, value))
385            }
386        }
387
388        impl Div<$ty> for Tick {
389            type Output = Self;
390
391            fn div(self, rhs: $ty) -> Self::Output {
392                let value = self.0 as $ty / rhs;
393                Self(round!($ty, value))
394            }
395        }
396    };
397}
398
399impl_mul_div_float!(f32);
400impl_mul_div_float!(f64);
401
402macro_rules! impl_mul_div_int {
403    ($ty:ty) => {
404        impl Mul<$ty> for Tick {
405            type Output = Self;
406
407            fn mul(self, rhs: $ty) -> Self::Output {
408                Self((self.0 as $ty * rhs) as _)
409            }
410        }
411
412        impl Div<$ty> for Tick {
413            type Output = Self;
414
415            fn div(self, rhs: $ty) -> Self::Output {
416                Self((self.0 as $ty / rhs) as _)
417            }
418        }
419    };
420}
421
422impl_mul_div_int!(u8);
423impl_mul_div_int!(u16);
424impl_mul_div_int!(u32);
425impl_mul_div_int!(u64);
426impl_mul_div_int!(u128);
427impl_mul_div_int!(usize);
428impl_mul_div_int!(i8);
429impl_mul_div_int!(i16);
430impl_mul_div_int!(i32);
431impl_mul_div_int!(i64);
432impl_mul_div_int!(i128);
433impl_mul_div_int!(isize);
434
435impl Tick {
436    #[inline]
437    pub fn new(value: i64) -> Self {
438        Self(value)
439    }
440
441    /// Create ticks from seconds.
442    #[inline]
443    pub fn from_secs(secs: f64) -> Self {
444        Self((secs * TICKS_PER_SECOND as f64) as i64)
445    }
446
447    /// Convert ticks to seconds.
448    #[inline]
449    pub fn to_secs(&self) -> f64 {
450        self.0 as f64 / TICKS_PER_SECOND as f64
451    }
452
453    /// Linearly interpolate between two ticks.
454    #[inline]
455    pub fn lerp(self, other: Self, t: f64) -> Self {
456        let t = t.clamp(0.0, 1.0);
457        Self(round!(f64, self.0 as f64 * (1.0 - t) + other.0 as f64 * t))
458    }
459
460    /// Convert ticks to timecode (hours, minutes, seconds, frames) at the
461    /// given frame rate.
462    ///
463    /// Returns `(hours, minutes, seconds, frames)`.
464    #[inline]
465    pub fn to_timecode(self, frame_rate: FrameRate) -> (i64, i64, i64, i64) {
466        // Calculate total frames using exact frame rate (with rounding):
467        // total_frames = ticks * (num/den) / TICKS_PER_SECOND
468        let divisor = TICKS_PER_SECOND as i128 * frame_rate.den() as i128;
469        let total_frames = ((self.0 as i128 * frame_rate.num() as i128
470            + divisor / 2)
471            / divisor) as i64;
472
473        // Nominal fps for h:m:s:f display (ceiling of actual fps).
474        let nominal_fps = (frame_rate.num() as i64 + frame_rate.den() as i64
475            - 1)
476            / frame_rate.den() as i64;
477
478        let frames = total_frames % nominal_fps;
479        let total_seconds = total_frames / nominal_fps;
480        let seconds = total_seconds % 60;
481        let total_minutes = total_seconds / 60;
482        let minutes = total_minutes % 60;
483        let hours = total_minutes / 60;
484
485        (hours, minutes, seconds, frames)
486    }
487
488    /// Create ticks from timecode (hours, minutes, seconds, frames) at the
489    /// given frame rate.
490    #[inline]
491    pub fn from_timecode(
492        hours: i64,
493        minutes: i64,
494        seconds: i64,
495        frames: i64,
496        frame_rate: FrameRate,
497    ) -> Self {
498        // Nominal fps for h:m:s:f display (ceiling of actual fps).
499        let nominal_fps = (frame_rate.num() as i64 + frame_rate.den() as i64
500            - 1)
501            / frame_rate.den() as i64;
502
503        let total_frames = hours * 3600 * nominal_fps
504            + minutes * 60 * nominal_fps
505            + seconds * nominal_fps
506            + frames;
507
508        // Convert frames to ticks using exact frame rate:
509        // ticks = total_frames * TICKS_PER_SECOND / (num/den)
510        //       = total_frames * TICKS_PER_SECOND * den / num
511        Self(
512            (total_frames as i128
513                * TICKS_PER_SECOND as i128
514                * frame_rate.den() as i128
515                / frame_rate.num() as i128) as i64,
516        )
517    }
518}
519
520/// Conversion to/from specified frame rates.
521pub trait FrameRateConversion<T> {
522    fn to_frames(self, frame_rate: T) -> i64;
523    fn from_frames(frames: i64, frame_rate: T) -> Self;
524}
525
526impl FrameRateConversion<FramesPerSec> for Tick {
527    /// Convert ticks to frame number at the specified integer frame rate.
528    fn to_frames(self, frame_rate: FramesPerSec) -> i64 {
529        (self.0 as i128 * frame_rate.get() as i128 / TICKS_PER_SECOND as i128)
530            as _
531    }
532
533    /// Convert frame number to ticks at the specified integer frame rate.
534    fn from_frames(frames: i64, frame_rate: FramesPerSec) -> Self {
535        Self(
536            (frames as i128 * TICKS_PER_SECOND as i128
537                / frame_rate.get() as i128) as _,
538        )
539    }
540}
541
542#[cfg(feature = "float_frame_rate")]
543impl FrameRateConversion<FramesPerSecF32> for Tick {
544    /// Convert ticks to frame number at the specified floating point frame
545    /// rate.
546    fn to_frames(self, frame_rate: FramesPerSecF32) -> i64 {
547        (self.0 as f64 * frame_rate.get() as f64 / TICKS_PER_SECOND as f64)
548            .round() as _
549    }
550
551    /// Convert frame number to ticks at the specified floating point frame
552    /// rate.
553    fn from_frames(frames: i64, frame_rate: FramesPerSecF32) -> Self {
554        Self(
555            (frames as f64 * TICKS_PER_SECOND as f64 / frame_rate.get() as f64)
556                .round() as _,
557        )
558    }
559}
560
561#[cfg(feature = "float_frame_rate")]
562impl FrameRateConversion<FramesPerSecF64> for Tick {
563    /// Convert ticks to frame number at the specified floating point frame
564    /// rate.
565    fn to_frames(self, frame_rate: FramesPerSecF64) -> i64 {
566        (self.0 as f64 * frame_rate.get() / TICKS_PER_SECOND as f64).round()
567            as _
568    }
569
570    /// Convert frame number to ticks at the specified floating point frame
571    /// rate.
572    fn from_frames(frames: i64, frame_rate: FramesPerSecF64) -> Self {
573        Self(
574            (frames as f64 * TICKS_PER_SECOND as f64 / frame_rate.get()).round()
575                as _,
576        )
577    }
578}