1use std::sync::LazyLock;
66
67use frust::{Brightness, ColorScheme, Theme};
68use peniko::Color;
69
70use super::color::{MaterialSemanticColors, semantic_dark, semantic_light};
71use super::extension::MaterialTokens;
72
73const SRGB_TO_XYZ: [[f64; 3]; 3] = [
77 [0.41233895, 0.35762064, 0.18051042],
78 [0.2126, 0.7152, 0.0722],
79 [0.01932141, 0.11916382, 0.95034478],
80];
81
82const WHITE_POINT_D65: [f64; 3] = [95.047, 100.0, 108.883];
85
86fn matrix_multiply(row: [f64; 3], matrix: &[[f64; 3]; 3]) -> [f64; 3] {
87 [
88 row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2],
89 row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2],
90 row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2],
91 ]
92}
93
94fn signum(value: f64) -> f64 {
98 if value < 0.0 {
99 -1.0
100 } else if value == 0.0 {
101 0.0
102 } else {
103 1.0
104 }
105}
106
107fn sanitize_degrees(degrees: f64) -> f64 {
108 degrees.rem_euclid(360.0)
109}
110
111fn linearized(component: u8) -> f64 {
113 linearized_f64(component as f64)
114}
115
116fn linearized_f64(component: f64) -> f64 {
119 let normalized = component / 255.0;
120 if normalized <= 0.040_449_936 {
121 normalized / 12.92 * 100.0
122 } else {
123 ((normalized + 0.055) / 1.055).powf(2.4) * 100.0
124 }
125}
126
127fn delinearized(rgb_component: f64) -> u8 {
129 let value = (true_delinearized(rgb_component)).round() as i32;
130 value.clamp(0, 255) as u8
131}
132
133fn true_delinearized(rgb_component: f64) -> f64 {
136 let normalized = rgb_component / 100.0;
137 let delinearized = if normalized <= 0.003_130_8 {
138 normalized * 12.92
139 } else {
140 1.055 * normalized.powf(1.0 / 2.4) - 0.055
141 };
142 delinearized * 255.0
143}
144
145fn lab_f(t: f64) -> f64 {
146 const E: f64 = 216.0 / 24389.0;
147 const KAPPA: f64 = 24389.0 / 27.0;
148 if t > E {
149 t.cbrt()
150 } else {
151 (KAPPA * t + 16.0) / 116.0
152 }
153}
154
155fn lab_inv_f(ft: f64) -> f64 {
156 const E: f64 = 216.0 / 24389.0;
157 const KAPPA: f64 = 24389.0 / 27.0;
158 let ft3 = ft * ft * ft;
159 if ft3 > E {
160 ft3
161 } else {
162 (116.0 * ft - 16.0) / KAPPA
163 }
164}
165
166fn y_from_lstar(lstar: f64) -> f64 {
168 100.0 * lab_inv_f((lstar + 16.0) / 116.0)
169}
170
171fn lstar_from_y(y: f64) -> f64 {
172 lab_f(y / 100.0) * 116.0 - 16.0
173}
174
175fn argb_from_rgb(red: u8, green: u8, blue: u8) -> u32 {
176 0xFF00_0000 | (red as u32) << 16 | (green as u32) << 8 | blue as u32
177}
178
179fn argb_from_linrgb(linrgb: [f64; 3]) -> u32 {
180 argb_from_rgb(
181 delinearized(linrgb[0]),
182 delinearized(linrgb[1]),
183 delinearized(linrgb[2]),
184 )
185}
186
187fn argb_from_lstar(lstar: f64) -> u32 {
191 let component = delinearized(y_from_lstar(lstar));
192 argb_from_rgb(component, component, component)
193}
194
195fn xyz_from_argb(argb: u32) -> [f64; 3] {
196 let r = linearized((argb >> 16 & 255) as u8);
197 let g = linearized((argb >> 8 & 255) as u8);
198 let b = linearized((argb & 255) as u8);
199 matrix_multiply([r, g, b], &SRGB_TO_XYZ)
200}
201
202fn lstar_from_argb(argb: u32) -> f64 {
203 lstar_from_y(xyz_from_argb(argb)[1])
204}
205
206fn argb_from_color(color: Color) -> u32 {
207 let [r, g, b, _] = color.to_rgba8().to_u8_array();
208 argb_from_rgb(r, g, b)
209}
210
211fn color_from_argb(argb: u32) -> Color {
212 Color::from_rgb8(
213 (argb >> 16 & 255) as u8,
214 (argb >> 8 & 255) as u8,
215 (argb & 255) as u8,
216 )
217}
218
219struct ViewingConditions {
230 background_y_to_white_point_y: f64,
231 aw: f64,
232 nbb: f64,
233 ncb: f64,
234 c: f64,
235 n_c: f64,
236 rgb_d: [f64; 3],
237 fl: f64,
238 z: f64,
239}
240
241static SRGB_VIEWING_CONDITIONS: LazyLock<ViewingConditions> = LazyLock::new(|| {
242 let white_point = WHITE_POINT_D65;
243 let adapting_luminance = 200.0 / std::f64::consts::PI * y_from_lstar(50.0) / 100.0;
244 let background_lstar = 50.0_f64;
245
246 let r_w = white_point[0] * 0.401288 + white_point[1] * 0.650173 + white_point[2] * -0.051461;
248 let g_w = white_point[0] * -0.250268 + white_point[1] * 1.204414 + white_point[2] * 0.045854;
249 let b_w = white_point[0] * -0.002079 + white_point[1] * 0.048952 + white_point[2] * 0.953127;
250
251 let f = 0.8 + 2.0 / 10.0;
253 let c = lerp(0.59, 0.69, (f - 0.9) * 10.0);
254 let d = (f * (1.0 - (1.0 / 3.6) * ((-adapting_luminance - 42.0) / 92.0).exp())).clamp(0.0, 1.0);
255 let n_c = f;
256
257 let rgb_d = [
261 d * (100.0 / r_w) + 1.0 - d,
262 d * (100.0 / g_w) + 1.0 - d,
263 d * (100.0 / b_w) + 1.0 - d,
264 ];
265
266 let k = 1.0 / (5.0 * adapting_luminance + 1.0);
267 let k4 = k * k * k * k;
268 let k4_f = 1.0 - k4;
269 let fl = (k4 * adapting_luminance) + (0.1 * k4_f * k4_f * (5.0 * adapting_luminance).cbrt());
270 let n = y_from_lstar(background_lstar) / white_point[1];
271 let z = 1.48 + n.sqrt();
272 let nbb = 0.725 / n.powf(0.2);
273
274 let rgb_a_factors = [
275 (fl * rgb_d[0] * r_w / 100.0).powf(0.42),
276 (fl * rgb_d[1] * g_w / 100.0).powf(0.42),
277 (fl * rgb_d[2] * b_w / 100.0).powf(0.42),
278 ];
279 let rgb_a = [
280 400.0 * rgb_a_factors[0] / (rgb_a_factors[0] + 27.13),
281 400.0 * rgb_a_factors[1] / (rgb_a_factors[1] + 27.13),
282 400.0 * rgb_a_factors[2] / (rgb_a_factors[2] + 27.13),
283 ];
284
285 ViewingConditions {
286 background_y_to_white_point_y: n,
287 aw: (40.0 * rgb_a[0] + 20.0 * rgb_a[1] + rgb_a[2]) / 20.0 * nbb,
288 nbb,
289 ncb: nbb,
290 c,
291 n_c,
292 rgb_d,
293 fl,
294 z,
295 }
296});
297
298fn lerp(start: f64, stop: f64, amount: f64) -> f64 {
299 (1.0 - amount) * start + amount * stop
300}
301
302struct Cam16 {
310 hue: f64,
311 chroma: f64,
312}
313
314impl Cam16 {
315 fn from_argb(argb: u32) -> Self {
316 let vc = &*SRGB_VIEWING_CONDITIONS;
317 let [x, y, z] = xyz_from_argb(argb);
318
319 let r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z;
321 let g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z;
322 let b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z;
323 let r_d = vc.rgb_d[0] * r_c;
324 let g_d = vc.rgb_d[1] * g_c;
325 let b_d = vc.rgb_d[2] * b_c;
326
327 let r_af = (vc.fl * r_d.abs() / 100.0).powf(0.42);
329 let g_af = (vc.fl * g_d.abs() / 100.0).powf(0.42);
330 let b_af = (vc.fl * b_d.abs() / 100.0).powf(0.42);
331 let r_a = signum(r_d) * 400.0 * r_af / (r_af + 27.13);
332 let g_a = signum(g_d) * 400.0 * g_af / (g_af + 27.13);
333 let b_a = signum(b_d) * 400.0 * b_af / (b_af + 27.13);
334
335 let a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
337 let b = (r_a + g_a - 2.0 * b_a) / 9.0;
338 let u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0;
339 let p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0;
340
341 let atan_degrees = b.atan2(a) * 180.0 / std::f64::consts::PI;
342 let hue = if atan_degrees < 0.0 {
343 atan_degrees + 360.0
344 } else if atan_degrees >= 360.0 {
345 atan_degrees - 360.0
346 } else {
347 atan_degrees
348 };
349
350 let ac = p2 * vc.nbb;
351 let j = 100.0 * (ac / vc.aw).powf(vc.c * vc.z);
352
353 let hue_prime = if hue < 20.14 { hue + 360.0 } else { hue };
355 let e_hue = 0.25 * ((hue_prime * std::f64::consts::PI / 180.0 + 2.0).cos() + 3.8);
356 let p1 = 50000.0 / 13.0 * e_hue * vc.n_c * vc.ncb;
357 let t = p1 * (a * a + b * b).sqrt() / (u + 0.305);
358 let alpha =
359 t.powf(0.9) * (1.64 - 0.29_f64.powf(vc.background_y_to_white_point_y)).powf(0.73);
360
361 Self {
362 hue,
363 chroma: alpha * (j / 100.0).sqrt(),
364 }
365 }
366}
367
368const SCALED_DISCOUNT_FROM_LINRGB: [[f64; 3]; 3] = [
371 [
372 0.001200833568784504,
373 0.002389694492170889,
374 0.0002795742885861124,
375 ],
376 [
377 0.0005891086651375999,
378 0.0029785502573438758,
379 0.0003270666104008398,
380 ],
381 [
382 0.00010146692491640572,
383 0.0005364214359186694,
384 0.0032979401770712076,
385 ],
386];
387
388const LINRGB_FROM_SCALED_DISCOUNT: [[f64; 3]; 3] = [
389 [1373.2198709594231, -1100.4251190754821, -7.278681089101213],
390 [-271.815969077903, 559.6580465940733, -32.46047482791194],
391 [1.9622899599665666, -57.173814538844006, 308.7233197812385],
392];
393
394const Y_FROM_LINRGB: [f64; 3] = [0.2126, 0.7152, 0.0722];
396
397fn critical_plane(index: usize) -> f64 {
403 linearized_f64(index as f64 + 0.5)
404}
405
406fn sanitize_radians(angle: f64) -> f64 {
408 (angle + std::f64::consts::PI * 8.0).rem_euclid(std::f64::consts::PI * 2.0)
409}
410
411fn chromatic_adaptation(component: f64) -> f64 {
412 let af = component.abs().powf(0.42);
413 signum(component) * 400.0 * af / (af + 27.13)
414}
415
416fn inverse_chromatic_adaptation(adapted: f64) -> f64 {
417 let adapted_abs = adapted.abs();
418 let base = (27.13 * adapted_abs / (400.0 - adapted_abs)).max(0.0);
419 signum(adapted) * base.powf(1.0 / 0.42)
420}
421
422fn hue_of(linrgb: [f64; 3]) -> f64 {
424 let scaled_discount = matrix_multiply(linrgb, &SCALED_DISCOUNT_FROM_LINRGB);
425 let r_a = chromatic_adaptation(scaled_discount[0]);
426 let g_a = chromatic_adaptation(scaled_discount[1]);
427 let b_a = chromatic_adaptation(scaled_discount[2]);
428 let a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
429 let b = (r_a + g_a - 2.0 * b_a) / 9.0;
430 b.atan2(a)
431}
432
433fn are_in_cyclic_order(a: f64, b: f64, c: f64) -> bool {
434 sanitize_radians(b - a) < sanitize_radians(c - a)
435}
436
437fn lerp_point(source: [f64; 3], t: f64, target: [f64; 3]) -> [f64; 3] {
438 [
439 source[0] + (target[0] - source[0]) * t,
440 source[1] + (target[1] - source[1]) * t,
441 source[2] + (target[2] - source[2]) * t,
442 ]
443}
444
445fn set_coordinate(source: [f64; 3], coordinate: f64, target: [f64; 3], axis: usize) -> [f64; 3] {
448 let t = (coordinate - source[axis]) / (target[axis] - source[axis]);
449 lerp_point(source, t, target)
450}
451
452fn nth_vertex(y: f64, n: usize) -> Option<[f64; 3]> {
456 let [k_r, k_g, k_b] = Y_FROM_LINRGB;
457 let coord_a = if n % 4 <= 1 { 0.0 } else { 100.0 };
458 let coord_b = if n.is_multiple_of(2) { 0.0 } else { 100.0 };
459 let bounded = |x: f64| (0.0..=100.0).contains(&x);
460 if n < 4 {
461 let (g, b) = (coord_a, coord_b);
462 let r = (y - g * k_g - b * k_b) / k_r;
463 bounded(r).then_some([r, g, b])
464 } else if n < 8 {
465 let (b, r) = (coord_a, coord_b);
466 let g = (y - r * k_r - b * k_b) / k_g;
467 bounded(g).then_some([r, g, b])
468 } else {
469 let (r, g) = (coord_a, coord_b);
470 let b = (y - r * k_r - g * k_g) / k_b;
471 bounded(b).then_some([r, g, b])
472 }
473}
474
475fn bisect_to_segment(y: f64, target_hue: f64) -> ([f64; 3], [f64; 3]) {
477 let mut left = [-1.0; 3];
478 let mut right = left;
479 let mut left_hue = 0.0;
480 let mut right_hue = 0.0;
481 let mut initialized = false;
482 let mut uncut = true;
483 for n in 0..12 {
484 let Some(mid) = nth_vertex(y, n) else {
485 continue;
486 };
487 let mid_hue = hue_of(mid);
488 if !initialized {
489 left = mid;
490 right = mid;
491 left_hue = mid_hue;
492 right_hue = mid_hue;
493 initialized = true;
494 continue;
495 }
496 if uncut || are_in_cyclic_order(left_hue, mid_hue, right_hue) {
497 uncut = false;
498 if are_in_cyclic_order(left_hue, target_hue, mid_hue) {
499 right = mid;
500 right_hue = mid_hue;
501 } else {
502 left = mid;
503 left_hue = mid_hue;
504 }
505 }
506 }
507 (left, right)
508}
509
510fn bisect_to_limit(y: f64, target_hue: f64) -> [f64; 3] {
514 let (mut left, mut right) = bisect_to_segment(y, target_hue);
515 let mut left_hue = hue_of(left);
516 for axis in 0..3 {
517 if left[axis] == right[axis] {
518 continue;
519 }
520 let (mut l_plane, mut r_plane) = if left[axis] < right[axis] {
521 (
522 (true_delinearized(left[axis]) - 0.5).floor() as i32,
523 (true_delinearized(right[axis]) - 0.5).ceil() as i32,
524 )
525 } else {
526 (
527 (true_delinearized(left[axis]) - 0.5).ceil() as i32,
528 (true_delinearized(right[axis]) - 0.5).floor() as i32,
529 )
530 };
531 for _ in 0..8 {
532 if (r_plane - l_plane).abs() <= 1 {
533 break;
534 }
535 let m_plane = ((l_plane + r_plane) as f64 / 2.0).floor() as i32;
536 let mid = set_coordinate(
537 left,
538 critical_plane(m_plane.clamp(0, 254) as usize),
539 right,
540 axis,
541 );
542 let mid_hue = hue_of(mid);
543 if are_in_cyclic_order(left_hue, target_hue, mid_hue) {
544 right = mid;
545 r_plane = m_plane;
546 } else {
547 left = mid;
548 left_hue = mid_hue;
549 l_plane = m_plane;
550 }
551 }
552 }
553 [
554 (left[0] + right[0]) / 2.0,
555 (left[1] + right[1]) / 2.0,
556 (left[2] + right[2]) / 2.0,
557 ]
558}
559
560fn find_result_by_j(hue_radians: f64, chroma: f64, y: f64) -> Option<u32> {
564 let vc = &*SRGB_VIEWING_CONDITIONS;
565 let mut j = y.sqrt() * 11.0;
566 let t_inner_coeff = 1.0 / (1.64 - 0.29_f64.powf(vc.background_y_to_white_point_y)).powf(0.73);
567 let e_hue = 0.25 * ((hue_radians + 2.0).cos() + 3.8);
568 let p1 = e_hue * (50000.0 / 13.0) * vc.n_c * vc.ncb;
569 let h_sin = hue_radians.sin();
570 let h_cos = hue_radians.cos();
571
572 for iteration in 0..5 {
573 let j_normalized = j / 100.0;
574 let alpha = if chroma == 0.0 || j == 0.0 {
575 0.0
576 } else {
577 chroma / j_normalized.sqrt()
578 };
579 let t = (alpha * t_inner_coeff).powf(1.0 / 0.9);
580 let ac = vc.aw * j_normalized.powf(1.0 / vc.c / vc.z);
581 let p2 = ac / vc.nbb;
582 let gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * h_cos + 108.0 * t * h_sin);
583 let a = gamma * h_cos;
584 let b = gamma * h_sin;
585 let r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
586 let g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
587 let b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
588 let linrgb = matrix_multiply(
589 [
590 inverse_chromatic_adaptation(r_a),
591 inverse_chromatic_adaptation(g_a),
592 inverse_chromatic_adaptation(b_a),
593 ],
594 &LINRGB_FROM_SCALED_DISCOUNT,
595 );
596 if linrgb[0] < 0.0 || linrgb[1] < 0.0 || linrgb[2] < 0.0 {
597 return None;
598 }
599 let fnj = Y_FROM_LINRGB[0] * linrgb[0]
600 + Y_FROM_LINRGB[1] * linrgb[1]
601 + Y_FROM_LINRGB[2] * linrgb[2];
602 if fnj <= 0.0 {
603 return None;
604 }
605 if iteration == 4 || (fnj - y).abs() < 0.002 {
606 if linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01 {
607 return None;
608 }
609 return Some(argb_from_linrgb(linrgb));
610 }
611 j -= (fnj - y) * j / (2.0 * fnj);
613 }
614 None
615}
616
617fn solve_to_argb(hue_degrees: f64, chroma: f64, lstar: f64) -> u32 {
620 if chroma < 0.0001 || !(0.0001..=99.9999).contains(&lstar) {
621 return argb_from_lstar(lstar);
622 }
623 let hue_radians = sanitize_degrees(hue_degrees) / 180.0 * std::f64::consts::PI;
624 let y = y_from_lstar(lstar);
625 if let Some(exact) = find_result_by_j(hue_radians, chroma, y) {
626 return exact;
627 }
628 argb_from_linrgb(bisect_to_limit(y, hue_radians))
629}
630
631#[derive(Clone, Copy, Debug, PartialEq)]
641pub struct Hct {
642 hue: f64,
643 chroma: f64,
644 tone: f64,
645 argb: u32,
646}
647
648impl Hct {
649 pub fn from_color(color: Color) -> Self {
651 Self::from_argb(argb_from_color(color))
652 }
653
654 pub fn new(hue: f64, chroma: f64, tone: f64) -> Self {
657 Self::from_argb(solve_to_argb(hue, chroma, tone))
658 }
659
660 fn from_argb(argb: u32) -> Self {
661 let cam = Cam16::from_argb(argb);
662 Self {
663 hue: cam.hue,
664 chroma: cam.chroma,
665 tone: lstar_from_argb(argb),
666 argb,
667 }
668 }
669
670 pub fn hue(&self) -> f64 {
672 self.hue
673 }
674
675 pub fn chroma(&self) -> f64 {
678 self.chroma
679 }
680
681 pub fn tone(&self) -> f64 {
683 self.tone
684 }
685
686 pub fn to_color(&self) -> Color {
688 color_from_argb(self.argb)
689 }
690}
691
692#[derive(Clone, Copy, Debug, PartialEq)]
699pub struct TonalPalette {
700 hue: f64,
701 chroma: f64,
702}
703
704impl TonalPalette {
705 pub fn of(hue: f64, chroma: f64) -> Self {
707 Self { hue, chroma }
708 }
709
710 pub fn from_color(color: Color) -> Self {
712 let hct = Hct::from_color(color);
713 Self::of(hct.hue(), hct.chroma())
714 }
715
716 pub fn hue(&self) -> f64 {
718 self.hue
719 }
720
721 pub fn chroma(&self) -> f64 {
724 self.chroma
725 }
726
727 pub fn tone(&self, tone: f64) -> Color {
729 color_from_argb(self.tone_argb(tone))
730 }
731
732 fn tone_argb(&self, tone: f64) -> u32 {
733 solve_to_argb(self.hue, self.chroma, tone)
734 }
735}
736
737const PRIMARY_MIN_CHROMA: f64 = 48.0;
740const SECONDARY_CHROMA: f64 = 16.0;
741const TERTIARY_HUE_ROTATION: f64 = 60.0;
742const TERTIARY_CHROMA: f64 = 24.0;
743const NEUTRAL_CHROMA: f64 = 6.0;
747const NEUTRAL_VARIANT_CHROMA: f64 = 8.0;
748const ERROR_HUE: f64 = 25.0;
750const ERROR_CHROMA: f64 = 84.0;
751
752#[derive(Clone, Copy, Debug, PartialEq)]
754pub struct CorePalette {
755 pub primary: TonalPalette,
757 pub secondary: TonalPalette,
759 pub tertiary: TonalPalette,
761 pub neutral: TonalPalette,
763 pub neutral_variant: TonalPalette,
766 pub error: TonalPalette,
768}
769
770impl CorePalette {
771 pub fn of(seed: Color) -> Self {
781 let hct = Hct::from_color(seed);
782 let hue = hct.hue();
783 Self {
784 primary: TonalPalette::of(hue, hct.chroma().max(PRIMARY_MIN_CHROMA)),
785 secondary: TonalPalette::of(hue, SECONDARY_CHROMA),
786 tertiary: TonalPalette::of(
787 sanitize_degrees(hue + TERTIARY_HUE_ROTATION),
788 TERTIARY_CHROMA,
789 ),
790 neutral: TonalPalette::of(hue, NEUTRAL_CHROMA),
791 neutral_variant: TonalPalette::of(hue, NEUTRAL_VARIANT_CHROMA),
792 error: TonalPalette::of(ERROR_HUE, ERROR_CHROMA),
793 }
794 }
795}
796
797const SURFACE_STRONG_ALPHA: f64 = 0.06;
802
803fn alpha_blend(accent: Color, surface: Color, alpha: f64) -> Color {
807 let [ar, ag, ab, _] = accent.to_rgba8().to_u8_array();
808 let [sr, sg, sb, _] = surface.to_rgba8().to_u8_array();
809 let mix = |a: u8, s: u8| -> u8 {
810 ((a as f64 / 255.0 * alpha + s as f64 / 255.0 * (1.0 - alpha)) * 255.0).round() as u8
811 };
812 Color::from_rgb8(mix(ar, sr), mix(ag, sg), mix(ab, sb))
813}
814
815fn scheme_from_palettes(core: &CorePalette, brightness: Brightness) -> ColorScheme {
820 let dark = matches!(brightness, Brightness::Dark);
821 let pick = |palette: &TonalPalette, light_tone: f64, dark_tone: f64| {
822 palette.tone(if dark { dark_tone } else { light_tone })
823 };
824 let (p, s, t, n, v, e) = (
825 &core.primary,
826 &core.secondary,
827 &core.tertiary,
828 &core.neutral,
829 &core.neutral_variant,
830 &core.error,
831 );
832 ColorScheme {
833 primary: pick(p, 40.0, 80.0),
834 on_primary: pick(p, 100.0, 20.0),
835 primary_container: pick(p, 90.0, 30.0),
836 on_primary_container: pick(p, 10.0, 90.0),
837 primary_fixed: p.tone(90.0),
841 primary_fixed_dim: p.tone(80.0),
842 on_primary_fixed: p.tone(10.0),
843 on_primary_fixed_variant: p.tone(30.0),
844
845 secondary: pick(s, 40.0, 80.0),
846 on_secondary: pick(s, 100.0, 20.0),
847 secondary_container: pick(s, 90.0, 30.0),
848 on_secondary_container: pick(s, 10.0, 90.0),
849 secondary_fixed: s.tone(90.0),
850 secondary_fixed_dim: s.tone(80.0),
851 on_secondary_fixed: s.tone(10.0),
852 on_secondary_fixed_variant: s.tone(30.0),
853
854 tertiary: pick(t, 40.0, 80.0),
855 on_tertiary: pick(t, 100.0, 20.0),
856 tertiary_container: pick(t, 90.0, 30.0),
857 on_tertiary_container: pick(t, 10.0, 90.0),
858 tertiary_fixed: t.tone(90.0),
859 tertiary_fixed_dim: t.tone(80.0),
860 on_tertiary_fixed: t.tone(10.0),
861 on_tertiary_fixed_variant: t.tone(30.0),
862
863 error: pick(e, 40.0, 80.0),
864 on_error: pick(e, 100.0, 20.0),
865 error_container: pick(e, 90.0, 30.0),
866 on_error_container: pick(e, 10.0, 90.0),
867
868 surface: pick(n, 98.0, 6.0),
869 on_surface: pick(n, 10.0, 90.0),
870 on_surface_variant: pick(v, 30.0, 80.0),
871 surface_dim: pick(n, 87.0, 6.0),
872 surface_bright: pick(n, 98.0, 24.0),
873 surface_container_lowest: pick(n, 100.0, 4.0),
874 surface_container_low: pick(n, 96.0, 10.0),
875 surface_container: pick(n, 94.0, 12.0),
876 surface_container_high: pick(n, 92.0, 17.0),
877 surface_container_highest: pick(n, 90.0, 22.0),
878
879 outline: pick(v, 50.0, 60.0),
880 outline_variant: pick(v, 80.0, 30.0),
881 shadow: n.tone(0.0),
882 scrim: n.tone(0.0),
883 inverse_surface: pick(n, 20.0, 90.0),
884 inverse_on_surface: pick(n, 95.0, 20.0),
885 inverse_primary: pick(p, 80.0, 40.0),
886 surface_tint: pick(p, 40.0, 80.0),
887 }
888}
889
890fn semantic_from_scheme(scheme: &ColorScheme, brightness: Brightness) -> MaterialSemanticColors {
896 let baked = match brightness {
897 Brightness::Light => semantic_light(),
898 Brightness::Dark => semantic_dark(),
899 };
900 MaterialSemanticColors {
901 emphasis: scheme.primary,
902 on_emphasis: scheme.on_primary,
903 info: scheme.tertiary,
904 success: baked.success,
905 warning: baked.warning,
906 danger: scheme.error,
907 surface_strong: alpha_blend(scheme.primary, scheme.surface, SURFACE_STRONG_ALPHA),
908 on_surface_strong: scheme.on_surface,
909 outline_strong: scheme.outline,
910 }
911}
912
913pub fn from_seed(seed: Color, brightness: Brightness) -> (ColorScheme, MaterialTokens) {
934 let (light, dark, tokens) = seeded_schemes(seed);
935 let scheme = match brightness {
936 Brightness::Light => light,
937 Brightness::Dark => dark,
938 };
939 (scheme, tokens)
940}
941
942fn seeded_schemes(seed: Color) -> (ColorScheme, ColorScheme, MaterialTokens) {
946 let core = CorePalette::of(seed);
947 let light = scheme_from_palettes(&core, Brightness::Light);
948 let dark = scheme_from_palettes(&core, Brightness::Dark);
949 let tokens = MaterialTokens {
950 light: semantic_from_scheme(&light, Brightness::Light),
951 dark: semantic_from_scheme(&dark, Brightness::Dark),
952 };
953 (light, dark, tokens)
954}
955
956pub fn theme_from_seed(seed: Color, brightness: Brightness) -> Theme {
967 let (light, dark, tokens) = seeded_schemes(seed);
968 let mut theme = super::baseline();
969 theme.light = light;
970 theme.dark = dark;
971 theme.brightness = brightness;
972 theme.extensions.insert(tokens);
973 theme
974}
975
976#[cfg(test)]
977mod tests {
978 use frust::StatusPalette;
979
980 use super::*;
981 use crate::tokens::color::{color_scheme_dark, color_scheme_light};
982
983 const BASELINE_SEED: Color = Color::from_rgb8(0x67, 0x50, 0xA4);
986
987 fn rgb(color: Color) -> u32 {
988 argb_from_color(color) & 0xFF_FFFF
989 }
990
991 fn max_channel_delta(a: Color, b: Color) -> i32 {
992 let [ar, ag, ab, _] = a.to_rgba8().to_u8_array();
993 let [br, bg, bb, _] = b.to_rgba8().to_u8_array();
994 [
995 (ar as i32 - br as i32).abs(),
996 (ag as i32 - bg as i32).abs(),
997 (ab as i32 - bb as i32).abs(),
998 ]
999 .into_iter()
1000 .max()
1001 .expect("three channels")
1002 }
1003
1004 fn roles(scheme: &ColorScheme) -> Vec<(&'static str, Color)> {
1007 vec![
1008 ("primary", scheme.primary),
1009 ("on_primary", scheme.on_primary),
1010 ("primary_container", scheme.primary_container),
1011 ("on_primary_container", scheme.on_primary_container),
1012 ("primary_fixed", scheme.primary_fixed),
1013 ("primary_fixed_dim", scheme.primary_fixed_dim),
1014 ("on_primary_fixed", scheme.on_primary_fixed),
1015 ("on_primary_fixed_variant", scheme.on_primary_fixed_variant),
1016 ("secondary", scheme.secondary),
1017 ("on_secondary", scheme.on_secondary),
1018 ("secondary_container", scheme.secondary_container),
1019 ("on_secondary_container", scheme.on_secondary_container),
1020 ("secondary_fixed", scheme.secondary_fixed),
1021 ("secondary_fixed_dim", scheme.secondary_fixed_dim),
1022 ("on_secondary_fixed", scheme.on_secondary_fixed),
1023 (
1024 "on_secondary_fixed_variant",
1025 scheme.on_secondary_fixed_variant,
1026 ),
1027 ("tertiary", scheme.tertiary),
1028 ("on_tertiary", scheme.on_tertiary),
1029 ("tertiary_container", scheme.tertiary_container),
1030 ("on_tertiary_container", scheme.on_tertiary_container),
1031 ("tertiary_fixed", scheme.tertiary_fixed),
1032 ("tertiary_fixed_dim", scheme.tertiary_fixed_dim),
1033 ("on_tertiary_fixed", scheme.on_tertiary_fixed),
1034 (
1035 "on_tertiary_fixed_variant",
1036 scheme.on_tertiary_fixed_variant,
1037 ),
1038 ("error", scheme.error),
1039 ("on_error", scheme.on_error),
1040 ("error_container", scheme.error_container),
1041 ("on_error_container", scheme.on_error_container),
1042 ("surface", scheme.surface),
1043 ("on_surface", scheme.on_surface),
1044 ("on_surface_variant", scheme.on_surface_variant),
1045 ("surface_dim", scheme.surface_dim),
1046 ("surface_bright", scheme.surface_bright),
1047 ("surface_container_lowest", scheme.surface_container_lowest),
1048 ("surface_container_low", scheme.surface_container_low),
1049 ("surface_container", scheme.surface_container),
1050 ("surface_container_high", scheme.surface_container_high),
1051 (
1052 "surface_container_highest",
1053 scheme.surface_container_highest,
1054 ),
1055 ("outline", scheme.outline),
1056 ("outline_variant", scheme.outline_variant),
1057 ("shadow", scheme.shadow),
1058 ("scrim", scheme.scrim),
1059 ("inverse_surface", scheme.inverse_surface),
1060 ("inverse_on_surface", scheme.inverse_on_surface),
1061 ("inverse_primary", scheme.inverse_primary),
1062 ("surface_tint", scheme.surface_tint),
1063 ]
1064 }
1065
1066 fn assert_golden(seed: Color, brightness: Brightness, golden: &[(&str, u32)]) {
1067 let (scheme, _) = from_seed(seed, brightness);
1068 let produced = roles(&scheme);
1069 for (role, want) in golden {
1070 let (_, got) = produced
1071 .iter()
1072 .find(|(name, _)| name == role)
1073 .unwrap_or_else(|| panic!("unknown role `{role}`"));
1074 assert_eq!(
1075 rgb(*got),
1076 *want,
1077 "role `{role}` at seed {:06X} ({brightness:?}): got {:06X}, want {:06X}",
1078 rgb(seed),
1079 rgb(*got),
1080 want
1081 );
1082 }
1083 }
1084
1085 #[test]
1089 fn critical_planes_match_upstream() {
1090 let upstream = [
1091 (0usize, 0.015176349177441876),
1092 (1, 0.045529047532325624),
1093 (127, 21.404114048223256),
1094 (254, 99.55452497210776),
1095 ];
1096 for (index, want) in upstream {
1097 let got = critical_plane(index);
1098 assert!(
1099 (got - want).abs() <= want.abs() * 1e-12,
1100 "critical plane {index}: got {got}, upstream {want}"
1101 );
1102 }
1103 }
1104
1105 #[test]
1108 fn cam16_matches_upstream_for_the_baseline_seed() {
1109 let hct = Hct::from_color(BASELINE_SEED);
1110 assert!((hct.hue() - 298.980997210704).abs() < 1e-9, "{}", hct.hue());
1111 assert!(
1112 (hct.chroma() - 47.8565263749703).abs() < 1e-9,
1113 "{}",
1114 hct.chroma()
1115 );
1116 assert!(
1117 (hct.tone() - 40.08324408746242).abs() < 1e-9,
1118 "{}",
1119 hct.tone()
1120 );
1121 }
1122
1123 #[test]
1129 fn hct_round_trips_across_the_rgb_cube() {
1130 let mut exact = 0;
1131 let mut samples = 0;
1132 for r in (0..=255).step_by(17) {
1133 for g in (0..=255).step_by(17) {
1134 for b in (0..=255).step_by(17) {
1135 let color = Color::from_rgb8(r as u8, g as u8, b as u8);
1136 let hct = Hct::from_color(color);
1137 let round_tripped = Hct::new(hct.hue(), hct.chroma(), hct.tone()).to_color();
1138 let delta = max_channel_delta(color, round_tripped);
1139 assert!(
1140 delta <= 1,
1141 "round trip of {:06X} landed on {:06X}",
1142 rgb(color),
1143 rgb(round_tripped)
1144 );
1145 samples += 1;
1146 if delta == 0 {
1147 exact += 1;
1148 }
1149 }
1150 }
1151 }
1152 assert_eq!(samples, 16 * 16 * 16);
1153 assert_eq!(exact, samples, "upstream round-trips this grid exactly");
1154 }
1155
1156 #[test]
1159 fn extreme_tones_resolve_to_black_and_white() {
1160 let palette = TonalPalette::of(298.98, 48.0);
1161 assert_eq!(rgb(palette.tone(0.0)), 0x000000);
1162 assert_eq!(rgb(palette.tone(100.0)), 0xFFFFFF);
1163 }
1164
1165 #[test]
1168 fn from_seed_matches_mcu_golden_light() {
1169 assert_golden(
1170 BASELINE_SEED,
1171 Brightness::Light,
1172 &[
1173 ("primary", 0x6750A4),
1174 ("on_primary", 0xFFFFFF),
1175 ("primary_container", 0xE9DDFF),
1176 ("on_primary_container", 0x22005D),
1177 ("primary_fixed", 0xE9DDFF),
1178 ("primary_fixed_dim", 0xCFBCFF),
1179 ("on_primary_fixed", 0x22005D),
1180 ("on_primary_fixed_variant", 0x4F378A),
1181 ("secondary", 0x625B71),
1182 ("on_secondary", 0xFFFFFF),
1183 ("secondary_container", 0xE8DEF8),
1184 ("on_secondary_container", 0x1E192B),
1185 ("secondary_fixed", 0xE8DEF8),
1186 ("secondary_fixed_dim", 0xCBC2DB),
1187 ("on_secondary_fixed", 0x1E192B),
1188 ("on_secondary_fixed_variant", 0x4A4458),
1189 ("tertiary", 0x7E5260),
1190 ("on_tertiary", 0xFFFFFF),
1191 ("tertiary_container", 0xFFD9E3),
1192 ("on_tertiary_container", 0x31101D),
1193 ("tertiary_fixed", 0xFFD9E3),
1194 ("tertiary_fixed_dim", 0xEFB8C8),
1195 ("on_tertiary_fixed", 0x31101D),
1196 ("on_tertiary_fixed_variant", 0x633B48),
1197 ("error", 0xBA1A1A),
1198 ("on_error", 0xFFFFFF),
1199 ("error_container", 0xFFDAD6),
1200 ("on_error_container", 0x410002),
1201 ("surface", 0xFDF7FF),
1202 ("on_surface", 0x1D1B20),
1203 ("on_surface_variant", 0x49454E),
1204 ("surface_dim", 0xDED8E0),
1205 ("surface_bright", 0xFDF7FF),
1206 ("surface_container_lowest", 0xFFFFFF),
1207 ("surface_container_low", 0xF8F2FA),
1208 ("surface_container", 0xF2ECF4),
1209 ("surface_container_high", 0xECE6EE),
1210 ("surface_container_highest", 0xE6E0E9),
1211 ("outline", 0x7A757F),
1212 ("outline_variant", 0xCAC4CF),
1213 ("shadow", 0x000000),
1214 ("scrim", 0x000000),
1215 ("inverse_surface", 0x322F35),
1216 ("inverse_on_surface", 0xF5EFF7),
1217 ("inverse_primary", 0xCFBCFF),
1218 ("surface_tint", 0x6750A4),
1219 ],
1220 );
1221 }
1222
1223 #[test]
1225 fn from_seed_matches_mcu_golden_dark() {
1226 assert_golden(
1227 BASELINE_SEED,
1228 Brightness::Dark,
1229 &[
1230 ("primary", 0xCFBCFF),
1231 ("on_primary", 0x381E72),
1232 ("primary_container", 0x4F378A),
1233 ("on_primary_container", 0xE9DDFF),
1234 ("primary_fixed", 0xE9DDFF),
1235 ("primary_fixed_dim", 0xCFBCFF),
1236 ("on_primary_fixed", 0x22005D),
1237 ("on_primary_fixed_variant", 0x4F378A),
1238 ("secondary", 0xCBC2DB),
1239 ("on_secondary", 0x332D41),
1240 ("secondary_container", 0x4A4458),
1241 ("on_secondary_container", 0xE8DEF8),
1242 ("secondary_fixed", 0xE8DEF8),
1243 ("secondary_fixed_dim", 0xCBC2DB),
1244 ("on_secondary_fixed", 0x1E192B),
1245 ("on_secondary_fixed_variant", 0x4A4458),
1246 ("tertiary", 0xEFB8C8),
1247 ("on_tertiary", 0x4A2532),
1248 ("tertiary_container", 0x633B48),
1249 ("on_tertiary_container", 0xFFD9E3),
1250 ("tertiary_fixed", 0xFFD9E3),
1251 ("tertiary_fixed_dim", 0xEFB8C8),
1252 ("on_tertiary_fixed", 0x31101D),
1253 ("on_tertiary_fixed_variant", 0x633B48),
1254 ("error", 0xFFB4AB),
1255 ("on_error", 0x690005),
1256 ("error_container", 0x93000A),
1257 ("on_error_container", 0xFFDAD6),
1258 ("surface", 0x141218),
1259 ("on_surface", 0xE6E0E9),
1260 ("on_surface_variant", 0xCAC4CF),
1261 ("surface_dim", 0x141218),
1262 ("surface_bright", 0x3B383E),
1263 ("surface_container_lowest", 0x0F0D13),
1264 ("surface_container_low", 0x1D1B20),
1265 ("surface_container", 0x211F24),
1266 ("surface_container_high", 0x2B292F),
1267 ("surface_container_highest", 0x36343A),
1268 ("outline", 0x948F99),
1269 ("outline_variant", 0x49454E),
1270 ("shadow", 0x000000),
1271 ("scrim", 0x000000),
1272 ("inverse_surface", 0xE6E0E9),
1273 ("inverse_on_surface", 0x322F35),
1274 ("inverse_primary", 0x6750A4),
1275 ("surface_tint", 0xCFBCFF),
1276 ],
1277 );
1278 }
1279
1280 #[test]
1283 fn from_seed_matches_mcu_goldens_for_further_seeds() {
1284 let blue = Color::from_rgb8(0x0B, 0x57, 0xD0);
1285 assert_golden(
1286 blue,
1287 Brightness::Light,
1288 &[
1289 ("primary", 0x0856CF),
1290 ("on_primary", 0xFFFFFF),
1291 ("primary_container", 0xDAE2FF),
1292 ("on_primary_container", 0x001847),
1293 ("secondary", 0x585E71),
1294 ("tertiary", 0x735471),
1295 ("surface", 0xFAF8FF),
1296 ("on_surface", 0x1A1B21),
1297 ("surface_container", 0xEEEDF4),
1298 ("outline", 0x757780),
1299 ("inverse_primary", 0xB2C5FF),
1300 ],
1301 );
1302 assert_golden(
1303 blue,
1304 Brightness::Dark,
1305 &[
1306 ("primary", 0xB2C5FF),
1307 ("on_primary", 0x002B72),
1308 ("primary_container", 0x0040A1),
1309 ("on_primary_container", 0xDAE2FF),
1310 ("secondary", 0xC0C6DD),
1311 ("tertiary", 0xE1BBDD),
1312 ("surface", 0x121318),
1313 ("on_surface", 0xE2E2E9),
1314 ("surface_container", 0x1E1F25),
1315 ("outline", 0x8F909A),
1316 ("inverse_primary", 0x0856CF),
1317 ],
1318 );
1319
1320 let green = Color::from_rgb8(0x00, 0xA8, 0x6B);
1321 assert_golden(
1322 green,
1323 Brightness::Light,
1324 &[
1325 ("primary", 0x006D43),
1326 ("primary_container", 0x78FBB6),
1327 ("on_primary_container", 0x002111),
1328 ("secondary", 0x4E6355),
1329 ("tertiary", 0x3C6471),
1330 ("surface", 0xF6FBF4),
1331 ("on_surface", 0x171D19),
1332 ("outline", 0x717972),
1333 ],
1334 );
1335 assert_golden(
1336 green,
1337 Brightness::Dark,
1338 &[
1339 ("primary", 0x59DE9B),
1340 ("primary_container", 0x005232),
1341 ("on_primary_container", 0x78FBB6),
1342 ("secondary", 0xB5CCBB),
1343 ("tertiary", 0xA3CDDC),
1344 ("surface", 0x0F1511),
1345 ("on_surface", 0xDFE4DD),
1346 ("outline", 0x8A938B),
1347 ],
1348 );
1349
1350 let orange = Color::from_rgb8(0xFF, 0x57, 0x22);
1351 assert_golden(
1352 orange,
1353 Brightness::Light,
1354 &[
1355 ("primary", 0xB02F00),
1356 ("primary_container", 0xFFDBD1),
1357 ("on_primary_container", 0x3B0900),
1358 ("secondary", 0x77574E),
1359 ("tertiary", 0x6C5D2F),
1360 ("surface", 0xFFF8F6),
1361 ("on_surface", 0x231917),
1362 ("outline", 0x85736E),
1363 ],
1364 );
1365 assert_golden(
1366 orange,
1367 Brightness::Dark,
1368 &[
1369 ("primary", 0xFFB5A0),
1370 ("primary_container", 0x862200),
1371 ("on_primary_container", 0xFFDBD1),
1372 ("secondary", 0xE7BDB2),
1373 ("tertiary", 0xD8C58D),
1374 ("surface", 0x1A110F),
1375 ("on_surface", 0xF1DFDA),
1376 ("outline", 0xA08C87),
1377 ],
1378 );
1379 }
1380
1381 #[test]
1383 fn the_error_ramp_is_seed_independent() {
1384 let (blue, _) = from_seed(Color::from_rgb8(0x0B, 0x57, 0xD0), Brightness::Light);
1385 let (green, _) = from_seed(Color::from_rgb8(0x00, 0xA8, 0x6B), Brightness::Light);
1386 assert_eq!(blue.error, green.error);
1387 assert_eq!(blue.error_container, green.error_container);
1388 assert_ne!(blue.primary, green.primary);
1389 }
1390
1391 const LIGHT_DIVERGENCE: &[(&str, u32, u32)] = &[
1399 ("primary_container", 0xE9DDFF, 0xEADDFF),
1400 ("on_primary_container", 0x22005D, 0x21005D),
1401 ("primary_fixed", 0xE9DDFF, 0xEADDFF),
1402 ("primary_fixed_dim", 0xCFBCFF, 0xD0BCFF),
1403 ("on_primary_fixed", 0x22005D, 0x21005D),
1404 ("on_primary_fixed_variant", 0x4F378A, 0x4F378B),
1405 ("on_secondary_container", 0x1E192B, 0x1D192B),
1406 ("secondary_fixed_dim", 0xCBC2DB, 0xCCC2DC),
1407 ("on_secondary_fixed", 0x1E192B, 0x1D192B),
1408 ("tertiary", 0x7E5260, 0x7D5260),
1409 ("tertiary_container", 0xFFD9E3, 0xFFD8E4),
1410 ("on_tertiary_container", 0x31101D, 0x31111D),
1411 ("tertiary_fixed", 0xFFD9E3, 0xFFD8E4),
1412 ("on_tertiary_fixed", 0x31101D, 0x31111D),
1413 ("error", 0xBA1A1A, 0xB3261E),
1414 ("error_container", 0xFFDAD6, 0xF9DEDC),
1415 ("on_error_container", 0x410002, 0x410E0B),
1416 ("surface", 0xFDF7FF, 0xFEF7FF),
1417 ("on_surface_variant", 0x49454E, 0x49454F),
1418 ("surface_dim", 0xDED8E0, 0xDED8E1),
1419 ("surface_bright", 0xFDF7FF, 0xFEF7FF),
1420 ("surface_container_low", 0xF8F2FA, 0xF7F2FA),
1421 ("surface_container", 0xF2ECF4, 0xF3EDF7),
1422 ("surface_container_high", 0xECE6EE, 0xECE6F0),
1423 ("outline", 0x7A757F, 0x79747E),
1424 ("outline_variant", 0xCAC4CF, 0xCAC4D0),
1425 ("inverse_primary", 0xCFBCFF, 0xD0BCFF),
1426 ];
1427
1428 const DARK_DIVERGENCE: &[(&str, u32, u32)] = &[
1430 ("primary", 0xCFBCFF, 0xD0BCFF),
1431 ("primary_container", 0x4F378A, 0x4F378B),
1432 ("on_primary_container", 0xE9DDFF, 0xEADDFF),
1433 ("primary_fixed", 0xE9DDFF, 0xEADDFF),
1434 ("primary_fixed_dim", 0xCFBCFF, 0xD0BCFF),
1435 ("on_primary_fixed", 0x22005D, 0x21005D),
1436 ("on_primary_fixed_variant", 0x4F378A, 0x4F378B),
1437 ("secondary", 0xCBC2DB, 0xCCC2DC),
1438 ("secondary_fixed_dim", 0xCBC2DB, 0xCCC2DC),
1439 ("on_secondary_fixed", 0x1E192B, 0x1D192B),
1440 ("on_tertiary", 0x4A2532, 0x492532),
1441 ("on_tertiary_container", 0xFFD9E3, 0xFFD8E4),
1442 ("tertiary_fixed", 0xFFD9E3, 0xFFD8E4),
1443 ("on_tertiary_fixed", 0x31101D, 0x31111D),
1444 ("error", 0xFFB4AB, 0xF2B8B5),
1445 ("on_error", 0x690005, 0x601410),
1446 ("error_container", 0x93000A, 0x8C1D18),
1447 ("on_error_container", 0xFFDAD6, 0xF9DEDC),
1448 ("on_surface_variant", 0xCAC4CF, 0xCAC4D0),
1449 ("surface_container", 0x211F24, 0x211F26),
1450 ("surface_container_high", 0x2B292F, 0x2B2930),
1451 ("surface_container_highest", 0x36343A, 0x36343B),
1452 ("outline", 0x948F99, 0x938F99),
1453 ("outline_variant", 0x49454E, 0x49454F),
1454 ("surface_tint", 0xCFBCFF, 0xD0BCFF),
1455 ];
1456
1457 fn assert_divergence(
1458 brightness: Brightness,
1459 baked: &ColorScheme,
1460 divergence: &[(&str, u32, u32)],
1461 ) {
1462 let (generated, _) = from_seed(BASELINE_SEED, brightness);
1463 let baked_roles = roles(baked);
1464 for (role, got) in roles(&generated) {
1465 let (_, want) = baked_roles
1466 .iter()
1467 .find(|(name, _)| *name == role)
1468 .expect("both schemes carry the same role set");
1469 match divergence.iter().find(|(name, _, _)| *name == role) {
1470 None => assert_eq!(
1471 rgb(got),
1472 rgb(*want),
1473 "role `{role}` ({brightness:?}) was expected to match the baked baseline"
1474 ),
1475 Some((_, expected_generated, expected_baked)) => {
1476 assert_eq!(
1477 rgb(got),
1478 *expected_generated,
1479 "recorded divergence for `{role}` ({brightness:?}) is stale"
1480 );
1481 assert_eq!(rgb(*want), *expected_baked, "baked `{role}` moved");
1482 }
1483 }
1484 }
1485 }
1486
1487 #[test]
1491 fn from_seed_diverges_from_the_baked_baseline_only_as_recorded() {
1492 assert_divergence(Brightness::Light, &color_scheme_light(), LIGHT_DIVERGENCE);
1493 assert_divergence(Brightness::Dark, &color_scheme_dark(), DARK_DIVERGENCE);
1494 }
1495
1496 #[test]
1500 fn baseline_divergence_outside_the_error_family_is_at_most_three_steps() {
1501 for (brightness, baked, divergence, expected_exact, expected_one_step) in [
1502 (
1503 Brightness::Light,
1504 color_scheme_light(),
1505 LIGHT_DIVERGENCE,
1506 18,
1507 22,
1508 ),
1509 (
1510 Brightness::Dark,
1511 color_scheme_dark(),
1512 DARK_DIVERGENCE,
1513 21,
1514 20,
1515 ),
1516 ] {
1517 let (generated, _) = from_seed(BASELINE_SEED, brightness);
1518 let baked_roles = roles(&baked);
1519 let (mut exact, mut one_step) = (0, 0);
1520 for (role, got) in roles(&generated) {
1521 let (_, want) = baked_roles
1522 .iter()
1523 .find(|(name, _)| *name == role)
1524 .expect("same role set");
1525 let delta = max_channel_delta(got, *want);
1526 if role.contains("error") {
1527 continue;
1528 }
1529 assert!(
1530 delta <= 3,
1531 "role `{role}` ({brightness:?}) drifted more than three steps"
1532 );
1533 match delta {
1534 0 => exact += 1,
1535 1 => one_step += 1,
1536 _ => {}
1537 }
1538 }
1539 assert_eq!(exact, expected_exact, "exact-match count ({brightness:?})");
1540 assert_eq!(
1541 one_step, expected_one_step,
1542 "single-step divergence count ({brightness:?})"
1543 );
1544 assert!(divergence.iter().any(|(name, _, _)| name.contains("error")));
1545 }
1546 }
1547
1548 #[test]
1553 fn degenerate_seeds_follow_upstream_rather_than_a_substituted_hue() {
1554 let black = Hct::from_color(Color::BLACK);
1555 assert_eq!(black.hue(), 0.0);
1556 assert_eq!(black.chroma(), 0.0);
1557 let gray = Hct::from_color(Color::from_rgb8(0x80, 0x80, 0x80));
1558 assert!(
1559 (gray.hue() - 209.49382556955806).abs() < 1e-9,
1560 "{}",
1561 gray.hue()
1562 );
1563 let white = Hct::from_color(Color::WHITE);
1564 assert!(
1565 (white.hue() - 209.49195947383808).abs() < 1e-9,
1566 "{}",
1567 white.hue()
1568 );
1569
1570 assert_golden(
1572 Color::BLACK,
1573 Brightness::Light,
1574 &[
1575 ("primary", 0x984061),
1576 ("surface", 0xFFF8F8),
1577 ("on_surface", 0x22191C),
1578 ("outline", 0x837377),
1579 ],
1580 );
1581 for colorless in [Color::from_rgb8(0x80, 0x80, 0x80), Color::WHITE] {
1582 assert_golden(
1583 colorless,
1584 Brightness::Light,
1585 &[
1586 ("primary", 0x006874),
1587 ("surface", 0xF5FAFB),
1588 ("on_surface", 0x171D1E),
1589 ("outline", 0x6F797A),
1590 ],
1591 );
1592 }
1593 }
1594
1595 #[test]
1598 fn every_seed_yields_a_well_formed_scheme() {
1599 for seed in [
1600 BASELINE_SEED,
1601 Color::BLACK,
1602 Color::WHITE,
1603 Color::from_rgb8(0x80, 0x80, 0x80),
1604 Color::from_rgb8(0xFF, 0x00, 0x00),
1605 Color::from_rgb8(0x00, 0x00, 0x01),
1606 ] {
1607 let (light, _) = from_seed(seed, Brightness::Light);
1608 let (dark, _) = from_seed(seed, Brightness::Dark);
1609 for (role, color) in roles(&light).into_iter().chain(roles(&dark)) {
1610 assert_eq!(
1611 color.to_rgba8().to_u8_array()[3],
1612 255,
1613 "role `{role}` came back translucent"
1614 );
1615 }
1616 assert!(
1617 Hct::from_color(light.surface).tone() > Hct::from_color(light.on_surface).tone(),
1618 "light surface must be lighter than its content"
1619 );
1620 assert!(
1621 Hct::from_color(dark.surface).tone() < Hct::from_color(dark.on_surface).tone(),
1622 "dark surface must be darker than its content"
1623 );
1624 assert_ne!(light.surface, dark.surface);
1625 assert_ne!(light.primary, dark.primary);
1626 }
1627 }
1628
1629 #[test]
1632 fn semantic_roles_reuse_the_baked_derivation_rules() {
1633 let seed = Color::from_rgb8(0x00, 0xA8, 0x6B);
1634 let (light, tokens) = from_seed(seed, Brightness::Light);
1635 let (dark, _) = from_seed(seed, Brightness::Dark);
1636
1637 assert_eq!(tokens.light.emphasis, light.primary);
1638 assert_eq!(tokens.light.on_emphasis, light.on_primary);
1639 assert_eq!(tokens.light.info, light.tertiary);
1640 assert_eq!(tokens.light.danger, light.error);
1641 assert_eq!(tokens.light.on_surface_strong, light.on_surface);
1642 assert_eq!(tokens.light.outline_strong, light.outline);
1643 assert_eq!(
1644 tokens.light.surface_strong,
1645 alpha_blend(light.primary, light.surface, SURFACE_STRONG_ALPHA)
1646 );
1647 assert_eq!(tokens.dark.emphasis, dark.primary);
1648 assert_eq!(tokens.dark.danger, dark.error);
1649
1650 assert_eq!(tokens.light.success, semantic_light().success);
1652 assert_eq!(tokens.light.warning, semantic_light().warning);
1653 assert_eq!(tokens.dark.success, semantic_dark().success);
1654 assert_eq!(tokens.dark.warning, semantic_dark().warning);
1655 }
1656
1657 #[test]
1660 fn from_seed_returns_the_requested_brightness_and_both_token_halves() {
1661 let seed = Color::from_rgb8(0x0B, 0x57, 0xD0);
1662 let (light, light_tokens) = from_seed(seed, Brightness::Light);
1663 let (dark, dark_tokens) = from_seed(seed, Brightness::Dark);
1664 assert_eq!(light_tokens, dark_tokens);
1665 assert_eq!(
1666 light_tokens.colors(Brightness::Light).emphasis,
1667 light.primary
1668 );
1669 assert_eq!(dark_tokens.colors(Brightness::Dark).emphasis, dark.primary);
1670 assert_ne!(light_tokens.light, light_tokens.dark);
1671 }
1672
1673 #[test]
1674 fn theme_from_seed_keeps_the_baseline_scales_and_swaps_the_colors() {
1675 let seed = Color::from_rgb8(0xFF, 0x57, 0x22);
1676 let theme = theme_from_seed(seed, Brightness::Dark);
1677 let baseline = super::super::baseline();
1678
1679 assert_eq!(theme.brightness, Brightness::Dark);
1680 assert_eq!(theme.type_scale, baseline.type_scale);
1681 assert_eq!(theme.shape, baseline.shape);
1682 assert_eq!(theme.elevation, baseline.elevation);
1683 assert_eq!(theme.motion, baseline.motion);
1684 assert_eq!(
1685 theme.extension::<StatusPalette>(),
1686 baseline.extension::<StatusPalette>()
1687 );
1688
1689 let (light, tokens) = from_seed(seed, Brightness::Light);
1690 assert_eq!(theme.light, light);
1691 assert_eq!(theme.extension::<MaterialTokens>(), Some(&tokens));
1692 assert_ne!(theme.light, baseline.light);
1693 assert_ne!(theme.dark, baseline.dark);
1694 }
1695
1696 #[test]
1700 fn theme_from_seed_attaches_the_seeded_tokens_not_the_baked_ones() {
1701 let theme = theme_from_seed(BASELINE_SEED, Brightness::Light);
1702 let attached = theme
1703 .extension::<MaterialTokens>()
1704 .expect("theme_from_seed attaches MaterialTokens");
1705 let (_, seeded) = from_seed(BASELINE_SEED, Brightness::Light);
1706 assert_eq!(attached, &seeded);
1707 assert_eq!(attached.light.emphasis, theme.light.primary);
1708 }
1709
1710 #[test]
1712 fn tonal_palette_tones_ascend_and_hold_their_hue() {
1713 let palette = TonalPalette::of(120.0, 40.0);
1714 let mut previous = -1.0;
1715 for tone in [0.0, 10.0, 20.0, 40.0, 60.0, 80.0, 90.0, 100.0] {
1716 let resolved = Hct::from_color(palette.tone(tone));
1717 assert!(
1718 resolved.tone() > previous,
1719 "tone {tone} did not ascend ({} after {previous})",
1720 resolved.tone()
1721 );
1722 previous = resolved.tone();
1723 if tone > 0.0 && tone < 100.0 {
1724 assert!(
1725 (resolved.hue() - 120.0).abs() < 2.0,
1726 "tone {tone} drifted to hue {}",
1727 resolved.hue()
1728 );
1729 }
1730 }
1731 }
1732
1733 #[test]
1736 fn tonal_palette_from_color_matches_the_core_primary_ramp() {
1737 let seed = Color::from_rgb8(0x0B, 0x57, 0xD0);
1738 let core = CorePalette::of(seed);
1739 let direct = TonalPalette::from_color(seed);
1740 assert!(Hct::from_color(seed).chroma() > PRIMARY_MIN_CHROMA);
1742 assert_eq!(direct.tone(40.0), core.primary.tone(40.0));
1743 }
1744
1745 #[test]
1746 fn core_palette_applies_the_chroma_floor_and_hue_rotation() {
1747 let gray = CorePalette::of(Color::from_rgb8(0x80, 0x80, 0x80));
1748 assert_eq!(gray.primary.chroma(), PRIMARY_MIN_CHROMA);
1749 let vivid = CorePalette::of(Color::from_rgb8(0xFF, 0x57, 0x22));
1750 assert!(vivid.primary.chroma() > PRIMARY_MIN_CHROMA);
1751 assert_eq!(
1752 vivid.tertiary.hue(),
1753 sanitize_degrees(vivid.primary.hue() + TERTIARY_HUE_ROTATION)
1754 );
1755 assert_eq!(vivid.error.hue(), ERROR_HUE);
1756 }
1757}