Skip to content

Commit

Permalink
[M3][Color] Internal color updates
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 501629282
  • Loading branch information
Material Design Team authored and hunterstich committed Jan 12, 2023
1 parent bf188c7 commit b892849
Show file tree
Hide file tree
Showing 12 changed files with 709 additions and 28 deletions.
6 changes: 3 additions & 3 deletions lib/java/com/google/android/material/color/DynamicColors.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import androidx.annotation.StyleRes;
import androidx.core.os.BuildCompat;
import com.google.android.material.color.utilities.Hct;
import com.google.android.material.color.utilities.SchemeTonalSpot;
import com.google.android.material.color.utilities.SchemeContent;
import com.google.android.material.resources.MaterialAttributes;
import java.lang.reflect.Method;
import java.util.Collections;
Expand Down Expand Up @@ -294,8 +294,8 @@ public static void applyToActivityIfAvailable(
if (dynamicColorsOptions.getPrecondition().shouldApplyDynamicColors(activity, theme)) {
// Applies content-based dynamic colors if content-based source is provided.
if (dynamicColorsOptions.getContentBasedSeedColor() != null) {
SchemeTonalSpot scheme =
new SchemeTonalSpot(
SchemeContent scheme =
new SchemeContent(
Hct.fromInt(dynamicColorsOptions.getContentBasedSeedColor()),
!MaterialAttributes.resolveBoolean(
activity, R.attr.isLightTheme, /* defaultValue= */ true),
Expand Down
22 changes: 21 additions & 1 deletion lib/java/com/google/android/material/color/utilities/Cam16.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public final class Cam16 {
private final double astar;
private final double bstar;

// Avoid allocations during conversion by pre-allocating an array.
private final double[] tempArray = new double[] {0.0, 0.0, 0.0};

/**
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
Expand Down Expand Up @@ -211,6 +214,11 @@ static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingCondi
double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;

return fromXyzInViewingConditions(x, y, z, viewingConditions);
}

static Cam16 fromXyzInViewingConditions(
double x, double y, double z, ViewingConditions viewingConditions) {
// Transform XYZ to 'cone'/'rgb' responses
double[][] matrix = XYZ_TO_CAM16RGB;
double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
Expand Down Expand Up @@ -375,6 +383,11 @@ public int toInt() {
* @return ARGB representation of color
*/
int viewed(ViewingConditions viewingConditions) {
double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
}

double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
double alpha =
(getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);

Expand Down Expand Up @@ -418,6 +431,13 @@ int viewed(ViewingConditions viewingConditions) {
double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);

return ColorUtils.argbFromXyz(x, y, z);
if (returnArray != null) {
returnArray[0] = x;
returnArray[1] = y;
returnArray[2] = z;
return returnArray;
} else {
return new double[] {x, y, z};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.material.color.utilities;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import androidx.annotation.RestrictTo;

/**
* Check and/or fix universally disliked colors.
*
* <p>Color science studies of color preference indicate universal distaste for dark yellow-greens,
* and also show this is correlated to distate for biological waste and rotting food.
*
* <p>See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook of Color
* Psychology (2015).
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public final class DislikeAnalyzer {

private DislikeAnalyzer() {
throw new UnsupportedOperationException();
}

/**
* Returns true if color is disliked.
*
* <p>Disliked is defined as a dark yellow-green that is not neutral.
*/
public static boolean isDisliked(Hct hct) {
final boolean huePasses = Math.round(hct.getHue()) >= 90.0 && Math.round(hct.getHue()) <= 111.0;
final boolean chromaPasses = Math.round(hct.getChroma()) > 16.0;
final boolean tonePasses = Math.round(hct.getTone()) < 70.0;

return huePasses && chromaPasses && tonePasses;
}

/** If color is disliked, lighten it to make it likable. */
public static Hct fixIfDisliked(Hct hct) {
if (isDisliked(hct)) {
return Hct.from(hct.getHue(), hct.getChroma(), 70.0);
}

return hct;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ public Hct getHct(DynamicScheme scheme) {
return answer;
}

double getTone(DynamicScheme scheme) {
/** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */
public double getTone(DynamicScheme scheme) {
double answer = tone.apply(scheme);

final boolean decreasingContrast = scheme.contrastLevel < 0.0;
Expand Down Expand Up @@ -527,7 +528,7 @@ static double ensureToneDelta(
* Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
* that is as close to ratio as possible.
*/
static double contrastingTone(double bgTone, double ratio) {
public static double contrastingTone(double bgTone, double ratio) {
final double lighterTone = Contrast.lighterUnsafe(bgTone, ratio);
final double darkerTone = Contrast.darkerUnsafe(bgTone, ratio);
final double lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone);
Expand Down Expand Up @@ -558,7 +559,7 @@ static double contrastingTone(double bgTone, double ratio) {
* Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
* supporting it.
*/
static double enableLightForeground(double tone) {
public static double enableLightForeground(double tone) {
if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
return 49.0;
}
Expand All @@ -572,12 +573,12 @@ static double enableLightForeground(double tone) {
* <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in order
* to ensure light foregrounds.
*/
static boolean tonePrefersLightForeground(double tone) {
public static boolean tonePrefersLightForeground(double tone) {
return Math.round(tone) <= 60;
}

/** Tones less than ~T50 always permit white at 4.5 contrast. */
static boolean toneAllowsLightForeground(double tone) {
public static boolean toneAllowsLightForeground(double tone) {
return Math.round(tone) <= 49;
}
}
30 changes: 30 additions & 0 deletions lib/java/com/google/android/material/color/utilities/Hct.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,36 @@ public void setTone(double newTone) {
setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
}

/**
* Translate a color into different ViewingConditions.
*
* <p>Colors change appearance. They look different with lights on versus off, the same color, as
* in hex code, on white looks different when on black. This is called color relativity, most
* famously explicated by Josef Albers in Interaction of Color.
*
* <p>In color science, color appearance models can account for this and calculate the appearance
* of a color in different settings. HCT is based on CAM16, a color appearance model, and uses it
* to make these calculations.
*
* <p>See ViewingConditions.make for parameters affecting color appearance.
*/
public Hct inViewingConditions(ViewingConditions vc) {
// 1. Use CAM16 to find XYZ coordinates of color in specified VC.
Cam16 cam16 = Cam16.fromInt(toInt());
double[] viewedInVc = cam16.xyzInViewingConditions(vc, null);

// 2. Create CAM16 of those XYZ coordinates in default VC.
Cam16 recastInVc =
Cam16.fromXyzInViewingConditions(
viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT);

// 3. Create HCT from:
// - CAM16 using default VC with XYZ coordinates in specified VC.
// - L* converted from Y in XYZ coordinates in specified VC.
return Hct.from(
recastInVc.getHue(), recastInVc.getChroma(), ColorUtils.lstarFromY(viewedInVc[1]));
}

private void setInternalState(int argb) {
this.argb = argb;
Cam16 cam = Cam16.fromInt(argb);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,26 @@ public static DynamicColor highestSurface(DynamicScheme s) {

public static final DynamicColor primaryContainer =
DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.isDark ? 30.0 : 90.0, (s) -> highestSurface(s));
(s) -> s.primaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 30.0 : 90.0;
}
return performAlbers(s.sourceColorHct, s);
},
(s) -> highestSurface(s));

public static final DynamicColor onPrimaryContainer =
DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> primaryContainer, null);
(s) -> s.primaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 90.0 : 10.0;
}
return DynamicColor.contrastingTone(primaryContainer.getTone(s), 4.5);
},
(s) -> primaryContainer,
null);

public static final DynamicColor primary =
DynamicColor.fromPalette(
Expand All @@ -121,11 +136,33 @@ public static DynamicColor highestSurface(DynamicScheme s) {

public static final DynamicColor secondaryContainer =
DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> s.isDark ? 30.0 : 90.0, (s) -> highestSurface(s));
(s) -> s.secondaryPalette,
(s) -> {
final double initialTone = s.isDark ? 30.0 : 90.0;
if (!isFidelity(s)) {
return initialTone;
}
double answer =
findDesiredChromaByTone(
s.secondaryPalette.getHue(),
s.secondaryPalette.getChroma(),
initialTone,
!s.isDark);
answer = performAlbers(s.secondaryPalette.getHct(answer), s);
return answer;
},
(s) -> highestSurface(s));

public static final DynamicColor onSecondaryContainer =
DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> secondaryContainer);
(s) -> s.secondaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 90.0 : 10.0;
}
return DynamicColor.contrastingTone(secondaryContainer.getTone(s), 4.5);
},
(s) -> secondaryContainer);

public static final DynamicColor secondary =
DynamicColor.fromPalette(
Expand All @@ -144,11 +181,28 @@ public static DynamicColor highestSurface(DynamicScheme s) {

public static final DynamicColor tertiaryContainer =
DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> s.isDark ? 30.0 : 90.0, (s) -> highestSurface(s));
(s) -> s.tertiaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 30.0 : 90.0;
}
final double albersTone =
performAlbers(s.tertiaryPalette.getHct(s.sourceColorHct.getTone()), s);
final Hct proposedHct = s.tertiaryPalette.getHct(albersTone);
return DislikeAnalyzer.fixIfDisliked(proposedHct).getTone();
},
(s) -> highestSurface(s));

public static final DynamicColor onTertiaryContainer =
DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> tertiaryContainer);
(s) -> s.tertiaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 90.0 : 10.0;
}
return DynamicColor.contrastingTone(tertiaryContainer.getTone(s), 4.5);
},
(s) -> tertiaryContainer);

