rust_motion_animation/
interpolate.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ExtrapolateType {
19 #[default]
21 Extend,
22 Clamp,
24 Identity,
26 Wrap,
28}
29
30#[derive(Clone, Default)]
32pub struct InterpolateOptions<'a> {
33 pub easing: Option<&'a dyn Fn(f64) -> f64>,
35 pub extrapolate_left: ExtrapolateType,
37 pub extrapolate_right: ExtrapolateType,
39}
40
41pub 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 let segment = if input <= input_range[0] {
68 0
70 } else if input >= input_range[n - 1] {
71 n - 2
73 } else {
74 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 let mut t = if range.abs() < f64::EPSILON {
97 0.0
98 } else {
99 (input - in_lo) / range
100 };
101
102 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 }
127 }
128
129 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 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 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}