Skip to main content

esoc_color/
scale.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Continuous `[0,1]` → Color mapping for GPU 1D texture lookups.
3
4use crate::palette::Palette;
5use crate::Color;
6
7/// A continuous color scale mapping `[0, 1]` → Color.
8#[derive(Clone, Debug)]
9pub struct ColorScale {
10    palette: Palette,
11}
12
13impl ColorScale {
14    /// Create a color scale from a palette.
15    pub fn new(palette: Palette) -> Self {
16        Self { palette }
17    }
18
19    /// Viridis color scale.
20    pub fn viridis() -> Self {
21        Self::new(Palette::viridis())
22    }
23
24    /// Red-Blue diverging scale.
25    pub fn rdbu() -> Self {
26        Self::new(Palette::rdbu())
27    }
28
29    /// Map a value in `[0, 1]` to a color.
30    pub fn map(&self, t: f32) -> Color {
31        self.palette.sample(t)
32    }
33
34    /// Generate texture data (RGBA8 sRGB) for GPU 1D lookup.
35    ///
36    /// Returns `width × 4` bytes of sRGB-encoded RGBA.
37    pub fn to_texture_data(&self, width: u32) -> Vec<u8> {
38        let mut data = Vec::with_capacity(width as usize * 4);
39        for i in 0..width {
40            let t = if width <= 1 {
41                0.5
42            } else {
43                i as f32 / (width - 1) as f32
44            };
45            let c = self.map(t);
46            let [r, g, b, a] = c.to_srgb8();
47            data.extend_from_slice(&[r, g, b, a]);
48        }
49        data
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn texture_data_length() {
59        let scale = ColorScale::viridis();
60        let data = scale.to_texture_data(256);
61        assert_eq!(data.len(), 256 * 4);
62    }
63
64    #[test]
65    fn endpoints() {
66        let scale = ColorScale::viridis();
67        let start = scale.map(0.0);
68        let end = scale.map(1.0);
69        // Viridis: dark purple → yellow
70        assert!(start.r < 0.1); // dark
71        assert!(end.r > 0.5); // bright
72    }
73}