#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExtrapolateType {
#[default]
Extend,
Clamp,
Identity,
Wrap,
}
#[derive(Clone, Default)]
pub struct InterpolateOptions<'a> {
pub easing: Option<&'a dyn Fn(f64) -> f64>,
pub extrapolate_left: ExtrapolateType,
pub extrapolate_right: ExtrapolateType,
}
pub fn interpolate(
input: f64,
input_range: &[f64],
output_range: &[f64],
options: InterpolateOptions<'_>,
) -> f64 {
assert!(
input_range.len() >= 2,
"interpolate: input_range must have at least 2 elements"
);
assert_eq!(
input_range.len(),
output_range.len(),
"interpolate: input_range and output_range must have the same length"
);
let n = input_range.len();
let segment = if input <= input_range[0] {
0
} else if input >= input_range[n - 1] {
n - 2
} else {
let mut lo = 0usize;
let mut hi = n - 2;
while lo < hi {
let mid = (lo + hi) / 2;
if input_range[mid + 1] <= input {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
};
let in_lo = input_range[segment];
let in_hi = input_range[segment + 1];
let out_lo = output_range[segment];
let out_hi = output_range[segment + 1];
let range = in_hi - in_lo;
let mut t = if range.abs() < f64::EPSILON {
0.0
} else {
(input - in_lo) / range
};
let is_left = input < in_lo || (segment == 0 && input <= input_range[0]);
let is_right = input > in_hi || (segment == n - 2 && input >= input_range[n - 1]);
let extrapolate = if is_left && t < 0.0 {
options.extrapolate_left
} else if is_right && t > 1.0 {
options.extrapolate_right
} else {
ExtrapolateType::Extend
};
match extrapolate {
ExtrapolateType::Clamp => {
t = t.clamp(0.0, 1.0);
}
ExtrapolateType::Identity => {
return input;
}
ExtrapolateType::Wrap => {
t = t.rem_euclid(1.0);
}
ExtrapolateType::Extend => {
}
}
let t_eased = if let Some(ease) = options.easing {
if (0.0..=1.0).contains(&t) {
ease(t)
} else {
t
}
} else {
t
};
out_lo + (out_hi - out_lo) * t_eased
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_linear() {
let v = interpolate(15.0, &[0.0, 30.0], &[0.0, 1.0], Default::default());
assert!((v - 0.5).abs() < 1e-10, "got {v}");
}
#[test]
fn clamp_left() {
let opts = InterpolateOptions {
extrapolate_left: ExtrapolateType::Clamp,
..Default::default()
};
let v = interpolate(-10.0, &[0.0, 30.0], &[0.0, 1.0], opts);
assert!((v - 0.0).abs() < 1e-10, "got {v}");
}
#[test]
fn clamp_right() {
let opts = InterpolateOptions {
extrapolate_right: ExtrapolateType::Clamp,
..Default::default()
};
let v = interpolate(60.0, &[0.0, 30.0], &[0.0, 1.0], opts);
assert!((v - 1.0).abs() < 1e-10, "got {v}");
}
#[test]
fn multi_segment() {
let v = interpolate(
45.0,
&[0.0, 30.0, 60.0],
&[0.0, 1.0, 0.0],
Default::default(),
);
assert!((v - 0.5).abs() < 1e-10, "got {v}");
}
#[test]
fn with_easing() {
use crate::easing::linear;
let opts = InterpolateOptions {
easing: Some(&linear),
..Default::default()
};
let v = interpolate(15.0, &[0.0, 30.0], &[0.0, 100.0], opts);
assert!((v - 50.0).abs() < 1e-10, "got {v}");
}
#[test]
fn identity_extrapolate() {
let opts = InterpolateOptions {
extrapolate_right: ExtrapolateType::Identity,
..Default::default()
};
let v = interpolate(999.0, &[0.0, 1.0], &[0.0, 100.0], opts);
assert!((v - 999.0).abs() < 1e-10, "got {v}");
}
}