#[derive(Debug, Clone)]
pub struct OpacityTransferFunction {
points: Vec<(f64, f64)>,
}
impl OpacityTransferFunction {
#[must_use]
pub fn new() -> Self {
Self { points: Vec::new() }
}
#[must_use]
pub fn linear_ramp(scalar_min: f64, scalar_max: f64) -> Self {
let mut otf = Self::new();
otf.add_point(scalar_min, 0.0);
otf.add_point(scalar_max, 1.0);
otf
}
pub fn add_point(&mut self, scalar: f64, opacity: f64) {
let opacity = opacity.clamp(0.0, 1.0);
match self
.points
.binary_search_by(|(s, _)| s.partial_cmp(&scalar).unwrap())
{
Ok(pos) => self.points[pos] = (scalar, opacity),
Err(pos) => self.points.insert(pos, (scalar, opacity)),
}
}
pub fn remove_point(&mut self, scalar: f64, epsilon: f64) {
if let Some(pos) = self
.points
.iter()
.position(|(s, _)| (s - scalar).abs() < epsilon)
{
self.points.remove(pos);
}
}
#[must_use]
pub fn evaluate(&self, scalar: f64) -> f64 {
if self.points.is_empty() {
return 0.0;
}
if scalar <= self.points.first().unwrap().0 {
return self.points.first().unwrap().1;
}
if scalar >= self.points.last().unwrap().0 {
return self.points.last().unwrap().1;
}
let pos = self
.points
.partition_point(|(s, _)| *s <= scalar)
.saturating_sub(1);
let (s0, a0) = self.points[pos];
let (s1, a1) = self.points[pos + 1];
let t = (scalar - s0) / (s1 - s0);
a0 + (a1 - a0) * t
}
#[must_use]
pub fn len(&self) -> usize {
self.points.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
impl Default for OpacityTransferFunction {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn linear_ramp_midpoint() {
let otf = OpacityTransferFunction::linear_ramp(0.0, 1.0);
assert_abs_diff_eq!(otf.evaluate(0.5), 0.5, epsilon = 1e-10);
}
#[test]
fn clamp_opacity_to_one() {
let mut otf = OpacityTransferFunction::new();
otf.add_point(0.0, 1.5);
assert_abs_diff_eq!(otf.evaluate(0.0), 1.0, epsilon = 1e-10);
}
#[test]
fn empty_returns_zero() {
assert_abs_diff_eq!(
OpacityTransferFunction::new().evaluate(0.5),
0.0,
epsilon = 1e-10
);
}
#[test]
fn monotone_ramp_is_monotone() {
let otf = OpacityTransferFunction::linear_ramp(-1000.0, 1000.0);
let mut prev = otf.evaluate(-1000.0);
for i in -999..=1000 {
let v = otf.evaluate(i as f64);
assert!(v >= prev - 1e-12, "monotonicity violated at {i}");
prev = v;
}
}
}