• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2023 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.ux.material.libmonet.dynamiccolor;
18 
19 import android.annotation.NonNull;
20 import com.google.ux.material.libmonet.dislike.DislikeAnalyzer;
21 import com.google.ux.material.libmonet.hct.Hct;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.function.Supplier;
25 
26 /** Named colors, otherwise known as tokens, or roles, in the Material Design system. */
27 // Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
28 // "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
29 // A java_library Bazel rule with an Android constraint cannot skip these warnings without this
30 // annotation; another solution would be to create an android_library rule and supply
31 // AndroidManifest with an SDK set higher than 14.
32 @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
33 public final class MaterialDynamicColors {
34   /** Optionally use fidelity on most color schemes. */
35   private final boolean isExtendedFidelity;
36 
MaterialDynamicColors()37   public MaterialDynamicColors() {
38     this.isExtendedFidelity = false;
39   }
40 
41   // Temporary constructor to support extended fidelity experiment.
42   // TODO(b/291720794): Once schemes that will permanently use fidelity are identified,
43   // remove this and default to the decided behavior.
MaterialDynamicColors(boolean isExtendedFidelity)44   public MaterialDynamicColors(boolean isExtendedFidelity) {
45     this.isExtendedFidelity = isExtendedFidelity;
46   }
47 
48   @NonNull
highestSurface(@onNull DynamicScheme s)49   public DynamicColor highestSurface(@NonNull DynamicScheme s) {
50     return s.isDark ? surfaceBright() : surfaceDim();
51   }
52 
53   // Compatibility Keys Colors for Android
54   @NonNull
primaryPaletteKeyColor()55   public DynamicColor primaryPaletteKeyColor() {
56     return DynamicColor.fromPalette(
57         /* name= */ "primary_palette_key_color",
58         /* palette= */ (s) -> s.primaryPalette,
59         /* tone= */ (s) -> s.primaryPalette.getKeyColor().getTone());
60   }
61 
62   @NonNull
secondaryPaletteKeyColor()63   public DynamicColor secondaryPaletteKeyColor() {
64     return DynamicColor.fromPalette(
65         /* name= */ "secondary_palette_key_color",
66         /* palette= */ (s) -> s.secondaryPalette,
67         /* tone= */ (s) -> s.secondaryPalette.getKeyColor().getTone());
68   }
69 
70   @NonNull
tertiaryPaletteKeyColor()71   public DynamicColor tertiaryPaletteKeyColor() {
72     return DynamicColor.fromPalette(
73         /* name= */ "tertiary_palette_key_color",
74         /* palette= */ (s) -> s.tertiaryPalette,
75         /* tone= */ (s) -> s.tertiaryPalette.getKeyColor().getTone());
76   }
77 
78   @NonNull
neutralPaletteKeyColor()79   public DynamicColor neutralPaletteKeyColor() {
80     return DynamicColor.fromPalette(
81         /* name= */ "neutral_palette_key_color",
82         /* palette= */ (s) -> s.neutralPalette,
83         /* tone= */ (s) -> s.neutralPalette.getKeyColor().getTone());
84   }
85 
86   @NonNull
neutralVariantPaletteKeyColor()87   public DynamicColor neutralVariantPaletteKeyColor() {
88     return DynamicColor.fromPalette(
89         /* name= */ "neutral_variant_palette_key_color",
90         /* palette= */ (s) -> s.neutralVariantPalette,
91         /* tone= */ (s) -> s.neutralVariantPalette.getKeyColor().getTone());
92   }
93 
94   @NonNull
background()95   public DynamicColor background() {
96     return new DynamicColor(
97         /* name= */ "background",
98         /* palette= */ (s) -> s.neutralPalette,
99         /* tone= */ (s) -> s.isDark ? 6.0 : 98.0,
100         /* isBackground= */ true,
101         /* background= */ null,
102         /* secondBackground= */ null,
103         /* contrastCurve= */ null,
104         /* toneDeltaPair= */ null);
105   }
106 
107   @NonNull
onBackground()108   public DynamicColor onBackground() {
109     return new DynamicColor(
110         /* name= */ "on_background",
111         /* palette= */ (s) -> s.neutralPalette,
112         /* tone= */ (s) -> s.isDark ? 90.0 : 10.0,
113         /* isBackground= */ false,
114         /* background= */ (s) -> background(),
115         /* secondBackground= */ null,
116         /* contrastCurve= */ new ContrastCurve(3.0, 3.0, 4.5, 7.0),
117         /* toneDeltaPair= */ null);
118   }
119 
120   @NonNull
surface()121   public DynamicColor surface() {
122     return new DynamicColor(
123         /* name= */ "surface",
124         /* palette= */ (s) -> s.neutralPalette,
125         /* tone= */ (s) -> s.isDark ? 6.0 : 98.0,
126         /* isBackground= */ true,
127         /* background= */ null,
128         /* secondBackground= */ null,
129         /* contrastCurve= */ null,
130         /* toneDeltaPair= */ null);
131   }
132 
133   @NonNull
surfaceDim()134   public DynamicColor surfaceDim() {
135     return new DynamicColor(
136         /* name= */ "surface_dim",
137         /* palette= */ (s) -> s.neutralPalette,
138         /* tone= */ (s) ->
139             s.isDark ? 6.0 : new ContrastCurve(87.0, 87.0, 80.0, 75.0).get(s.contrastLevel),
140         /* isBackground= */ true,
141         /* background= */ null,
142         /* secondBackground= */ null,
143         /* contrastCurve= */ null,
144         /* toneDeltaPair= */ null);
145   }
146 
147   @NonNull
surfaceBright()148   public DynamicColor surfaceBright() {
149     return new DynamicColor(
150         /* name= */ "surface_bright",
151         /* palette= */ (s) -> s.neutralPalette,
152         /* tone= */ (s) ->
153             s.isDark ? new ContrastCurve(24.0, 24.0, 29.0, 34.0).get(s.contrastLevel) : 98.0,
154         /* isBackground= */ true,
155         /* background= */ null,
156         /* secondBackground= */ null,
157         /* contrastCurve= */ null,
158         /* toneDeltaPair= */ null);
159   }
160 
161   @NonNull
surfaceContainerLowest()162   public DynamicColor surfaceContainerLowest() {
163     return new DynamicColor(
164         /* name= */ "surface_container_lowest",
165         /* palette= */ (s) -> s.neutralPalette,
166         /* tone= */ (s) ->
167             s.isDark ? new ContrastCurve(4.0, 4.0, 2.0, 0.0).get(s.contrastLevel) : 100.0,
168         /* isBackground= */ true,
169         /* background= */ null,
170         /* secondBackground= */ null,
171         /* contrastCurve= */ null,
172         /* toneDeltaPair= */ null);
173   }
174 
175   @NonNull
surfaceContainerLow()176   public DynamicColor surfaceContainerLow() {
177     return new DynamicColor(
178         /* name= */ "surface_container_low",
179         /* palette= */ (s) -> s.neutralPalette,
180         /* tone= */ (s) ->
181             s.isDark
182                 ? new ContrastCurve(10.0, 10.0, 11.0, 12.0).get(s.contrastLevel)
183                 : new ContrastCurve(96.0, 96.0, 96.0, 95.0).get(s.contrastLevel),
184         /* isBackground= */ true,
185         /* background= */ null,
186         /* secondBackground= */ null,
187         /* contrastCurve= */ null,
188         /* toneDeltaPair= */ null);
189   }
190 
191   @NonNull
surfaceContainer()192   public DynamicColor surfaceContainer() {
193     return new DynamicColor(
194         /* name= */ "surface_container",
195         /* palette= */ (s) -> s.neutralPalette,
196         /* tone= */ (s) ->
197             s.isDark
198                 ? new ContrastCurve(12.0, 12.0, 16.0, 20.0).get(s.contrastLevel)
199                 : new ContrastCurve(94.0, 94.0, 92.0, 90.0).get(s.contrastLevel),
200         /* isBackground= */ true,
201         /* background= */ null,
202         /* secondBackground= */ null,
203         /* contrastCurve= */ null,
204         /* toneDeltaPair= */ null);
205   }
206 
207   @NonNull
surfaceContainerHigh()208   public DynamicColor surfaceContainerHigh() {
209     return new DynamicColor(
210         /* name= */ "surface_container_high",
211         /* palette= */ (s) -> s.neutralPalette,
212         /* tone= */ (s) ->
213             s.isDark
214                 ? new ContrastCurve(17.0, 17.0, 21.0, 25.0).get(s.contrastLevel)
215                 : new ContrastCurve(92.0, 92.0, 88.0, 85.0).get(s.contrastLevel),
216         /* isBackground= */ true,
217         /* background= */ null,
218         /* secondBackground= */ null,
219         /* contrastCurve= */ null,
220         /* toneDeltaPair= */ null);
221   }
222 
223   @NonNull
surfaceContainerHighest()224   public DynamicColor surfaceContainerHighest() {
225     return new DynamicColor(
226         /* name= */ "surface_container_highest",
227         /* palette= */ (s) -> s.neutralPalette,
228         /* tone= */ (s) ->
229             s.isDark
230                 ? new ContrastCurve(22.0, 22.0, 26.0, 30.0).get(s.contrastLevel)
231                 : new ContrastCurve(90.0, 90.0, 84.0, 80.0).get(s.contrastLevel),
232         /* isBackground= */ true,
233         /* background= */ null,
234         /* secondBackground= */ null,
235         /* contrastCurve= */ null,
236         /* toneDeltaPair= */ null);
237   }
238 
239   @NonNull
onSurface()240   public DynamicColor onSurface() {
241     return new DynamicColor(
242         /* name= */ "on_surface",
243         /* palette= */ (s) -> s.neutralPalette,
244         /* tone= */ (s) -> s.isDark ? 90.0 : 10.0,
245         /* isBackground= */ false,
246         /* background= */ this::highestSurface,
247         /* secondBackground= */ null,
248         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
249         /* toneDeltaPair= */ null);
250   }
251 
252   @NonNull
surfaceVariant()253   public DynamicColor surfaceVariant() {
254     return new DynamicColor(
255         /* name= */ "surface_variant",
256         /* palette= */ (s) -> s.neutralVariantPalette,
257         /* tone= */ (s) -> s.isDark ? 30.0 : 90.0,
258         /* isBackground= */ true,
259         /* background= */ null,
260         /* secondBackground= */ null,
261         /* contrastCurve= */ null,
262         /* toneDeltaPair= */ null);
263   }
264 
265   @NonNull
onSurfaceVariant()266   public DynamicColor onSurfaceVariant() {
267     return new DynamicColor(
268         /* name= */ "on_surface_variant",
269         /* palette= */ (s) -> s.neutralVariantPalette,
270         /* tone= */ (s) -> s.isDark ? 80.0 : 30.0,
271         /* isBackground= */ false,
272         /* background= */ this::highestSurface,
273         /* secondBackground= */ null,
274         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
275         /* toneDeltaPair= */ null);
276   }
277 
278   @NonNull
inverseSurface()279   public DynamicColor inverseSurface() {
280     return new DynamicColor(
281         /* name= */ "inverse_surface",
282         /* palette= */ (s) -> s.neutralPalette,
283         /* tone= */ (s) -> s.isDark ? 90.0 : 20.0,
284         /* isBackground= */ false,
285         /* background= */ null,
286         /* secondBackground= */ null,
287         /* contrastCurve= */ null,
288         /* toneDeltaPair= */ null);
289   }
290 
291   @NonNull
inverseOnSurface()292   public DynamicColor inverseOnSurface() {
293     return new DynamicColor(
294         /* name= */ "inverse_on_surface",
295         /* palette= */ (s) -> s.neutralPalette,
296         /* tone= */ (s) -> s.isDark ? 20.0 : 95.0,
297         /* isBackground= */ false,
298         /* background= */ (s) -> inverseSurface(),
299         /* secondBackground= */ null,
300         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
301         /* toneDeltaPair= */ null);
302   }
303 
304   @NonNull
outline()305   public DynamicColor outline() {
306     return new DynamicColor(
307         /* name= */ "outline",
308         /* palette= */ (s) -> s.neutralVariantPalette,
309         /* tone= */ (s) -> s.isDark ? 60.0 : 50.0,
310         /* isBackground= */ false,
311         /* background= */ this::highestSurface,
312         /* secondBackground= */ null,
313         /* contrastCurve= */ new ContrastCurve(1.5, 3.0, 4.5, 7.0),
314         /* toneDeltaPair= */ null);
315   }
316 
317   @NonNull
outlineVariant()318   public DynamicColor outlineVariant() {
319     return new DynamicColor(
320         /* name= */ "outline_variant",
321         /* palette= */ (s) -> s.neutralVariantPalette,
322         /* tone= */ (s) -> s.isDark ? 30.0 : 80.0,
323         /* isBackground= */ false,
324         /* background= */ this::highestSurface,
325         /* secondBackground= */ null,
326         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
327         /* toneDeltaPair= */ null);
328   }
329 
330   @NonNull
shadow()331   public DynamicColor shadow() {
332     return new DynamicColor(
333         /* name= */ "shadow",
334         /* palette= */ (s) -> s.neutralPalette,
335         /* tone= */ (s) -> 0.0,
336         /* isBackground= */ false,
337         /* background= */ null,
338         /* secondBackground= */ null,
339         /* contrastCurve= */ null,
340         /* toneDeltaPair= */ null);
341   }
342 
343   @NonNull
scrim()344   public DynamicColor scrim() {
345     return new DynamicColor(
346         /* name= */ "scrim",
347         /* palette= */ (s) -> s.neutralPalette,
348         /* tone= */ (s) -> 0.0,
349         /* isBackground= */ false,
350         /* background= */ null,
351         /* secondBackground= */ null,
352         /* contrastCurve= */ null,
353         /* toneDeltaPair= */ null);
354   }
355 
356   @NonNull
surfaceTint()357   public DynamicColor surfaceTint() {
358     return new DynamicColor(
359         /* name= */ "surface_tint",
360         /* palette= */ (s) -> s.primaryPalette,
361         /* tone= */ (s) -> s.isDark ? 80.0 : 40.0,
362         /* isBackground= */ true,
363         /* background= */ null,
364         /* secondBackground= */ null,
365         /* contrastCurve= */ null,
366         /* toneDeltaPair= */ null);
367   }
368 
369   @NonNull
primary()370   public DynamicColor primary() {
371     return new DynamicColor(
372         /* name= */ "primary",
373         /* palette= */ (s) -> s.primaryPalette,
374         /* tone= */ (s) -> {
375           if (isMonochrome(s)) {
376             return s.isDark ? 100.0 : 0.0;
377           }
378           return s.isDark ? 80.0 : 40.0;
379         },
380         /* isBackground= */ true,
381         /* background= */ this::highestSurface,
382         /* secondBackground= */ null,
383         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 7.0),
384         /* toneDeltaPair= */ (s) ->
385             new ToneDeltaPair(primaryContainer(), primary(), 10.0, TonePolarity.NEARER, false));
386   }
387 
388   @NonNull
389   public DynamicColor onPrimary() {
390     return new DynamicColor(
391         /* name= */ "on_primary",
392         /* palette= */ (s) -> s.primaryPalette,
393         /* tone= */ (s) -> {
394           if (isMonochrome(s)) {
395             return s.isDark ? 10.0 : 90.0;
396           }
397           return s.isDark ? 20.0 : 100.0;
398         },
399         /* isBackground= */ false,
400         /* background= */ (s) -> primary(),
401         /* secondBackground= */ null,
402         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
403         /* toneDeltaPair= */ null);
404   }
405 
406   @NonNull
407   public DynamicColor primaryContainer() {
408     return new DynamicColor(
409         /* name= */ "primary_container",
410         /* palette= */ (s) -> s.primaryPalette,
411         /* tone= */ (s) -> {
412           if (isFidelity(s)) {
413             return s.sourceColorHct.getTone();
414           }
415           if (isMonochrome(s)) {
416             return s.isDark ? 85.0 : 25.0;
417           }
418           return s.isDark ? 30.0 : 90.0;
419         },
420         /* isBackground= */ true,
421         /* background= */ this::highestSurface,
422         /* secondBackground= */ null,
423         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
424         /* toneDeltaPair= */ (s) ->
425             new ToneDeltaPair(primaryContainer(), primary(), 10.0, TonePolarity.NEARER, false));
426   }
427 
428   @NonNull
429   public DynamicColor onPrimaryContainer() {
430     return new DynamicColor(
431         /* name= */ "on_primary_container",
432         /* palette= */ (s) -> s.primaryPalette,
433         /* tone= */ (s) -> {
434           if (isFidelity(s)) {
435             return DynamicColor.foregroundTone(primaryContainer().tone.apply(s), 4.5);
436           }
437           if (isMonochrome(s)) {
438             return s.isDark ? 0.0 : 100.0;
439           }
440           return s.isDark ? 90.0 : 30.0;
441         },
442         /* isBackground= */ false,
443         /* background= */ (s) -> primaryContainer(),
444         /* secondBackground= */ null,
445         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
446         /* toneDeltaPair= */ null);
447   }
448 
449   @NonNull
450   public DynamicColor inversePrimary() {
451     return new DynamicColor(
452         /* name= */ "inverse_primary",
453         /* palette= */ (s) -> s.primaryPalette,
454         /* tone= */ (s) -> s.isDark ? 40.0 : 80.0,
455         /* isBackground= */ false,
456         /* background= */ (s) -> inverseSurface(),
457         /* secondBackground= */ null,
458         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 7.0),
459         /* toneDeltaPair= */ null);
460   }
461 
462   @NonNull
463   public DynamicColor secondary() {
464     return new DynamicColor(
465         /* name= */ "secondary",
466         /* palette= */ (s) -> s.secondaryPalette,
467         /* tone= */ (s) -> s.isDark ? 80.0 : 40.0,
468         /* isBackground= */ true,
469         /* background= */ this::highestSurface,
470         /* secondBackground= */ null,
471         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 7.0),
472         /* toneDeltaPair= */ (s) ->
473             new ToneDeltaPair(secondaryContainer(), secondary(), 10.0, TonePolarity.NEARER, false));
474   }
475 
476   @NonNull
477   public DynamicColor onSecondary() {
478     return new DynamicColor(
479         /* name= */ "on_secondary",
480         /* palette= */ (s) -> s.secondaryPalette,
481         /* tone= */ (s) -> {
482           if (isMonochrome(s)) {
483             return s.isDark ? 10.0 : 100.0;
484           } else {
485             return s.isDark ? 20.0 : 100.0;
486           }
487         },
488         /* isBackground= */ false,
489         /* background= */ (s) -> secondary(),
490         /* secondBackground= */ null,
491         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
492         /* toneDeltaPair= */ null);
493   }
494 
495   @NonNull
496   public DynamicColor secondaryContainer() {
497     return new DynamicColor(
498         /* name= */ "secondary_container",
499         /* palette= */ (s) -> s.secondaryPalette,
500         /* tone= */ (s) -> {
501           final double initialTone = s.isDark ? 30.0 : 90.0;
502           if (isMonochrome(s)) {
503             return s.isDark ? 30.0 : 85.0;
504           }
505           if (!isFidelity(s)) {
506             return initialTone;
507           }
508           return findDesiredChromaByTone(
509               s.secondaryPalette.getHue(), s.secondaryPalette.getChroma(), initialTone, !s.isDark);
510         },
511         /* isBackground= */ true,
512         /* background= */ this::highestSurface,
513         /* secondBackground= */ null,
514         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
515         /* toneDeltaPair= */ (s) ->
516             new ToneDeltaPair(secondaryContainer(), secondary(), 10.0, TonePolarity.NEARER, false));
517   }
518 
519   @NonNull
520   public DynamicColor onSecondaryContainer() {
521     return new DynamicColor(
522         /* name= */ "on_secondary_container",
523         /* palette= */ (s) -> s.secondaryPalette,
524         /* tone= */ (s) -> {
525           if (isMonochrome(s)) {
526             return s.isDark ? 90.0 : 10.0;
527           }
528           if (!isFidelity(s)) {
529             return s.isDark ? 90.0 : 30.0;
530           }
531           return DynamicColor.foregroundTone(secondaryContainer().tone.apply(s), 4.5);
532         },
533         /* isBackground= */ false,
534         /* background= */ (s) -> secondaryContainer(),
535         /* secondBackground= */ null,
536         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
537         /* toneDeltaPair= */ null);
538   }
539 
540   @NonNull
541   public DynamicColor tertiary() {
542     return new DynamicColor(
543         /* name= */ "tertiary",
544         /* palette= */ (s) -> s.tertiaryPalette,
545         /* tone= */ (s) -> {
546           if (isMonochrome(s)) {
547             return s.isDark ? 90.0 : 25.0;
548           }
549           return s.isDark ? 80.0 : 40.0;
550         },
551         /* isBackground= */ true,
552         /* background= */ this::highestSurface,
553         /* secondBackground= */ null,
554         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 7.0),
555         /* toneDeltaPair= */ (s) ->
556             new ToneDeltaPair(tertiaryContainer(), tertiary(), 10.0, TonePolarity.NEARER, false));
557   }
558 
559   @NonNull
560   public DynamicColor onTertiary() {
561     return new DynamicColor(
562         /* name= */ "on_tertiary",
563         /* palette= */ (s) -> s.tertiaryPalette,
564         /* tone= */ (s) -> {
565           if (isMonochrome(s)) {
566             return s.isDark ? 10.0 : 90.0;
567           }
568           return s.isDark ? 20.0 : 100.0;
569         },
570         /* isBackground= */ false,
571         /* background= */ (s) -> tertiary(),
572         /* secondBackground= */ null,
573         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
574         /* toneDeltaPair= */ null);
575   }
576 
577   @NonNull
578   public DynamicColor tertiaryContainer() {
579     return new DynamicColor(
580         /* name= */ "tertiary_container",
581         /* palette= */ (s) -> s.tertiaryPalette,
582         /* tone= */ (s) -> {
583           if (isMonochrome(s)) {
584             return s.isDark ? 60.0 : 49.0;
585           }
586           if (!isFidelity(s)) {
587             return s.isDark ? 30.0 : 90.0;
588           }
589           final Hct proposedHct = s.tertiaryPalette.getHct(s.sourceColorHct.getTone());
590           return DislikeAnalyzer.fixIfDisliked(proposedHct).getTone();
591         },
592         /* isBackground= */ true,
593         /* background= */ this::highestSurface,
594         /* secondBackground= */ null,
595         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
596         /* toneDeltaPair= */ (s) ->
597             new ToneDeltaPair(tertiaryContainer(), tertiary(), 10.0, TonePolarity.NEARER, false));
598   }
599 
600   @NonNull
601   public DynamicColor onTertiaryContainer() {
602     return new DynamicColor(
603         /* name= */ "on_tertiary_container",
604         /* palette= */ (s) -> s.tertiaryPalette,
605         /* tone= */ (s) -> {
606           if (isMonochrome(s)) {
607             return s.isDark ? 0.0 : 100.0;
608           }
609           if (!isFidelity(s)) {
610             return s.isDark ? 90.0 : 30.0;
611           }
612           return DynamicColor.foregroundTone(tertiaryContainer().tone.apply(s), 4.5);
613         },
614         /* isBackground= */ false,
615         /* background= */ (s) -> tertiaryContainer(),
616         /* secondBackground= */ null,
617         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
618         /* toneDeltaPair= */ null);
619   }
620 
621   @NonNull
622   public DynamicColor error() {
623     return new DynamicColor(
624         /* name= */ "error",
625         /* palette= */ (s) -> s.errorPalette,
626         /* tone= */ (s) -> s.isDark ? 80.0 : 40.0,
627         /* isBackground= */ true,
628         /* background= */ this::highestSurface,
629         /* secondBackground= */ null,
630         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 7.0),
631         /* toneDeltaPair= */ (s) ->
632             new ToneDeltaPair(errorContainer(), error(), 10.0, TonePolarity.NEARER, false));
633   }
634 
635   @NonNull
636   public DynamicColor onError() {
637     return new DynamicColor(
638         /* name= */ "on_error",
639         /* palette= */ (s) -> s.errorPalette,
640         /* tone= */ (s) -> s.isDark ? 20.0 : 100.0,
641         /* isBackground= */ false,
642         /* background= */ (s) -> error(),
643         /* secondBackground= */ null,
644         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
645         /* toneDeltaPair= */ null);
646   }
647 
648   @NonNull
649   public DynamicColor errorContainer() {
650     return new DynamicColor(
651         /* name= */ "error_container",
652         /* palette= */ (s) -> s.errorPalette,
653         /* tone= */ (s) -> s.isDark ? 30.0 : 90.0,
654         /* isBackground= */ true,
655         /* background= */ this::highestSurface,
656         /* secondBackground= */ null,
657         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
658         /* toneDeltaPair= */ (s) ->
659             new ToneDeltaPair(errorContainer(), error(), 10.0, TonePolarity.NEARER, false));
660   }
661 
662   @NonNull
663   public DynamicColor onErrorContainer() {
664     return new DynamicColor(
665         /* name= */ "on_error_container",
666         /* palette= */ (s) -> s.errorPalette,
667         /* tone= */ (s) -> {
668           if (isMonochrome(s)) {
669             return s.isDark ? 90.0 : 10.0;
670           }
671           return s.isDark ? 90.0 : 30.0;
672         },
673         /* isBackground= */ false,
674         /* background= */ (s) -> errorContainer(),
675         /* secondBackground= */ null,
676         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
677         /* toneDeltaPair= */ null);
678   }
679 
680   @NonNull
681   public DynamicColor primaryFixed() {
682     return new DynamicColor(
683         /* name= */ "primary_fixed",
684         /* palette= */ (s) -> s.primaryPalette,
685         /* tone= */ (s) -> isMonochrome(s) ? 40.0 : 90.0,
686         /* isBackground= */ true,
687         /* background= */ this::highestSurface,
688         /* secondBackground= */ null,
689         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
690         /* toneDeltaPair= */ (s) ->
691             new ToneDeltaPair(primaryFixed(), primaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
692   }
693 
694   @NonNull
695   public DynamicColor primaryFixedDim() {
696     return new DynamicColor(
697         /* name= */ "primary_fixed_dim",
698         /* palette= */ (s) -> s.primaryPalette,
699         /* tone= */ (s) -> isMonochrome(s) ? 30.0 : 80.0,
700         /* isBackground= */ true,
701         /* background= */ this::highestSurface,
702         /* secondBackground= */ null,
703         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
704         /* toneDeltaPair= */ (s) ->
705             new ToneDeltaPair(primaryFixed(), primaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
706   }
707 
708   @NonNull
709   public DynamicColor onPrimaryFixed() {
710     return new DynamicColor(
711         /* name= */ "on_primary_fixed",
712         /* palette= */ (s) -> s.primaryPalette,
713         /* tone= */ (s) -> isMonochrome(s) ? 100.0 : 10.0,
714         /* isBackground= */ false,
715         /* background= */ (s) -> primaryFixedDim(),
716         /* secondBackground= */ (s) -> primaryFixed(),
717         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
718         /* toneDeltaPair= */ null);
719   }
720 
721   @NonNull
722   public DynamicColor onPrimaryFixedVariant() {
723     return new DynamicColor(
724         /* name= */ "on_primary_fixed_variant",
725         /* palette= */ (s) -> s.primaryPalette,
726         /* tone= */ (s) -> isMonochrome(s) ? 90.0 : 30.0,
727         /* isBackground= */ false,
728         /* background= */ (s) -> primaryFixedDim(),
729         /* secondBackground= */ (s) -> primaryFixed(),
730         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
731         /* toneDeltaPair= */ null);
732   }
733 
734   @NonNull
735   public DynamicColor secondaryFixed() {
736     return new DynamicColor(
737         /* name= */ "secondary_fixed",
738         /* palette= */ (s) -> s.secondaryPalette,
739         /* tone= */ (s) -> isMonochrome(s) ? 80.0 : 90.0,
740         /* isBackground= */ true,
741         /* background= */ this::highestSurface,
742         /* secondBackground= */ null,
743         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
744         /* toneDeltaPair= */ (s) ->
745             new ToneDeltaPair(
746                 secondaryFixed(), secondaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
747   }
748 
749   @NonNull
750   public DynamicColor secondaryFixedDim() {
751     return new DynamicColor(
752         /* name= */ "secondary_fixed_dim",
753         /* palette= */ (s) -> s.secondaryPalette,
754         /* tone= */ (s) -> isMonochrome(s) ? 70.0 : 80.0,
755         /* isBackground= */ true,
756         /* background= */ this::highestSurface,
757         /* secondBackground= */ null,
758         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
759         /* toneDeltaPair= */ (s) ->
760             new ToneDeltaPair(
761                 secondaryFixed(), secondaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
762   }
763 
764   @NonNull
765   public DynamicColor onSecondaryFixed() {
766     return new DynamicColor(
767         /* name= */ "on_secondary_fixed",
768         /* palette= */ (s) -> s.secondaryPalette,
769         /* tone= */ (s) -> 10.0,
770         /* isBackground= */ false,
771         /* background= */ (s) -> secondaryFixedDim(),
772         /* secondBackground= */ (s) -> secondaryFixed(),
773         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
774         /* toneDeltaPair= */ null);
775   }
776 
777   @NonNull
778   public DynamicColor onSecondaryFixedVariant() {
779     return new DynamicColor(
780         /* name= */ "on_secondary_fixed_variant",
781         /* palette= */ (s) -> s.secondaryPalette,
782         /* tone= */ (s) -> isMonochrome(s) ? 25.0 : 30.0,
783         /* isBackground= */ false,
784         /* background= */ (s) -> secondaryFixedDim(),
785         /* secondBackground= */ (s) -> secondaryFixed(),
786         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
787         /* toneDeltaPair= */ null);
788   }
789 
790   @NonNull
791   public DynamicColor tertiaryFixed() {
792     return new DynamicColor(
793         /* name= */ "tertiary_fixed",
794         /* palette= */ (s) -> s.tertiaryPalette,
795         /* tone= */ (s) -> isMonochrome(s) ? 40.0 : 90.0,
796         /* isBackground= */ true,
797         /* background= */ this::highestSurface,
798         /* secondBackground= */ null,
799         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
800         /* toneDeltaPair= */ (s) ->
801             new ToneDeltaPair(
802                 tertiaryFixed(), tertiaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
803   }
804 
805   @NonNull
806   public DynamicColor tertiaryFixedDim() {
807     return new DynamicColor(
808         /* name= */ "tertiary_fixed_dim",
809         /* palette= */ (s) -> s.tertiaryPalette,
810         /* tone= */ (s) -> isMonochrome(s) ? 30.0 : 80.0,
811         /* isBackground= */ true,
812         /* background= */ this::highestSurface,
813         /* secondBackground= */ null,
814         /* contrastCurve= */ new ContrastCurve(1.0, 1.0, 3.0, 4.5),
815         /* toneDeltaPair= */ (s) ->
816             new ToneDeltaPair(
817                 tertiaryFixed(), tertiaryFixedDim(), 10.0, TonePolarity.LIGHTER, true));
818   }
819 
820   @NonNull
821   public DynamicColor onTertiaryFixed() {
822     return new DynamicColor(
823         /* name= */ "on_tertiary_fixed",
824         /* palette= */ (s) -> s.tertiaryPalette,
825         /* tone= */ (s) -> isMonochrome(s) ? 100.0 : 10.0,
826         /* isBackground= */ false,
827         /* background= */ (s) -> tertiaryFixedDim(),
828         /* secondBackground= */ (s) -> tertiaryFixed(),
829         /* contrastCurve= */ new ContrastCurve(4.5, 7.0, 11.0, 21.0),
830         /* toneDeltaPair= */ null);
831   }
832 
833   @NonNull
834   public DynamicColor onTertiaryFixedVariant() {
835     return new DynamicColor(
836         /* name= */ "on_tertiary_fixed_variant",
837         /* palette= */ (s) -> s.tertiaryPalette,
838         /* tone= */ (s) -> isMonochrome(s) ? 90.0 : 30.0,
839         /* isBackground= */ false,
840         /* background= */ (s) -> tertiaryFixedDim(),
841         /* secondBackground= */ (s) -> tertiaryFixed(),
842         /* contrastCurve= */ new ContrastCurve(3.0, 4.5, 7.0, 11.0),
843         /* toneDeltaPair= */ null);
844   }
845 
846   /**
847    * These colors were present in Android framework before Android U, and used by MDC controls. They
848    * should be avoided, if possible. It's unclear if they're used on multiple backgrounds, and if
849    * they are, they can't be adjusted for contrast.* For now, they will be set with no background,
850    * and those won't adjust for contrast, avoiding issues.
851    *
852    * <p>* For example, if the same color is on a white background _and_ black background, there's no
853    * way to increase contrast with either without losing contrast with the other.
854    */
855   // colorControlActivated documented as colorAccent in M3 & GM3.
856   // colorAccent documented as colorSecondary in M3 and colorPrimary in GM3.
857   // Android used Material's Container as Primary/Secondary/Tertiary at launch.
858   // Therefore, this is a duplicated version of Primary Container.
859   @NonNull
860   public DynamicColor controlActivated() {
861     return DynamicColor.fromPalette(
862         "control_activated", (s) -> s.primaryPalette, (s) -> s.isDark ? 30.0 : 90.0);
863   }
864 
865   // colorControlNormal documented as textColorSecondary in M3 & GM3.
866   // In Material, textColorSecondary points to onSurfaceVariant in the non-disabled state,
867   // which is Neutral Variant T30/80 in light/dark.
868   @NonNull
869   public DynamicColor controlNormal() {
870     return DynamicColor.fromPalette(
871         "control_normal", (s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0);
872   }
873 
874   // colorControlHighlight documented, in both M3 & GM3:
875   // Light mode: #1f000000 dark mode: #33ffffff.
876   // These are black and white with some alpha.
877   // 1F hex = 31 decimal; 31 / 255 = 12% alpha.
878   // 33 hex = 51 decimal; 51 / 255 = 20% alpha.
879   // DynamicColors do not support alpha currently, and _may_ not need it for this use case,
880   // depending on how MDC resolved alpha for the other cases.
881   // Returning black in dark mode, white in light mode.
882   @NonNull
883   public DynamicColor controlHighlight() {
884     return new DynamicColor(
885         /* name= */ "control_highlight",
886         /* palette= */ (s) -> s.neutralPalette,
887         /* tone= */ (s) -> s.isDark ? 100.0 : 0.0,
888         /* isBackground= */ false,
889         /* background= */ null,
890         /* secondBackground= */ null,
891         /* contrastCurve= */ null,
892         /* toneDeltaPair= */ null,
893         /* opacity= */ s -> s.isDark ? 0.20 : 0.12);
894   }
895 
896   // textColorPrimaryInverse documented, in both M3 & GM3, documented as N10/N90.
897   @NonNull
898   public DynamicColor textPrimaryInverse() {
899     return DynamicColor.fromPalette(
900         "text_primary_inverse", (s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
901   }
902 
903   // textColorSecondaryInverse and textColorTertiaryInverse both documented, in both M3 & GM3, as
904   // NV30/NV80
905   @NonNull
906   public DynamicColor textSecondaryAndTertiaryInverse() {
907     return DynamicColor.fromPalette(
908         "text_secondary_and_tertiary_inverse",
909         (s) -> s.neutralVariantPalette,
910         (s) -> s.isDark ? 30.0 : 80.0);
911   }
912 
913   // textColorPrimaryInverseDisableOnly documented, in both M3 & GM3, as N10/N90
914   @NonNull
915   public DynamicColor textPrimaryInverseDisableOnly() {
916     return DynamicColor.fromPalette(
917         "text_primary_inverse_disable_only",
918         (s) -> s.neutralPalette,
919         (s) -> s.isDark ? 10.0 : 90.0);
920   }
921 
922   // textColorSecondaryInverse and textColorTertiaryInverse in disabled state both documented,
923   // in both M3 & GM3, as N10/N90
924   @NonNull
925   public DynamicColor textSecondaryAndTertiaryInverseDisabled() {
926     return DynamicColor.fromPalette(
927         "text_secondary_and_tertiary_inverse_disabled",
928         (s) -> s.neutralPalette,
929         (s) -> s.isDark ? 10.0 : 90.0);
930   }
931 
932   // textColorHintInverse documented, in both M3 & GM3, as N10/N90
933   @NonNull
934   public DynamicColor textHintInverse() {
935     return DynamicColor.fromPalette(
936         "text_hint_inverse", (s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
937   }
938 
939   /** All dynamic colors in Material Design system. */
940   public final List<Supplier<DynamicColor>> allDynamicColors() {
941     return Arrays.asList(
942         this::primaryPaletteKeyColor,
943         this::secondaryPaletteKeyColor,
944         this::tertiaryPaletteKeyColor,
945         this::neutralPaletteKeyColor,
946         this::neutralVariantPaletteKeyColor,
947         this::background,
948         this::onBackground,
949         this::surface,
950         this::surfaceDim,
951         this::surfaceBright,
952         this::surfaceContainerLowest,
953         this::surfaceContainerLow,
954         this::surfaceContainer,
955         this::surfaceContainerHigh,
956         this::surfaceContainerHighest,
957         this::onSurface,
958         this::surfaceVariant,
959         this::onSurfaceVariant,
960         this::inverseSurface,
961         this::inverseOnSurface,
962         this::outline,
963         this::outlineVariant,
964         this::shadow,
965         this::scrim,
966         this::surfaceTint,
967         this::primary,
968         this::onPrimary,
969         this::primaryContainer,
970         this::onPrimaryContainer,
971         this::inversePrimary,
972         this::secondary,
973         this::onSecondary,
974         this::secondaryContainer,
975         this::onSecondaryContainer,
976         this::tertiary,
977         this::onTertiary,
978         this::tertiaryContainer,
979         this::onTertiaryContainer,
980         this::error,
981         this::onError,
982         this::errorContainer,
983         this::onErrorContainer,
984         this::primaryFixed,
985         this::primaryFixedDim,
986         this::onPrimaryFixed,
987         this::onPrimaryFixedVariant,
988         this::secondaryFixed,
989         this::secondaryFixedDim,
990         this::onSecondaryFixed,
991         this::onSecondaryFixedVariant,
992         this::tertiaryFixed,
993         this::tertiaryFixedDim,
994         this::onTertiaryFixed,
995         this::onTertiaryFixedVariant,
996         this::controlActivated,
997         this::controlNormal,
998         this::controlHighlight,
999         this::textPrimaryInverse,
1000         this::textSecondaryAndTertiaryInverse,
1001         this::textPrimaryInverseDisableOnly,
1002         this::textSecondaryAndTertiaryInverseDisabled,
1003         this::textHintInverse);
1004   }
1005 
1006   private boolean isFidelity(DynamicScheme scheme) {
1007     if (this.isExtendedFidelity
1008         && scheme.variant != Variant.MONOCHROME
1009         && scheme.variant != Variant.NEUTRAL) {
1010       return true;
1011     }
1012     return scheme.variant == Variant.FIDELITY || scheme.variant == Variant.CONTENT;
1013   }
1014 
1015   private static boolean isMonochrome(DynamicScheme scheme) {
1016     return scheme.variant == Variant.MONOCHROME;
1017   }
1018 
1019   static double findDesiredChromaByTone(
1020       double hue, double chroma, double tone, boolean byDecreasingTone) {
1021     double answer = tone;
1022 
1023     Hct closestToChroma = Hct.from(hue, chroma, tone);
1024     if (closestToChroma.getChroma() < chroma) {
1025       double chromaPeak = closestToChroma.getChroma();
1026       while (closestToChroma.getChroma() < chroma) {
1027         answer += byDecreasingTone ? -1.0 : 1.0;
1028         Hct potentialSolution = Hct.from(hue, chroma, answer);
1029         if (chromaPeak > potentialSolution.getChroma()) {
1030           break;
1031         }
1032         if (Math.abs(potentialSolution.getChroma() - chroma) < 0.4) {
1033           break;
1034         }
1035 
1036         double potentialDelta = Math.abs(potentialSolution.getChroma() - chroma);
1037         double currentDelta = Math.abs(closestToChroma.getChroma() - chroma);
1038         if (potentialDelta < currentDelta) {
1039           closestToChroma = potentialSolution;
1040         }
1041         chromaPeak = Math.max(chromaPeak, potentialSolution.getChroma());
1042       }
1043     }
1044 
1045     return answer;
1046   }
1047 }
1048