Skip to main content

rust_motion_animation/
interpolate.rs

1//! `interpolate()` — Rust port of Remotion's `interpolate()` function.
2//!
3//! Maps an input value from one numeric range to another, with optional
4//! easing and extrapolation control.
5//!
6//! # Example
7//! ```rust
8//! use rust_motion_animation::interpolate::{interpolate, InterpolateOptions};
9//!
10//! // Scale from 0→1 as frame goes 0→30
11//! let frame = 15.0_f64;
12//! let scale = interpolate(frame, &[0.0, 30.0], &[0.0, 1.0], Default::default());
13//! assert!((scale - 0.5).abs() < 1e-10);
14//! ```
15
16/// How to handle values outside the input range.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ExtrapolateType {
19    /// Extend the linear mapping beyond the range (default).
20    #[default]
21    Extend,
22    /// Clamp to the nearest output value.
23    Clamp,
24    /// Return the input unchanged when out of range.
25    Identity,
26    /// Wrap around (modular arithmetic on the input range).
27    Wrap,
28}
29
30/// Options for [`interpolate`].
31#[derive(Clone, Default)]
32pub struct InterpolateOptions<'a> {
33    /// Optional easing function applied to the normalised `t` inside each segment.
34    pub easing: Option<&'a dyn Fn(f64) -> f64>,
35    /// Extrapolation strategy for values **below** the first input point.
36    pub extrapolate_left: ExtrapolateType,
37    /// Extrapolation strategy for values **above** the last input point.
38    pub extrapolate_right: ExtrapolateType,
39}
40
41/// Maps `input` from `input_range` to `output_range`.
42///
43/// Equivalent to Remotion's `interpolate(input, inputRange, outputRange, options?)`.
44///
45/// # Panics
46/// Panics if `input_range` and `output_range` have different lengths, or if
47/// either has fewer than 2 elements.
48pub fn interpolate(
49    input: f64,
50    input_range: &[f64],
51    output_range: &[f64],
52    options: InterpolateOptions<'_>,
53) -> f64 {
54    assert!(
55        input_range.len() >= 2,
56        "interpolate: input_range must have at least 2 elements"
57    );
58    assert_eq!(
59        input_range.len(),
60        output_range.len(),
61        "interpolate: input_range and output_range must have the same length"
62    );
63
64    let n = input_range.len();
65
66    // ── Find the segment that contains `input` ────────────────────────────────
67    let segment = if input <= input_range[0] {
68        // Left of first point
69        0
70    } else if input >= input_range[n - 1] {
71        // Right of last point — use the last segment
72        n - 2
73    } else {
74        // Binary search for the segment
75        let mut lo = 0usize;
76        let mut hi = n - 2;
77        while lo < hi {
78            let mid = (lo + hi) / 2;
79            if input_range[mid + 1] <= input {
80                lo = mid + 1;
81            } else {
82                hi = mid;
83            }
84        }
85        lo
86    };
87
88    let in_lo = input_range[segment];
89    let in_hi = input_range[segment + 1];
90    let out_lo = output_range[segment];
91    let out_hi = output_range[segment + 1];
92
93    let range = in_hi - in_lo;
94
95    // ── Normalise the input to [0, 1] within the segment ─────────────────────
96    let mut t = if range.abs() < f64::EPSILON {
97        0.0
98    } else {
99        (input - in_lo) / range
100    };
101
102    // ── Apply extrapolation ───────────────────────────────────────────────────
103    let is_left = input < in_lo || (segment == 0 && input <= input_range[0]);
104    let is_right = input > in_hi || (segment == n - 2 && input >= input_range[n - 1]);
105
106    let extrapolate = if is_left && t < 0.0 {
107        options.extrapolate_left
108    } else if is_right && t > 1.0 {
109        options.extrapolate_right
110    } else {
111        ExtrapolateType::Extend
112    };
113
114    match extrapolate {
115        ExtrapolateType::Clamp => {
116            t = t.clamp(0.0, 1.0);
117        }
118        ExtrapolateType::Identity => {
119            return input;
120        }
121        ExtrapolateType::Wrap => {
122            t = t.rem_euclid(1.0);
123        }
124        ExtrapolateType::Extend => {
125            // t stays as-is (can be negative or > 1)
126        }
127    }
128
129    // ── Apply easing (only within [0, 1]) ────────────────────────────────────
130    let t_eased = if let Some(ease) = options.easing {
131        if (0.0..=1.0).contains(&t) {
132            ease(t)
133        } else {
134            t
135        }
136    } else {
137        t
138    };
139
140    // ── Linear interpolation in output space ─────────────────────────────────
141    out_lo + (out_hi - out_lo) * t_eased
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn basic_linear() {
150        let v = interpolate(15.0, &[0.0, 30.0], &[0.0, 1.0], Default::default());
151        assert!((v - 0.5).abs() < 1e-10, "got {v}");
152    }
153
154    #[test]
155    fn clamp_left() {
156        let opts = InterpolateOptions {
157            extrapolate_left: ExtrapolateType::Clamp,
158            ..Default::default()
159        };
160        let v = interpolate(-10.0, &[0.0, 30.0], &[0.0, 1.0], opts);
161        assert!((v - 0.0).abs() < 1e-10, "got {v}");
162    }
163
164    #[test]
165    fn clamp_right() {
166        let opts = InterpolateOptions {
167            extrapolate_right: ExtrapolateType::Clamp,
168            ..Default::default()
169        };
170        let v = interpolate(60.0, &[0.0, 30.0], &[0.0, 1.0], opts);
171        assert!((v - 1.0).abs() < 1e-10, "got {v}");
172    }
173
174    #[test]
175    fn multi_segment() {
176        // Maps 0..30..60 → 0..1..0  (triangle)
177        let v = interpolate(
178            45.0,
179            &[0.0, 30.0, 60.0],
180            &[0.0, 1.0, 0.0],
181            Default::default(),
182        );
183        assert!((v - 0.5).abs() < 1e-10, "got {v}");
184    }
185
186    #[test]
187    fn with_easing() {
188        use crate::easing::linear;
189        let opts = InterpolateOptions {
190            easing: Some(&linear),
191            ..Default::default()
192        };
193        let v = interpolate(15.0, &[0.0, 30.0], &[0.0, 100.0], opts);
194        assert!((v - 50.0).abs() < 1e-10, "got {v}");
195    }
196
197    #[test]
198    fn identity_extrapolate() {
199        let opts = InterpolateOptions {
200            extrapolate_right: ExtrapolateType::Identity,
201            ..Default::default()
202        };
203        let v = interpolate(999.0, &[0.0, 1.0], &[0.0, 100.0], opts);
204        assert!((v - 999.0).abs() < 1e-10, "got {v}");
205    }
206}