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| {
34 a.value
35 .partial_cmp(&b.value)
36 .unwrap_or(std::cmp::Ordering::Equal)
37 });
38 Self { stops }
39 }
40
41 pub fn evaluate(&self, t: f32) -> [f32; 4] {
44 if self.stops.len() == 1 || t <= self.stops[0].value {
45 return self.stops[0].color;
46 }
47 let last = &self.stops[self.stops.len() - 1];
48 if t >= last.value {
49 return last.color;
50 }
51 for i in 1..self.stops.len() {
53 if t <= self.stops[i].value {
54 let a = &self.stops[i - 1];
55 let b = &self.stops[i];
56 let range = b.value - a.value;
57 let frac = if range.abs() < f32::EPSILON {
58 0.0
59 } else {
60 (t - a.value) / range
61 };
62 return lerp_color(&a.color, &b.color, frac);
63 }
64 }
65 last.color
66 }
67
68 pub fn as_texture_data(&self, width: u32) -> Vec<u8> {
72 let mut out = Vec::with_capacity(width as usize * 4);
73 for i in 0..width {
74 let t = if width <= 1 {
75 0.5
76 } else {
77 i as f32 / (width - 1) as f32
78 };
79 let [r, g, b, a] = self.evaluate(t);
80 out.push((r.clamp(0.0, 1.0) * 255.0) as u8);
81 out.push((g.clamp(0.0, 1.0) * 255.0) as u8);
82 out.push((b.clamp(0.0, 1.0) * 255.0) as u8);
83 out.push((a.clamp(0.0, 1.0) * 255.0) as u8);
84 }
85 out
86 }
87
88 #[inline]
90 pub fn len(&self) -> usize {
91 self.stops.len()
92 }
93
94 #[inline]
96 pub fn is_empty(&self) -> bool {
97 self.stops.is_empty()
98 }
99}
100
101fn lerp_color(a: &[f32; 4], b: &[f32; 4], t: f32) -> [f32; 4] {
102 [
103 a[0] + (b[0] - a[0]) * t,
104 a[1] + (b[1] - a[1]) * t,
105 a[2] + (b[2] - a[2]) * t,
106 a[3] + (b[3] - a[3]) * t,
107 ]
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn blue_to_red() -> ColorRamp {
115 ColorRamp::new(vec![
116 ColorStop {
117 value: 0.0,
118 color: [0.0, 0.0, 1.0, 1.0],
119 },
120 ColorStop {
121 value: 1.0,
122 color: [1.0, 0.0, 0.0, 1.0],
123 },
124 ])
125 }
126
127 #[test]
128 fn evaluate_at_stops() {
129 let ramp = blue_to_red();
130 assert_eq!(ramp.evaluate(0.0), [0.0, 0.0, 1.0, 1.0]);
131 assert_eq!(ramp.evaluate(1.0), [1.0, 0.0, 0.0, 1.0]);
132 }
133
134 #[test]
135 fn evaluate_midpoint() {
136 let ramp = blue_to_red();
137 let c = ramp.evaluate(0.5);
138 assert!((c[0] - 0.5).abs() < 1e-5);
139 assert!((c[2] - 0.5).abs() < 1e-5);
140 }
141
142 #[test]
143 fn evaluate_clamps_below() {
144 let ramp = blue_to_red();
145 assert_eq!(ramp.evaluate(-1.0), [0.0, 0.0, 1.0, 1.0]);
146 }
147
148 #[test]
149 fn evaluate_clamps_above() {
150 let ramp = blue_to_red();
151 assert_eq!(ramp.evaluate(2.0), [1.0, 0.0, 0.0, 1.0]);
152 }
153
154 #[test]
155 fn three_stop_ramp() {
156 let ramp = ColorRamp::new(vec![
157 ColorStop {
158 value: 0.0,
159 color: [0.0, 0.0, 0.0, 1.0],
160 },
161 ColorStop {
162 value: 0.5,
163 color: [1.0, 1.0, 1.0, 1.0],
164 },
165 ColorStop {
166 value: 1.0,
167 color: [1.0, 0.0, 0.0, 1.0],
168 },
169 ]);
170 let mid = ramp.evaluate(0.5);
171 assert!((mid[0] - 1.0).abs() < 1e-5);
172 assert!((mid[1] - 1.0).abs() < 1e-5);
173 assert!((mid[2] - 1.0).abs() < 1e-5);
174 }
175
176 #[test]
177 fn as_texture_data_length() {
178 let ramp = blue_to_red();
179 let tex = ramp.as_texture_data(256);
180 assert_eq!(tex.len(), 256 * 4);
181 }
182
183 #[test]
184 fn as_texture_data_boundary_values() {
185 let ramp = blue_to_red();
186 let tex = ramp.as_texture_data(2);
187 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); }
198
199 #[test]
200 fn single_stop_ramp() {
201 let ramp = ColorRamp::new(vec![ColorStop {
202 value: 0.5,
203 color: [0.5, 0.5, 0.5, 1.0],
204 }]);
205 assert_eq!(ramp.evaluate(0.0), [0.5, 0.5, 0.5, 1.0]);
206 assert_eq!(ramp.evaluate(1.0), [0.5, 0.5, 0.5, 1.0]);
207 }
208}