public static final DynamicColor tertiary =
DynamicColor.fromPalette(
Expand Down Expand Up @@ -288,4 +342,51 @@ public static DynamicColor highestSurface(DynamicScheme s) {
// textColorHintInverse documented, in M3, as N10/N90
public static final DynamicColor textHintInverse =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);

private static ViewingConditions viewingConditionsForAlbers(DynamicScheme scheme) {
return ViewingConditions.defaultWithBackgroundLstar(scheme.isDark ? 30.0 : 80.0);
}

private static boolean isFidelity(DynamicScheme scheme) {
return scheme.variant == Variant.FIDELITY || scheme.variant == Variant.CONTENT;
}

static double findDesiredChromaByTone(
double hue, double chroma, double tone, boolean byDecreasingTone) {
double answer = tone;

Hct closestToChroma = Hct.from(hue, chroma, tone);
if (closestToChroma.getChroma() < chroma) {
double chromaPeak = closestToChroma.getChroma();
while (closestToChroma.getChroma() < chroma) {
answer += byDecreasingTone ? -1.0 : 1.0;
Hct potentialSolution = Hct.from(hue, chroma, answer);
if (chromaPeak > potentialSolution.getChroma()) {
break;
}
if (Math.abs(potentialSolution.getChroma() - chroma) < 0.4) {
break;
}

double potentialDelta = Math.abs(potentialSolution.getChroma() - chroma);
double currentDelta = Math.abs(closestToChroma.getChroma() - chroma);
if (potentialDelta < currentDelta) {
closestToChroma = potentialSolution;
}
chromaPeak = Math.max(chromaPeak, potentialSolution.getChroma());
}
}

return answer;
}

static double performAlbers(Hct prealbers, DynamicScheme scheme) {
final Hct albersd = prealbers.inViewingConditions(viewingConditionsForAlbers(scheme));
if (DynamicColor.tonePrefersLightForeground(prealbers.getTone())
&& !DynamicColor.toneAllowsLightForeground(albersd.getTone())) {
return DynamicColor.enableLightForeground(prealbers.getTone());
} else {
return DynamicColor.enableLightForeground(albersd.getTone());
}
}
}

0 comments on commit b892849

Please sign in to comment.