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