Skip to main content

polytope_playground/
color.rs

1use loam::runtime::{EdgeShading, PaletteId};
2use loam::shape::polytope::{vertex_color_by_position, Polytope4Topology};
3
4// Matches the depth cue in the LineRasterStaticR4 shader.
5pub(crate) const W_DEPTH_BACK: [f32; 4] = [0.30, 0.42, 0.58, 1.0];
6
7pub(crate) const W_DEPTH_FRONT: [f32; 4] = [1.00, 0.78, 0.45, 1.0];
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub(crate) enum ColorMode {
11    #[default]
12    VertexGradient,
13    UniqueEdge,
14    WDepth,
15}
16
17impl ColorMode {
18    pub(crate) const ALL: [ColorMode; 3] = [
19        ColorMode::VertexGradient,
20        ColorMode::UniqueEdge,
21        ColorMode::WDepth,
22    ];
23
24    pub(crate) fn name(self) -> &'static str {
25        match self {
26            ColorMode::VertexGradient => "vertex",
27            ColorMode::UniqueEdge => "edge",
28            ColorMode::WDepth => "w-depth",
29        }
30    }
31
32    pub(crate) fn from_token(token: &str) -> Option<Self> {
33        ColorMode::ALL.into_iter().find(|mode| mode.name() == token)
34    }
35}
36
37#[derive(Clone, Copy)]
38pub(crate) struct Shades {
39    pub(crate) gradient: PaletteId,
40    pub(crate) unique: PaletteId,
41    pub(crate) extent: f32,
42}
43
44impl Shades {
45    pub(crate) fn of(self, mode: ColorMode) -> EdgeShading {
46        match mode {
47            ColorMode::VertexGradient => EdgeShading::Palette(self.gradient),
48            ColorMode::UniqueEdge => EdgeShading::Palette(self.unique),
49            ColorMode::WDepth => EdgeShading::Depth {
50                back: W_DEPTH_BACK,
51                front: W_DEPTH_FRONT,
52                extent: self.extent,
53            },
54        }
55    }
56}
57
58fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> [f32; 3] {
59    let sixth = hue.fract() * 6.0;
60    let chroma = value * saturation;
61    let second = chroma * (1.0 - (sixth % 2.0 - 1.0).abs());
62    let base = value - chroma;
63    let (r, g, b) = match sixth.floor() as i32 % 6 {
64        0 => (chroma, second, 0.0),
65        1 => (second, chroma, 0.0),
66        2 => (0.0, chroma, second),
67        3 => (0.0, second, chroma),
68        4 => (second, 0.0, chroma),
69        _ => (chroma, 0.0, second),
70    };
71    [r + base, g + base, b + base]
72}
73
74fn palette_color(index: usize) -> [f32; 4] {
75    const PHI_INV: f32 = 0.618_034;
76    let hue = ((index as f32) * PHI_INV).fract();
77    let saturation = 0.78 + 0.18 * ((index % 3) as f32 / 2.0);
78    let value = 0.92 - 0.18 * (((index / 3) % 2) as f32);
79    let [r, g, b] = hsv_to_rgb(hue, saturation, value);
80    [r, g, b, 1.0]
81}
82
83pub(crate) fn unique_edge_colors(edges: &[[u32; 2]]) -> Vec<[f32; 4]> {
84    let mut chosen = Vec::with_capacity(edges.len());
85    let mut used = vec![usize::MAX; edges.len()];
86    for (index, &[a, b]) in edges.iter().enumerate() {
87        for (other, &[c, d]) in edges[..index].iter().enumerate() {
88            if a == c || a == d || b == c || b == d {
89                used[chosen[other]] = index;
90            }
91        }
92        let color = used
93            .iter()
94            .position(|generation| *generation != index)
95            .unwrap_or(index);
96        chosen.push(color);
97    }
98    chosen
99        .into_iter()
100        .flat_map(|index| [palette_color(index); 2])
101        .collect()
102}
103
104pub(crate) fn vertex_gradient_colors(topology: &Polytope4Topology) -> Vec<[f32; 4]> {
105    topology
106        .edges
107        .iter()
108        .flat_map(|&[a, b]| {
109            [
110                vertex_color_by_position(topology.vertices[a as usize]),
111                vertex_color_by_position(topology.vertices[b as usize]),
112            ]
113        })
114        .collect()
115}
116
117pub(crate) fn w_extent(topology: &Polytope4Topology, scale: f32) -> f32 {
118    topology
119        .vertices
120        .iter()
121        .map(|vertex| vertex.w.abs())
122        .fold(0.0_f32, f32::max)
123        .max(1e-6)
124        * scale
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn the_greedy_palette_separates_edges_that_share_a_vertex() {
133        let edges: &[[u32; 2]] = &[[0, 1], [0, 2], [0, 3]];
134        let colors = unique_edge_colors(edges);
135        assert_eq!(colors.len(), 6, "one color per endpoint");
136        for edge in 0..3 {
137            assert_eq!(
138                colors[edge * 2],
139                colors[edge * 2 + 1],
140                "an edge is one color"
141            );
142            for other in (edge + 1)..3 {
143                assert_ne!(
144                    colors[edge * 2],
145                    colors[other * 2],
146                    "edges {edge} and {other} share vertex 0 and got one color"
147                );
148            }
149        }
150    }
151}