color_palettes/
lib.rs

1use color::OklabColor;
2use color::OklabResult;
3use color::RGBColor;
4use plotters::prelude::BitMapBackend;
5use plotters::prelude::DrawingBackend;
6use std::cmp;
7use std::error::Error;
8
9pub mod color;
10
11type Palette = Vec<RGBColor>;
12const MAX_PALETTE_SIZE: u32 = 256;
13
14pub enum GeneratorMode {
15    Auto,
16    // TODO: add more modes (monochromatic, complementary etc.)
17}
18
19pub struct GeneratorConfig {
20    pub mode: GeneratorMode,
21    pub similarity: f32,
22    pub max_iterations: u32,
23}
24
25impl Default for GeneratorConfig {
26    fn default() -> Self {
27        Self {
28            mode: GeneratorMode::Auto,
29            similarity: 0.6,
30            max_iterations: 50,
31        }
32    }
33}
34
35// Generate and add a color to the palette using farthest point sampling.
36// step_size is the distance of the generated color to the last color in the palette
37fn generate_with_fps(colors: &mut Palette, step_size: f32, max_iterations: u32) {
38    let last_color = colors.last().unwrap().to_oklab();
39
40    // Generate a set of candidate colors by taking random steps from the last color in the palette
41    const POOL_SIZE: usize = 8; // generate up to 8 candidates
42    let mut candidates = [OklabColor::new(); POOL_SIZE];
43    {
44        // try to generate unclipped candidates
45        let mut num_candidates = 032;
46        for i in 0..candidates.len() {
47            let step_result = last_color.random_step_sgrb(step_size, max_iterations);
48            if let OklabResult::Ok(c) = step_result {
49                candidates[i] = c;
50                num_candidates += 1;
51            }
52        }
53
54        // generate and copy a single candidate if we failed above
55        if num_candidates == 0 {
56            candidates[0] = last_color
57                .random_step_sgrb(step_size, max_iterations)
58                .unwrap();
59            for i in 1..candidates.len() {
60                candidates[i] = candidates[0];
61            }
62        }
63    }
64
65    // Pick the candidate color that is the furthest away from existing colors
66    let mut min_distances = [f32::INFINITY; POOL_SIZE];
67    for (i, candidate) in candidates.iter().enumerate() {
68        for color in colors.iter() {
69            let color = color.to_oklab();
70            let dist = candidate.squared_distance(&color);
71            if dist < min_distances[i] {
72                min_distances[i] = dist;
73            }
74        }
75    }
76
77    let mut max_idx: usize = 0;
78    let mut max_dist: f32 = min_distances[0];
79    for i in 1..min_distances.len() {
80        if min_distances[i] > max_dist {
81            max_dist = min_distances[i];
82            max_idx = i;
83        }
84    }
85    colors.push(RGBColor::from_oklab(candidates[max_idx]));
86}
87
88fn distance_from_similarity(similarity: f32) -> f32 {
89    let similarity = similarity.clamp(0.0, 1.0);
90
91    const MIN_DIST: f32 = 0.01;
92    const MAX_DIST: f32 = 0.26;
93
94    (MAX_DIST - similarity * (MAX_DIST - MIN_DIST)).max(0.0)
95}
96
97pub fn generate_palette(
98    num_colors: u32,
99    config: &GeneratorConfig,
100) -> Result<Palette, Box<dyn Error>> {
101    if num_colors == 0 || num_colors > MAX_PALETTE_SIZE {
102        return Err(format!(
103            "Number of colors must be number between  [1-{}].",
104            MAX_PALETTE_SIZE
105        )
106        .into());
107    }
108
109    match config.mode {
110        GeneratorMode::Auto => return Ok(generate_auto(num_colors, config)),
111    }
112}
113
114fn generate_auto(num_colors: u32, config: &GeneratorConfig) -> Palette {
115    let mut palette: Palette = Palette::with_capacity(num_colors as usize);
116    let first_color = OklabColor::random_in_sgrb(0.3, 0.6, 100);
117    palette.push(RGBColor::from_oklab(first_color.unwrap()));
118
119    let color_distance = distance_from_similarity(config.similarity);
120    for _ in 1..num_colors {
121        generate_with_fps(&mut palette, color_distance, config.max_iterations);
122    }
123
124    palette
125}
126
127pub fn save_palette(palette: &Palette, path: &str) {
128    if palette.len() == 0 {
129        panic!("palette is length 0!");
130    }
131
132    let num_colors = palette.len() as u32;
133    let squares_per_row = 256;
134    let num_rows = (num_colors as f32 / squares_per_row as f32).ceil() as u32;
135
136    let square_size: u32 = 32;
137    let canvas_width = square_size * cmp::min(squares_per_row, num_colors);
138    let canvas_height = square_size * num_rows;
139    let mut backend = BitMapBackend::new(path, (canvas_width, canvas_height));
140
141    let square_size = square_size as i32;
142    for (i, &color) in palette.iter().enumerate() {
143        let i = i as i32;
144        let r: i32 = i / squares_per_row as i32;
145        let c: i32 = i % squares_per_row as i32;
146
147        let upper_left: (i32, i32) = (c * square_size, r * square_size);
148        let bottom_right: (i32, i32) = (upper_left.0 + square_size, upper_left.1 + square_size);
149        backend
150            .draw_rect(
151                upper_left,
152                bottom_right,
153                &plotters::style::RGBColor(color.r, color.g, color.b),
154                true,
155            )
156            .unwrap();
157    }
158}