const LUT_SIZE: usize = 256;
#[derive(Clone, Debug)]
pub struct CurveLut {
table: [f32; LUT_SIZE],
}
impl CurveLut {
pub fn from_points(points: &[[f32; 2]]) -> Self {
assert!(points.len() >= 2, "curve needs at least 2 control points");
let n = points.len();
let mut table = [0.0f32; LUT_SIZE];
if n == 2 {
let [x0, y0] = points[0];
let [x1, y1] = points[1];
let dx = (x1 - x0).max(1e-6);
for (i, slot) in table.iter_mut().enumerate() {
let t = i as f32 / (LUT_SIZE - 1) as f32;
let frac = ((t - x0) / dx).clamp(0.0, 1.0);
*slot = (y0 + frac * (y1 - y0)).clamp(0.0, 1.0);
}
return CurveLut { table };
}
let spline = NaturalCubicSpline::from_points(points);
for (i, slot) in table.iter_mut().enumerate() {
let t = i as f32 / (LUT_SIZE - 1) as f32;
*slot = spline.evaluate(t).clamp(0.0, 1.0);
}
CurveLut { table }
}
#[inline]
pub fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
let idx = t * (LUT_SIZE - 1) as f32;
let lo = idx as usize;
let hi = (lo + 1).min(LUT_SIZE - 1);
let frac = idx - lo as f32;
self.table[lo] * (1.0 - frac) + self.table[hi] * frac
}
pub fn table(&self) -> &[f32; LUT_SIZE] {
&self.table
}
}
struct NaturalCubicSpline {
a: Vec<f32>,
b: Vec<f32>,
c: Vec<f32>,
d: Vec<f32>,
h: Vec<f32>,
x: Vec<f32>,
}
impl NaturalCubicSpline {
fn from_points(points: &[[f32; 2]]) -> Self {
let intervals = points.len() - 1;
let x: Vec<f32> = points.iter().map(|p| p[0]).collect();
let a: Vec<f32> = points.iter().map(|p| p[1]).collect();
let mut h = vec![0.0f32; intervals];
for i in 0..intervals {
h[i] = (x[i + 1] - x[i]).max(1e-6);
}
let c = if intervals > 1 {
let inner = intervals - 1; let mut tri_b = vec![0.0f32; inner]; let mut tri_f = vec![0.0f32; inner];
for i in 0..inner {
tri_b[i] = 2.0 * (h[i] + h[i + 1]);
tri_f[i] = 6.0 * ((a[i + 2] - a[i + 1]) / h[i + 1] - (a[i + 1] - a[i]) / h[i]);
}
let tri_a: Vec<f32> = (1..inner).map(|i| h[i]).collect();
let inner_c = tridiagonal_solve(&tri_a, &tri_b, &tri_a, &tri_f);
let mut c = Vec::with_capacity(intervals + 1);
c.push(0.0);
c.extend_from_slice(&inner_c);
c.push(0.0);
c
} else {
vec![0.0; intervals + 1]
};
let mut d = vec![0.0f32; intervals];
let mut b = vec![0.0f32; intervals];
for i in 0..intervals {
d[i] = (c[i + 1] - c[i]) / h[i];
b[i] = -0.5 * c[i] * h[i] - (1.0 / 6.0) * d[i] * h[i] * h[i] + (a[i + 1] - a[i]) / h[i];
}
NaturalCubicSpline { a, b, c, d, h, x }
}
fn evaluate(&self, t: f32) -> f32 {
let t = t.clamp(self.x[0], *self.x.last().unwrap());
let intervals = self.h.len();
let mut i = 0;
while i < intervals - 1 && t >= self.x[i + 1] {
i += 1;
}
let dx = t - self.x[i];
self.a[i]
+ self.b[i] * dx
+ 0.5 * self.c[i] * dx * dx
+ (1.0 / 6.0) * self.d[i] * dx * dx * dx
}
}
fn tridiagonal_solve(sub: &[f32], diag: &[f32], sup: &[f32], rhs: &[f32]) -> Vec<f32> {
let n = diag.len();
if n == 1 {
return vec![rhs[0] / diag[0]];
}
let mut alpha = vec![0.0f32; n];
let mut beta = vec![0.0f32; n];
alpha[1] = -sup[0] / diag[0];
beta[1] = rhs[0] / diag[0];
for i in 1..n - 1 {
let denom = sub[i - 1] * alpha[i] + diag[i];
alpha[i + 1] = -sup[i] / denom;
beta[i + 1] = (rhs[i] - sub[i - 1] * beta[i]) / denom;
}
let mut x = vec![0.0f32; n];
x[n - 1] = (rhs[n - 1] - sub[n - 2] * beta[n - 1]) / (diag[n - 1] + sub[n - 2] * alpha[n - 1]);
for i in (0..n - 1).rev() {
x[i] = alpha[i + 1] * x[i + 1] + beta[i + 1];
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_curve() {
let lut = CurveLut::from_points(&[[0.0, 0.0], [1.0, 1.0]]);
for i in 0..=10 {
let t = i as f32 / 10.0;
let v = lut.evaluate(t);
assert!((v - t).abs() < 0.01, "identity at {t}: got {v}");
}
}
#[test]
fn endpoints_exact() {
let points = [[0.0, 0.2], [0.5, 0.8], [1.0, 0.6]];
let lut = CurveLut::from_points(&points);
assert!(
(lut.evaluate(0.0) - 0.2).abs() < 0.01,
"start: {}",
lut.evaluate(0.0)
);
assert!(
(lut.evaluate(1.0) - 0.6).abs() < 0.01,
"end: {}",
lut.evaluate(1.0)
);
}
#[test]
fn s_curve_midpoint() {
let lut = CurveLut::from_points(&[[0.0, 0.0], [0.5, 0.2], [1.0, 1.0]]);
let v = lut.evaluate(0.5);
assert!(
(v - 0.2).abs() < 0.05,
"s-curve at 0.5: got {v}, expected ~0.2"
);
}
#[test]
fn clamped_output() {
let lut = CurveLut::from_points(&[[0.0, 0.0], [1.0, 1.0]]);
assert!(lut.evaluate(-0.5) >= 0.0);
assert!(lut.evaluate(1.5) <= 1.0);
}
#[test]
fn smooth_bump() {
let lut = CurveLut::from_points(&[[0.0, 0.0], [0.5, 1.0], [1.0, 0.0]]);
let v = lut.evaluate(0.5);
assert!((v - 1.0).abs() < 0.05, "peak at 0.5: got {v}");
let v_low = lut.evaluate(0.25);
let v_high = lut.evaluate(0.75);
assert!(
(v_low - v_high).abs() < 0.05,
"symmetry: {v_low} vs {v_high}"
);
assert!(
v_low > 0.3 && v_low < 0.8,
"smooth rise at 0.25: got {v_low}"
);
}
#[test]
fn many_points() {
let lut = CurveLut::from_points(&[
[0.0, 0.0],
[0.1, 0.05],
[0.2, 0.15],
[0.3, 0.3],
[0.5, 0.5],
[0.7, 0.7],
[0.8, 0.85],
[0.9, 0.95],
[1.0, 1.0],
]);
for i in 0..=10 {
let t = i as f32 / 10.0;
let v = lut.evaluate(t);
assert!((v - t).abs() < 0.15, "near-identity at {t}: got {v}");
}
}
#[test]
fn endpoint_independence() {
let lut_a = CurveLut::from_points(&[[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]);
let lut_b = CurveLut::from_points(&[
[0.0, 0.3], [0.5, 0.5],
[1.0, 0.7], ]);
let va = lut_a.evaluate(0.5);
let vb = lut_b.evaluate(0.5);
assert!((va - 0.5).abs() < 0.01, "lut_a at 0.5: {va}");
assert!((vb - 0.5).abs() < 0.01, "lut_b at 0.5: {vb}");
}
}