rustial_engine/visualization/
color_ramp.rs1#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct ColorStop {
6 pub value: f32,
8 pub color: [f32; 4],
10}
11
12#[derive(Debug, Clone)]
18pub struct ColorRamp {
19 pub stops: Vec<ColorStop>,
21}
22
23impl ColorRamp {
24 pub fn new(mut stops: Vec<ColorStop>) -> Self {
32 assert!(!stops.is_empty(), "ColorRamp requires at least one stop");
33 stops.sort_by(|a, b| a.value.partial_cmp(&b.value).unwrap_or(std::cmp::Ordering::Equal));
34 Self { stops }
35 }
36
37 pub fn evaluate(&self, t: f32) -> [f32; 4] {
40 if self.stops.len() == 1 || t <= self.stops[0].value {
41 return self.stops[0].color;
42 }
43 let last = &self.stops[self.stops.len() - 1];
44 if t >= last.value {
45 return last.color;
46 }
47 for i in 1..self.stops.len() {
49 if t <= self.stops[i].value {
50 let a = &self.stops[i - 1];
51 let b = &self.stops[i];
52 let range = b.value - a.value;
53 let frac = if range.abs() < f32::EPSILON {
54 0.0
55 } else {
56 (t - a.value) / range
57 };
58 return lerp_color(&a.color, &b.color, frac);
59 }
60 }
61 last.color
62 }
63
64 pub fn as_texture_data(&self, width: u32) -> Vec<u8> {
68 let mut out = Vec::with_capacity(width as usize * 4);
69 for i in 0..width {
70 let t = if width <= 1 {
71 0.5
72 } else {
73 i as f32 / (width - 1) as f32
74 };
75 let [r, g, b, a] = self.evaluate(t);
76 out.push((r.clamp(0.0, 1.0) * 255.0) as u8);
77 out.push((g.clamp(0.0, 1.0) * 255.0) as u8);
78 out.push((b.clamp(0.0, 1.0) * 255.0) as u8);
79 out.push((a.clamp(0.0, 1.0) * 255.0) as u8);
80 }
81 out
82 }
83
84 #[inline]
86 pub fn len(&self) -> usize {
87 self.stops.len()
88 }
89
90 #[inline]
92 pub fn is_empty(&self) -> bool {
93 self.stops.is_empty()
94 }
95}
96
97fn lerp_color(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
98 [
99 a[0] + (b[0] - a[0]) * t,
100 a[1] + (b[1] - a[1]) * t,
101 a[2] + (b[2] - a[2]) * t,
102 a[3] + (b[3] - a[3]) * t,
103 ]
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn blue_to_red() -> ColorRamp {
111 ColorRamp::new(vec![
112 ColorStop { value: 0.0, color: [0.0, 0.0, 1.0, 1.0] },
113 ColorStop { value: 1.0, color: [1.0, 0.0, 0.0, 1.0] },
114 ])
115 }
116
117 #[test]
118 fn evaluate_at_stops() {
119 let ramp = blue_to_red();
120 assert_eq!(ramp.evaluate(0.0), [0.0, 0.0, 1.0, 1.0]);
121 assert_eq!(ramp.evaluate(1.0), [1.0, 0.0, 0.0, 1.0]);
122 }
123
124 #[test]
125 fn evaluate_midpoint() {
126 let ramp = blue_to_red();
127 let c = ramp.evaluate(0.5);
128 assert!((c[0] - 0.5).abs() < 1e-5);
129 assert!((c[2] - 0.5).abs() < 1e-5);
130 }
131
132 #[test]
133 fn evaluate_clamps_below() {
134 let ramp = blue_to_red();
135 assert_eq!(ramp.evaluate(-1.0), [0.0, 0.0, 1.0, 1.0]);
136 }
137
138 #[test]
139 fn evaluate_clamps_above() {
140 let ramp = blue_to_red();
141 assert_eq!(ramp.evaluate(2.0), [1.0, 0.0, 0.0, 1.0]);
142 }
143
144 #[test]
145 fn three_stop_ramp() {
146 let ramp = ColorRamp::new(vec![
147 ColorStop { value: 0.0, color: [0.0, 0.0, 0.0, 1.0] },
148 ColorStop { value: 0.5, color: [1.0, 1.0, 1.0, 1.0] },
149 ColorStop { value: 1.0, color: [1.0, 0.0, 0.0, 1.0] },
150 ]);
151 let mid = ramp.evaluate(0.5);
152 assert!((mid[0] - 1.0).abs() < 1e-5);
153 assert!((mid[1] - 1.0).abs() < 1e-5);
154 assert!((mid[2] - 1.0).abs() < 1e-5);
155 }
156
157 #[test]
158 fn as_texture_data_length() {
159 let ramp = blue_to_red();
160 let tex = ramp.as_texture_data(256);
161 assert_eq!(tex.len(), 256 * 4);
162 }
163
164 #[test]
165 fn as_texture_data_boundary_values() {
166 let ramp = blue_to_red();
167 let tex = ramp.as_texture_data(2);
168 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); }
179
180 #[test]
181 fn single_stop_ramp() {
182 let ramp = ColorRamp::new(vec![
183 ColorStop { value: 0.5, color: [0.5, 0.5, 0.5, 1.0] },
184 ]);
185 assert_eq!(ramp.evaluate(0.0), [0.5, 0.5, 0.5, 1.0]);
186 assert_eq!(ramp.evaluate(1.0), [0.5, 0.5, 0.5, 1.0]);
187 }
188}