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