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 }