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