1mod color;
2mod utils;
3
4use palette::{rgb::Rgb, FromColor, Hsl, Srgb};
5use std::{collections::HashMap, path::PathBuf};
6use tinted_builder::{Base16Scheme, Color as SchemeColor};
7
8use crate::{
9 color::Color,
10 utils::{
11 create_palette_with_color_thief_colors, create_palette_with_inverse_colors, dark_color,
12 find_closest_palette, fix_colors, generate_gradient, light_color, load_image,
13 },
14};
15
16pub use tinted_builder::{SchemeSystem, SchemeVariant};
17
18#[non_exhaustive]
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error("no colors")]
22 NoColors(String),
23 #[error("generate colors")]
24 GenerateColors(String),
25 #[error("unsupported scheme variant")]
26 UnsupportedSchemeVariant(String),
27 #[error("other")]
28 Other(String),
29}
30
31#[derive(Debug)]
32pub struct SchemeParams {
33 pub image_path: PathBuf,
34 pub author: String,
35 pub description: Option<String>,
36 pub name: String,
37 pub slug: String,
38 pub system: SchemeSystem,
39 pub variant: SchemeVariant,
40 pub verbose: bool,
41}
42
43pub fn create_scheme_from_image(params: SchemeParams) -> Result<Base16Scheme, Error> {
44 let SchemeParams {
45 image_path,
46 author,
47 description,
48 name,
49 slug,
50 system,
51 variant,
52 verbose,
53 } = params;
54 let image = load_image(&image_path);
55 let initial_palette: Vec<Color> = find_closest_palette(&image);
56 let inital_inverse_palette: Vec<Color> = find_closest_palette(&image)
57 .iter()
58 .map(|color| color.get_inverse())
59 .collect();
60 let curated_palette =
61 create_palette_with_inverse_colors(&initial_palette, &inital_inverse_palette);
62 let color_thief_palette: Vec<Srgb<u8>> = color_thief::get_palette(
63 image.to_rgba8().into_raw().as_slice(),
64 color_thief::ColorFormat::Rgba,
65 1,
66 15,
67 )
68 .map_err(|err| Error::GenerateColors(err.to_string()))?
69 .iter()
70 .map(|c| Srgb::new(c.r, c.g, c.b))
71 .collect();
72 let combined_palette =
73 create_palette_with_color_thief_colors(&curated_palette, &color_thief_palette)?;
74 let color_thief_pallette_as_rgb_vec: Vec<Rgb> = color_thief_palette
75 .clone()
76 .iter()
77 .map(|c| {
78 Rgb::new(
79 c.red as f32 / 255.0,
80 c.green as f32 / 255.0,
81 c.blue as f32 / 255.0,
82 )
83 })
84 .collect();
85 let light = light_color(&color_thief_pallette_as_rgb_vec, verbose)?;
86 let dark = dark_color(&color_thief_pallette_as_rgb_vec, verbose)?;
87 let (background, foreground) = match &variant {
88 SchemeVariant::Dark | SchemeVariant::Light => Ok(fix_colors(dark, light, &variant)),
89 variant => Err(Error::UnsupportedSchemeVariant(variant.to_string())),
90 }?;
91 let gradient = generate_gradient(Srgb::from(background), Srgb::from(foreground), 8);
92
93 let mut scheme_palette: HashMap<String, SchemeColor> = HashMap::new();
94
95 for (index, rgb) in gradient.iter().enumerate() {
96 scheme_palette.entry(format!("base0{}", index)).or_insert(
97 SchemeColor::new(format!("{:02X}{:02X}{:02X}", rgb.red, rgb.green, rgb.blue))
98 .map_err(|err| Error::GenerateColors(err.to_string()))?,
99 );
100 }
101
102 for color in &combined_palette {
103 let diff = get_lightness_weight_difference(color, 0.7);
104 let color = color.add_lightness(diff);
105
106 match color.associated_pure_color.as_str() {
107 "red" => {
108 scheme_palette.entry("base08".to_string()).or_insert(
109 SchemeColor::new(color.to_hex())
110 .map_err(|err| Error::GenerateColors(err.to_string()))?,
111 );
112 }
113 "orange" => {
114 scheme_palette.entry("base09".to_string()).or_insert(
115 SchemeColor::new(color.to_hex())
116 .map_err(|err| Error::GenerateColors(err.to_string()))?,
117 );
118 }
119 "yellow" => {
120 scheme_palette.entry("base0A".to_string()).or_insert(
121 SchemeColor::new(color.to_hex())
122 .map_err(|err| Error::GenerateColors(err.to_string()))?,
123 );
124 }
125 "green" => {
126 scheme_palette.entry("base0B".to_string()).or_insert(
127 SchemeColor::new(color.to_hex())
128 .map_err(|err| Error::GenerateColors(err.to_string()))?,
129 );
130 }
131 "cyan" => {
132 scheme_palette.entry("base0C".to_string()).or_insert(
133 SchemeColor::new(color.to_hex())
134 .map_err(|err| Error::GenerateColors(err.to_string()))?,
135 );
136 }
137 "blue" => {
138 scheme_palette.entry("base0D".to_string()).or_insert(
139 SchemeColor::new(color.to_hex())
140 .map_err(|err| Error::GenerateColors(err.to_string()))?,
141 );
142 }
143 "purple" => {
144 scheme_palette.entry("base0E".to_string()).or_insert(
145 SchemeColor::new(color.to_hex())
146 .map_err(|err| Error::GenerateColors(err.to_string()))?,
147 );
148 }
149 "brown" => {
150 scheme_palette.entry("base0F".to_string()).or_insert(
151 SchemeColor::new(color.to_hex())
152 .map_err(|err| Error::GenerateColors(err.to_string()))?,
153 );
154 }
155 _ => {}
156 }
157
158 if let SchemeSystem::Base24 = system {
159 let updated_color = color.to_saturated(0.7);
160
161 match updated_color.associated_pure_color.as_str() {
162 "red" => {
163 scheme_palette.entry("base10".to_string()).or_insert(
164 SchemeColor::new(updated_color.to_hex())
165 .map_err(|err| Error::GenerateColors(err.to_string()))?,
166 );
167 }
168 "orange" => {
169 scheme_palette.entry("base11".to_string()).or_insert(
170 SchemeColor::new(updated_color.to_hex())
171 .map_err(|err| Error::GenerateColors(err.to_string()))?,
172 );
173 }
174 "yellow" => {
175 scheme_palette.entry("base12".to_string()).or_insert(
176 SchemeColor::new(updated_color.to_hex())
177 .map_err(|err| Error::GenerateColors(err.to_string()))?,
178 );
179 }
180 "green" => {
181 scheme_palette.entry("base13".to_string()).or_insert(
182 SchemeColor::new(updated_color.to_hex())
183 .map_err(|err| Error::GenerateColors(err.to_string()))?,
184 );
185 }
186 "cyan" => {
187 scheme_palette.entry("base14".to_string()).or_insert(
188 SchemeColor::new(updated_color.to_hex())
189 .map_err(|err| Error::GenerateColors(err.to_string()))?,
190 );
191 }
192 "blue" => {
193 scheme_palette.entry("base15".to_string()).or_insert(
194 SchemeColor::new(updated_color.to_hex())
195 .map_err(|err| Error::GenerateColors(err.to_string()))?,
196 );
197 }
198 "purple" => {
199 scheme_palette.entry("base16".to_string()).or_insert(
200 SchemeColor::new(updated_color.to_hex())
201 .map_err(|err| Error::GenerateColors(err.to_string()))?,
202 );
203 }
204 "brown" => {
205 scheme_palette.entry("base17".to_string()).or_insert(
206 SchemeColor::new(updated_color.to_hex())
207 .map_err(|err| Error::GenerateColors(err.to_string()))?,
208 );
209 }
210 _ => {}
211 }
212 }
213 }
214
215 let scheme = Base16Scheme {
216 author,
217 description,
218 name,
219 slug,
220 system,
221 variant,
222 palette: scheme_palette,
223 };
224
225 Ok(scheme)
226}
227
228fn get_lightness_weight_difference(color: &Color, threshold: f32) -> f32 {
229 let color: Hsl = Hsl::from_color(color.value.into_format::<f32>());
230 let alpha = 0.5; let beta = 1.0; let visibility_metric = alpha * color.saturation + beta * color.lightness;
234
235 let value = ((threshold - visibility_metric) / beta).clamp(0.0, 1.0);
236
237 value / 2.0
238}