1use super::color::{self, DynamicRole};
10use super::hct::Hct;
11use super::palette::TonalPalette;
12use super::temperature::TemperatureCache;
13use super::utils::sanitize_degrees_double;
14use super::{dislike, utils::clamp_double};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
20pub enum Variant {
21 Monochrome,
23 Neutral,
25 #[default]
27 TonalSpot,
28 Vibrant,
30 Expressive,
32 Fidelity,
34 Content,
36 Rainbow,
38 FruitSalad,
40}
41
42impl Variant {
43 pub const ALL: [Self; 9] = [
45 Self::Monochrome,
46 Self::Neutral,
47 Self::TonalSpot,
48 Self::Vibrant,
49 Self::Expressive,
50 Self::Fidelity,
51 Self::Content,
52 Self::Rainbow,
53 Self::FruitSalad,
54 ];
55
56 pub const fn has_2025_spec(self) -> bool {
59 matches!(
60 self,
61 Self::Expressive | Self::Vibrant | Self::TonalSpot | Self::Neutral
62 )
63 }
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub enum SpecVersion {
73 #[default]
75 #[cfg_attr(feature = "serde", serde(rename = "2021"))]
76 Spec2021,
77 #[cfg_attr(feature = "serde", serde(rename = "2025"))]
79 Spec2025,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct DynamicScheme {
100 pub source: Hct,
102 pub variant: Variant,
104 pub contrast_level: f64,
106 pub is_dark: bool,
108 pub spec: SpecVersion,
110 pub primary_palette: TonalPalette,
112 pub secondary_palette: TonalPalette,
114 pub tertiary_palette: TonalPalette,
116 pub neutral_palette: TonalPalette,
118 pub neutral_variant_palette: TonalPalette,
120 pub error_palette: TonalPalette,
122}
123
124impl DynamicScheme {
125 pub fn new(
128 source: Hct,
129 variant: Variant,
130 is_dark: bool,
131 contrast_level: f64,
132 spec: SpecVersion,
133 ) -> Self {
134 let spec = if variant.has_2025_spec() {
135 spec
136 } else {
137 SpecVersion::Spec2021
138 };
139 let contrast_level = if contrast_level.is_finite() {
140 clamp_double(-1., 1., contrast_level)
141 } else {
142 0.
143 };
144 let palettes = match spec {
145 SpecVersion::Spec2021 => palettes_2021(variant, source),
146 SpecVersion::Spec2025 => palettes_2025(variant, source, is_dark),
147 };
148 Self {
149 source,
150 variant,
151 contrast_level,
152 is_dark,
153 spec,
154 primary_palette: palettes[0],
155 secondary_palette: palettes[1],
156 tertiary_palette: palettes[2],
157 neutral_palette: palettes[3],
158 neutral_variant_palette: palettes[4],
159 error_palette: palettes[5],
160 }
161 }
162
163 pub fn piecewise_hue(source: Hct, breakpoints: &[f64], hues: &[f64]) -> f64 {
166 let size = breakpoints.len().saturating_sub(1).min(hues.len());
167 for i in 0..size {
168 if source.hue() >= breakpoints[i] && source.hue() < breakpoints[i + 1] {
169 return sanitize_degrees_double(hues[i]);
170 }
171 }
172 source.hue()
173 }
174
175 pub fn rotated_hue(source: Hct, breakpoints: &[f64], rotations: &[f64]) -> f64 {
178 let rotation = if breakpoints.len().saturating_sub(1).min(rotations.len()) == 0 {
179 0.
180 } else {
181 Self::piecewise_hue(source, breakpoints, rotations)
182 };
183 sanitize_degrees_double(source.hue() + rotation)
184 }
185
186 pub fn argb(&self, role: DynamicRole) -> u32 {
188 self.hct(role).argb()
189 }
190
191 pub fn hct(&self, role: DynamicRole) -> Hct {
193 color::hct(self, role)
194 }
195
196 pub fn tone(&self, role: DynamicRole) -> f64 {
199 color::tone(self, role)
200 }
201}
202
203fn hue_chroma(hue: f64, chroma: f64) -> TonalPalette {
204 TonalPalette::from_hue_and_chroma(hue, chroma)
205}
206
207const DEFAULT_ERROR: (f64, f64) = (25., 84.);
208
209fn palettes_2021(variant: Variant, source: Hct) -> [TonalPalette; 6] {
210 [
211 primary_2021(variant, source),
212 secondary_2021(variant, source),
213 tertiary_2021(variant, source),
214 neutral_2021(variant, source),
215 neutral_variant_2021(variant, source),
216 hue_chroma(DEFAULT_ERROR.0, DEFAULT_ERROR.1),
217 ]
218}
219
220fn primary_2021(variant: Variant, source: Hct) -> TonalPalette {
221 let hue = source.hue();
222 match variant {
223 Variant::Content | Variant::Fidelity => hue_chroma(hue, source.chroma()),
224 Variant::FruitSalad => hue_chroma(sanitize_degrees_double(hue - 50.), 48.),
225 Variant::Monochrome => hue_chroma(hue, 0.),
226 Variant::Neutral => hue_chroma(hue, 12.),
227 Variant::Rainbow => hue_chroma(hue, 48.),
228 Variant::TonalSpot => hue_chroma(hue, 36.),
229 Variant::Expressive => hue_chroma(sanitize_degrees_double(hue + 240.), 40.),
230 Variant::Vibrant => hue_chroma(hue, 200.),
231 }
232}
233
234fn secondary_2021(variant: Variant, source: Hct) -> TonalPalette {
235 let hue = source.hue();
236 match variant {
237 Variant::Content | Variant::Fidelity => {
238 hue_chroma(hue, (source.chroma() - 32.).max(source.chroma() * 0.5))
239 }
240 Variant::FruitSalad => hue_chroma(sanitize_degrees_double(hue - 50.), 36.),
241 Variant::Monochrome => hue_chroma(hue, 0.),
242 Variant::Neutral => hue_chroma(hue, 8.),
243 Variant::Rainbow | Variant::TonalSpot => hue_chroma(hue, 16.),
244 Variant::Expressive => hue_chroma(
245 DynamicScheme::rotated_hue(
246 source,
247 &[0., 21., 51., 121., 151., 191., 271., 321., 360.],
248 &[45., 95., 45., 20., 45., 90., 45., 45., 45.],
249 ),
250 24.,
251 ),
252 Variant::Vibrant => hue_chroma(
253 DynamicScheme::rotated_hue(
254 source,
255 &[0., 41., 61., 101., 131., 181., 251., 301., 360.],
256 &[18., 15., 10., 12., 15., 18., 15., 12., 12.],
257 ),
258 24.,
259 ),
260 }
261}
262
263fn tertiary_2021(variant: Variant, source: Hct) -> TonalPalette {
264 let hue = source.hue();
265 match variant {
266 Variant::Content => TonalPalette::from_hct(dislike::fix_if_disliked(
267 TemperatureCache::new(source).analogous(3, 6)[2],
268 )),
269 Variant::Fidelity => TonalPalette::from_hct(dislike::fix_if_disliked(
270 TemperatureCache::new(source).complement(),
271 )),
272 Variant::FruitSalad => hue_chroma(hue, 36.),
273 Variant::Monochrome => hue_chroma(hue, 0.),
274 Variant::Neutral => hue_chroma(hue, 16.),
275 Variant::Rainbow | Variant::TonalSpot => {
276 hue_chroma(sanitize_degrees_double(hue + 60.), 24.)
277 }
278 Variant::Expressive => hue_chroma(
279 DynamicScheme::rotated_hue(
280 source,
281 &[0., 21., 51., 121., 151., 191., 271., 321., 360.],
282 &[120., 120., 20., 45., 20., 15., 20., 120., 120.],
283 ),
284 32.,
285 ),
286 Variant::Vibrant => hue_chroma(
287 DynamicScheme::rotated_hue(
288 source,
289 &[0., 41., 61., 101., 131., 181., 251., 301., 360.],
290 &[35., 30., 20., 25., 30., 35., 30., 25., 25.],
291 ),
292 32.,
293 ),
294 }
295}
296
297fn neutral_2021(variant: Variant, source: Hct) -> TonalPalette {
298 let hue = source.hue();
299 match variant {
300 Variant::Content | Variant::Fidelity => hue_chroma(hue, source.chroma() / 8.),
301 Variant::FruitSalad => hue_chroma(hue, 10.),
302 Variant::Monochrome | Variant::Rainbow => hue_chroma(hue, 0.),
303 Variant::Neutral => hue_chroma(hue, 2.),
304 Variant::TonalSpot => hue_chroma(hue, 6.),
305 Variant::Expressive => hue_chroma(sanitize_degrees_double(hue + 15.), 8.),
306 Variant::Vibrant => hue_chroma(hue, 10.),
307 }
308}
309
310fn neutral_variant_2021(variant: Variant, source: Hct) -> TonalPalette {
311 let hue = source.hue();
312 match variant {
313 Variant::Content | Variant::Fidelity => hue_chroma(hue, source.chroma() / 8. + 4.),
314 Variant::FruitSalad => hue_chroma(hue, 16.),
315 Variant::Monochrome | Variant::Rainbow => hue_chroma(hue, 0.),
316 Variant::Neutral => hue_chroma(hue, 2.),
317 Variant::TonalSpot => hue_chroma(hue, 8.),
318 Variant::Expressive => hue_chroma(sanitize_degrees_double(hue + 15.), 12.),
319 Variant::Vibrant => hue_chroma(hue, 12.),
320 }
321}
322
323fn expressive_neutral_hue(source: Hct) -> f64 {
324 DynamicScheme::rotated_hue(
325 source,
326 &[0., 71., 124., 253., 278., 300., 360.],
327 &[10., 0., 10., 0., 10., 0.],
328 )
329}
330
331fn expressive_neutral_chroma(source: Hct, is_dark: bool) -> f64 {
332 if is_dark {
333 if Hct::is_yellow(expressive_neutral_hue(source)) {
334 6.
335 } else {
336 14.
337 }
338 } else {
339 18.
340 }
341}
342
343fn vibrant_neutral_hue(source: Hct) -> f64 {
344 DynamicScheme::rotated_hue(
345 source,
346 &[0., 38., 105., 140., 333., 360.],
347 &[-14., 10., -14., 10., -14.],
348 )
349}
350
351const VIBRANT_NEUTRAL_CHROMA: f64 = 28.;
352
353fn palettes_2025(variant: Variant, source: Hct, is_dark: bool) -> [TonalPalette; 6] {
356 let hue = source.hue();
357 let error_hue = DynamicScheme::piecewise_hue(
358 source,
359 &[0., 3., 13., 23., 33., 43., 153., 273., 360.],
360 &[12., 22., 32., 12., 22., 32., 22., 12.],
361 );
362 match variant {
363 Variant::Neutral => {
364 let blue = Hct::is_blue(hue);
365 [
366 hue_chroma(hue, if blue { 12. } else { 8. }),
367 hue_chroma(hue, if blue { 6. } else { 4. }),
368 hue_chroma(
369 DynamicScheme::rotated_hue(
370 source,
371 &[0., 38., 105., 161., 204., 278., 333., 360.],
372 &[-32., 26., 10., -39., 24., -15., -32.],
373 ),
374 20.,
375 ),
376 hue_chroma(hue, 1.4),
377 hue_chroma(hue, 1.4 * 2.2),
378 hue_chroma(error_hue, 50.),
379 ]
380 }
381 Variant::TonalSpot => [
382 hue_chroma(hue, if is_dark { 26. } else { 32. }),
383 hue_chroma(hue, 16.),
384 hue_chroma(
385 DynamicScheme::rotated_hue(
386 source,
387 &[0., 20., 71., 161., 333., 360.],
388 &[-40., 48., -32., 40., -32.],
389 ),
390 28.,
391 ),
392 hue_chroma(hue, 5.),
393 hue_chroma(hue, 5. * 1.7),
394 hue_chroma(error_hue, 60.),
395 ],
396 Variant::Expressive => {
397 let neutral_hue = expressive_neutral_hue(source);
398 let neutral_chroma = expressive_neutral_chroma(source, is_dark);
399 [
400 hue_chroma(hue, if is_dark { 36. } else { 48. }),
401 hue_chroma(
402 DynamicScheme::rotated_hue(
403 source,
404 &[0., 105., 140., 204., 253., 278., 300., 333., 360.],
405 &[-160., 155., -100., 96., -96., -156., -165., -160.],
406 ),
407 if is_dark { 16. } else { 24. },
408 ),
409 hue_chroma(
410 DynamicScheme::rotated_hue(
411 source,
412 &[0., 105., 140., 204., 253., 278., 300., 333., 360.],
413 &[-165., 160., -105., 101., -101., -160., -170., -165.],
414 ),
415 48.,
416 ),
417 hue_chroma(neutral_hue, neutral_chroma),
418 hue_chroma(
419 neutral_hue,
420 neutral_chroma
421 * if (105. ..125.).contains(&neutral_hue) {
422 1.6
423 } else {
424 2.3
425 },
426 ),
427 hue_chroma(error_hue, 64.),
428 ]
429 }
430 Variant::Vibrant => {
431 let neutral_hue = vibrant_neutral_hue(source);
432 [
433 hue_chroma(hue, 74.),
434 hue_chroma(neutral_hue, 56.),
435 hue_chroma(
436 DynamicScheme::rotated_hue(
437 source,
438 &[0., 38., 71., 105., 140., 161., 253., 333., 360.],
439 &[-72., 35., 24., -24., 62., 50., 62., -72.],
440 ),
441 56.,
442 ),
443 hue_chroma(neutral_hue, VIBRANT_NEUTRAL_CHROMA),
444 hue_chroma(neutral_hue, VIBRANT_NEUTRAL_CHROMA * 1.29),
445 hue_chroma(error_hue, 80.),
446 ]
447 }
448 _ => palettes_2021(variant, source),
449 }
450}