#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColorStop {
pub value: f32,
pub color: [f32; 4],
}
#[derive(Debug, Clone)]
pub struct ColorRamp {
pub stops: Vec<ColorStop>,
}
impl ColorRamp {
pub fn new(mut stops: Vec<ColorStop>) -> Self {
assert!(!stops.is_empty(), "ColorRamp requires at least one stop");
stops.sort_by(|a, b| {
a.value
.partial_cmp(&b.value)
.unwrap_or(std::cmp::Ordering::Equal)
});
Self { stops }
}
pub fn evaluate(&self, t: f32) -> [f32; 4] {
if self.stops.len() == 1 || t <= self.stops[0].value {
return self.stops[0].color;
}
let last = &self.stops[self.stops.len() - 1];
if t >= last.value {
return last.color;
}
for i in 1..self.stops.len() {
if t <= self.stops[i].value {
let a = &self.stops[i - 1];
let b = &self.stops[i];
let range = b.value - a.value;
let frac = if range.abs() < f32::EPSILON {
0.0
} else {
(t - a.value) / range
};
return lerp_color(&a.color, &b.color, frac);
}
}
last.color
}
pub fn as_texture_data(&self, width: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(width as usize * 4);
for i in 0..width {
let t = if width <= 1 {
0.5
} else {
i as f32 / (width - 1) as f32
};
let [r, g, b, a] = self.evaluate(t);
out.push((r.clamp(0.0, 1.0) * 255.0) as u8);
out.push((g.clamp(0.0, 1.0) * 255.0) as u8);
out.push((b.clamp(0.0, 1.0) * 255.0) as u8);
out.push((a.clamp(0.0, 1.0) * 255.0) as u8);
}
out
}
#[inline]
pub fn len(&self) -> usize {
self.stops.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.stops.is_empty()
}
}
fn lerp_color(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
]
}
#[cfg(test)]
mod tests {
use super::*;
fn blue_to_red() -> ColorRamp {
ColorRamp::new(vec![
ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 1.0],
},
ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 1.0],
},
])
}
#[test]
fn evaluate_at_stops() {
let ramp = blue_to_red();
assert_eq!(ramp.evaluate(0.0), [0.0, 0.0, 1.0, 1.0]);
assert_eq!(ramp.evaluate(1.0), [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn evaluate_midpoint() {
let ramp = blue_to_red();
let c = ramp.evaluate(0.5);
assert!((c[0] - 0.5).abs() < 1e-5);
assert!((c[2] - 0.5).abs() < 1e-5);
}
#[test]
fn evaluate_clamps_below() {
let ramp = blue_to_red();
assert_eq!(ramp.evaluate(-1.0), [0.0, 0.0, 1.0, 1.0]);
}
#[test]
fn evaluate_clamps_above() {
let ramp = blue_to_red();
assert_eq!(ramp.evaluate(2.0), [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn three_stop_ramp() {
let ramp = ColorRamp::new(vec![
ColorStop {
value: 0.0,
color: [0.0, 0.0, 0.0, 1.0],
},
ColorStop {
value: 0.5,
color: [1.0, 1.0, 1.0, 1.0],
},
ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 1.0],
},
]);
let mid = ramp.evaluate(0.5);
assert!((mid[0] - 1.0).abs() < 1e-5);
assert!((mid[1] - 1.0).abs() < 1e-5);
assert!((mid[2] - 1.0).abs() < 1e-5);
}
#[test]
fn as_texture_data_length() {
let ramp = blue_to_red();
let tex = ramp.as_texture_data(256);
assert_eq!(tex.len(), 256 * 4);
}
#[test]
fn as_texture_data_boundary_values() {
let ramp = blue_to_red();
let tex = ramp.as_texture_data(2);
assert_eq!(tex[0], 0); assert_eq!(tex[1], 0); assert_eq!(tex[2], 255); assert_eq!(tex[3], 255); assert_eq!(tex[4], 255); assert_eq!(tex[5], 0); assert_eq!(tex[6], 0); assert_eq!(tex[7], 255); }
#[test]
fn single_stop_ramp() {
let ramp = ColorRamp::new(vec![ColorStop {
value: 0.5,
color: [0.5, 0.5, 0.5, 1.0],
}]);
assert_eq!(ramp.evaluate(0.0), [0.5, 0.5, 0.5, 1.0]);
assert_eq!(ramp.evaluate(1.0), [0.5, 0.5, 0.5, 1.0]);
}
}