1use crate::color::best_text_color;
9
10pub type Rgb = (u8, u8, u8);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Scheme {
16 Light,
18 Dark,
20}
21
22#[derive(Debug, Clone)]
24pub struct PaletteSpec {
25 pub scheme: Scheme,
27 pub primary: Rgb,
29 pub secondary: Option<Rgb>,
31 pub accent: Option<Rgb>,
33 pub neutral: Option<Rgb>,
35 pub info: Option<Rgb>,
37 pub success: Option<Rgb>,
39 pub warning: Option<Rgb>,
41 pub error: Option<Rgb>,
43}
44
45const DARK_FG: Rgb = (24, 26, 30);
47const LIGHT_FG: Rgb = (247, 248, 250);
48
49const INFO: Rgb = (14, 165, 233);
51const SUCCESS: Rgb = (22, 163, 74);
52const WARNING: Rgb = (245, 158, 11);
53const ERROR: Rgb = (239, 68, 68);
54
55pub const PALETTE_ORDER: [&str; 20] = [
58 "base.100",
59 "base.200",
60 "base.300",
61 "base.content",
62 "primary",
63 "primary.content",
64 "secondary",
65 "secondary.content",
66 "accent",
67 "accent.content",
68 "neutral",
69 "neutral.content",
70 "info",
71 "info.content",
72 "success",
73 "success.content",
74 "warning",
75 "warning.content",
76 "error",
77 "error.content",
78];
79
80fn mix(a: Rgb, b: Rgb, t: f64) -> Rgb {
82 let m = |x: u8, y: u8| -> u8 {
83 let v = x as f64 * (1.0 - t) + y as f64 * t;
84 v.round().clamp(0.0, 255.0) as u8
85 };
86 (m(a.0, b.0), m(a.1, b.1), m(a.2, b.2))
87}
88
89fn content(bg: Rgb) -> Rgb {
91 best_text_color(bg, DARK_FG, LIGHT_FG)
92}
93
94pub fn synth_palette(spec: &PaletteSpec) -> Vec<(&'static str, Rgb)> {
97 let primary = spec.primary;
98 let secondary = spec.secondary.unwrap_or(primary);
99 let accent = spec.accent.unwrap_or(secondary);
100
101 let (b100, b200, b300, neutral_default) = match spec.scheme {
104 Scheme::Light => (
105 mix((248, 248, 249), primary, 0.02),
106 mix((241, 242, 244), primary, 0.03),
107 mix((227, 229, 233), primary, 0.05),
108 mix((72, 78, 90), primary, 0.10),
109 ),
110 Scheme::Dark => (
111 mix((12, 14, 18), primary, 0.05),
112 mix((22, 24, 29), primary, 0.05),
113 mix((35, 39, 48), primary, 0.05),
114 mix((68, 74, 86), primary, 0.10),
115 ),
116 };
117 let neutral = spec.neutral.unwrap_or(neutral_default);
118 let info = spec.info.unwrap_or(INFO);
119 let success = spec.success.unwrap_or(SUCCESS);
120 let warning = spec.warning.unwrap_or(WARNING);
121 let error = spec.error.unwrap_or(ERROR);
122
123 vec![
124 ("base.100", b100),
125 ("base.200", b200),
126 ("base.300", b300),
127 ("base.content", content(b100)),
128 ("primary", primary),
129 ("primary.content", content(primary)),
130 ("secondary", secondary),
131 ("secondary.content", content(secondary)),
132 ("accent", accent),
133 ("accent.content", content(accent)),
134 ("neutral", neutral),
135 ("neutral.content", content(neutral)),
136 ("info", info),
137 ("info.content", content(info)),
138 ("success", success),
139 ("success.content", content(success)),
140 ("warning", warning),
141 ("warning.content", content(warning)),
142 ("error", error),
143 ("error.content", content(error)),
144 ]
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::color::apca_lc;
151
152 fn spec(scheme: Scheme, primary: Rgb) -> PaletteSpec {
153 PaletteSpec {
154 scheme,
155 primary,
156 secondary: None,
157 accent: None,
158 neutral: None,
159 info: None,
160 success: None,
161 warning: None,
162 error: None,
163 }
164 }
165
166 fn get(p: &[(&'static str, Rgb)], id: &str) -> Rgb {
167 p.iter().find(|(k, _)| *k == id).expect("role present").1
168 }
169
170 #[test]
173 fn content_pairs_meet_apca_in_light_and_dark() {
174 for scheme in [Scheme::Light, Scheme::Dark] {
175 for primary in [(124, 58, 237), (34, 197, 94), (252, 183, 0), (248, 40, 52)] {
176 let p = synth_palette(&spec(scheme, primary));
177 let base_lc = apca_lc(get(&p, "base.content"), get(&p, "base.100")).abs();
178 assert!(
179 base_lc >= 75.0,
180 "{scheme:?} primary {primary:?}: base text Lc {base_lc:.1} < 75"
181 );
182 for role in [
183 "primary",
184 "secondary",
185 "accent",
186 "neutral",
187 "info",
188 "success",
189 "warning",
190 "error",
191 ] {
192 let lc = apca_lc(get(&p, &format!("{role}.content")), get(&p, role)).abs();
193 assert!(
194 lc >= 45.0,
195 "{scheme:?} {primary:?}: {role} label Lc {lc:.1} < 45"
196 );
197 }
198 }
199 }
200 }
201
202 #[test]
203 fn synthesis_is_deterministic() {
204 let a = synth_palette(&spec(Scheme::Light, (97, 93, 255)));
205 let b = synth_palette(&spec(Scheme::Light, (97, 93, 255)));
206 assert_eq!(a, b);
207 }
208
209 #[test]
210 fn overrides_are_respected() {
211 let mut s = spec(Scheme::Dark, (10, 20, 30));
212 s.accent = Some((255, 0, 128));
213 let p = synth_palette(&s);
214 assert_eq!(get(&p, "accent"), (255, 0, 128));
215 }
216
217 #[test]
218 fn order_is_the_contract() {
219 let p = synth_palette(&spec(Scheme::Light, (100, 100, 100)));
220 let ids: Vec<&str> = p.iter().map(|(k, _)| *k).collect();
221 assert_eq!(ids, PALETTE_ORDER.to_vec());
222 }
223}