#[derive(Debug, Clone, PartialEq)]
pub struct Spline {
xs: Vec<f64>,
ys: Vec<f64>,
}
const STEPS: usize = 256;
impl Spline {
pub fn new(points: &[(f64, f64)]) -> Self {
let (mut xs, mut ys) = (Vec::new(), Vec::new());
let mut push = |(x, y): (f64, f64)| {
xs.push(x);
ys.push(y);
};
match points.len() {
0 => [(0.0, 0.0), (1.0, 1.0)].into_iter().for_each(&mut push),
1 | 2 => points.iter().copied().for_each(&mut push),
n => {
let mid = |a: (f64, f64), b: (f64, f64)| ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
for j in 0..n - 2 {
let start = if j == 0 { points[0] } else { mid(points[j], points[j + 1]) };
let control = points[j + 1];
let end = if j == n - 3 { points[n - 1] } else { mid(points[j + 1], points[j + 2]) };
for step in usize::from(j > 0)..=STEPS {
let t = step as f64 / STEPS as f64;
let (a, b, c) = ((1.0 - t) * (1.0 - t), 2.0 * t * (1.0 - t), t * t);
push((a * start.0 + b * control.0 + c * end.0, a * start.1 + b * control.1 + c * end.1));
}
}
}
}
Spline { xs, ys }
}
pub fn at(&self, x: f64) -> f64 {
let (xs, ys) = (&self.xs, &self.ys);
if x <= xs[0] {
return ys[0];
}
let i = xs.partition_point(|&v| v < x);
if i >= xs.len() {
return ys[ys.len() - 1];
}
let (x0, x1) = (xs[i - 1], xs[i]);
if x1 <= x0 { ys[i] } else { ys[i - 1] + (ys[i] - ys[i - 1]) * (x - x0) / (x1 - x0) }
}
pub fn table(&self) -> [f32; 256] {
std::array::from_fn(|v| (self.at(v as f64 / 255.0) * 255.0).clamp(0.0, 255.0) as f32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_points_are_a_line_and_ends_are_kept() {
let line = Spline::new(&[(0.0, 0.2), (1.0, 0.8)]);
assert!((line.at(0.5) - 0.5).abs() < 1e-9);
assert_eq!(line.at(-1.0), 0.2);
assert_eq!(line.at(2.0), 0.8);
}
#[test]
fn three_points_are_one_quadratic() {
let bump = Spline::new(&[(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]);
assert!((bump.at(0.5) - 0.5).abs() < 1e-3);
}
#[test]
fn default_gradient_curve_is_symmetric() {
let s = Spline::new(&[(0.0, 0.0), (0.25, 0.1), (0.75, 0.9), (1.0, 1.0)]);
assert!((s.at(0.5) - 0.5).abs() < 1e-3);
assert!((s.at(0.25) - 0.178).abs() < 0.005, "{}", s.at(0.25));
}
}