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