Import libmonet update from internal repo

Bug: 266731167
Test: atest com.android.systemui.monet, 380 pass.
Original test count was 13. Library has 90%+ coverage overall and is
maintaining 90%+ coverage as CLs are added, per stats from internal
source repository.
Change-Id: I922b352facb5da89d77583602381291ac571842a
This commit is contained in:
James O'Leary
2023-01-29 12:43:33 -05:00
parent 741394ab6c
commit bf07f0cc8e
62 changed files with 10150 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
Subpackages, i.e. folders, in this directory have their source of truth in the internal Google
source code repository.
The code is manually copied because:
- Alternatives are automatically syncing Material Color Utilities from the internal Google source
repository, or GitHub.
- Having Android's core automatically taking updates from code that changes regularly, and can
easily affect design, is undesirable.
To update the code:
1. Copy Material Color Utilities (libmonet internally) from the internal Google source repository to
this directory.
2. Replace package definitions. Use Find and Replace in directory, replace
com.google.ux.material.libmonet with com.android.systemui.monet.
3. Remove @CanIgnoreReturnValue and @CheckReturnValue, both usage and imports. Currently only in
Scheme.java. Chose removal because only Android /external libraries depend on the library with
these annotations.
4. Copy tests for Material Color Utilities to
frameworks/base/packages/SystemUI/tests/src/com/android/systemui/monet/
5. Remove HctRoundTripTest (~3 minutes to run) and QuantizerCelebiTest (depends on internal source
repository code for handling images)
6. Reformat regular and test code to Android bracing/spacing style.
In Android Studio, right click on the package, and choose Reformat Code.
7. Change tests to subclass SysuiTestCase and use an annotation such as @SmallTest / @MediumTest.

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.blend;
import com.android.systemui.monet.hct.Cam16;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.utils.ColorUtils;
import com.android.systemui.monet.utils.MathUtils;
/** Functions for blending in HCT and CAM16. */
public class Blend {
private Blend() {
}
/**
* Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the
* original color recognizable and recognizably shifted towards the key color.
*
* @param designColor ARGB representation of an arbitrary color.
* @param sourceColor ARGB representation of the main theme color.
* @return The design color with a hue shifted towards the system's color, a slightly
* warmer/cooler variant of the design color's hue.
*/
public static int harmonize(int designColor, int sourceColor) {
Hct fromHct = Hct.fromInt(designColor);
Hct toHct = Hct.fromInt(sourceColor);
double differenceDegrees = MathUtils.differenceDegrees(fromHct.getHue(), toHct.getHue());
double rotationDegrees = Math.min(differenceDegrees * 0.5, 15.0);
double outputHue =
MathUtils.sanitizeDegreesDouble(
fromHct.getHue()
+ rotationDegrees * MathUtils.rotationDirection(fromHct.getHue(),
toHct.getHue()));
return Hct.from(outputHue, fromHct.getChroma(), fromHct.getTone()).toInt();
}
/**
* Blends hue from one color into another. The chroma and tone of the original color are
* maintained.
*
* @param from ARGB representation of color
* @param to ARGB representation of color
* @param amount how much blending to perform; 0.0 >= and <= 1.0
* @return from, with a hue blended towards to. Chroma and tone are constant.
*/
public static int hctHue(int from, int to, double amount) {
int ucs = cam16Ucs(from, to, amount);
Cam16 ucsCam = Cam16.fromInt(ucs);
Cam16 fromCam = Cam16.fromInt(from);
Hct blended = Hct.from(ucsCam.getHue(), fromCam.getChroma(),
ColorUtils.lstarFromArgb(from));
return blended.toInt();
}
/**
* Blend in CAM16-UCS space.
*
* @param from ARGB representation of color
* @param to ARGB representation of color
* @param amount how much blending to perform; 0.0 >= and <= 1.0
* @return from, blended towards to. Hue, chroma, and tone will change.
*/
public static int cam16Ucs(int from, int to, double amount) {
Cam16 fromCam = Cam16.fromInt(from);
Cam16 toCam = Cam16.fromInt(to);
double fromJ = fromCam.getJstar();
double fromA = fromCam.getAstar();
double fromB = fromCam.getBstar();
double toJ = toCam.getJstar();
double toA = toCam.getAstar();
double toB = toCam.getBstar();
double jstar = fromJ + (toJ - fromJ) * amount;
double astar = fromA + (toA - fromA) * amount;
double bstar = fromB + (toB - fromB) * amount;
return Cam16.fromUcs(jstar, astar, bstar).toInt();
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.contrast;
import static java.lang.Math.max;
import com.android.systemui.monet.utils.ColorUtils;
/**
* Color science for contrast utilities.
*
* <p>Utility methods for calculating contrast given two colors, or calculating a color given one
* color and a contrast ratio.
*
* <p>Contrast ratio is calculated using XYZ's Y. When linearized to match human perception, Y
* becomes HCT's tone and L*a*b*'s' L*.
*/
public final class Contrast {
// The minimum contrast ratio of two colors.
// Contrast ratio equation = lighter + 5 / darker + 5, if lighter == darker, ratio == 1.
public static final double RATIO_MIN = 1.0;
// The maximum contrast ratio of two colors.
// Contrast ratio equation = lighter + 5 / darker + 5. Lighter and darker scale from 0 to 100.
// If lighter == 100, darker = 0, ratio == 21.
public static final double RATIO_MAX = 21.0;
public static final double RATIO_30 = 3.0;
public static final double RATIO_45 = 4.5;
public static final double RATIO_70 = 7.0;
// Given a color and a contrast ratio to reach, the luminance of a color that reaches that ratio
// with the color can be calculated. However, that luminance may not contrast as desired, i.e.
// the contrast ratio of the input color and the returned luminance may not reach the contrast
// ratio asked for.
//
// When the desired contrast ratio and the result contrast ratio differ by more than this
// amount, an error value should be returned, or the method should be documented as 'unsafe',
// meaning, it will return a valid luminance but that luminance may not meet the requested
// contrast ratio.
//
// 0.04 selected because it ensures the resulting ratio rounds to the same tenth.
private static final double CONTRAST_RATIO_EPSILON = 0.04;
// Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*, or T in HCT, are
// known as perceptually accurate color spaces.
//
// To be displayed, they must gamut map to a "display space", one that has a defined limit on
// the number of colors. Display spaces include sRGB, more commonly understood as RGB/HSL/HSV.
//
// Gamut mapping is undefined and not defined by the color space. Any gamut mapping algorithm
// must choose how to sacrifice accuracy in hue, saturation, and/or lightness.
//
// A principled solution is to maintain lightness, thus maintaining contrast/a11y, maintain hue,
// thus maintaining aesthetic intent, and reduce chroma until the color is in gamut.
//
// HCT chooses this solution, but, that doesn't mean it will _exactly_ matched desired
// lightness, if only because RGB is quantized: RGB is expressed as a set of integers: there may
// be an RGB color with, for example, 47.892 lightness, but not 47.891.
//
// To allow for this inherent incompatibility between perceptually accurate color spaces and
// display color spaces, methods that take a contrast ratio and luminance, and return a
// luminance that reaches that contrast ratio for the input luminance, purposefully
// darken/lighten their result such that the desired contrast ratio will be reached even if
// inaccuracy is introduced.
//
// 0.4 is generous, ex. HCT requires much less delta. It was chosen because it provides a rough
// guarantee that as long as a percetual color space gamut maps lightness such that the
// resulting lightness rounds to the same as the requested, the desired contrast ratio will be
// reached.
private static final double LUMINANCE_GAMUT_MAP_TOLERANCE = 0.4;
private Contrast() {
}
/**
* Contrast ratio is a measure of legibility, its used to compare the lightness of two colors.
* This method is used commonly in industry due to its use by WCAG.
*
* <p>To compare lightness, the colors are expressed in the XYZ color space, where Y is
* lightness,
* also known as relative luminance.
*
* <p>The equation is ratio = lighter Y + 5 / darker Y + 5.
*/
public static double ratioOfYs(double y1, double y2) {
final double lighter = max(y1, y2);
final double darker = (lighter == y2) ? y1 : y2;
return (lighter + 5.0) / (darker + 5.0);
}
/**
* Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perpectual
* luminance.
*
* <p>Contrast ratio is defined using Y in XYZ, relative luminance. However, relative luminance
* is linear to number of photons, not to perception of lightness. Perceptual luminance, L* in
* L*a*b*, T in HCT, is. Designers prefer color spaces with perceptual luminance since they're
* accurate to the eye.
*
* <p>Y and L* are pure functions of each other, so it possible to use perceptually accurate
* color spaces, and measure contrast, and measure contrast in a much more understandable way:
* instead of a ratio, a linear difference. This allows a designer to determine what they need
* to adjust a color's lightness to in order to reach their desired contrast, instead of
* guessing & checking with hex codes.
*/
public static double ratioOfTones(double t1, double t2) {
return ratioOfYs(ColorUtils.yFromLstar(t1), ColorUtils.yFromLstar(t2));
}
/**
* Returns T in HCT, L* in L*a*b* >= tone parameter that ensures ratio with input T/L*. Returns
* -1 if ratio cannot be achieved.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
*/
public static double lighter(double tone, double ratio) {
if (tone < 0.0 || tone > 100.0) {
return -1.0;
}
// Invert the contrast ratio equation to determine lighter Y given a ratio and darker Y.
final double darkY = ColorUtils.yFromLstar(tone);
final double lightY = ratio * (darkY + 5.0) - 5.0;
if (lightY < 0.0 || lightY > 100.0) {
return -1.0;
}
final double realContrast = ratioOfYs(lightY, darkY);
final double delta = Math.abs(realContrast - ratio);
if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
return -1.0;
}
final double returnValue = ColorUtils.lstarFromY(lightY) + LUMINANCE_GAMUT_MAP_TOLERANCE;
// NOMUTANTS--important validation step; functions it is calling may change implementation.
if (returnValue < 0 || returnValue > 100) {
return -1.0;
}
return returnValue;
}
/**
* Tone >= tone parameter that ensures ratio. 100 if ratio cannot be achieved.
*
* <p>This method is unsafe because the returned value is guaranteed to be in bounds, but, the
* in bounds return value may not reach the desired ratio.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
*/
public static double lighterUnsafe(double tone, double ratio) {
double lighterSafe = lighter(tone, ratio);
return lighterSafe < 0.0 ? 100.0 : lighterSafe;
}
/**
* Returns T in HCT, L* in L*a*b* <= tone parameter that ensures ratio with input T/L*. Returns
* -1 if ratio cannot be achieved.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
*/
public static double darker(double tone, double ratio) {
if (tone < 0.0 || tone > 100.0) {
return -1.0;
}
// Invert the contrast ratio equation to determine darker Y given a ratio and lighter Y.
final double lightY = ColorUtils.yFromLstar(tone);
final double darkY = ((lightY + 5.0) / ratio) - 5.0;
if (darkY < 0.0 || darkY > 100.0) {
return -1.0;
}
final double realContrast = ratioOfYs(lightY, darkY);
final double delta = Math.abs(realContrast - ratio);
if (realContrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
return -1.0;
}
// For information on 0.4 constant, see comment in lighter(tone, ratio).
final double returnValue = ColorUtils.lstarFromY(darkY) - LUMINANCE_GAMUT_MAP_TOLERANCE;
// NOMUTANTS--important validation step; functions it is calling may change implementation.
if (returnValue < 0 || returnValue > 100) {
return -1.0;
}
return returnValue;
}
/**
* Tone <= tone parameter that ensures ratio. 0 if ratio cannot be achieved.
*
* <p>This method is unsafe because the returned value is guaranteed to be in bounds, but, the
* in bounds return value may not reach the desired ratio.
*
* @param tone Tone return value must contrast with.
* @param ratio Desired contrast ratio of return value and tone parameter.
*/
public static double darkerUnsafe(double tone, double ratio) {
double darkerSafe = darker(tone, ratio);
return max(0.0, darkerSafe);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.dislike;
import com.android.systemui.monet.hct.Hct;
/**
* 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).
*/
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;
}
}

View File

@@ -0,0 +1,646 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.dynamiccolor;
import static java.lang.Math.max;
import static java.lang.Math.min;
import com.android.systemui.monet.contrast.Contrast;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.scheme.DynamicScheme;
import com.android.systemui.monet.utils.MathUtils;
import java.util.HashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* A color that adjusts itself based on UI state, represented by DynamicScheme.
*
* <p>This color automatically adjusts to accommodate a desired contrast level, or other adjustments
* such as differing in light mode versus dark mode, or what the theme is, or what the color that
* produced the theme is, etc.
*
* <p>Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
* become closer to their background as contrast lowers, and further when contrast increases.
*
* <p>Prefer the static constructors. They provide a much more simple interface, such as requiring
* just a hexcode, or just a hexcode and a background.
*
* <p>Ultimately, each component necessary for calculating a color, adjusting it for a desired
* contrast level, and ensuring it has a certain lightness/tone difference from another color, is
* provided by a function that takes a DynamicScheme and returns a value. This ensures ultimate
* flexibility, any desired behavior of a color for any design system, but it usually unnecessary.
* See the default constructor for more information.
*/
// Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
// "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
// A java_library Bazel rule with an Android constraint cannot skip these warnings without this
// annotation; another solution would be to create an android_library rule and supply
// AndroidManifest with an SDK set higher than 14.
@SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
public final class DynamicColor {
public final Function<DynamicScheme, Double> hue;
public final Function<DynamicScheme, Double> chroma;
public final Function<DynamicScheme, Double> tone;
public final Function<DynamicScheme, Double> opacity;
public final Function<DynamicScheme, DynamicColor> background;
public final Function<DynamicScheme, Double> toneMinContrast;
public final Function<DynamicScheme, Double> toneMaxContrast;
public final Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint;
private final HashMap<DynamicScheme, Hct> hctCache = new HashMap<>();
/**
* The base constructor for DynamicColor.
*
* <p>Functional arguments allow overriding without risks that come with subclasses. _Strongly_
* prefer using one of the static convenience constructors. This class is arguably too flexible
* to
* ensure it can support any scenario.
*
* <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio
* with
* its background is principled and matches a11y guidance. That does not mean it's the desired
* approach for _every_ design system, and every color pairing, always, in every case.
*
* @param hue given DynamicScheme, return the hue in HCT of the output color.
* @param chroma given DynamicScheme, return chroma in HCT of the output color.
* @param tone given DynamicScheme, return tone in HCT of the output color.
* @param background given DynamicScheme, return the DynamicColor that is the
* background of this
* DynamicColor. When this is provided, automated adjustments to
* lower and raise contrast are
* made.
* @param toneMinContrast given DynamicScheme, return tone in HCT/L* in L*a*b* this color
* should
* be at minimum contrast. See toneMinContrastDefault for the default
* behavior, and strongly
* consider using it unless you have strong opinions on a11y. The
* static constructors use it.
* @param toneMaxContrast given DynamicScheme, return tone in HCT/L* in L*a*b* this color
* should
* be at maximum contrast. See toneMaxContrastDefault for the default
* behavior, and strongly
* consider using it unless you have strong opinions on a11y. The
* static constructors use it.
* @param toneDeltaConstraint given DynamicScheme, return a ToneDeltaConstraint instance that
* describes a requirement that this DynamicColor must always have
* some difference in tone/L*
* from another DynamicColor.<br>
* Unlikely to be useful unless a design system has some distortions
* where colors that don't
* have a background/foreground relationship must have _some_
* difference in tone, yet, not
* enough difference to create meaningful contrast.
*/
public DynamicColor(
Function<DynamicScheme, Double> hue,
Function<DynamicScheme, Double> chroma,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, Double> opacity,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, Double> toneMinContrast,
Function<DynamicScheme, Double> toneMaxContrast,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
this.hue = hue;
this.chroma = chroma;
this.tone = tone;
this.opacity = opacity;
this.background = background;
this.toneMinContrast = toneMinContrast;
this.toneMaxContrast = toneMaxContrast;
this.toneDeltaConstraint = toneDeltaConstraint;
}
/**
* Create a DynamicColor from a hex code.
*
* <p>Result has no background; thus no support for increasing/decreasing contrast for a11y.
*/
public static DynamicColor fromArgb(int argb) {
final Hct hct = Hct.fromInt(argb);
final TonalPalette palette = TonalPalette.fromInt(argb);
return DynamicColor.fromPalette((s) -> palette, (s) -> hct.getTone());
}
/**
* Create a DynamicColor from just a hex code.
*
* <p>Result has no background; thus cannot support increasing/decreasing contrast for a11y.
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. Useful for adjusting for dark
* vs. light mode.
*/
public static DynamicColor fromArgb(int argb, Function<DynamicScheme, Double> tone) {
return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone);
}
/**
* Create a DynamicColor.
*
* <p>If you don't understand HCT fully, or your design team doesn't, but wants support for
* automated contrast adjustment, this method is _extremely_ useful: you can take a standard
* design system expressed as hex codes, create DynamicColors corresponding to each color, and
* then wire up backgrounds.
*
* <p>If the design system uses the same hex code on multiple backgrounds, define that in
* multiple
* DynamicColors so that the background is accurate for each one. If you define a DynamicColor
* with one background, and actually use it on another, DynamicColor can't guarantee contrast.
* For
* example, if you use a color on both black and white, increasing the contrast on one
* necessarily
* decreases contrast of the other.
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs.
* light mode)
* @param background Function that provides background DynamicColor given DynamicScheme. Useful
* for contrast, given a background, colors can adjust to increase/decrease
* contrast.
*/
public static DynamicColor fromArgb(
int argb,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background) {
return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone, background);
}
/**
* Create a DynamicColor from:
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. (useful for
* dark vs. light mode)
* @param background Function that provides background DynamicColor given
* DynamicScheme. Useful
* for contrast, given a background, colors can adjust to
* increase/decrease contrast.
* @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
* Useful for ensuring lightness difference between colors that don't
* _require_ contrast or
* have a formal background/foreground relationship.
*/
public static DynamicColor fromArgb(
int argb,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.fromPalette(
(s) -> TonalPalette.fromInt(argb), tone, background, toneDeltaConstraint);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
* defined by a hue and chroma, so this replaces the need to specify hue/chroma.
* By providing
* a tonal palette, when contrast adjustments are made, intended chroma can be
* preserved. For
* example, at T/L* 90, there is a significant limit to the amount of chroma.
* There is no
* colorful red, a red that light is pink. By preserving the _intended_ chroma if
* lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs. light
* mode)
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette, Function<DynamicScheme, Double> tone) {
return fromPalette(palette, tone, null, null);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette
* is
* defined by a hue and chroma, so this replaces the need to specify
* hue/chroma. By providing
* a tonal palette, when contrast adjustments are made, intended chroma can be
* preserved. For
* example, at T/L* 90, there is a significant limit to the amount of chroma.
* There is no
* colorful red, a red that light is pink. By preserving the _intended_ chroma
* if lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs.
* light mode)
* @param background Function that provides background DynamicColor given DynamicScheme. Useful
* for contrast, given a background, colors can adjust to increase/decrease
* contrast.
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background) {
return fromPalette(palette, tone, background, null);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A
* TonalPalette is
* defined by a hue and chroma, so this replaces the need to specify
* hue/chroma. By providing
* a tonal palette, when contrast adjustments are made, intended
* chroma can be preserved. For
* example, at T/L* 90, there is a significant limit to the amount of
* chroma. There is no
* colorful red, a red that light is pink. By preserving the
* _intended_ chroma if lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for
* dark vs. light mode)
* @param background Function that provides background DynamicColor given
* DynamicScheme. Useful
* for contrast, given a background, colors can adjust to
* increase/decrease contrast.
* @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
* Useful for ensuring lightness difference between colors that don't
* _require_ contrast or
* have a formal background/foreground relationship.
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return new DynamicColor(
scheme -> palette.apply(scheme).getHue(),
scheme -> palette.apply(scheme).getChroma(),
tone,
null,
background,
scheme -> toneMinContrastDefault(tone, background, scheme, toneDeltaConstraint),
scheme -> toneMaxContrastDefault(tone, background, scheme, toneDeltaConstraint),
toneDeltaConstraint);
}
/** Returns the ARGB (i.e. hex code) representation of the resolved color given scheme. */
public int getArgb(DynamicScheme scheme) {
final int argb = getHct(scheme).toInt();
if (opacity == null) {
return argb;
}
final double percentage = opacity.apply(scheme);
final int alpha = MathUtils.clampInt(0, 255, (int) Math.round(percentage * 255));
return (argb & 0x00ffffff) | (alpha << 24);
}
/** Returns the HCT representation of the resolved color given scheme. */
public Hct getHct(DynamicScheme scheme) {
final Hct cachedAnswer = hctCache.get(scheme);
if (cachedAnswer != null) {
return cachedAnswer;
}
// This is crucial for aesthetics: we aren't simply the taking the standard color
// and changing its tone for contrast. Rather, we find the tone for contrast, then
// use the specified chroma from the palette to construct a new color.
//
// For example, this enables colors with standard tone of T90, which has limited chroma, to
// "recover" intended chroma as contrast increases.
final Hct answer = Hct.from(hue.apply(scheme), chroma.apply(scheme), getTone(scheme));
// NOMUTANTS--trivial test with onerous dependency injection requirement.
if (hctCache.size() > 4) {
hctCache.clear();
}
// NOMUTANTS--trivial test with onerous dependency injection requirement.
hctCache.put(scheme, answer);
return answer;
}
/** 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;
if (scheme.contrastLevel != 0.0) {
final double startTone = tone.apply(scheme);
final double endTone =
decreasingContrast ? toneMinContrast.apply(scheme) : toneMaxContrast.apply(
scheme);
final double delta = (endTone - startTone) * Math.abs(scheme.contrastLevel);
answer = delta + startTone;
}
final DynamicColor bgDynamicColor = background == null ? null : background.apply(scheme);
double minRatio = Contrast.RATIO_MIN;
double maxRatio = Contrast.RATIO_MAX;
if (bgDynamicColor != null) {
final boolean bgHasBg =
bgDynamicColor.background != null && bgDynamicColor.background.apply(scheme)
== null;
final double standardRatio =
Contrast.ratioOfTones(tone.apply(scheme), bgDynamicColor.tone.apply(scheme));
if (decreasingContrast) {
final double minContrastRatio =
Contrast.ratioOfTones(
toneMinContrast.apply(scheme),
bgDynamicColor.toneMinContrast.apply(scheme));
minRatio = bgHasBg ? 1.0 : minContrastRatio;
maxRatio = standardRatio;
} else {
final double maxContrastRatio =
Contrast.ratioOfTones(
toneMaxContrast.apply(scheme),
bgDynamicColor.toneMaxContrast.apply(scheme));
minRatio = !bgHasBg ? 1.0 : min(maxContrastRatio, standardRatio);
maxRatio = !bgHasBg ? 21.0 : max(maxContrastRatio, standardRatio);
}
}
final double finalMinRatio = minRatio;
final double finalMaxRatio = maxRatio;
final double finalAnswer = answer;
answer =
calculateDynamicTone(
scheme,
this.tone,
(dynamicColor) -> dynamicColor.getTone(scheme),
(a, b) -> finalAnswer,
(s) -> bgDynamicColor,
toneDeltaConstraint,
(s) -> finalMinRatio,
(s) -> finalMaxRatio);
return answer;
}
/**
* The default algorithm for calculating the tone of a color at minimum contrast.<br>
* If the original contrast ratio was >= 7.0, reach contrast 4.5.<br>
* If the original contrast ratio was >= 3.0, reach contrast 3.0.<br>
* If the original contrast ratio was < 3.0, reach that ratio.
*/
public static double toneMinContrastDefault(
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.calculateDynamicTone(
scheme,
tone,
c -> c.toneMinContrast.apply(scheme),
(stdRatio, bgTone) -> {
double answer = tone.apply(scheme);
if (stdRatio >= Contrast.RATIO_70) {
answer = contrastingTone(bgTone, Contrast.RATIO_45);
} else if (stdRatio >= Contrast.RATIO_30) {
answer = contrastingTone(bgTone, Contrast.RATIO_30);
} else {
final boolean backgroundHasBackground =
background != null
&& background.apply(scheme) != null
&& background.apply(scheme).background != null
&& background.apply(scheme).background.apply(scheme)
!= null;
if (backgroundHasBackground) {
answer = contrastingTone(bgTone, stdRatio);
}
}
return answer;
},
background,
toneDeltaConstraint,
null,
standardRatio -> standardRatio);
}
/**
* The default algorithm for calculating the tone of a color at maximum contrast.<br>
* If the color's background has a background, reach contrast 7.0.<br>
* If it doesn't, maintain the original contrast ratio.<br>
*
* <p>This ensures text on surfaces maintains its original, often detrimentally excessive,
* contrast ratio. But, text on buttons can soften to not have excessive contrast.
*
* <p>Historically, digital design uses pure whites and black for text and surfaces. It's too
* much
* of a jump at this point in history to introduce a dynamic contrast system _and_ insist that
* text always had excessive contrast and should reach 7.0, it would deterimentally affect
* desire
* to understand and use dynamic contrast.
*/
public static double toneMaxContrastDefault(
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.calculateDynamicTone(
scheme,
tone,
c -> c.toneMaxContrast.apply(scheme),
(stdRatio, bgTone) -> {
final boolean backgroundHasBackground =
background != null
&& background.apply(scheme) != null
&& background.apply(scheme).background != null
&& background.apply(scheme).background.apply(scheme) != null;
if (backgroundHasBackground) {
return contrastingTone(bgTone, Contrast.RATIO_70);
} else {
return contrastingTone(bgTone, max(Contrast.RATIO_70, stdRatio));
}
},
background,
toneDeltaConstraint,
null,
null);
}
/**
* Core method for calculating a tone for under dynamic contrast.
*
* <p>It enforces important properties:<br>
* #1. Desired contrast ratio is reached.<br>
* As contrast increases from standard to max, the tones involved should always be at least the
* standard ratio. For example, if a button is T90, and button text is T0, and the button is T0
* at
* max contrast, the button text cannot simply linearly interpolate from T0 to T100, or at some
* point they'll both be at the same tone.
*
* <p>#2. Enable light foregrounds on midtones.<br>
* The eye prefers light foregrounds on T50 to T60, possibly up to T70, but, contrast ratio 4.5
* can't be reached with T100 unless the foreground is T50. Contrast ratio 4.5 is crucial, it
* represents 'readable text', i.e. text smaller than ~40 dp / 1/4". So, if a tone is between
* T50
* and T60, it is proactively changed to T49 to enable light foregrounds.
*
* <p>#3. Ensure tone delta with another color.<br>
* In design systems, there may be colors that don't have a pure background/foreground
* relationship, but, do require different tones for visual differentiation.
* ToneDeltaConstraint
* models this requirement, and DynamicColor enforces it.
*/
public static double calculateDynamicTone(
DynamicScheme scheme,
Function<DynamicScheme, Double> toneStandard,
Function<DynamicColor, Double> toneToJudge,
BiFunction<Double, Double, Double> desiredTone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
Function<Double, Double> minRatio,
Function<Double, Double> maxRatio) {
// Start with the tone with no adjustment for contrast.
// If there is no background, don't perform any adjustment, return immediately.
final double toneStd = toneStandard.apply(scheme);
double answer = toneStd;
final DynamicColor bgDynamic = background == null ? null : background.apply(scheme);
if (bgDynamic == null) {
return answer;
}
final double bgToneStd = bgDynamic.tone.apply(scheme);
final double stdRatio = Contrast.ratioOfTones(toneStd, bgToneStd);
// If there is a background, determine its tone after contrast adjustment.
// Then, calculate the foreground tone that ensures the caller's desired contrast ratio
// is met.
final double bgTone = toneToJudge.apply(bgDynamic);
final double myDesiredTone = desiredTone.apply(stdRatio, bgTone);
final double currentRatio = Contrast.ratioOfTones(bgTone, myDesiredTone);
final double minRatioRealized =
minRatio == null
? Contrast.RATIO_MIN
: minRatio.apply(stdRatio) == null ? Contrast.RATIO_MIN : minRatio.apply(
stdRatio);
final double maxRatioRealized =
maxRatio == null
? Contrast.RATIO_MAX
: maxRatio.apply(stdRatio) == null ? Contrast.RATIO_MAX : maxRatio.apply(
stdRatio);
final double desiredRatio =
MathUtils.clampDouble(minRatioRealized, maxRatioRealized, currentRatio);
if (desiredRatio == currentRatio) {
answer = myDesiredTone;
} else {
answer = DynamicColor.contrastingTone(bgTone, desiredRatio);
}
// If the background has no background, adjust the foreground tone to ensure that
// it is dark enough to have a light foreground.
if (bgDynamic.background == null || bgDynamic.background.apply(scheme) == null) {
answer = DynamicColor.enableLightForeground(answer);
}
// If the caller has specified a constraint where it must have a certain tone distance from
// another color, enforce that constraint.
answer = ensureToneDelta(answer, toneStd, scheme, toneDeltaConstraint, toneToJudge);
return answer;
}
static double ensureToneDelta(
double tone,
double toneStandard,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
Function<DynamicColor, Double> toneToDistanceFrom) {
final ToneDeltaConstraint constraint =
(toneDeltaConstraint == null ? null : toneDeltaConstraint.apply(scheme));
if (constraint == null) {
return tone;
}
final double requiredDelta = constraint.delta;
final double keepAwayTone = toneToDistanceFrom.apply(constraint.keepAway);
final double delta = Math.abs(tone - keepAwayTone);
if (delta >= requiredDelta) {
return tone;
}
switch (constraint.keepAwayPolarity) {
case DARKER:
return MathUtils.clampDouble(0, 100, keepAwayTone + requiredDelta);
case LIGHTER:
return MathUtils.clampDouble(0, 100, keepAwayTone - requiredDelta);
case NO_PREFERENCE:
final double keepAwayToneStandard = constraint.keepAway.tone.apply(scheme);
final boolean preferLighten = toneStandard > keepAwayToneStandard;
final double alterAmount = Math.abs(delta - requiredDelta);
final boolean lighten =
preferLighten ? (tone + alterAmount <= 100.0) : tone < alterAmount;
return lighten ? tone + alterAmount : tone - alterAmount;
}
return tone;
}
/**
* Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
* that is as close to ratio as possible.
*/
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);
final double darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone);
final boolean preferLighter = tonePrefersLightForeground(bgTone);
if (preferLighter) {
// "Negligible difference" handles an edge case where the initial contrast ratio is high
// (ex. 13.0), and the ratio passed to the function is that high ratio, and both the
// lighter and darker ratio fails to pass that ratio.
//
// This was observed with Tonal Spot's On Primary Container turning black momentarily
// between high and max contrast in light mode. PC's standard tone was T90, OPC's was
// T10, it was light mode, and the contrast level was 0.6568521221032331.
final boolean negligibleDifference =
Math.abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio
&& darkerRatio < ratio;
if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) {
return lighterTone;
} else {
return darkerTone;
}
} else {
return darkerRatio >= ratio || darkerRatio >= lighterRatio ? darkerTone : lighterTone;
}
}
/**
* Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
* supporting it.
*/
public static double enableLightForeground(double tone) {
if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
return 49.0;
}
return tone;
}
/**
* People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
* during research for APCA.
*
* <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in
* order
* to ensure light foregrounds.
*/
public static boolean tonePrefersLightForeground(double tone) {
return Math.round(tone) <= 60;
}
/** Tones less than ~T50 always permit white at 4.5 contrast. */
public static boolean toneAllowsLightForeground(double tone) {
return Math.round(tone) <= 49;
}
}

View File

@@ -0,0 +1,415 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.dynamiccolor;
import com.android.systemui.monet.dislike.DislikeAnalyzer;
import com.android.systemui.monet.dynamiccolor.DynamicColor;
import com.android.systemui.monet.dynamiccolor.ToneDeltaConstraint;
import com.android.systemui.monet.dynamiccolor.TonePolarity;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.hct.ViewingConditions;
import com.android.systemui.monet.scheme.DynamicScheme;
import com.android.systemui.monet.scheme.Variant;
/** Named colors, otherwise known as tokens, or roles, in the Material Design system. */
public final class MaterialDynamicColors {
private static final double CONTAINER_ACCENT_TONE_DELTA = 15.0;
private MaterialDynamicColors() {
}
/** In light mode, the darkest surface. In dark mode, the lightest surface. */
public static DynamicColor highestSurface(DynamicScheme s) {
return s.isDark ? surfaceBright : surfaceDim;
}
public static final DynamicColor background =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
public static final DynamicColor onBackground =
DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> background);
public static final DynamicColor surface =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
public static final DynamicColor surfaceInverse =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 20.0);
public static final DynamicColor surfaceBright =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 24.0 : 98.0);
public static final DynamicColor surfaceDim =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 87.0);
public static final DynamicColor surfaceSub2 =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 4.0 : 100.0);
public static final DynamicColor surfaceSub1 =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 96.0);
public static final DynamicColor surfaceContainer =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 12.0 : 94.0);
public static final DynamicColor surfaceAdd1 =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 17.0 : 92.0);
public static final DynamicColor surfaceAdd2 =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 22.0 : 90.0);
public static final DynamicColor onSurface =
DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0,
(s) -> highestSurface(s));
public static final DynamicColor onSurfaceInverse =
DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 20.0 : 95.0, (s) -> surfaceInverse);
public static final DynamicColor surfaceVariant =
DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 30.0 : 90.0);
public static final DynamicColor onSurfaceVariant =
DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0,
(s) -> surfaceVariant);
public static final DynamicColor outline =
DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> 50.0, (s) -> highestSurface(s));
public static final DynamicColor outlineVariant =
DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> 80.0, (s) -> highestSurface(s));
public static final DynamicColor primaryContainer =
DynamicColor.fromPalette(
(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) -> {
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(
(s) -> s.primaryPalette,
(s) -> s.isDark ? 80.0 : 40.0,
(s) -> highestSurface(s),
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
primaryContainer,
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
public static final DynamicColor primaryInverse =
DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.isDark ? 40.0 : 80.0, (s) -> surfaceInverse);
public static final DynamicColor onPrimary =
DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> primary);
public static final DynamicColor secondaryContainer =
DynamicColor.fromPalette(
(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) -> {
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(
(s) -> s.secondaryPalette,
(s) -> s.isDark ? 80.0 : 40.0,
(s) -> highestSurface(s),
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
secondaryContainer,
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
public static final DynamicColor onSecondary =
DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> secondary);
public static final DynamicColor tertiaryContainer =
DynamicColor.fromPalette(
(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) -> {
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(
(s) -> s.tertiaryPalette,
(s) -> s.isDark ? 80.0 : 40.0,
(s) -> highestSurface(s),
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
tertiaryContainer,
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
public static final DynamicColor onTertiary =
DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> tertiary);
public static final DynamicColor errorContainer =
DynamicColor.fromPalette(
(s) -> s.errorPalette, (s) -> s.isDark ? 30.0 : 90.0, (s) -> highestSurface(s));
public static final DynamicColor onErrorContainer =
DynamicColor.fromPalette(
(s) -> s.errorPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> errorContainer);
public static final DynamicColor error =
DynamicColor.fromPalette(
(s) -> s.errorPalette,
(s) -> s.isDark ? 80.0 : 40.0,
(s) -> highestSurface(s),
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
errorContainer,
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
public static final DynamicColor onError =
DynamicColor.fromPalette((s) -> s.errorPalette, (s) -> s.isDark ? 20.0 : 100.0,
(s) -> error);
public static final DynamicColor primaryFixed =
DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 90.0,
(s) -> highestSurface(s));
public static final DynamicColor primaryFixedDarker =
DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 80.0,
(s) -> highestSurface(s));
public static final DynamicColor onPrimaryFixed =
DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 10.0,
(s) -> primaryFixedDarker);
public static final DynamicColor onPrimaryFixedVariant =
DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> 30.0,
(s) -> primaryFixedDarker);
public static final DynamicColor secondaryFixed =
DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 90.0,
(s) -> highestSurface(s));
public static final DynamicColor secondaryFixedDarker =
DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 80.0,
(s) -> highestSurface(s));
public static final DynamicColor onSecondaryFixed =
DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 10.0,
(s) -> secondaryFixedDarker);
public static final DynamicColor onSecondaryFixedVariant =
DynamicColor.fromPalette((s) -> s.secondaryPalette, (s) -> 30.0,
(s) -> secondaryFixedDarker);
public static final DynamicColor tertiaryFixed =
DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 90.0,
(s) -> highestSurface(s));
public static final DynamicColor tertiaryFixedDarker =
DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 80.0,
(s) -> highestSurface(s));
public static final DynamicColor onTertiaryFixed =
DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 10.0,
(s) -> tertiaryFixedDarker);
public static final DynamicColor onTertiaryFixedVariant =
DynamicColor.fromPalette((s) -> s.tertiaryPalette, (s) -> 30.0,
(s) -> tertiaryFixedDarker);
/**
* These colors were present in Android framework before Android U, and used by MDC controls.
* They
* should be avoided, if possible. It's unclear if they're used on multiple backgrounds, and if
* they are, they can't be adjusted for contrast.* For now, they will be set with no
* background,
* and those won't adjust for contrast, avoiding issues.
*
* <p>* For example, if the same color is on a white background _and_ black background, there's
* no
* way to increase contrast with either without losing contrast with the other.
*/
// colorControlActivated documented as colorAccent in M3 & GM3.
// colorAccent documented as colorSecondary in M3 and colorPrimary in GM3.
// Android used Material's Container as Primary/Secondary/Tertiary at launch.
// Therefore, this is a duplicated version of Primary Container.
public static final DynamicColor controlActivated =
DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> s.isDark ? 30.0 : 90.0, null);
// colorControlNormal documented as textColorSecondary in M3 & GM3.
// In Material, textColorSecondary points to onSurfaceVariant in the non-disabled state,
// which is Neutral Variant T30/80 in light/dark.
public static final DynamicColor controlNormal =
DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0);
// colorControlHighlight documented, in both M3 & GM3:
// Light mode: #1f000000 dark mode: #33ffffff.
// These are black and white with some alpha.
// 1F hex = 31 decimal; 31 / 255 = 12% alpha.
// 33 hex = 51 decimal; 51 / 255 = 20% alpha.
// DynamicColors do not support alpha currently, and _may_ not need it for this use case,
// depending on how MDC resolved alpha for the other cases.
// Returning black in dark mode, white in light mode.
public static final DynamicColor controlHighlight =
new DynamicColor(
s -> 0.0,
s -> 0.0,
s -> s.isDark ? 100.0 : 0.0,
s -> s.isDark ? 0.20 : 0.12,
null,
scheme ->
DynamicColor.toneMinContrastDefault(
(s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
scheme ->
DynamicColor.toneMaxContrastDefault(
(s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
null);
// textColorPrimaryInverse documented, in both M3 & GM3, documented as N10/N90.
public static final DynamicColor textPrimaryInverse =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
// textColorSecondaryInverse and textColorTertiaryInverse both documented, in both M3 & GM3, as
// NV30/NV80
public static final DynamicColor textSecondaryAndTertiaryInverse =
DynamicColor.fromPalette((s) -> s.neutralVariantPalette, (s) -> s.isDark ? 30.0 : 80.0);
// textColorPrimaryInverseDisableOnly documented, in both M3 & GM3, as N10/N90
public static final DynamicColor textPrimaryInverseDisableOnly =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
// textColorSecondaryInverse and textColorTertiaryInverse in disabled state both documented,
// in both M3 & GM3, as N10/N90
public static final DynamicColor textSecondaryAndTertiaryInverseDisabled =
DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
// textColorHintInverse documented, in both M3 & GM3, 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());
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.dynamiccolor;
/**
* Documents a constraint between two DynamicColors, in which their tones must have a certain
* distance from each other.
*/
public final class ToneDeltaConstraint {
public final double delta;
public final DynamicColor keepAway;
public final TonePolarity keepAwayPolarity;
/**
* @param delta the difference in tone required
* @param keepAway the color to distance in tone from
* @param keepAwayPolarity whether the color to keep away from must be lighter, darker, or no
* preference, in which case it should
*/
public ToneDeltaConstraint(double delta, DynamicColor keepAway, TonePolarity keepAwayPolarity) {
this.delta = delta;
this.keepAway = keepAway;
this.keepAwayPolarity = keepAwayPolarity;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.dynamiccolor;
/** Describes the relationship in lightness between two colors. */
public enum TonePolarity {
DARKER,
LIGHTER,
NO_PREFERENCE;
}

View File

@@ -0,0 +1,454 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.hct;
import static java.lang.Math.max;
import com.android.systemui.monet.utils.ColorUtils;
/**
* CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
* code and viewing conditions.
*
* <p>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 should be used when
* measuring distances between colors.
*
* <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
* the color. Color appearance models such as CAM16 also use information about the environment where
* the color was observed, known as the viewing conditions.
*
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
*/
public final class Cam16 {
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
static final double[][] XYZ_TO_CAM16RGB = {
{0.401288, 0.650173, -0.051461},
{-0.250268, 1.204414, 0.045854},
{-0.002079, 0.048952, 0.953127}
};
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
static final double[][] CAM16RGB_TO_XYZ = {
{1.8620678, -1.0112547, 0.14918678},
{0.38752654, 0.62144744, -0.00897398},
{-0.01584150, -0.03412294, 1.0499644}
};
// CAM16 color dimensions, see getters for documentation.
private final double hue;
private final double chroma;
private final double j;
private final double q;
private final double m;
private final double s;
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
private final double jstar;
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
* distances between colors.
*/
double distance(Cam16 other) {
double dJ = getJstar() - other.getJstar();
double dA = getAstar() - other.getAstar();
double dB = getBstar() - other.getBstar();
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
double dE = 1.41 * Math.pow(dEPrime, 0.63);
return dE;
}
/** Hue in CAM16 */
public double getHue() {
return hue;
}
/** Chroma in CAM16 */
public double getChroma() {
return chroma;
}
/** Lightness in CAM16 */
public double getJ() {
return j;
}
/**
* Brightness in CAM16.
*
* <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper
* is
* much brighter viewed in sunlight than in indoor light, but it is the lightest object under
* any
* lighting.
*/
public double getQ() {
return q;
}
/**
* Colorfulness in CAM16.
*
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
* more colorful outside than inside, but it has the same chroma in both environments.
*/
public double getM() {
return m;
}
/**
* Saturation in CAM16.
*
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
* relative to the color's own brightness, where chroma is colorfulness relative to white.
*/
public double getS() {
return s;
}
/** Lightness coordinate in CAM16-UCS */
public double getJstar() {
return jstar;
}
/** a* coordinate in CAM16-UCS */
public double getAstar() {
return astar;
}
/** b* coordinate in CAM16-UCS */
public double getBstar() {
return bstar;
}
/**
* All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
* combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
* method that constructs from 3 of those dimensions. This constructor is intended for those
* methods to use to return all possible dimensions.
*
* @param hue for example, red, orange, yellow, green, etc.
* @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
* perceptually accurate.
* @param j lightness
* @param q brightness; ratio of lightness to white point's lightness
* @param m colorfulness
* @param s saturation; ratio of chroma to white point's chroma
* @param jstar CAM16-UCS J coordinate
* @param astar CAM16-UCS a coordinate
* @param bstar CAM16-UCS b coordinate
*/
private Cam16(
double hue,
double chroma,
double j,
double q,
double m,
double s,
double jstar,
double astar,
double bstar) {
this.hue = hue;
this.chroma = chroma;
this.j = j;
this.q = q;
this.m = m;
this.s = s;
this.jstar = jstar;
this.astar = astar;
this.bstar = bstar;
}
/**
* Create a CAM16 color from a color, assuming the color was viewed in default viewing
* conditions.
*
* @param argb ARGB representation of a color.
*/
public static Cam16 fromInt(int argb) {
return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
}
/**
* Create a CAM16 color from a color in defined viewing conditions.
*
* @param argb ARGB representation of a color.
* @param viewingConditions Information about the environment where the color was observed.
*/
// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
// may differ at runtime due to floating point imprecision, keeping the values the same, and
// accurate, across implementations takes precedence.
@SuppressWarnings("FloatingPointLiteralPrecision")
static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
// Transform ARGB int to XYZ
int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff);
double redL = ColorUtils.linearized(red);
double greenL = ColorUtils.linearized(green);
double blueL = ColorUtils.linearized(blue);
double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
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]);
double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
// Discount illuminant
double rD = viewingConditions.getRgbD()[0] * rT;
double gD = viewingConditions.getRgbD()[1] * gT;
double bD = viewingConditions.getRgbD()[2] * bT;
// Chromatic adaptation
double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
// redness-greenness
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
// yellowness-blueness
double b = (rA + gA - 2.0 * bA) / 9.0;
// auxiliary components
double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
// hue
double atan2 = Math.atan2(b, a);
double atanDegrees = Math.toDegrees(atan2);
double hue =
atanDegrees < 0
? atanDegrees + 360.0
: atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
double hueRadians = Math.toRadians(hue);
// achromatic response to color
double ac = p2 * viewingConditions.getNbb();
// CAM16 lightness and brightness
double j =
100.0
* Math.pow(
ac / viewingConditions.getAw(),
viewingConditions.getC() * viewingConditions.getZ());
double q =
4.0
/ viewingConditions.getC()
* Math.sqrt(j / 100.0)
* (viewingConditions.getAw() + 4.0)
* viewingConditions.getFlRoot();
// CAM16 chroma, colorfulness, and saturation.
double huePrime = (hue < 20.14) ? hue + 360 : hue;
double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
double t = p1 * Math.hypot(a, b) / (u + 0.305);
double alpha =
Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
// CAM16 chroma, colorfulness, saturation
double c = alpha * Math.sqrt(j / 100.0);
double m = c * viewingConditions.getFlRoot();
double s =
50.0 * Math.sqrt(
(alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
// CAM16-UCS components
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
double astar = mstar * Math.cos(hueRadians);
double bstar = mstar * Math.sin(hueRadians);
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
}
/**
* @param j CAM16 lightness
* @param c CAM16 chroma
* @param h CAM16 hue
*/
static Cam16 fromJch(double j, double c, double h) {
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
}
/**
* @param j CAM16 lightness
* @param c CAM16 chroma
* @param h CAM16 hue
* @param viewingConditions Information about the environment where the color was observed.
*/
private static Cam16 fromJchInViewingConditions(
double j, double c, double h, ViewingConditions viewingConditions) {
double q =
4.0
/ viewingConditions.getC()
* Math.sqrt(j / 100.0)
* (viewingConditions.getAw() + 4.0)
* viewingConditions.getFlRoot();
double m = c * viewingConditions.getFlRoot();
double alpha = c / Math.sqrt(j / 100.0);
double s =
50.0 * Math.sqrt(
(alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
double hueRadians = Math.toRadians(h);
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
double astar = mstar * Math.cos(hueRadians);
double bstar = mstar * Math.sin(hueRadians);
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
}
/**
* Create a CAM16 color from CAM16-UCS coordinates.
*
* @param jstar CAM16-UCS lightness.
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
* axis.
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
* axis.
*/
public static Cam16 fromUcs(double jstar, double astar, double bstar) {
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
}
/**
* Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
*
* @param jstar CAM16-UCS lightness.
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian
* coordinate on the Y
* axis.
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian
* coordinate on the X
* axis.
* @param viewingConditions Information about the environment where the color was observed.
*/
public static Cam16 fromUcsInViewingConditions(
double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
double m = Math.hypot(astar, bstar);
double m2 = Math.expm1(m * 0.0228) / 0.0228;
double c = m2 / viewingConditions.getFlRoot();
double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
if (h < 0.0) {
h += 360.0;
}
double j = jstar / (1. - (jstar - 100.) * 0.007);
return fromJchInViewingConditions(j, c, h, viewingConditions);
}
/**
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
* which are near-identical to the default viewing conditions for sRGB.
*/
public int toInt() {
return viewed(ViewingConditions.DEFAULT);
}
/**
* ARGB representation of the color, in defined viewing conditions.
*
* @param viewingConditions Information about the environment where the color will be viewed.
* @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);
double t =
Math.pow(
alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73),
1.0 / 0.9);
double hRad = Math.toRadians(getHue());
double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
double ac =
viewingConditions.getAw()
* Math.pow(getJ() / 100.0,
1.0 / viewingConditions.getC() / viewingConditions.getZ());
double p1 =
eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
double p2 = (ac / viewingConditions.getNbb());
double hSin = Math.sin(hRad);
double hCos = Math.cos(hRad);
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
double a = gamma * hCos;
double b = gamma * hSin;
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
double rC =
Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase,
1.0 / 0.42);
double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
double gC =
Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase,
1.0 / 0.42);
double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
double bC =
Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase,
1.0 / 0.42);
double rF = rC / viewingConditions.getRgbD()[0];
double gF = gC / viewingConditions.getRgbD()[1];
double bF = bC / viewingConditions.getRgbD()[2];
double[][] matrix = CAM16RGB_TO_XYZ;
double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
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]);
if (returnArray != null) {
returnArray[0] = x;
returnArray[1] = y;
returnArray[2] = z;
return returnArray;
} else {
return new double[]{x, y, z};
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.hct;
import com.android.systemui.monet.utils.ColorUtils;
/**
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
*
* <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
* ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
* be calculated from Y.
*
* <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
*
* <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A
* difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50
* guarantees a contrast ratio >= 4.5.
*/
/**
* HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
* measurement system that can also accurately render what colors will appear as in different
* lighting environments.
*/
public final class Hct {
private double hue;
private double chroma;
private double tone;
private int argb;
/**
* Create an HCT color from hue, chroma, and tone.
*
* @param hue 0 <= hue < 360; invalid values are corrected.
* @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower
* than
* the requested chroma. Chroma has a different maximum for any given hue and
* tone.
* @param tone 0 <= tone <= 100; invalid values are corrected.
* @return HCT representation of a color in default viewing conditions.
*/
public static Hct from(double hue, double chroma, double tone) {
int argb = HctSolver.solveToInt(hue, chroma, tone);
return new Hct(argb);
}
/**
* Create an HCT color from a color.
*
* @param argb ARGB representation of a color.
* @return HCT representation of a color in default viewing conditions
*/
public static Hct fromInt(int argb) {
return new Hct(argb);
}
private Hct(int argb) {
setInternalState(argb);
}
public double getHue() {
return hue;
}
public double getChroma() {
return chroma;
}
public double getTone() {
return tone;
}
/** This color converted to ARGB, i.e. a hex code. */
public int toInt() {
return argb;
}
/**
* Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
* given hue and tone.
*
* @param newHue 0 <= newHue < 360; invalid values are corrected.
*/
public void setHue(double newHue) {
setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
}
/**
* Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
* any given hue and tone.
*
* @param newChroma 0 <= newChroma < ?
*/
public void setChroma(double newChroma) {
setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
}
/**
* Set the tone of this color. Chroma may decrease because chroma has a different maximum for
* any
* given hue and tone.
*
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
*/
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);
hue = cam.getHue();
chroma = cam.getChroma();
this.tone = ColorUtils.lstarFromArgb(argb);
}
}

View File

@@ -0,0 +1,677 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.hct;
import com.android.systemui.monet.utils.ColorUtils;
import com.android.systemui.monet.utils.MathUtils;
/** A class that solves the HCT equation. */
public class HctSolver {
private HctSolver() {
}
static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
new double[][]{
new double[]{
0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
},
new double[]{
0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
},
new double[]{
0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
},
};
static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
new double[][]{
new double[]{
1373.2198709594231, -1100.4251190754821, -7.278681089101213,
},
new double[]{
-271.815969077903, 559.6580465940733, -32.46047482791194,
},
new double[]{
1.9622899599665666, -57.173814538844006, 308.7233197812385,
},
};
static final double[] Y_FROM_LINRGB = new double[]{0.2126, 0.7152, 0.0722};
static final double[] CRITICAL_PLANES =
new double[]{
0.015176349177441876,
0.045529047532325624,
0.07588174588720938,
0.10623444424209313,
0.13658714259697685,
0.16693984095186062,
0.19729253930674434,
0.2276452376616281,
0.2579979360165119,
0.28835063437139563,
0.3188300904430532,
0.350925934958123,
0.3848314933096426,
0.42057480301049466,
0.458183274052838,
0.4976837250274023,
0.5391024159806381,
0.5824650784040898,
0.6277969426914107,
0.6751227633498623,
0.7244668422128921,
0.775853049866786,
0.829304845476233,
0.8848452951698498,
0.942497089126609,
1.0022825574869039,
1.0642236851973577,
1.1283421258858297,
1.1946592148522128,
1.2631959812511864,
1.3339731595349034,
1.407011200216447,
1.4823302800086415,
1.5599503113873272,
1.6398909516233677,
1.7221716113234105,
1.8068114625156377,
1.8938294463134073,
1.9832442801866852,
2.075074464868551,
2.1693382909216234,
2.2660538449872063,
2.36523901573795,
2.4669114995532007,
2.5710888059345764,
2.6777882626779785,
2.7870270208169257,
2.898822059350997,
3.0131901897720907,
3.1301480604002863,
3.2497121605402226,
3.3718988244681087,
3.4967242352587946,
3.624204428461639,
3.754355295633311,
3.887192587735158,
4.022731918402185,
4.160988767090289,
4.301978482107941,
4.445716283538092,
4.592217266055746,
4.741496401646282,
4.893568542229298,
5.048448422192488,
5.20615066083972,
5.3666897647573375,
5.5300801301023865,
5.696336044816294,
5.865471690767354,
6.037501145825082,
6.212438385869475,
6.390297286737924,
6.571091626112461,
6.7548350853498045,
6.941541251256611,
7.131223617812143,
7.323895587840543,
7.5195704746346665,
7.7182615035334345,
7.919981813454504,
8.124744458384042,
8.332562408825165,
8.543448553206703,
8.757415699253682,
8.974476575321063,
9.194643831691977,
9.417930041841839,
9.644347703669503,
9.873909240696694,
10.106627003236781,
10.342513269534024,
10.58158024687427,
10.8238400726681,
11.069304815507364,
11.317986476196008,
11.569896988756009,
11.825048221409341,
12.083451977536606,
12.345119996613247,
12.610063955123938,
12.878295467455942,
13.149826086772048,
13.42466730586372,
13.702830557985108,
13.984327217668513,
14.269168601521828,
14.55736596900856,
14.848930523210871,
15.143873411576273,
15.44220572664832,
15.743938506781891,
16.04908273684337,
16.35764934889634,
16.66964922287304,
16.985093187232053,
17.30399201960269,
17.62635644741625,
17.95219714852476,
18.281524751807332,
18.614349837764564,
18.95068293910138,
19.290534541298456,
19.633915083172692,
19.98083495742689,
20.331304511189067,
20.685334046541502,
21.042933821039977,
21.404114048223256,
21.76888489811322,
22.137256497705877,
22.50923893145328,
22.884842241736916,
23.264076429332462,
23.6469514538663,
24.033477234264016,
24.42366364919083,
24.817520537484558,
25.21505769858089,
25.61628489293138,
26.021211842414342,
26.429848230738664,
26.842203703840827,
27.258287870275353,
27.678110301598522,
28.10168053274597,
28.529008062403893,
28.96010235337422,
29.39497283293396,
29.83362889318845,
30.276079891419332,
30.722335150426627,
31.172403958865512,
31.62629557157785,
32.08401920991837,
32.54558406207592,
33.010999283389665,
33.4802739966603,
33.953417292456834,
34.430438229418264,
34.911345834551085,
35.39614910352207,
35.88485700094671,
36.37747846067349,
36.87402238606382,
37.37449765026789,
37.87891309649659,
38.38727753828926,
38.89959975977785,
39.41588851594697,
39.93615253289054,
40.460400508064545,
40.98864111053629,
41.520882981230194,
42.05713473317016,
42.597404951718396,
43.141702194811224,
43.6900349931913,
44.24241185063697,
44.798841244188324,
45.35933162437017,
45.92389141541209,
46.49252901546552,
47.065252796817916,
47.64207110610409,
48.22299226451468,
48.808024568002054,
49.3971762874833,
49.9904556690408,
50.587870934119984,
51.189430279724725,
51.79514187861014,
52.40501387947288,
53.0190544071392,
53.637271562750364,
54.259673423945976,
54.88626804504493,
55.517063457223934,
56.15206766869424,
56.79128866487574,
57.43473440856916,
58.08241284012621,
58.734331877617365,
59.39049941699807,
60.05092333227251,
60.715611475655585,
61.38457167773311,
62.057811747619894,
62.7353394731159,
63.417162620860914,
64.10328893648692,
64.79372614476921,
65.48848194977529,
66.18756403501224,
66.89098006357258,
67.59873767827808,
68.31084450182222,
69.02730813691093,
69.74813616640164,
70.47333615344107,
71.20291564160104,
71.93688215501312,
72.67524319850172,
73.41800625771542,
74.16517879925733,
74.9167682708136,
75.67278210128072,
76.43322770089146,
77.1981124613393,
77.96744375590167,
78.74122893956174,
79.51947534912904,
80.30219030335869,
81.08938110306934,
81.88105503125999,
82.67721935322541,
83.4778813166706,
84.28304815182372,
85.09272707154808,
85.90692527145302,
86.72564993000343,
87.54890820862819,
88.3767072518277,
89.2090541872801,
90.04595612594655,
90.88742016217518,
91.73345337380438,
92.58406282226491,
93.43925555268066,
94.29903859396902,
95.16341895893969,
96.03240364439274,
96.9059996312159,
97.78421388448044,
98.6670533535366,
99.55452497210776,
};
/**
* Sanitizes a small enough angle in radians.
*
* @param angle An angle in radians; must not deviate too much from 0.
* @return A coterminal angle between 0 and 2pi.
*/
static double sanitizeRadians(double angle) {
return (angle + Math.PI * 8) % (Math.PI * 2);
}
/**
* Delinearizes an RGB component, returning a floating-point number.
*
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
* @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
*/
static double trueDelinearized(double rgbComponent) {
double normalized = rgbComponent / 100.0;
double delinearized = 0.0;
if (normalized <= 0.0031308) {
delinearized = normalized * 12.92;
} else {
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
}
return delinearized * 255.0;
}
static double chromaticAdaptation(double component) {
double af = Math.pow(Math.abs(component), 0.42);
return MathUtils.signum(component) * 400.0 * af / (af + 27.13);
}
/**
* Returns the hue of a linear RGB color in CAM16.
*
* @param linrgb The linear RGB coordinates of a color.
* @return The hue of the color in CAM16, in radians.
*/
static double hueOf(double[] linrgb) {
double[] scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
double rA = chromaticAdaptation(scaledDiscount[0]);
double gA = chromaticAdaptation(scaledDiscount[1]);
double bA = chromaticAdaptation(scaledDiscount[2]);
// redness-greenness
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
// yellowness-blueness
double b = (rA + gA - 2.0 * bA) / 9.0;
return Math.atan2(b, a);
}
static boolean areInCyclicOrder(double a, double b, double c) {
double deltaAB = sanitizeRadians(b - a);
double deltaAC = sanitizeRadians(c - a);
return deltaAB < deltaAC;
}
/**
* Solves the lerp equation.
*
* @param source The starting number.
* @param mid The number in the middle.
* @param target The ending number.
* @return A number t such that lerp(source, target, t) = mid.
*/
static double intercept(double source, double mid, double target) {
return (mid - source) / (target - source);
}
static double[] lerpPoint(double[] source, double t, double[] target) {
return new double[]{
source[0] + (target[0] - source[0]) * t,
source[1] + (target[1] - source[1]) * t,
source[2] + (target[2] - source[2]) * t,
};
}
/**
* Intersects a segment with a plane.
*
* @param source The coordinates of point A.
* @param coordinate The R-, G-, or B-coordinate of the plane.
* @param target The coordinates of point B.
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
* @return The intersection point of the segment AB with the plane R=coordinate,
* G=coordinate, or
* B=coordinate
*/
static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
double t = intercept(source[axis], coordinate, target[axis]);
return lerpPoint(source, t, target);
}
static boolean isBounded(double x) {
return 0.0 <= x && x <= 100.0;
}
/**
* Returns the nth possible vertex of the polygonal intersection.
*
* @param y The Y value of the plane.
* @param n The zero-based index of the point. 0 <= n <= 11.
* @return The nth possible vertex of the polygonal intersection of the y plane and the RGB
* cube,
* in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
* [-1.0, -1.0, -1.0] is returned.
*/
static double[] nthVertex(double y, int n) {
double kR = Y_FROM_LINRGB[0];
double kG = Y_FROM_LINRGB[1];
double kB = Y_FROM_LINRGB[2];
double coordA = n % 4 <= 1 ? 0.0 : 100.0;
double coordB = n % 2 == 0 ? 0.0 : 100.0;
if (n < 4) {
double g = coordA;
double b = coordB;
double r = (y - g * kG - b * kB) / kR;
if (isBounded(r)) {
return new double[]{r, g, b};
} else {
return new double[]{-1.0, -1.0, -1.0};
}
} else if (n < 8) {
double b = coordA;
double r = coordB;
double g = (y - r * kR - b * kB) / kG;
if (isBounded(g)) {
return new double[]{r, g, b};
} else {
return new double[]{-1.0, -1.0, -1.0};
}
} else {
double r = coordA;
double g = coordB;
double b = (y - r * kR - g * kG) / kB;
if (isBounded(b)) {
return new double[]{r, g, b};
} else {
return new double[]{-1.0, -1.0, -1.0};
}
}
}
/**
* Finds the segment containing the desired color.
*
* @param y The Y value of the color.
* @param targetHue The hue of the color.
* @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of
* the
* segment containing the desired color.
*/
static double[][] bisectToSegment(double y, double targetHue) {
double[] left = new double[]{-1.0, -1.0, -1.0};
double[] right = left;
double leftHue = 0.0;
double rightHue = 0.0;
boolean initialized = false;
boolean uncut = true;
for (int n = 0; n < 12; n++) {
double[] mid = nthVertex(y, n);
if (mid[0] < 0) {
continue;
}
double midHue = hueOf(mid);
if (!initialized) {
left = mid;
right = mid;
leftHue = midHue;
rightHue = midHue;
initialized = true;
continue;
}
if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
uncut = false;
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
right = mid;
rightHue = midHue;
} else {
left = mid;
leftHue = midHue;
}
}
}
return new double[][]{left, right};
}
static double[] midpoint(double[] a, double[] b) {
return new double[]{
(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2,
};
}
static int criticalPlaneBelow(double x) {
return (int) Math.floor(x - 0.5);
}
static int criticalPlaneAbove(double x) {
return (int) Math.ceil(x - 0.5);
}
/**
* Finds a color with the given Y and hue on the boundary of the cube.
*
* @param y The Y value of the color.
* @param targetHue The hue of the color.
* @return The desired color, in linear RGB coordinates.
*/
static double[] bisectToLimit(double y, double targetHue) {
double[][] segment = bisectToSegment(y, targetHue);
double[] left = segment[0];
double leftHue = hueOf(left);
double[] right = segment[1];
for (int axis = 0; axis < 3; axis++) {
if (left[axis] != right[axis]) {
int lPlane = -1;
int rPlane = 255;
if (left[axis] < right[axis]) {
lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
} else {
lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
}
for (int i = 0; i < 8; i++) {
if (Math.abs(rPlane - lPlane) <= 1) {
break;
} else {
int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
double midHue = hueOf(mid);
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
right = mid;
rPlane = mPlane;
} else {
left = mid;
leftHue = midHue;
lPlane = mPlane;
}
}
}
}
}
return midpoint(left, right);
}
static double inverseChromaticAdaptation(double adapted) {
double adaptedAbs = Math.abs(adapted);
double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
return MathUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
}
/**
* Finds a color with the given hue, chroma, and Y.
*
* @param hueRadians The desired hue in radians.
* @param chroma The desired chroma.
* @param y The desired Y.
* @return The desired color as a hexadecimal integer, if found; 0 otherwise.
*/
static int findResultByJ(double hueRadians, double chroma, double y) {
// Initial estimate of j.
double j = Math.sqrt(y) * 11.0;
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
ViewingConditions viewingConditions = ViewingConditions.DEFAULT;
double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
double p1 =
eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
double hSin = Math.sin(hueRadians);
double hCos = Math.cos(hueRadians);
for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
double jNormalized = j / 100.0;
double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
double ac =
viewingConditions.getAw()
* Math.pow(jNormalized,
1.0 / viewingConditions.getC() / viewingConditions.getZ());
double p2 = ac / viewingConditions.getNbb();
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
double a = gamma * hCos;
double b = gamma * hSin;
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
double rCScaled = inverseChromaticAdaptation(rA);
double gCScaled = inverseChromaticAdaptation(gA);
double bCScaled = inverseChromaticAdaptation(bA);
double[] linrgb =
MathUtils.matrixMultiply(
new double[]{rCScaled, gCScaled, bCScaled},
LINRGB_FROM_SCALED_DISCOUNT);
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
return 0;
}
double kR = Y_FROM_LINRGB[0];
double kG = Y_FROM_LINRGB[1];
double kB = Y_FROM_LINRGB[2];
double fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
if (fnj <= 0) {
return 0;
}
if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
return 0;
}
return ColorUtils.argbFromLinrgb(linrgb);
}
// Iterates with Newton method,
// Using 2 * fn(j) / j as the approximation of fn'(j)
j = j - (fnj - y) * j / (2 * fnj);
}
return 0;
}
/**
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
*
* @param hueDegrees The desired hue, in degrees.
* @param chroma The desired chroma.
* @param lstar The desired L*.
* @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
* sufficiently close, and chroma will be maximized.
*/
public static int solveToInt(double hueDegrees, double chroma, double lstar) {
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
return ColorUtils.argbFromLstar(lstar);
}
hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees);
double hueRadians = hueDegrees / 180 * Math.PI;
double y = ColorUtils.yFromLstar(lstar);
int exactAnswer = findResultByJ(hueRadians, chroma, y);
if (exactAnswer != 0) {
return exactAnswer;
}
double[] linrgb = bisectToLimit(y, hueRadians);
return ColorUtils.argbFromLinrgb(linrgb);
}
/**
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
*
* @param hueDegrees The desired hue, in degrees.
* @param chroma The desired chroma.
* @param lstar The desired L*.
* @return A CAM16 object representing the sRGB color. The color has sufficiently close hue,
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
* sufficiently close, and chroma will be maximized.
*/
public static Cam16 solveToCam(double hueDegrees, double chroma, double lstar) {
return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar));
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.hct;
import com.android.systemui.monet.utils.ColorUtils;
import com.android.systemui.monet.utils.MathUtils;
/**
* In traditional color spaces, a color can be identified solely by the observer's measurement of
* the color. Color appearance models such as CAM16 also use information about the environment where
* the color was observed, known as the viewing conditions.
*
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
*
* <p>This class caches intermediate values of the CAM16 conversion process that depend only on
* viewing conditions, enabling speed ups.
*/
public final class ViewingConditions {
/** sRGB-like viewing conditions. */
public static final ViewingConditions DEFAULT =
ViewingConditions.defaultWithBackgroundLstar(50.0);
private final double aw;
private final double nbb;
private final double ncb;
private final double c;
private final double nc;
private final double n;
private final double[] rgbD;
private final double fl;
private final double flRoot;
private final double z;
public double getAw() {
return aw;
}
public double getN() {
return n;
}
public double getNbb() {
return nbb;
}
double getNcb() {
return ncb;
}
double getC() {
return c;
}
double getNc() {
return nc;
}
public double[] getRgbD() {
return rgbD;
}
double getFl() {
return fl;
}
public double getFlRoot() {
return flRoot;
}
double getZ() {
return z;
}
/**
* Create ViewingConditions from a simple, physically relevant, set of parameters.
*
* @param whitePoint White point, measured in the XYZ color space. default = D65, or
* sunny day
* afternoon
* @param adaptingLuminance The luminance of the adapting field. Informally, how bright it
* is in
* the room where the color is viewed. Can be calculated from lux
* by multiplying lux by
* 0.0586. default = 11.72, or 200 lux.
* @param backgroundLstar The lightness of the area surrounding the color. measured by L*
* in
* L*a*b*. default = 50.0
* @param surround A general description of the lighting surrounding the color. 0
* is pitch dark,
* like watching a movie in a theater. 1.0 is a dimly light room,
* like watching TV at home at
* night. 2.0 means there is no difference between the lighting on
* the color and around it.
* default = 2.0
* @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
* such as knowing an apple is still red in green light. default =
* false, the eye does not
* perform this process on self-luminous objects like displays.
*/
public static ViewingConditions make(
double[] whitePoint,
double adaptingLuminance,
double backgroundLstar,
double surround,
boolean discountingIlluminant) {
// A background of pure black is non-physical and leads to infinities that represent the
// idea
// that any color viewed in pure black can't be seen.
backgroundLstar = Math.max(0.1, backgroundLstar);
// Transform white point XYZ to 'cone'/'rgb' responses
double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
double[] xyz = whitePoint;
double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
double f = 0.8 + (surround / 10.0);
double c =
(f >= 0.9)
? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
: MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
double d =
discountingIlluminant
? 1.0
: f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
d = MathUtils.clampDouble(0.0, 1.0, d);
double nc = f;
double[] rgbD =
new double[]{
d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d,
d * (100.0 / bW) + 1.0 - d
};
double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
double k4 = k * k * k * k;
double k4F = 1.0 - k4;
double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(
5.0 * adaptingLuminance));
double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
double z = 1.48 + Math.sqrt(n);
double nbb = 0.725 / Math.pow(n, 0.2);
double ncb = nbb;
double[] rgbAFactors =
new double[]{
Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
};
double[] rgbA =
new double[]{
(400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
(400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
(400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
};
double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
}
/**
* Create sRGB-like viewing conditions with a custom background lstar.
*
* <p>Default viewing conditions have a lstar of 50, midgray.
*/
public static ViewingConditions defaultWithBackgroundLstar(double lstar) {
return ViewingConditions.make(
ColorUtils.whitePointD65(),
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
lstar,
2.0,
false);
}
/**
* Parameters are intermediate values of the CAM16 conversion process. Their names are
* shorthand
* for technical color science terminology, this class would not benefit from documenting them
* individually. A brief overview is available in the CAM16 specification, and a complete
* overview
* requires a color science textbook, such as Fairchild's Color Appearance Models.
*/
private ViewingConditions(
double n,
double aw,
double nbb,
double ncb,
double c,
double nc,
double[] rgbD,
double fl,
double flRoot,
double z) {
this.n = n;
this.aw = aw;
this.nbb = nbb;
this.ncb = ncb;
this.c = c;
this.nc = nc;
this.rgbD = rgbD;
this.fl = fl;
this.flRoot = flRoot;
this.z = z;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.palettes;
import static java.lang.Math.max;
import static java.lang.Math.min;
import com.android.systemui.monet.hct.Hct;
/**
* An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of
* tones are generated, all except one use the same hue as the key color, and all vary in chroma.
*/
public final class CorePalette {
public TonalPalette a1;
public TonalPalette a2;
public TonalPalette a3;
public TonalPalette n1;
public TonalPalette n2;
public TonalPalette error;
/**
* Create key tones from a color.
*
* @param argb ARGB representation of a color
*/
public static CorePalette of(int argb) {
return new CorePalette(argb, false);
}
/**
* Create content key tones from a color.
*
* @param argb ARGB representation of a color
*/
public static CorePalette contentOf(int argb) {
return new CorePalette(argb, true);
}
private CorePalette(int argb, boolean isContent) {
Hct hct = Hct.fromInt(argb);
double hue = hct.getHue();
double chroma = hct.getChroma();
if (isContent) {
this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.));
this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.));
} else {
this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma));
this.a2 = TonalPalette.fromHueAndChroma(hue, 16.);
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.);
this.n1 = TonalPalette.fromHueAndChroma(hue, 4.);
this.n2 = TonalPalette.fromHueAndChroma(hue, 8.);
}
this.error = TonalPalette.fromHueAndChroma(25, 84.);
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.palettes;
import com.android.systemui.monet.hct.Hct;
import java.util.HashMap;
import java.util.Map;
/**
* A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
*/
public final class TonalPalette {
Map<Integer, Integer> cache;
double hue;
double chroma;
/**
* Create tones using the HCT hue and chroma from a color.
*
* @param argb ARGB representation of a color
* @return Tones matching that color's hue and chroma.
*/
public static TonalPalette fromInt(int argb) {
return fromHct(Hct.fromInt(argb));
}
/**
* Create tones using a HCT color.
*
* @param hct HCT representation of a color.
* @return Tones matching that color's hue and chroma.
*/
public static TonalPalette fromHct(Hct hct) {
return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma());
}
/**
* Create tones from a defined HCT hue and chroma.
*
* @param hue HCT hue
* @param chroma HCT chroma
* @return Tones matching hue and chroma.
*/
public static TonalPalette fromHueAndChroma(double hue, double chroma) {
return new TonalPalette(hue, chroma);
}
private TonalPalette(double hue, double chroma) {
cache = new HashMap<>();
this.hue = hue;
this.chroma = chroma;
}
/**
* Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT
* tone.
*
* @param tone HCT tone, measured from 0 to 100.
* @return ARGB representation of a color with that tone.
*/
// AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
@SuppressWarnings("ComputeIfAbsentUseValue")
public int tone(int tone) {
Integer color = cache.get(tone);
if (color == null) {
color = Hct.from(this.hue, this.chroma, tone).toInt();
cache.put(tone, color);
}
return color;
}
/** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
public Hct getHct(double tone) {
return Hct.from(this.hue, this.chroma, tone);
}
/** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
public double getChroma() {
return this.chroma;
}
/** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
public double getHue() {
return this.hue;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
/** An interface to allow use of different color spaces by quantizers. */
public interface PointProvider {
/** The three components in the color space of an sRGB color. */
double[] fromInt(int argb);
/** The ARGB (i.e. hex code) representation of this color. */
int toInt(double[] point);
/**
* Squared distance between two colors.
* Distance is defined by scientific color spaces and referred to as delta E.
*/
double distance(double[] a, double[] b);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import com.android.systemui.monet.utils.ColorUtils;
/**
* Provides conversions needed for K-Means quantization. Converting input to points, and converting
* the final state of the K-Means algorithm to colors.
*/
public final class PointProviderLab implements PointProvider {
/**
* Convert a color represented in ARGB to a 3-element array of L*a*b* coordinates of the color.
*/
@Override
public double[] fromInt(int argb) {
double[] lab = ColorUtils.labFromArgb(argb);
return new double[]{lab[0], lab[1], lab[2]};
}
/** Convert a 3-element array to a color represented in ARGB. */
@Override
public int toInt(double[] lab) {
return ColorUtils.argbFromLab(lab[0], lab[1], lab[2]);
}
/**
* Standard CIE 1976 delta E formula also takes the square root, unneeded here. This method is
* used by quantization algorithms to compare distance, and the relative ordering is the same,
* with or without a square root.
*
* <p>This relatively minor optimization is helpful because this method is called at least once
* for each pixel in an image.
*/
@Override
public double distance(double[] one, double[] two) {
double dL = (one[0] - two[0]);
double dA = (one[1] - two[1]);
double dB = (one[2] - two[2]);
return (dL * dL + dA * dA + dB * dB);
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
interface Quantizer {
QuantizerResult quantize(int[] pixels, int maxColors);
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import java.util.Map;
import java.util.Set;
/**
* An image quantizer that improves on the quality of a standard K-Means algorithm by setting the
* K-Means initial state to the output of a Wu quantizer, instead of random centroids. Improves on
* speed by several optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means with
* those optimizations.
*
* <p>This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving
* the Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
*/
public final class QuantizerCelebi {
private QuantizerCelebi() {
}
/**
* Reduce the number of colors needed to represented the input, minimizing the difference
* between
* the original image and the recolored image.
*
* @param pixels Colors in ARGB format.
* @param maxColors The number of colors to divide the image into. A lower number of colors may
* be
* returned.
* @return Map with keys of colors in ARGB format, and values of number of pixels in the
* original
* image that correspond to the color in the quantized image.
*/
public static Map<Integer, Integer> quantize(int[] pixels, int maxColors) {
QuantizerWu wu = new QuantizerWu();
QuantizerResult wuResult = wu.quantize(pixels, maxColors);
Set<Integer> wuClustersAsObjects = wuResult.colorToCount.keySet();
int index = 0;
int[] wuClusters = new int[wuClustersAsObjects.size()];
for (Integer argb : wuClustersAsObjects) {
wuClusters[index++] = argb;
}
return QuantizerWsmeans.quantize(pixels, wuClusters, maxColors);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import java.util.LinkedHashMap;
import java.util.Map;
/** Creates a dictionary with keys of colors, and values of count of the color */
public final class QuantizerMap implements Quantizer {
Map<Integer, Integer> colorToCount;
@Override
public QuantizerResult quantize(int[] pixels, int colorCount) {
final Map<Integer, Integer> pixelByCount = new LinkedHashMap<>();
for (int pixel : pixels) {
final Integer currentPixelCount = pixelByCount.get(pixel);
final int newPixelCount = currentPixelCount == null ? 1 : currentPixelCount + 1;
pixelByCount.put(pixel, newPixelCount);
}
colorToCount = pixelByCount;
return new QuantizerResult(pixelByCount);
}
public Map<Integer, Integer> getColorToCount() {
return colorToCount;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import java.util.Map;
/** Represents result of a quantizer run */
public final class QuantizerResult {
public final Map<Integer, Integer> colorToCount;
QuantizerResult(Map<Integer, Integer> colorToCount) {
this.colorToCount = colorToCount;
}
}

View File

@@ -0,0 +1,233 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import static java.lang.Math.min;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
/**
* An image quantizer that improves on the speed of a standard K-Means algorithm by implementing
* several optimizations, including deduping identical pixels and a triangle inequality rule that
* reduces the number of comparisons needed to identify which cluster a point should be moved to.
*
* <p>Wsmeans stands for Weighted Square Means.
*
* <p>This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving
* the Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395
*/
public final class QuantizerWsmeans {
private QuantizerWsmeans() {
}
private static final class Distance implements Comparable<Distance> {
int index;
double distance;
Distance() {
this.index = -1;
this.distance = -1;
}
@Override
public int compareTo(Distance other) {
return ((Double) this.distance).compareTo(other.distance);
}
}
private static final int MAX_ITERATIONS = 10;
private static final double MIN_MOVEMENT_DISTANCE = 3.0;
/**
* Reduce the number of colors needed to represented the input, minimizing the difference
* between
* the original image and the recolored image.
*
* @param inputPixels Colors in ARGB format.
* @param startingClusters Defines the initial state of the quantizer. Passing an empty array
* is
* fine, the implementation will create its own initial state that leads
* to reproducible
* results for the same inputs. Passing an array that is the result of
* Wu quantization leads
* to higher quality results.
* @param maxColors The number of colors to divide the image into. A lower number of
* colors may be
* returned.
* @return Map with keys of colors in ARGB format, values of how many of the input pixels belong
* to the color.
*/
public static Map<Integer, Integer> quantize(
int[] inputPixels, int[] startingClusters, int maxColors) {
// Uses a seeded random number generator to ensure consistent results.
Random random = new Random(0x42688);
Map<Integer, Integer> pixelToCount = new LinkedHashMap<>();
double[][] points = new double[inputPixels.length][];
int[] pixels = new int[inputPixels.length];
PointProvider pointProvider = new PointProviderLab();
int pointCount = 0;
for (int i = 0; i < inputPixels.length; i++) {
int inputPixel = inputPixels[i];
Integer pixelCount = pixelToCount.get(inputPixel);
if (pixelCount == null) {
points[pointCount] = pointProvider.fromInt(inputPixel);
pixels[pointCount] = inputPixel;
pointCount++;
pixelToCount.put(inputPixel, 1);
} else {
pixelToCount.put(inputPixel, pixelCount + 1);
}
}
int[] counts = new int[pointCount];
for (int i = 0; i < pointCount; i++) {
int pixel = pixels[i];
int count = pixelToCount.get(pixel);
counts[i] = count;
}
int clusterCount = min(maxColors, pointCount);
if (startingClusters.length != 0) {
clusterCount = min(clusterCount, startingClusters.length);
}
double[][] clusters = new double[clusterCount][];
for (int i = 0; i < startingClusters.length; i++) {
clusters[i] = pointProvider.fromInt(startingClusters[i]);
}
int[] clusterIndices = new int[pointCount];
for (int i = 0; i < pointCount; i++) {
clusterIndices[i] = random.nextInt(clusterCount);
}
int[][] indexMatrix = new int[clusterCount][];
for (int i = 0; i < clusterCount; i++) {
indexMatrix[i] = new int[clusterCount];
}
Distance[][] distanceToIndexMatrix = new Distance[clusterCount][];
for (int i = 0; i < clusterCount; i++) {
distanceToIndexMatrix[i] = new Distance[clusterCount];
for (int j = 0; j < clusterCount; j++) {
distanceToIndexMatrix[i][j] = new Distance();
}
}
int[] pixelCountSums = new int[clusterCount];
for (int iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
for (int i = 0; i < clusterCount; i++) {
for (int j = i + 1; j < clusterCount; j++) {
double distance = pointProvider.distance(clusters[i], clusters[j]);
distanceToIndexMatrix[j][i].distance = distance;
distanceToIndexMatrix[j][i].index = i;
distanceToIndexMatrix[i][j].distance = distance;
distanceToIndexMatrix[i][j].index = j;
}
Arrays.sort(distanceToIndexMatrix[i]);
for (int j = 0; j < clusterCount; j++) {
indexMatrix[i][j] = distanceToIndexMatrix[i][j].index;
}
}
int pointsMoved = 0;
for (int i = 0; i < pointCount; i++) {
double[] point = points[i];
int previousClusterIndex = clusterIndices[i];
double[] previousCluster = clusters[previousClusterIndex];
double previousDistance = pointProvider.distance(point, previousCluster);
double minimumDistance = previousDistance;
int newClusterIndex = -1;
for (int j = 0; j < clusterCount; j++) {
if (distanceToIndexMatrix[previousClusterIndex][j].distance
>= 4 * previousDistance) {
continue;
}
double distance = pointProvider.distance(point, clusters[j]);
if (distance < minimumDistance) {
minimumDistance = distance;
newClusterIndex = j;
}
}
if (newClusterIndex != -1) {
double distanceChange =
Math.abs(Math.sqrt(minimumDistance) - Math.sqrt(previousDistance));
if (distanceChange > MIN_MOVEMENT_DISTANCE) {
pointsMoved++;
clusterIndices[i] = newClusterIndex;
}
}
}
if (pointsMoved == 0 && iteration != 0) {
break;
}
double[] componentASums = new double[clusterCount];
double[] componentBSums = new double[clusterCount];
double[] componentCSums = new double[clusterCount];
Arrays.fill(pixelCountSums, 0);
for (int i = 0; i < pointCount; i++) {
int clusterIndex = clusterIndices[i];
double[] point = points[i];
int count = counts[i];
pixelCountSums[clusterIndex] += count;
componentASums[clusterIndex] += (point[0] * count);
componentBSums[clusterIndex] += (point[1] * count);
componentCSums[clusterIndex] += (point[2] * count);
}
for (int i = 0; i < clusterCount; i++) {
int count = pixelCountSums[i];
if (count == 0) {
clusters[i] = new double[]{0., 0., 0.};
continue;
}
double a = componentASums[i] / count;
double b = componentBSums[i] / count;
double c = componentCSums[i] / count;
clusters[i][0] = a;
clusters[i][1] = b;
clusters[i][2] = c;
}
}
Map<Integer, Integer> argbToPopulation = new LinkedHashMap<>();
for (int i = 0; i < clusterCount; i++) {
int count = pixelCountSums[i];
if (count == 0) {
continue;
}
int possibleNewCluster = pointProvider.toInt(clusters[i]);
if (argbToPopulation.containsKey(possibleNewCluster)) {
continue;
}
argbToPopulation.put(possibleNewCluster, count);
}
return argbToPopulation;
}
}

View File

@@ -0,0 +1,407 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.quantize;
import com.android.systemui.monet.utils.ColorUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* An image quantizer that divides the image's pixels into clusters by recursively cutting an RGB
* cube, based on the weight of pixels in each area of the cube.
*
* <p>The algorithm was described by Xiaolin Wu in Graphic Gems II, published in 1991.
*/
public final class QuantizerWu implements Quantizer {
int[] weights;
int[] momentsR;
int[] momentsG;
int[] momentsB;
double[] moments;
Box[] cubes;
// A histogram of all the input colors is constructed. It has the shape of a
// cube. The cube would be too large if it contained all 16 million colors:
// historical best practice is to use 5 bits of the 8 in each channel,
// reducing the histogram to a volume of ~32,000.
private static final int INDEX_BITS = 5;
private static final int INDEX_COUNT = 33; // ((1 << INDEX_BITS) + 1)
private static final int TOTAL_SIZE = 35937; // INDEX_COUNT * INDEX_COUNT * INDEX_COUNT
@Override
public QuantizerResult quantize(int[] pixels, int colorCount) {
QuantizerResult mapResult = new QuantizerMap().quantize(pixels, colorCount);
constructHistogram(mapResult.colorToCount);
createMoments();
CreateBoxesResult createBoxesResult = createBoxes(colorCount);
List<Integer> colors = createResult(createBoxesResult.resultCount);
Map<Integer, Integer> resultMap = new LinkedHashMap<>();
for (int color : colors) {
resultMap.put(color, 0);
}
return new QuantizerResult(resultMap);
}
static int getIndex(int r, int g, int b) {
return (r << (INDEX_BITS * 2)) + (r << (INDEX_BITS + 1)) + r + (g << INDEX_BITS) + g + b;
}
void constructHistogram(Map<Integer, Integer> pixels) {
weights = new int[TOTAL_SIZE];
momentsR = new int[TOTAL_SIZE];
momentsG = new int[TOTAL_SIZE];
momentsB = new int[TOTAL_SIZE];
moments = new double[TOTAL_SIZE];
for (Map.Entry<Integer, Integer> pair : pixels.entrySet()) {
int pixel = pair.getKey();
int count = pair.getValue();
int red = ColorUtils.redFromArgb(pixel);
int green = ColorUtils.greenFromArgb(pixel);
int blue = ColorUtils.blueFromArgb(pixel);
int bitsToRemove = 8 - INDEX_BITS;
int iR = (red >> bitsToRemove) + 1;
int iG = (green >> bitsToRemove) + 1;
int iB = (blue >> bitsToRemove) + 1;
int index = getIndex(iR, iG, iB);
weights[index] += count;
momentsR[index] += (red * count);
momentsG[index] += (green * count);
momentsB[index] += (blue * count);
moments[index] += (count * ((red * red) + (green * green) + (blue * blue)));
}
}
void createMoments() {
for (int r = 1; r < INDEX_COUNT; ++r) {
int[] area = new int[INDEX_COUNT];
int[] areaR = new int[INDEX_COUNT];
int[] areaG = new int[INDEX_COUNT];
int[] areaB = new int[INDEX_COUNT];
double[] area2 = new double[INDEX_COUNT];
for (int g = 1; g < INDEX_COUNT; ++g) {
int line = 0;
int lineR = 0;
int lineG = 0;
int lineB = 0;
double line2 = 0.0;
for (int b = 1; b < INDEX_COUNT; ++b) {
int index = getIndex(r, g, b);
line += weights[index];
lineR += momentsR[index];
lineG += momentsG[index];
lineB += momentsB[index];
line2 += moments[index];
area[b] += line;
areaR[b] += lineR;
areaG[b] += lineG;
areaB[b] += lineB;
area2[b] += line2;
int previousIndex = getIndex(r - 1, g, b);
weights[index] = weights[previousIndex] + area[b];
momentsR[index] = momentsR[previousIndex] + areaR[b];
momentsG[index] = momentsG[previousIndex] + areaG[b];
momentsB[index] = momentsB[previousIndex] + areaB[b];
moments[index] = moments[previousIndex] + area2[b];
}
}
}
}
CreateBoxesResult createBoxes(int maxColorCount) {
cubes = new Box[maxColorCount];
for (int i = 0; i < maxColorCount; i++) {
cubes[i] = new Box();
}
double[] volumeVariance = new double[maxColorCount];
Box firstBox = cubes[0];
firstBox.r1 = INDEX_COUNT - 1;
firstBox.g1 = INDEX_COUNT - 1;
firstBox.b1 = INDEX_COUNT - 1;
int generatedColorCount = maxColorCount;
int next = 0;
for (int i = 1; i < maxColorCount; i++) {
if (cut(cubes[next], cubes[i])) {
volumeVariance[next] = (cubes[next].vol > 1) ? variance(cubes[next]) : 0.0;
volumeVariance[i] = (cubes[i].vol > 1) ? variance(cubes[i]) : 0.0;
} else {
volumeVariance[next] = 0.0;
i--;
}
next = 0;
double temp = volumeVariance[0];
for (int j = 1; j <= i; j++) {
if (volumeVariance[j] > temp) {
temp = volumeVariance[j];
next = j;
}
}
if (temp <= 0.0) {
generatedColorCount = i + 1;
break;
}
}
return new CreateBoxesResult(maxColorCount, generatedColorCount);
}
List<Integer> createResult(int colorCount) {
List<Integer> colors = new ArrayList<>();
for (int i = 0; i < colorCount; ++i) {
Box cube = cubes[i];
int weight = volume(cube, weights);
if (weight > 0) {
int r = volume(cube, momentsR) / weight;
int g = volume(cube, momentsG) / weight;
int b = volume(cube, momentsB) / weight;
int color = (255 << 24) | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff);
colors.add(color);
}
}
return colors;
}
double variance(Box cube) {
int dr = volume(cube, momentsR);
int dg = volume(cube, momentsG);
int db = volume(cube, momentsB);
double xx =
moments[getIndex(cube.r1, cube.g1, cube.b1)]
- moments[getIndex(cube.r1, cube.g1, cube.b0)]
- moments[getIndex(cube.r1, cube.g0, cube.b1)]
+ moments[getIndex(cube.r1, cube.g0, cube.b0)]
- moments[getIndex(cube.r0, cube.g1, cube.b1)]
+ moments[getIndex(cube.r0, cube.g1, cube.b0)]
+ moments[getIndex(cube.r0, cube.g0, cube.b1)]
- moments[getIndex(cube.r0, cube.g0, cube.b0)];
int hypotenuse = dr * dr + dg * dg + db * db;
int volume = volume(cube, weights);
return xx - hypotenuse / ((double) volume);
}
Boolean cut(Box one, Box two) {
int wholeR = volume(one, momentsR);
int wholeG = volume(one, momentsG);
int wholeB = volume(one, momentsB);
int wholeW = volume(one, weights);
MaximizeResult maxRResult =
maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
MaximizeResult maxGResult =
maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
MaximizeResult maxBResult =
maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
Direction cutDirection;
double maxR = maxRResult.maximum;
double maxG = maxGResult.maximum;
double maxB = maxBResult.maximum;
if (maxR >= maxG && maxR >= maxB) {
if (maxRResult.cutLocation < 0) {
return false;
}
cutDirection = Direction.RED;
} else if (maxG >= maxR && maxG >= maxB) {
cutDirection = Direction.GREEN;
} else {
cutDirection = Direction.BLUE;
}
two.r1 = one.r1;
two.g1 = one.g1;
two.b1 = one.b1;
switch (cutDirection) {
case RED:
one.r1 = maxRResult.cutLocation;
two.r0 = one.r1;
two.g0 = one.g0;
two.b0 = one.b0;
break;
case GREEN:
one.g1 = maxGResult.cutLocation;
two.r0 = one.r0;
two.g0 = one.g1;
two.b0 = one.b0;
break;
case BLUE:
one.b1 = maxBResult.cutLocation;
two.r0 = one.r0;
two.g0 = one.g0;
two.b0 = one.b1;
break;
}
one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
return true;
}
MaximizeResult maximize(
Box cube,
Direction direction,
int first,
int last,
int wholeR,
int wholeG,
int wholeB,
int wholeW) {
int bottomR = bottom(cube, direction, momentsR);
int bottomG = bottom(cube, direction, momentsG);
int bottomB = bottom(cube, direction, momentsB);
int bottomW = bottom(cube, direction, weights);
double max = 0.0;
int cut = -1;
int halfR = 0;
int halfG = 0;
int halfB = 0;
int halfW = 0;
for (int i = first; i < last; i++) {
halfR = bottomR + top(cube, direction, i, momentsR);
halfG = bottomG + top(cube, direction, i, momentsG);
halfB = bottomB + top(cube, direction, i, momentsB);
halfW = bottomW + top(cube, direction, i, weights);
if (halfW == 0) {
continue;
}
double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
double tempDenominator = halfW;
double temp = tempNumerator / tempDenominator;
halfR = wholeR - halfR;
halfG = wholeG - halfG;
halfB = wholeB - halfB;
halfW = wholeW - halfW;
if (halfW == 0) {
continue;
}
tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB;
tempDenominator = halfW;
temp += (tempNumerator / tempDenominator);
if (temp > max) {
max = temp;
cut = i;
}
}
return new MaximizeResult(cut, max);
}
static int volume(Box cube, int[] moment) {
return (moment[getIndex(cube.r1, cube.g1, cube.b1)]
- moment[getIndex(cube.r1, cube.g1, cube.b0)]
- moment[getIndex(cube.r1, cube.g0, cube.b1)]
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
- moment[getIndex(cube.r0, cube.g1, cube.b1)]
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
- moment[getIndex(cube.r0, cube.g0, cube.b0)]);
}
static int bottom(Box cube, Direction direction, int[] moment) {
switch (direction) {
case RED:
return -moment[getIndex(cube.r0, cube.g1, cube.b1)]
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
case GREEN:
return -moment[getIndex(cube.r1, cube.g0, cube.b1)]
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
+ moment[getIndex(cube.r0, cube.g0, cube.b1)]
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
case BLUE:
return -moment[getIndex(cube.r1, cube.g1, cube.b0)]
+ moment[getIndex(cube.r1, cube.g0, cube.b0)]
+ moment[getIndex(cube.r0, cube.g1, cube.b0)]
- moment[getIndex(cube.r0, cube.g0, cube.b0)];
}
throw new IllegalArgumentException("unexpected direction " + direction);
}
static int top(Box cube, Direction direction, int position, int[] moment) {
switch (direction) {
case RED:
return (moment[getIndex(position, cube.g1, cube.b1)]
- moment[getIndex(position, cube.g1, cube.b0)]
- moment[getIndex(position, cube.g0, cube.b1)]
+ moment[getIndex(position, cube.g0, cube.b0)]);
case GREEN:
return (moment[getIndex(cube.r1, position, cube.b1)]
- moment[getIndex(cube.r1, position, cube.b0)]
- moment[getIndex(cube.r0, position, cube.b1)]
+ moment[getIndex(cube.r0, position, cube.b0)]);
case BLUE:
return (moment[getIndex(cube.r1, cube.g1, position)]
- moment[getIndex(cube.r1, cube.g0, position)]
- moment[getIndex(cube.r0, cube.g1, position)]
+ moment[getIndex(cube.r0, cube.g0, position)]);
}
throw new IllegalArgumentException("unexpected direction " + direction);
}
private enum Direction {
RED,
GREEN,
BLUE
}
private static final class MaximizeResult {
// < 0 if cut impossible
int cutLocation;
double maximum;
MaximizeResult(int cut, double max) {
this.cutLocation = cut;
this.maximum = max;
}
}
private static final class CreateBoxesResult {
int requestedCount;
int resultCount;
CreateBoxesResult(int requestedCount, int resultCount) {
this.requestedCount = requestedCount;
this.resultCount = resultCount;
}
}
private static final class Box {
int r0 = 0;
int r1 = 0;
int g0 = 0;
int g1 = 0;
int b0 = 0;
int b1 = 0;
int vol = 0;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.utils.MathUtils;
/**
* Provides important settings for creating colors dynamically, and 6 color palettes. Requires: 1. A
* color. (source color) 2. A theme. (Variant) 3. Whether or not its dark mode. 4. Contrast level.
* (-1 to 1, currently contrast ratio 3.0 and 7.0)
*/
public class DynamicScheme {
public final int sourceColorArgb;
public final Hct sourceColorHct;
public final Variant variant;
public final boolean isDark;
public final double contrastLevel;
public final TonalPalette primaryPalette;
public final TonalPalette secondaryPalette;
public final TonalPalette tertiaryPalette;
public final TonalPalette neutralPalette;
public final TonalPalette neutralVariantPalette;
public final TonalPalette errorPalette;
public DynamicScheme(
Hct sourceColorHct,
Variant variant,
boolean isDark,
double contrastLevel,
TonalPalette primaryPalette,
TonalPalette secondaryPalette,
TonalPalette tertiaryPalette,
TonalPalette neutralPalette,
TonalPalette neutralVariantPalette) {
this.sourceColorArgb = sourceColorHct.toInt();
this.sourceColorHct = sourceColorHct;
this.variant = variant;
this.isDark = isDark;
this.contrastLevel = contrastLevel;
this.primaryPalette = primaryPalette;
this.secondaryPalette = secondaryPalette;
this.tertiaryPalette = tertiaryPalette;
this.neutralPalette = neutralPalette;
this.neutralVariantPalette = neutralVariantPalette;
this.errorPalette = TonalPalette.fromHueAndChroma(25.0, 84.0);
}
/**
* Given a set of hues and set of hue rotations, locate which hues the source color's hue is
* between, apply the rotation at the same index as the first hue in the range, and return the
* rotated hue.
*
* @param sourceColorHct The color whose hue should be rotated.
* @param hues A set of hues.
* @param rotations A set of hue rotations.
* @return Color's hue with a rotation applied.
*/
public static double getRotatedHue(Hct sourceColorHct, double[] hues, double[] rotations) {
final double sourceHue = sourceColorHct.getHue();
if (rotations.length == 1) {
return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[0]);
}
final int size = hues.length;
for (int i = 0; i <= (size - 2); i++) {
final double thisHue = hues[i];
final double nextHue = hues[i + 1];
if (thisHue < sourceHue && sourceHue < nextHue) {
return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[i]);
}
}
// If this statement executes, something is wrong, there should have been a rotation
// found using the arrays.
return sourceHue;
}
}

View File

@@ -0,0 +1,784 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.palettes.CorePalette;
/** Represents a Material color scheme, a mapping of color roles to colors. */
public class Scheme {
private int primary;
private int onPrimary;
private int primaryContainer;
private int onPrimaryContainer;
private int secondary;
private int onSecondary;
private int secondaryContainer;
private int onSecondaryContainer;
private int tertiary;
private int onTertiary;
private int tertiaryContainer;
private int onTertiaryContainer;
private int error;
private int onError;
private int errorContainer;
private int onErrorContainer;
private int background;
private int onBackground;
private int surface;
private int onSurface;
private int surfaceVariant;
private int onSurfaceVariant;
private int outline;
private int outlineVariant;
private int shadow;
private int scrim;
private int inverseSurface;
private int inverseOnSurface;
private int inversePrimary;
public Scheme() {
}
public Scheme(
int primary,
int onPrimary,
int primaryContainer,
int onPrimaryContainer,
int secondary,
int onSecondary,
int secondaryContainer,
int onSecondaryContainer,
int tertiary,
int onTertiary,
int tertiaryContainer,
int onTertiaryContainer,
int error,
int onError,
int errorContainer,
int onErrorContainer,
int background,
int onBackground,
int surface,
int onSurface,
int surfaceVariant,
int onSurfaceVariant,
int outline,
int outlineVariant,
int shadow,
int scrim,
int inverseSurface,
int inverseOnSurface,
int inversePrimary) {
super();
this.primary = primary;
this.onPrimary = onPrimary;
this.primaryContainer = primaryContainer;
this.onPrimaryContainer = onPrimaryContainer;
this.secondary = secondary;
this.onSecondary = onSecondary;
this.secondaryContainer = secondaryContainer;
this.onSecondaryContainer = onSecondaryContainer;
this.tertiary = tertiary;
this.onTertiary = onTertiary;
this.tertiaryContainer = tertiaryContainer;
this.onTertiaryContainer = onTertiaryContainer;
this.error = error;
this.onError = onError;
this.errorContainer = errorContainer;
this.onErrorContainer = onErrorContainer;
this.background = background;
this.onBackground = onBackground;
this.surface = surface;
this.onSurface = onSurface;
this.surfaceVariant = surfaceVariant;
this.onSurfaceVariant = onSurfaceVariant;
this.outline = outline;
this.outlineVariant = outlineVariant;
this.shadow = shadow;
this.scrim = scrim;
this.inverseSurface = inverseSurface;
this.inverseOnSurface = inverseOnSurface;
this.inversePrimary = inversePrimary;
}
/** Create a light theme Scheme from a source color in ARGB, i.e. a hex code. */
public static Scheme light(int argb) {
return lightFromCorePalette(CorePalette.of(argb));
}
/** Create a dark theme Scheme from a source color in ARGB, i.e. a hex code. */
public static Scheme dark(int argb) {
return darkFromCorePalette(CorePalette.of(argb));
}
/** Create a light theme content-based Scheme from a source color in ARGB, i.e. a hex code. */
public static Scheme lightContent(int argb) {
return lightFromCorePalette(CorePalette.contentOf(argb));
}
/** Create a dark theme content-based Scheme from a source color in ARGB, i.e. a hex code. */
public static Scheme darkContent(int argb) {
return darkFromCorePalette(CorePalette.contentOf(argb));
}
private static Scheme lightFromCorePalette(CorePalette core) {
return new Scheme()
.withPrimary(core.a1.tone(40))
.withOnPrimary(core.a1.tone(100))
.withPrimaryContainer(core.a1.tone(90))
.withOnPrimaryContainer(core.a1.tone(10))
.withSecondary(core.a2.tone(40))
.withOnSecondary(core.a2.tone(100))
.withSecondaryContainer(core.a2.tone(90))
.withOnSecondaryContainer(core.a2.tone(10))
.withTertiary(core.a3.tone(40))
.withOnTertiary(core.a3.tone(100))
.withTertiaryContainer(core.a3.tone(90))
.withOnTertiaryContainer(core.a3.tone(10))
.withError(core.error.tone(40))
.withOnError(core.error.tone(100))
.withErrorContainer(core.error.tone(90))
.withOnErrorContainer(core.error.tone(10))
.withBackground(core.n1.tone(99))
.withOnBackground(core.n1.tone(10))
.withSurface(core.n1.tone(99))
.withOnSurface(core.n1.tone(10))
.withSurfaceVariant(core.n2.tone(90))
.withOnSurfaceVariant(core.n2.tone(30))
.withOutline(core.n2.tone(50))
.withOutlineVariant(core.n2.tone(80))
.withShadow(core.n1.tone(0))
.withScrim(core.n1.tone(0))
.withInverseSurface(core.n1.tone(20))
.withInverseOnSurface(core.n1.tone(95))
.withInversePrimary(core.a1.tone(80));
}
private static Scheme darkFromCorePalette(CorePalette core) {
return new Scheme()
.withPrimary(core.a1.tone(80))
.withOnPrimary(core.a1.tone(20))
.withPrimaryContainer(core.a1.tone(30))
.withOnPrimaryContainer(core.a1.tone(90))
.withSecondary(core.a2.tone(80))
.withOnSecondary(core.a2.tone(20))
.withSecondaryContainer(core.a2.tone(30))
.withOnSecondaryContainer(core.a2.tone(90))
.withTertiary(core.a3.tone(80))
.withOnTertiary(core.a3.tone(20))
.withTertiaryContainer(core.a3.tone(30))
.withOnTertiaryContainer(core.a3.tone(90))
.withError(core.error.tone(80))
.withOnError(core.error.tone(20))
.withErrorContainer(core.error.tone(30))
.withOnErrorContainer(core.error.tone(80))
.withBackground(core.n1.tone(10))
.withOnBackground(core.n1.tone(90))
.withSurface(core.n1.tone(10))
.withOnSurface(core.n1.tone(90))
.withSurfaceVariant(core.n2.tone(30))
.withOnSurfaceVariant(core.n2.tone(80))
.withOutline(core.n2.tone(60))
.withOutlineVariant(core.n2.tone(30))
.withShadow(core.n1.tone(0))
.withScrim(core.n1.tone(0))
.withInverseSurface(core.n1.tone(90))
.withInverseOnSurface(core.n1.tone(20))
.withInversePrimary(core.a1.tone(40));
}
public int getPrimary() {
return primary;
}
public void setPrimary(int primary) {
this.primary = primary;
}
public Scheme withPrimary(int primary) {
this.primary = primary;
return this;
}
public int getOnPrimary() {
return onPrimary;
}
public void setOnPrimary(int onPrimary) {
this.onPrimary = onPrimary;
}
public Scheme withOnPrimary(int onPrimary) {
this.onPrimary = onPrimary;
return this;
}
public int getPrimaryContainer() {
return primaryContainer;
}
public void setPrimaryContainer(int primaryContainer) {
this.primaryContainer = primaryContainer;
}
public Scheme withPrimaryContainer(int primaryContainer) {
this.primaryContainer = primaryContainer;
return this;
}
public int getOnPrimaryContainer() {
return onPrimaryContainer;
}
public void setOnPrimaryContainer(int onPrimaryContainer) {
this.onPrimaryContainer = onPrimaryContainer;
}
public Scheme withOnPrimaryContainer(int onPrimaryContainer) {
this.onPrimaryContainer = onPrimaryContainer;
return this;
}
public int getSecondary() {
return secondary;
}
public void setSecondary(int secondary) {
this.secondary = secondary;
}
public Scheme withSecondary(int secondary) {
this.secondary = secondary;
return this;
}
public int getOnSecondary() {
return onSecondary;
}
public void setOnSecondary(int onSecondary) {
this.onSecondary = onSecondary;
}
public Scheme withOnSecondary(int onSecondary) {
this.onSecondary = onSecondary;
return this;
}
public int getSecondaryContainer() {
return secondaryContainer;
}
public void setSecondaryContainer(int secondaryContainer) {
this.secondaryContainer = secondaryContainer;
}
public Scheme withSecondaryContainer(int secondaryContainer) {
this.secondaryContainer = secondaryContainer;
return this;
}
public int getOnSecondaryContainer() {
return onSecondaryContainer;
}
public void setOnSecondaryContainer(int onSecondaryContainer) {
this.onSecondaryContainer = onSecondaryContainer;
}
public Scheme withOnSecondaryContainer(int onSecondaryContainer) {
this.onSecondaryContainer = onSecondaryContainer;
return this;
}
public int getTertiary() {
return tertiary;
}
public void setTertiary(int tertiary) {
this.tertiary = tertiary;
}
public Scheme withTertiary(int tertiary) {
this.tertiary = tertiary;
return this;
}
public int getOnTertiary() {
return onTertiary;
}
public void setOnTertiary(int onTertiary) {
this.onTertiary = onTertiary;
}
public Scheme withOnTertiary(int onTertiary) {
this.onTertiary = onTertiary;
return this;
}
public int getTertiaryContainer() {
return tertiaryContainer;
}
public void setTertiaryContainer(int tertiaryContainer) {
this.tertiaryContainer = tertiaryContainer;
}
public Scheme withTertiaryContainer(int tertiaryContainer) {
this.tertiaryContainer = tertiaryContainer;
return this;
}
public int getOnTertiaryContainer() {
return onTertiaryContainer;
}
public void setOnTertiaryContainer(int onTertiaryContainer) {
this.onTertiaryContainer = onTertiaryContainer;
}
public Scheme withOnTertiaryContainer(int onTertiaryContainer) {
this.onTertiaryContainer = onTertiaryContainer;
return this;
}
public int getError() {
return error;
}
public void setError(int error) {
this.error = error;
}
public Scheme withError(int error) {
this.error = error;
return this;
}
public int getOnError() {
return onError;
}
public void setOnError(int onError) {
this.onError = onError;
}
public Scheme withOnError(int onError) {
this.onError = onError;
return this;
}
public int getErrorContainer() {
return errorContainer;
}
public void setErrorContainer(int errorContainer) {
this.errorContainer = errorContainer;
}
public Scheme withErrorContainer(int errorContainer) {
this.errorContainer = errorContainer;
return this;
}
public int getOnErrorContainer() {
return onErrorContainer;
}
public void setOnErrorContainer(int onErrorContainer) {
this.onErrorContainer = onErrorContainer;
}
public Scheme withOnErrorContainer(int onErrorContainer) {
this.onErrorContainer = onErrorContainer;
return this;
}
public int getBackground() {
return background;
}
public void setBackground(int background) {
this.background = background;
}
public Scheme withBackground(int background) {
this.background = background;
return this;
}
public int getOnBackground() {
return onBackground;
}
public void setOnBackground(int onBackground) {
this.onBackground = onBackground;
}
public Scheme withOnBackground(int onBackground) {
this.onBackground = onBackground;
return this;
}
public int getSurface() {
return surface;
}
public void setSurface(int surface) {
this.surface = surface;
}
public Scheme withSurface(int surface) {
this.surface = surface;
return this;
}
public int getOnSurface() {
return onSurface;
}
public void setOnSurface(int onSurface) {
this.onSurface = onSurface;
}
public Scheme withOnSurface(int onSurface) {
this.onSurface = onSurface;
return this;
}
public int getSurfaceVariant() {
return surfaceVariant;
}
public void setSurfaceVariant(int surfaceVariant) {
this.surfaceVariant = surfaceVariant;
}
public Scheme withSurfaceVariant(int surfaceVariant) {
this.surfaceVariant = surfaceVariant;
return this;
}
public int getOnSurfaceVariant() {
return onSurfaceVariant;
}
public void setOnSurfaceVariant(int onSurfaceVariant) {
this.onSurfaceVariant = onSurfaceVariant;
}
public Scheme withOnSurfaceVariant(int onSurfaceVariant) {
this.onSurfaceVariant = onSurfaceVariant;
return this;
}
public int getOutline() {
return outline;
}
public void setOutline(int outline) {
this.outline = outline;
}
public Scheme withOutline(int outline) {
this.outline = outline;
return this;
}
public int getOutlineVariant() {
return outlineVariant;
}
public void setOutlineVariant(int outlineVariant) {
this.outlineVariant = outlineVariant;
}
public Scheme withOutlineVariant(int outlineVariant) {
this.outlineVariant = outlineVariant;
return this;
}
public int getShadow() {
return shadow;
}
public void setShadow(int shadow) {
this.shadow = shadow;
}
public Scheme withShadow(int shadow) {
this.shadow = shadow;
return this;
}
public int getScrim() {
return scrim;
}
public void setScrim(int scrim) {
this.scrim = scrim;
}
public Scheme withScrim(int scrim) {
this.scrim = scrim;
return this;
}
public int getInverseSurface() {
return inverseSurface;
}
public void setInverseSurface(int inverseSurface) {
this.inverseSurface = inverseSurface;
}
public Scheme withInverseSurface(int inverseSurface) {
this.inverseSurface = inverseSurface;
return this;
}
public int getInverseOnSurface() {
return inverseOnSurface;
}
public void setInverseOnSurface(int inverseOnSurface) {
this.inverseOnSurface = inverseOnSurface;
}
public Scheme withInverseOnSurface(int inverseOnSurface) {
this.inverseOnSurface = inverseOnSurface;
return this;
}
public int getInversePrimary() {
return inversePrimary;
}
public void setInversePrimary(int inversePrimary) {
this.inversePrimary = inversePrimary;
}
public Scheme withInversePrimary(int inversePrimary) {
this.inversePrimary = inversePrimary;
return this;
}
@Override
public String toString() {
return "Scheme{"
+ "primary="
+ primary
+ ", onPrimary="
+ onPrimary
+ ", primaryContainer="
+ primaryContainer
+ ", onPrimaryContainer="
+ onPrimaryContainer
+ ", secondary="
+ secondary
+ ", onSecondary="
+ onSecondary
+ ", secondaryContainer="
+ secondaryContainer
+ ", onSecondaryContainer="
+ onSecondaryContainer
+ ", tertiary="
+ tertiary
+ ", onTertiary="
+ onTertiary
+ ", tertiaryContainer="
+ tertiaryContainer
+ ", onTertiaryContainer="
+ onTertiaryContainer
+ ", error="
+ error
+ ", onError="
+ onError
+ ", errorContainer="
+ errorContainer
+ ", onErrorContainer="
+ onErrorContainer
+ ", background="
+ background
+ ", onBackground="
+ onBackground
+ ", surface="
+ surface
+ ", onSurface="
+ onSurface
+ ", surfaceVariant="
+ surfaceVariant
+ ", onSurfaceVariant="
+ onSurfaceVariant
+ ", outline="
+ outline
+ ", outlineVariant="
+ outlineVariant
+ ", shadow="
+ shadow
+ ", scrim="
+ scrim
+ ", inverseSurface="
+ inverseSurface
+ ", inverseOnSurface="
+ inverseOnSurface
+ ", inversePrimary="
+ inversePrimary
+ '}';
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Scheme)) {
return false;
}
if (!super.equals(object)) {
return false;
}
Scheme scheme = (Scheme) object;
if (primary != scheme.primary) {
return false;
}
if (onPrimary != scheme.onPrimary) {
return false;
}
if (primaryContainer != scheme.primaryContainer) {
return false;
}
if (onPrimaryContainer != scheme.onPrimaryContainer) {
return false;
}
if (secondary != scheme.secondary) {
return false;
}
if (onSecondary != scheme.onSecondary) {
return false;
}
if (secondaryContainer != scheme.secondaryContainer) {
return false;
}
if (onSecondaryContainer != scheme.onSecondaryContainer) {
return false;
}
if (tertiary != scheme.tertiary) {
return false;
}
if (onTertiary != scheme.onTertiary) {
return false;
}
if (tertiaryContainer != scheme.tertiaryContainer) {
return false;
}
if (onTertiaryContainer != scheme.onTertiaryContainer) {
return false;
}
if (error != scheme.error) {
return false;
}
if (onError != scheme.onError) {
return false;
}
if (errorContainer != scheme.errorContainer) {
return false;
}
if (onErrorContainer != scheme.onErrorContainer) {
return false;
}
if (background != scheme.background) {
return false;
}
if (onBackground != scheme.onBackground) {
return false;
}
if (surface != scheme.surface) {
return false;
}
if (onSurface != scheme.onSurface) {
return false;
}
if (surfaceVariant != scheme.surfaceVariant) {
return false;
}
if (onSurfaceVariant != scheme.onSurfaceVariant) {
return false;
}
if (outline != scheme.outline) {
return false;
}
if (outlineVariant != scheme.outlineVariant) {
return false;
}
if (shadow != scheme.shadow) {
return false;
}
if (scrim != scheme.scrim) {
return false;
}
if (inverseSurface != scheme.inverseSurface) {
return false;
}
if (inverseOnSurface != scheme.inverseOnSurface) {
return false;
}
if (inversePrimary != scheme.inversePrimary) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + primary;
result = 31 * result + onPrimary;
result = 31 * result + primaryContainer;
result = 31 * result + onPrimaryContainer;
result = 31 * result + secondary;
result = 31 * result + onSecondary;
result = 31 * result + secondaryContainer;
result = 31 * result + onSecondaryContainer;
result = 31 * result + tertiary;
result = 31 * result + onTertiary;
result = 31 * result + tertiaryContainer;
result = 31 * result + onTertiaryContainer;
result = 31 * result + error;
result = 31 * result + onError;
result = 31 * result + errorContainer;
result = 31 * result + onErrorContainer;
result = 31 * result + background;
result = 31 * result + onBackground;
result = 31 * result + surface;
result = 31 * result + onSurface;
result = 31 * result + surfaceVariant;
result = 31 * result + onSurfaceVariant;
result = 31 * result + outline;
result = 31 * result + outlineVariant;
result = 31 * result + shadow;
result = 31 * result + scrim;
result = 31 * result + inverseSurface;
result = 31 * result + inverseOnSurface;
result = 31 * result + inversePrimary;
return result;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import static java.lang.Math.max;
import com.android.systemui.monet.dislike.DislikeAnalyzer;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.temperature.TemperatureCache;
/**
* A scheme that places the source color in Scheme.primaryContainer.
*
* <p>Primary Container is the source color, adjusted for color relativity. It maintains constant
* appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
* dark mode.
*
* <p>Tertiary Container is an analogous color, specifically, the analog of a color wheel divided
* into 6, and the precise analog is the one found by increasing hue. This is a scientifically
* grounded equivalent to rotating hue clockwise by 60 degrees. It also maintains constant
* appearance.
*/
public class SchemeContent extends DynamicScheme {
public SchemeContent(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.CONTENT,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()),
TonalPalette.fromHueAndChroma(
sourceColorHct.getHue(),
max(sourceColorHct.getChroma() - 32.0, sourceColorHct.getChroma() * 0.5)),
TonalPalette.fromHct(
DislikeAnalyzer.fixIfDisliked(
new TemperatureCache(sourceColorHct)
.getAnalogousColors(/* count= */ 3, /* divisions= */ 6)
.get(2))),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(),
sourceColorHct.getChroma() / 8.0),
TonalPalette.fromHueAndChroma(
sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0));
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.utils.MathUtils;
/** A playful theme - the source color's hue does not appear in the theme. */
public class SchemeExpressive extends DynamicScheme {
// NOMUTANTS--arbitrary increments/decrements, correctly, still passes tests.
private static final double[] HUES = {0, 21, 51, 121, 151, 191, 271, 321, 360};
private static final double[] SECONDARY_ROTATIONS = {45, 95, 45, 20, 45, 90, 45, 45, 45};
private static final double[] TERTIARY_ROTATIONS = {120, 120, 20, 45, 20, 15, 20, 120, 120};
public SchemeExpressive(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.EXPRESSIVE,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 120.0), 40.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS),
24.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
32.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.dislike.DislikeAnalyzer;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.temperature.TemperatureCache;
/**
* A scheme that places the source color in Scheme.primaryContainer.
*
* <p>Primary Container is the source color, adjusted for color relativity. It maintains constant
* appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in
* dark mode.
*
* <p>Tertiary Container is the complement to the source color, using TemperatureCache. It also
* maintains constant appearance.
*/
public class SchemeFidelity extends DynamicScheme {
public SchemeFidelity(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.FIDELITY,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()),
TonalPalette.fromHueAndChroma(
sourceColorHct.getHue(),
Math.max(sourceColorHct.getChroma() - 32.0,
sourceColorHct.getChroma() * 0.5)),
TonalPalette.fromHct(
DislikeAnalyzer.fixIfDisliked(
new TemperatureCache(sourceColorHct).getComplement())),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(),
sourceColorHct.getChroma() / 8.0),
TonalPalette.fromHueAndChroma(
sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0));
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.utils.MathUtils;
/** A playful theme - the source color's hue does not appear in the theme. */
public class SchemeFruitSalad extends DynamicScheme {
public SchemeFruitSalad(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.FRUIT_SALAD,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 48.0),
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 36.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 36.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 10.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0));
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
/** A monochrome theme, colors are purely black / white / gray. */
public class SchemeMonochrome extends DynamicScheme {
public SchemeMonochrome(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.MONOCHROME,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
/** A theme that's slightly more chromatic than monochrome, which is purely black / white / gray. */
public class SchemeNeutral extends DynamicScheme {
public SchemeNeutral(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.NEUTRAL,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0));
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.utils.MathUtils;
/** A playful theme - the source color's hue does not appear in the theme. */
public class SchemeRainbow extends DynamicScheme {
public SchemeRainbow(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.RAINBOW,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 48.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0));
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.utils.MathUtils;
/** A calm theme, sedated colors that aren't particularly chromatic. */
public class SchemeTonalSpot extends DynamicScheme {
public SchemeTonalSpot(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.TONAL_SPOT,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 40.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0),
TonalPalette.fromHueAndChroma(
MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 6.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0));
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
/** A loud theme, colorfulness is maximum for Primary palette, increased for others. */
public class SchemeVibrant extends DynamicScheme {
private static final double[] HUES = {0, 41, 61, 101, 131, 181, 251, 301, 360};
private static final double[] SECONDARY_ROTATIONS = {18, 15, 10, 12, 15, 18, 15, 12, 12};
private static final double[] TERTIARY_ROTATIONS = {35, 30, 20, 25, 30, 35, 30, 25, 25};
public SchemeVibrant(Hct sourceColorHct, boolean isDark, double contrastLevel) {
super(
sourceColorHct,
Variant.VIBRANT,
isDark,
contrastLevel,
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 200.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS),
24.0),
TonalPalette.fromHueAndChroma(
DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS),
32.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0),
TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0));
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.scheme;
/** Themes for Dynamic Color. */
public enum Variant {
MONOCHROME,
NEUTRAL,
TONAL_SPOT,
VIBRANT,
EXPRESSIVE,
FIDELITY,
CONTENT,
RAINBOW,
FRUIT_SALAD
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.score;
import com.android.systemui.monet.hct.Cam16;
import com.android.systemui.monet.utils.ColorUtils;
import com.android.systemui.monet.utils.MathUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Given a large set of colors, remove colors that are unsuitable for a UI theme, and rank the rest
* based on suitability.
*
* <p>Enables use of a high cluster count for image quantization, thus ensuring colors aren't
* muddied, while curating the high cluster count to a much smaller number of appropriate choices.
*/
public final class Score {
private static final double CUTOFF_CHROMA = 15.;
private static final double CUTOFF_EXCITED_PROPORTION = 0.01;
private static final double CUTOFF_TONE = 10.;
private static final double TARGET_CHROMA = 48.;
private static final double WEIGHT_PROPORTION = 0.7;
private static final double WEIGHT_CHROMA_ABOVE = 0.3;
private static final double WEIGHT_CHROMA_BELOW = 0.1;
private Score() {
}
/**
* Given a map with keys of colors and values of how often the color appears, rank the colors
* based on suitability for being used for a UI theme.
*
* @param colorsToPopulation map with keys of colors and values of how often the color appears,
* usually from a source image.
* @return Colors sorted by suitability for a UI theme. The most suitable color is the first
* item,
* the least suitable is the last. There will always be at least one color returned. If all
* the input colors were not suitable for a theme, a default fallback color will be provided.
*/
public static List<Integer> score(Map<Integer, Integer> colorsToPopulation) {
// Determine the total count of all colors.
double populationSum = 0.;
for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
populationSum += entry.getValue();
}
// Turn the count of each color into a proportion by dividing by the total
// count. Also, fill a cache of CAM16 colors representing each color, and
// record the proportion of colors for each CAM16 hue.
Map<Integer, Cam16> colorsToCam = new HashMap<>();
double[] hueProportions = new double[361];
for (Map.Entry<Integer, Integer> entry : colorsToPopulation.entrySet()) {
int color = entry.getKey();
double population = entry.getValue();
double proportion = population / populationSum;
Cam16 cam = Cam16.fromInt(color);
colorsToCam.put(color, cam);
int hue = (int) Math.round(cam.getHue());
hueProportions[hue] += proportion;
}
// Determine the proportion of the colors around each color, by summing the
// proportions around each color's hue.
Map<Integer, Double> colorsToExcitedProportion = new HashMap<>();
for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
int color = entry.getKey();
Cam16 cam = entry.getValue();
int hue = (int) Math.round(cam.getHue());
double excitedProportion = 0.;
for (int j = (hue - 15); j < (hue + 15); j++) {
int neighborHue = MathUtils.sanitizeDegreesInt(j);
excitedProportion += hueProportions[neighborHue];
}
colorsToExcitedProportion.put(color, excitedProportion);
}
// Score the colors by their proportion, as well as how chromatic they are.
Map<Integer, Double> colorsToScore = new HashMap<>();
for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
int color = entry.getKey();
Cam16 cam = entry.getValue();
double proportion = colorsToExcitedProportion.get(color);
double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
double chromaWeight =
cam.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
double chromaScore = (cam.getChroma() - TARGET_CHROMA) * chromaWeight;
double score = proportionScore + chromaScore;
colorsToScore.put(color, score);
}
// Remove colors that are unsuitable, ex. very dark or unchromatic colors.
// Also, remove colors that are very similar in hue.
List<Integer> filteredColors = filter(colorsToExcitedProportion, colorsToCam);
Map<Integer, Double> filteredColorsToScore = new HashMap<>();
for (int color : filteredColors) {
filteredColorsToScore.put(color, colorsToScore.get(color));
}
// Ensure the list of colors returned is sorted such that the first in the
// list is the most suitable, and the last is the least suitable.
List<Map.Entry<Integer, Double>> entryList = new ArrayList<>(
filteredColorsToScore.entrySet());
Collections.sort(entryList, new ScoredComparator());
List<Integer> colorsByScoreDescending = new ArrayList<>();
for (Map.Entry<Integer, Double> entry : entryList) {
int color = entry.getKey();
Cam16 cam = colorsToCam.get(color);
boolean duplicateHue = false;
for (Integer alreadyChosenColor : colorsByScoreDescending) {
Cam16 alreadyChosenCam = colorsToCam.get(alreadyChosenColor);
if (MathUtils.differenceDegrees(cam.getHue(), alreadyChosenCam.getHue()) < 15) {
duplicateHue = true;
break;
}
}
if (duplicateHue) {
continue;
}
colorsByScoreDescending.add(entry.getKey());
}
// Ensure that at least one color is returned.
if (colorsByScoreDescending.isEmpty()) {
colorsByScoreDescending.add(0xff4285F4);
}
return colorsByScoreDescending;
}
private static List<Integer> filter(
Map<Integer, Double> colorsToExcitedProportion, Map<Integer, Cam16> colorsToCam) {
List<Integer> filtered = new ArrayList<>();
for (Map.Entry<Integer, Cam16> entry : colorsToCam.entrySet()) {
int color = entry.getKey();
Cam16 cam = entry.getValue();
double proportion = colorsToExcitedProportion.get(color);
if (cam.getChroma() >= CUTOFF_CHROMA
&& ColorUtils.lstarFromArgb(color) >= CUTOFF_TONE
&& proportion >= CUTOFF_EXCITED_PROPORTION) {
filtered.add(color);
}
}
return filtered;
}
static class ScoredComparator implements Comparator<Map.Entry<Integer, Double>> {
ScoredComparator() {
}
@Override
public int compare(Map.Entry<Integer, Double> entry1, Map.Entry<Integer, Double> entry2) {
return -entry1.getValue().compareTo(entry2.getValue());
}
}
}

View File

@@ -0,0 +1,337 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.temperature;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.utils.ColorUtils;
import com.android.systemui.monet.utils.MathUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Design utilities using color temperature theory.
*
* <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
* calculations when needed.
*/
public final class TemperatureCache {
private final Hct input;
private Hct precomputedComplement;
private List<Hct> precomputedHctsByTemp;
private List<Hct> precomputedHctsByHue;
private Map<Hct, Double> precomputedTempsByHct;
private TemperatureCache() {
throw new UnsupportedOperationException();
}
/**
* Create a cache that allows calculation of ex. complementary and analogous colors.
*
* @param input Color to find complement/analogous colors of. Any colors will have the same
* tone,
* and chroma as the input color, modulo any restrictions due to the other hues
* having lower
* limits on chroma.
*/
public TemperatureCache(Hct input) {
this.input = input;
}
/**
* A color that complements the input color aesthetically.
*
* <p>In art, this is usually described as being across the color wheel. History of this shows
* intent as a color that is just as cool-warm as the input color is warm-cool.
*/
public Hct getComplement() {
if (precomputedComplement != null) {
return precomputedComplement;
}
double coldestHue = getColdest().getHue();
double coldestTemp = getTempsByHct().get(getColdest());
double warmestHue = getWarmest().getHue();
double warmestTemp = getTempsByHct().get(getWarmest());
double range = warmestTemp - coldestTemp;
boolean startHueIsColdestToWarmest = isBetween(input.getHue(), coldestHue, warmestHue);
double startHue = startHueIsColdestToWarmest ? warmestHue : coldestHue;
double endHue = startHueIsColdestToWarmest ? coldestHue : warmestHue;
double directionOfRotation = 1.;
double smallestError = 1000.;
Hct answer = getHctsByHue().get((int) Math.round(input.getHue()));
double complementRelativeTemp = (1. - getRelativeTemperature(input));
// Find the color in the other section, closest to the inverse percentile
// of the input color. This is the complement.
for (double hueAddend = 0.; hueAddend <= 360.; hueAddend += 1.) {
double hue = MathUtils.sanitizeDegreesDouble(
startHue + directionOfRotation * hueAddend);
if (!isBetween(hue, startHue, endHue)) {
continue;
}
Hct possibleAnswer = getHctsByHue().get((int) Math.round(hue));
double relativeTemp =
(getTempsByHct().get(possibleAnswer) - coldestTemp) / range;
double error = Math.abs(complementRelativeTemp - relativeTemp);
if (error < smallestError) {
smallestError = error;
answer = possibleAnswer;
}
}
precomputedComplement = answer;
return precomputedComplement;
}
/**
* 5 colors that pair well with the input color.
*
* <p>The colors are equidistant in temperature and adjacent in hue.
*/
public List<Hct> getAnalogousColors() {
return getAnalogousColors(5, 12);
}
/**
* A set of colors with differing hues, equidistant in temperature.
*
* <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12
* sections. This method allows provision of either of those values.
*
* <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
*
* @param count The number of colors to return, includes the input color.
* @param divisions The number of divisions on the color wheel.
*/
public List<Hct> getAnalogousColors(int count, int divisions) {
// The starting hue is the hue of the input color.
int startHue = (int) Math.round(input.getHue());
Hct startHct = getHctsByHue().get(startHue);
double lastTemp = getRelativeTemperature(startHct);
List<Hct> allColors = new ArrayList<>();
allColors.add(startHct);
double absoluteTotalTempDelta = 0.f;
for (int i = 0; i < 360; i++) {
int hue = MathUtils.sanitizeDegreesInt(startHue + i);
Hct hct = getHctsByHue().get(hue);
double temp = getRelativeTemperature(hct);
double tempDelta = Math.abs(temp - lastTemp);
lastTemp = temp;
absoluteTotalTempDelta += tempDelta;
}
int hueAddend = 1;
double tempStep = absoluteTotalTempDelta / (double) divisions;
double totalTempDelta = 0.0;
lastTemp = getRelativeTemperature(startHct);
while (allColors.size() < divisions) {
int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend);
Hct hct = getHctsByHue().get(hue);
double temp = getRelativeTemperature(hct);
double tempDelta = Math.abs(temp - lastTemp);
totalTempDelta += tempDelta;
double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep);
boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
int indexAddend = 1;
// Keep adding this hue to the answers until its temperature is
// insufficient. This ensures consistent behavior when there aren't
// `divisions` discrete steps between 0 and 360 in hue with `tempStep`
// delta in temperature between them.
//
// For example, white and black have no analogues: there are no other
// colors at T100/T0. Therefore, they should just be added to the array
// as answers.
while (indexSatisfied && allColors.size() < divisions) {
allColors.add(hct);
desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep);
indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
indexAddend++;
}
lastTemp = temp;
hueAddend++;
if (hueAddend > 360) {
while (allColors.size() < divisions) {
allColors.add(hct);
}
break;
}
}
List<Hct> answers = new ArrayList<>();
answers.add(input);
int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0);
for (int i = 1; i < (ccwCount + 1); i++) {
int index = 0 - i;
while (index < 0) {
index = allColors.size() + index;
}
if (index >= allColors.size()) {
index = index % allColors.size();
}
answers.add(0, allColors.get(index));
}
int cwCount = count - ccwCount - 1;
for (int i = 1; i < (cwCount + 1); i++) {
int index = i;
while (index < 0) {
index = allColors.size() + index;
}
if (index >= allColors.size()) {
index = index % allColors.size();
}
answers.add(allColors.get(index));
}
return answers;
}
/**
* Temperature relative to all colors with the same chroma and tone.
*
* @param hct HCT to find the relative temperature of.
* @return Value on a scale from 0 to 1.
*/
public double getRelativeTemperature(Hct hct) {
double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest());
double differenceFromColdest =
getTempsByHct().get(hct) - getTempsByHct().get(getColdest());
// Handle when there's no difference in temperature between warmest and
// coldest: for example, at T100, only one color is available, white.
if (range == 0.) {
return 0.5;
}
return differenceFromColdest / range;
}
/**
* Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
* warm.
*
* <p>Color science has researched emotion and harmony, which art uses to select colors.
* Warm-cool
* is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in
* Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and
* 21.
*
* <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
* Return value has these properties:<br>
* - Values below 0 are cool, above 0 are warm.<br>
* - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br>
* - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
*/
public static double rawTemperature(Hct color) {
double[] lab = ColorUtils.labFromArgb(color.toInt());
double hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(Math.atan2(lab[2], lab[1])));
double chroma = Math.hypot(lab[1], lab[2]);
return -0.5
+ 0.02
* Math.pow(chroma, 1.07)
* Math.cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.)));
}
/** Coldest color with same chroma and tone as input. */
private Hct getColdest() {
return getHctsByTemp().get(0);
}
/**
* HCTs for all colors with the same chroma/tone as the input.
*
* <p>Sorted by hue, ex. index 0 is hue 0.
*/
private List<Hct> getHctsByHue() {
if (precomputedHctsByHue != null) {
return precomputedHctsByHue;
}
List<Hct> hcts = new ArrayList<>();
for (double hue = 0.; hue <= 360.; hue += 1.) {
Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone());
hcts.add(colorAtHue);
}
precomputedHctsByHue = Collections.unmodifiableList(hcts);
return precomputedHctsByHue;
}
/**
* HCTs for all colors with the same chroma/tone as the input.
*
* <p>Sorted from coldest first to warmest last.
*/
// Prevent lint for Comparator not being available on Android before API level 24, 7.0, 2016.
// "AndroidJdkLibsChecker" for one linter, "NewApi" for another.
// A java_library Bazel rule with an Android constraint cannot skip these warnings without this
// annotation; another solution would be to create an android_library rule and supply
// AndroidManifest with an SDK set higher than 23.
@SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
private List<Hct> getHctsByTemp() {
if (precomputedHctsByTemp != null) {
return precomputedHctsByTemp;
}
List<Hct> hcts = new ArrayList<>(getHctsByHue());
hcts.add(input);
Comparator<Hct> temperaturesComparator =
Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo);
Collections.sort(hcts, temperaturesComparator);
precomputedHctsByTemp = hcts;
return precomputedHctsByTemp;
}
/** Keys of HCTs in getHctsByTemp, values of raw temperature. */
private Map<Hct, Double> getTempsByHct() {
if (precomputedTempsByHct != null) {
return precomputedTempsByHct;
}
List<Hct> allHcts = new ArrayList<>(getHctsByHue());
allHcts.add(input);
Map<Hct, Double> temperaturesByHct = new HashMap<>();
for (Hct hct : allHcts) {
temperaturesByHct.put(hct, rawTemperature(hct));
}
precomputedTempsByHct = temperaturesByHct;
return precomputedTempsByHct;
}
/** Warmest color with same chroma and tone as input. */
private Hct getWarmest() {
return getHctsByTemp().get(getHctsByTemp().size() - 1);
}
/** Determines if an angle is between two other angles, rotating clockwise. */
private static boolean isBetween(double angle, double a, double b) {
if (a < b) {
return a <= angle && angle <= b;
}
return a <= angle || angle <= b;
}
}

View File

@@ -0,0 +1,266 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.utils;
/**
* Color science utilities.
*
* <p>Utility methods for color science constants and color space conversions that aren't HCT or
* CAM16.
*/
public class ColorUtils {
private ColorUtils() {
}
static final double[][] SRGB_TO_XYZ =
new double[][]{
new double[]{0.41233895, 0.35762064, 0.18051042},
new double[]{0.2126, 0.7152, 0.0722},
new double[]{0.01932141, 0.11916382, 0.95034478},
};
static final double[][] XYZ_TO_SRGB =
new double[][]{
new double[]{
3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
},
new double[]{
-0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
},
new double[]{
0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
},
};
static final double[] WHITE_POINT_D65 = new double[]{95.047, 100.0, 108.883};
/** Converts a color from RGB components to ARGB format. */
public static int argbFromRgb(int red, int green, int blue) {
return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
}
/** Converts a color from linear RGB components to ARGB format. */
public static int argbFromLinrgb(double[] linrgb) {
int r = delinearized(linrgb[0]);
int g = delinearized(linrgb[1]);
int b = delinearized(linrgb[2]);
return argbFromRgb(r, g, b);
}
/** Returns the alpha component of a color in ARGB format. */
public static int alphaFromArgb(int argb) {
return (argb >> 24) & 255;
}
/** Returns the red component of a color in ARGB format. */
public static int redFromArgb(int argb) {
return (argb >> 16) & 255;
}
/** Returns the green component of a color in ARGB format. */
public static int greenFromArgb(int argb) {
return (argb >> 8) & 255;
}
/** Returns the blue component of a color in ARGB format. */
public static int blueFromArgb(int argb) {
return argb & 255;
}
/** Returns whether a color in ARGB format is opaque. */
public static boolean isOpaque(int argb) {
return alphaFromArgb(argb) >= 255;
}
/** Converts a color from ARGB to XYZ. */
public static int argbFromXyz(double x, double y, double z) {
double[][] matrix = XYZ_TO_SRGB;
double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z;
double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z;
double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z;
int r = delinearized(linearR);
int g = delinearized(linearG);
int b = delinearized(linearB);
return argbFromRgb(r, g, b);
}
/** Converts a color from XYZ to ARGB. */
public static double[] xyzFromArgb(int argb) {
double r = linearized(redFromArgb(argb));
double g = linearized(greenFromArgb(argb));
double b = linearized(blueFromArgb(argb));
return MathUtils.matrixMultiply(new double[]{r, g, b}, SRGB_TO_XYZ);
}
/** Converts a color represented in Lab color space into an ARGB integer. */
public static int argbFromLab(double l, double a, double b) {
double[] whitePoint = WHITE_POINT_D65;
double fy = (l + 16.0) / 116.0;
double fx = a / 500.0 + fy;
double fz = fy - b / 200.0;
double xNormalized = labInvf(fx);
double yNormalized = labInvf(fy);
double zNormalized = labInvf(fz);
double x = xNormalized * whitePoint[0];
double y = yNormalized * whitePoint[1];
double z = zNormalized * whitePoint[2];
return argbFromXyz(x, y, z);
}
/**
* Converts a color from ARGB representation to L*a*b* representation.
*
* @param argb the ARGB representation of a color
* @return a Lab object representing the color
*/
public static double[] labFromArgb(int argb) {
double linearR = linearized(redFromArgb(argb));
double linearG = linearized(greenFromArgb(argb));
double linearB = linearized(blueFromArgb(argb));
double[][] matrix = SRGB_TO_XYZ;
double x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
double y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
double z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
double[] whitePoint = WHITE_POINT_D65;
double xNormalized = x / whitePoint[0];
double yNormalized = y / whitePoint[1];
double zNormalized = z / whitePoint[2];
double fx = labF(xNormalized);
double fy = labF(yNormalized);
double fz = labF(zNormalized);
double l = 116.0 * fy - 16;
double a = 500.0 * (fx - fy);
double b = 200.0 * (fy - fz);
return new double[]{l, a, b};
}
/**
* Converts an L* value to an ARGB representation.
*
* @param lstar L* in L*a*b*
* @return ARGB representation of grayscale color with lightness matching L*
*/
public static int argbFromLstar(double lstar) {
double y = yFromLstar(lstar);
int component = delinearized(y);
return argbFromRgb(component, component, component);
}
/**
* Computes the L* value of a color in ARGB representation.
*
* @param argb ARGB representation of a color
* @return L*, from L*a*b*, coordinate of the color
*/
public static double lstarFromArgb(int argb) {
double y = xyzFromArgb(argb)[1];
return 116.0 * labF(y / 100.0) - 16.0;
}
/**
* Converts an L* value to a Y value.
*
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
* logarithmic scale.
*
* @param lstar L* in L*a*b*
* @return Y in XYZ
*/
public static double yFromLstar(double lstar) {
return 100.0 * labInvf((lstar + 16.0) / 116.0);
}
/**
* Converts a Y value to an L* value.
*
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
* logarithmic scale.
*
* @param y Y in XYZ
* @return L* in L*a*b*
*/
public static double lstarFromY(double y) {
return labF(y / 100.0) * 116.0 - 16.0;
}
/**
* Linearizes an RGB component.
*
* @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
* @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
*/
public static double linearized(int rgbComponent) {
double normalized = rgbComponent / 255.0;
if (normalized <= 0.040449936) {
return normalized / 12.92 * 100.0;
} else {
return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
}
}
/**
* Delinearizes an RGB component.
*
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
* @return 0 <= output <= 255, color channel converted to regular RGB space
*/
public static int delinearized(double rgbComponent) {
double normalized = rgbComponent / 100.0;
double delinearized = 0.0;
if (normalized <= 0.0031308) {
delinearized = normalized * 12.92;
} else {
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
}
return MathUtils.clampInt(0, 255, (int) Math.round(delinearized * 255.0));
}
/**
* Returns the standard white point; white on a sunny day.
*
* @return The white point
*/
public static double[] whitePointD65() {
return WHITE_POINT_D65;
}
static double labF(double t) {
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
if (t > e) {
return Math.pow(t, 1.0 / 3.0);
} else {
return (kappa * t + 16) / 116;
}
}
static double labInvf(double ft) {
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
double ft3 = ft * ft * ft;
if (ft3 > e) {
return ft3;
} else {
return (116 * ft - 16) / kappa;
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.utils;
/** Utility methods for mathematical operations. */
public class MathUtils {
private MathUtils() {
}
/**
* The signum function.
*
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
*/
public static int signum(double num) {
if (num < 0) {
return -1;
} else if (num == 0) {
return 0;
} else {
return 1;
}
}
/**
* The linear interpolation function.
*
* @return start if amount = 0 and stop if amount = 1
*/
public static double lerp(double start, double stop, double amount) {
return (1.0 - amount) * start + amount * stop;
}
/**
* Clamps an integer between two integers.
*
* @return input when min <= input <= max, and either min or max otherwise.
*/
public static int clampInt(int min, int max, int input) {
if (input < min) {
return min;
} else if (input > max) {
return max;
}
return input;
}
/**
* Clamps an integer between two floating-point numbers.
*
* @return input when min <= input <= max, and either min or max otherwise.
*/
public static double clampDouble(double min, double max, double input) {
if (input < min) {
return min;
} else if (input > max) {
return max;
}
return input;
}
/**
* Sanitizes a degree measure as an integer.
*
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
*/
public static int sanitizeDegreesInt(int degrees) {
degrees = degrees % 360;
if (degrees < 0) {
degrees = degrees + 360;
}
return degrees;
}
/**
* Sanitizes a degree measure as a floating-point number.
*
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
*/
public static double sanitizeDegreesDouble(double degrees) {
degrees = degrees % 360.0;
if (degrees < 0) {
degrees = degrees + 360.0;
}
return degrees;
}
/**
* Sign of direction change needed to travel from one angle to another.
*
* <p>For angles that are 180 degrees apart from each other, both directions have the same
* travel
* distance, so either direction is shortest. The value 1.0 is returned in this case.
*
* @param from The angle travel starts from, in degrees.
* @param to The angle travel ends at, in degrees.
* @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from
* leads
* to the shortest travel distance.
*/
public static double rotationDirection(double from, double to) {
double increasingDifference = sanitizeDegreesDouble(to - from);
return increasingDifference <= 180.0 ? 1.0 : -1.0;
}
/** Distance of two points on a circle, represented using degrees. */
public static double differenceDegrees(double a, double b) {
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
}
/** Multiplies a 1x3 row vector with a 3x3 matrix. */
public static double[] matrixMultiply(double[] row, double[][] matrix) {
double a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
double b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
return new double[]{a, b, c};
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.utils;
/** Utility methods for string representations of colors. */
final class StringUtils {
private StringUtils() {
}
/**
* Hex string representing color, ex. #ff0000 for red.
*
* @param argb ARGB representation of a color.
*/
public static String hexFromArgb(int argb) {
int red = ColorUtils.redFromArgb(argb);
int blue = ColorUtils.blueFromArgb(argb);
int green = ColorUtils.greenFromArgb(argb);
return String.format("#%02x%02x%02x", red, green, blue);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.blend.Blend;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class BlendTest extends SysuiTestCase {
private static final int RED = 0xffff0000;
private static final int BLUE = 0xff0000ff;
private static final int GREEN = 0xff00ff00;
private static final int YELLOW = 0xffffff00;
@Test
public void harmonize_redToBlue() {
int blended = Blend.harmonize(RED, BLUE);
assertThat(blended).isSameColorAs(0xffFB0057);
}
@Test
public void harmonize_redToGreen() {
int answer = Blend.harmonize(RED, GREEN);
assertThat(answer).isSameColorAs(0xffD85600);
}
@Test
public void harmonize_redToYellow() {
int answer = Blend.harmonize(RED, YELLOW);
assertThat(answer).isSameColorAs(0xffD85600);
}
@Test
public void harmonize_blueToGreen() {
int answer = Blend.harmonize(BLUE, GREEN);
assertThat(answer).isSameColorAs(0xff0047A3);
}
@Test
public void harmonize_blueToRed() {
int answer = Blend.harmonize(BLUE, RED);
assertThat(answer).isSameColorAs(0xff5700DC);
}
@Test
public void harmonize_blueToYellow() {
int answer = Blend.harmonize(BLUE, YELLOW);
assertThat(answer).isSameColorAs(0xff0047A3);
}
@Test
public void harmonize_greenToBlue() {
int answer = Blend.harmonize(GREEN, BLUE);
assertThat(answer).isSameColorAs(0xff00FC94);
}
@Test
public void harmonize_greenToRed() {
int answer = Blend.harmonize(GREEN, RED);
assertThat(answer).isSameColorAs(0xffB1F000);
}
@Test
public void harmonize_greenToYellow() {
int answer = Blend.harmonize(GREEN, YELLOW);
assertThat(answer).isSameColorAs(0xffB1F000);
}
@Test
public void harmonize_yellowToBlue() {
int answer = Blend.harmonize(YELLOW, BLUE);
assertThat(answer).isSameColorAs(0xffEBFFBA);
}
@Test
public void harmonize_yellowToGreen() {
int answer = Blend.harmonize(YELLOW, GREEN);
assertThat(answer).isSameColorAs(0xffEBFFBA);
}
@Test
public void harmonize_yellowToRed() {
int answer = Blend.harmonize(YELLOW, RED);
assertThat(answer).isSameColorAs(0xffFFF6E3);
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.utils.ColorUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class ColorUtilsTest extends SysuiTestCase {
@Test
public void argbFromLab() {
assertThat(ColorUtils.argbFromLab(0.0, 0.0, 0.0)).isEqualTo(0xff000000);
assertThat(ColorUtils.argbFromLab(0.25, 0.0, 0.0)).isEqualTo(0xff010101);
assertThat(ColorUtils.argbFromLab(0.5, 0.0, 0.0)).isEqualTo(0xff020202);
assertThat(ColorUtils.argbFromLab(0.75, 0.0, 0.0)).isEqualTo(0xff030303);
assertThat(ColorUtils.argbFromLab(1.0, 0.0, 0.0)).isEqualTo(0xff040404);
assertThat(ColorUtils.argbFromLab(1.5, 0.0, 0.0)).isEqualTo(0xff050505);
assertThat(ColorUtils.argbFromLab(2.0, 0.0, 0.0)).isEqualTo(0xff070707);
assertThat(ColorUtils.argbFromLab(3.0, 0.0, 0.0)).isEqualTo(0xff0b0b0b);
assertThat(ColorUtils.argbFromLab(4.0, 0.0, 0.0)).isEqualTo(0xff0e0e0e);
assertThat(ColorUtils.argbFromLab(5.0, 0.0, 0.0)).isEqualTo(0xff111111);
assertThat(ColorUtils.argbFromLab(6.0, 0.0, 0.0)).isEqualTo(0xff131313);
assertThat(ColorUtils.argbFromLab(7.0, 0.0, 0.0)).isEqualTo(0xff151515);
assertThat(ColorUtils.argbFromLab(8.0, 0.0, 0.0)).isEqualTo(0xff181818);
assertThat(ColorUtils.argbFromLab(9.0, 0.0, 0.0)).isEqualTo(0xff191919);
assertThat(ColorUtils.argbFromLab(10.0, 0.0, 0.0)).isEqualTo(0xff1b1b1b);
assertThat(ColorUtils.argbFromLab(20.0, 0.0, 0.0)).isEqualTo(0xff303030);
assertThat(ColorUtils.argbFromLab(30.0, 0.0, 0.0)).isEqualTo(0xff474747);
assertThat(ColorUtils.argbFromLab(40.0, 0.0, 0.0)).isEqualTo(0xff5e5e5e);
assertThat(ColorUtils.argbFromLab(50.0, 0.0, 0.0)).isEqualTo(0xff777777);
assertThat(ColorUtils.argbFromLab(60.0, 0.0, 0.0)).isEqualTo(0xff919191);
assertThat(ColorUtils.argbFromLab(70.0, 0.0, 0.0)).isEqualTo(0xffababab);
assertThat(ColorUtils.argbFromLab(80.0, 0.0, 0.0)).isEqualTo(0xffc6c6c6);
assertThat(ColorUtils.argbFromLab(90.0, 0.0, 0.0)).isEqualTo(0xffe2e2e2);
assertThat(ColorUtils.argbFromLab(100.0, 0.0, 0.0)).isEqualTo(0xffffffff);
}
@Test
public void labFromArgb() {
assertThat(ColorUtils.labFromArgb(0xff000000)[0]).isWithin(0.001).of(0.0);
assertThat(ColorUtils.labFromArgb(0xff010101)[0]).isWithin(0.001).of(0.2741748000656514);
assertThat(ColorUtils.labFromArgb(0xff020202)[0]).isWithin(0.001).of(0.5483496001313029);
assertThat(ColorUtils.labFromArgb(0xff030303)[0]).isWithin(0.001).of(0.8225244001969543);
assertThat(ColorUtils.labFromArgb(0xff040404)[0]).isWithin(0.001).of(1.0966992002626057);
assertThat(ColorUtils.labFromArgb(0xff050505)[0]).isWithin(0.001).of(1.3708740003282571);
assertThat(ColorUtils.labFromArgb(0xff060606)[0]).isWithin(0.001).of(1.645048800393912);
assertThat(ColorUtils.labFromArgb(0xff070707)[0]).isWithin(0.001).of(1.9192236004595635);
assertThat(ColorUtils.labFromArgb(0xff080808)[0]).isWithin(0.001).of(2.193398400525215);
assertThat(ColorUtils.labFromArgb(0xff0c0c0c)[0]).isWithin(0.001).of(3.3209754491182544);
assertThat(ColorUtils.labFromArgb(0xff101010)[0]).isWithin(0.001).of(4.680444846419661);
assertThat(ColorUtils.labFromArgb(0xff181818)[0]).isWithin(0.001).of(8.248186036170349);
assertThat(ColorUtils.labFromArgb(0xff202020)[0]).isWithin(0.001).of(12.250030101522828);
assertThat(ColorUtils.labFromArgb(0xff404040)[0]).isWithin(0.001).of(27.093413739449055);
assertThat(ColorUtils.labFromArgb(0xff808080)[0]).isWithin(0.001).of(53.585013452169036);
assertThat(ColorUtils.labFromArgb(0xffffffff)[0]).isWithin(0.001).of(100.0);
}
@Test
public void argbFromLstar() {
assertThat(ColorUtils.argbFromLstar(0.0)).isEqualTo(0xff000000);
assertThat(ColorUtils.argbFromLstar(0.25)).isEqualTo(0xff010101);
assertThat(ColorUtils.argbFromLstar(0.5)).isEqualTo(0xff020202);
assertThat(ColorUtils.argbFromLstar(0.75)).isEqualTo(0xff030303);
assertThat(ColorUtils.argbFromLstar(1.0)).isEqualTo(0xff040404);
assertThat(ColorUtils.argbFromLstar(1.5)).isEqualTo(0xff050505);
assertThat(ColorUtils.argbFromLstar(2.0)).isEqualTo(0xff070707);
assertThat(ColorUtils.argbFromLstar(3.0)).isEqualTo(0xff0b0b0b);
assertThat(ColorUtils.argbFromLstar(4.0)).isEqualTo(0xff0e0e0e);
assertThat(ColorUtils.argbFromLstar(5.0)).isEqualTo(0xff111111);
assertThat(ColorUtils.argbFromLstar(6.0)).isEqualTo(0xff131313);
assertThat(ColorUtils.argbFromLstar(7.0)).isEqualTo(0xff151515);
assertThat(ColorUtils.argbFromLstar(8.0)).isEqualTo(0xff181818);
assertThat(ColorUtils.argbFromLstar(9.0)).isEqualTo(0xff191919);
assertThat(ColorUtils.argbFromLstar(10.0)).isEqualTo(0xff1b1b1b);
assertThat(ColorUtils.argbFromLstar(20.0)).isEqualTo(0xff303030);
assertThat(ColorUtils.argbFromLstar(30.0)).isEqualTo(0xff474747);
assertThat(ColorUtils.argbFromLstar(40.0)).isEqualTo(0xff5e5e5e);
assertThat(ColorUtils.argbFromLstar(50.0)).isEqualTo(0xff777777);
assertThat(ColorUtils.argbFromLstar(60.0)).isEqualTo(0xff919191);
assertThat(ColorUtils.argbFromLstar(70.0)).isEqualTo(0xffababab);
assertThat(ColorUtils.argbFromLstar(80.0)).isEqualTo(0xffc6c6c6);
assertThat(ColorUtils.argbFromLstar(90.0)).isEqualTo(0xffe2e2e2);
assertThat(ColorUtils.argbFromLstar(100.0)).isEqualTo(0xffffffff);
}
@Test
public void lstarFromArgb() {
assertThat(ColorUtils.lstarFromArgb(0xff000000)).isWithin(0.001).of(0.0);
assertThat(ColorUtils.lstarFromArgb(0xff010101)).isWithin(0.001).of(0.2741748000656518);
assertThat(ColorUtils.lstarFromArgb(0xff020202)).isWithin(0.001).of(0.5483496001313036);
assertThat(ColorUtils.lstarFromArgb(0xff030303)).isWithin(0.001).of(0.8225244001969553);
assertThat(ColorUtils.lstarFromArgb(0xff040404)).isWithin(0.001).of(1.0966992002626073);
assertThat(ColorUtils.lstarFromArgb(0xff050505)).isWithin(0.001).of(1.3708740003282587);
assertThat(ColorUtils.lstarFromArgb(0xff060606)).isWithin(0.001).of(1.6450488003939105);
assertThat(ColorUtils.lstarFromArgb(0xff070707)).isWithin(0.001).of(1.9192236004595624);
assertThat(ColorUtils.lstarFromArgb(0xff080808)).isWithin(0.001).of(2.1933984005252145);
assertThat(ColorUtils.lstarFromArgb(0xff0c0c0c)).isWithin(0.001).of(3.3209754491182557);
assertThat(ColorUtils.lstarFromArgb(0xff101010)).isWithin(0.001).of(4.680444846419663);
assertThat(ColorUtils.lstarFromArgb(0xff181818)).isWithin(0.001).of(8.248186036170349);
assertThat(ColorUtils.lstarFromArgb(0xff202020)).isWithin(0.001).of(12.250030101522828);
assertThat(ColorUtils.lstarFromArgb(0xff404040)).isWithin(0.001).of(27.093413739449055);
assertThat(ColorUtils.lstarFromArgb(0xff808080)).isWithin(0.001).of(53.585013452169036);
assertThat(ColorUtils.lstarFromArgb(0xffffffff)).isWithin(0.001).of(100.0);
}
@Test
public void yFromLstar() {
assertThat(ColorUtils.yFromLstar(0.0)).isWithin(1e-5).of(0.0);
assertThat(ColorUtils.yFromLstar(0.1)).isWithin(1e-5).of(0.0110705);
assertThat(ColorUtils.yFromLstar(0.2)).isWithin(1e-5).of(0.0221411);
assertThat(ColorUtils.yFromLstar(0.3)).isWithin(1e-5).of(0.0332116);
assertThat(ColorUtils.yFromLstar(0.4)).isWithin(1e-5).of(0.0442822);
assertThat(ColorUtils.yFromLstar(0.5)).isWithin(1e-5).of(0.0553528);
assertThat(ColorUtils.yFromLstar(1.0)).isWithin(1e-5).of(0.1107056);
assertThat(ColorUtils.yFromLstar(2.0)).isWithin(1e-5).of(0.2214112);
assertThat(ColorUtils.yFromLstar(3.0)).isWithin(1e-5).of(0.3321169);
assertThat(ColorUtils.yFromLstar(4.0)).isWithin(1e-5).of(0.4428225);
assertThat(ColorUtils.yFromLstar(5.0)).isWithin(1e-5).of(0.5535282);
assertThat(ColorUtils.yFromLstar(8.0)).isWithin(1e-5).of(0.8856451);
assertThat(ColorUtils.yFromLstar(10.0)).isWithin(1e-5).of(1.1260199);
assertThat(ColorUtils.yFromLstar(15.0)).isWithin(1e-5).of(1.9085832);
assertThat(ColorUtils.yFromLstar(20.0)).isWithin(1e-5).of(2.9890524);
assertThat(ColorUtils.yFromLstar(25.0)).isWithin(1e-5).of(4.4154767);
assertThat(ColorUtils.yFromLstar(30.0)).isWithin(1e-5).of(6.2359055);
assertThat(ColorUtils.yFromLstar(40.0)).isWithin(1e-5).of(11.2509737);
assertThat(ColorUtils.yFromLstar(50.0)).isWithin(1e-5).of(18.4186518);
assertThat(ColorUtils.yFromLstar(60.0)).isWithin(1e-5).of(28.1233342);
assertThat(ColorUtils.yFromLstar(70.0)).isWithin(1e-5).of(40.7494157);
assertThat(ColorUtils.yFromLstar(80.0)).isWithin(1e-5).of(56.6812907);
assertThat(ColorUtils.yFromLstar(90.0)).isWithin(1e-5).of(76.3033539);
assertThat(ColorUtils.yFromLstar(95.0)).isWithin(1e-5).of(87.6183294);
assertThat(ColorUtils.yFromLstar(99.0)).isWithin(1e-5).of(97.4360239);
assertThat(ColorUtils.yFromLstar(100.0)).isWithin(1e-5).of(100.0);
}
@Test
public void lstarFromY() {
assertThat(ColorUtils.lstarFromY(0.0)).isWithin(1e-5).of(0.0);
assertThat(ColorUtils.lstarFromY(0.1)).isWithin(1e-5).of(0.9032962);
assertThat(ColorUtils.lstarFromY(0.2)).isWithin(1e-5).of(1.8065925);
assertThat(ColorUtils.lstarFromY(0.3)).isWithin(1e-5).of(2.7098888);
assertThat(ColorUtils.lstarFromY(0.4)).isWithin(1e-5).of(3.6131851);
assertThat(ColorUtils.lstarFromY(0.5)).isWithin(1e-5).of(4.5164814);
assertThat(ColorUtils.lstarFromY(0.8856451)).isWithin(1e-5).of(8.0);
assertThat(ColorUtils.lstarFromY(1.0)).isWithin(1e-5).of(8.9914424);
assertThat(ColorUtils.lstarFromY(2.0)).isWithin(1e-5).of(15.4872443);
assertThat(ColorUtils.lstarFromY(3.0)).isWithin(1e-5).of(20.0438970);
assertThat(ColorUtils.lstarFromY(4.0)).isWithin(1e-5).of(23.6714419);
assertThat(ColorUtils.lstarFromY(5.0)).isWithin(1e-5).of(26.7347653);
assertThat(ColorUtils.lstarFromY(10.0)).isWithin(1e-5).of(37.8424304);
assertThat(ColorUtils.lstarFromY(15.0)).isWithin(1e-5).of(45.6341970);
assertThat(ColorUtils.lstarFromY(20.0)).isWithin(1e-5).of(51.8372115);
assertThat(ColorUtils.lstarFromY(25.0)).isWithin(1e-5).of(57.0754208);
assertThat(ColorUtils.lstarFromY(30.0)).isWithin(1e-5).of(61.6542222);
assertThat(ColorUtils.lstarFromY(40.0)).isWithin(1e-5).of(69.4695307);
assertThat(ColorUtils.lstarFromY(50.0)).isWithin(1e-5).of(76.0692610);
assertThat(ColorUtils.lstarFromY(60.0)).isWithin(1e-5).of(81.8381891);
assertThat(ColorUtils.lstarFromY(70.0)).isWithin(1e-5).of(86.9968642);
assertThat(ColorUtils.lstarFromY(80.0)).isWithin(1e-5).of(91.6848609);
assertThat(ColorUtils.lstarFromY(90.0)).isWithin(1e-5).of(95.9967686);
assertThat(ColorUtils.lstarFromY(95.0)).isWithin(1e-5).of(98.0335184);
assertThat(ColorUtils.lstarFromY(99.0)).isWithin(1e-5).of(99.6120372);
assertThat(ColorUtils.lstarFromY(100.0)).isWithin(1e-5).of(100.0);
}
@Test
public void yToLstarToY() {
for (double y = 0.0; y <= 100.0; y += 0.1) {
double lstar = ColorUtils.lstarFromY(y);
double reconstructedY = ColorUtils.yFromLstar(lstar);
assertThat(reconstructedY).isWithin(1e-8).of(y);
}
}
@Test
public void lstarToYToLstar() {
for (double lstar = 0.0; lstar <= 100.0; lstar += 0.1) {
double y = ColorUtils.yFromLstar(lstar);
double reconstructedLstar = ColorUtils.lstarFromY(y);
assertThat(reconstructedLstar).isWithin(1e-8).of(lstar);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static org.junit.Assert.assertEquals;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.contrast.Contrast;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class ContrastTest extends SysuiTestCase {
@Test
public void lighter_impossibleRatioErrors() {
assertEquals(-1.0, Contrast.lighter(90.0, 10.0), 0.001);
}
@Test
public void lighter_outOfBoundsInputAboveErrors() {
assertEquals(-1.0, Contrast.lighter(110.0, 2.0), 0.001);
}
@Test
public void lighter_outOfBoundsInputBelowErrors() {
assertEquals(-1.0, Contrast.lighter(-10.0, 2.0), 0.001);
}
@Test
public void lighterUnsafe_returnsMaxTone() {
assertEquals(100, Contrast.lighterUnsafe(100.0, 2.0), 0.001);
}
@Test
public void darker_impossibleRatioErrors() {
assertEquals(-1.0, Contrast.darker(10.0, 20.0), 0.001);
}
@Test
public void darker_outOfBoundsInputAboveErrors() {
assertEquals(-1.0, Contrast.darker(110.0, 2.0), 0.001);
}
@Test
public void darker_outOfBoundsInputBelowErrors() {
assertEquals(-1.0, Contrast.darker(-10.0, 2.0), 0.001);
}
@Test
public void darkerUnsafe_returnsMinTone() {
assertEquals(0.0, Contrast.darkerUnsafe(0.0, 2.0), 0.001);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dislike.DislikeAnalyzer;
import com.android.systemui.monet.hct.Hct;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class DislikeTest extends SysuiTestCase {
@Test
public void monkSkinToneScaleColorsLiked() {
// From https://skintone.google#/get-started
int[] monkSkinToneScaleColors =
new int[]{
0xfff6ede4,
0xfff3e7db,
0xfff7ead0,
0xffeadaba,
0xffd7bd96,
0xffa07e56,
0xff825c43,
0xff604134,
0xff3a312a,
0xff292420,
};
for (int color : monkSkinToneScaleColors) {
assertFalse(DislikeAnalyzer.isDisliked(Hct.fromInt(color)));
}
}
@Test
public void bileColorsDisliked() {
int[] unlikable =
new int[]{
0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, 0xff464521,
};
for (int color : unlikable) {
assertTrue(DislikeAnalyzer.isDisliked(Hct.fromInt(color)));
}
}
@Test
public void bileColorsBecameLikable() {
int[] unlikable =
new int[]{
0xff95884B, 0xff716B40, 0xffB08E00, 0xff4C4308, 0xff464521,
};
for (int color : unlikable) {
Hct hct = Hct.fromInt(color);
assertTrue(DislikeAnalyzer.isDisliked(hct));
Hct likable = DislikeAnalyzer.fixIfDisliked(hct);
assertFalse(DislikeAnalyzer.isDisliked(likable));
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.DynamicColor;
import com.android.systemui.monet.dynamiccolor.ToneDeltaConstraint;
import com.android.systemui.monet.dynamiccolor.TonePolarity;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeTonalSpot;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class DynamicColorTest extends SysuiTestCase {
@Test
public void fromArgbNoBackground_doesntChangeForContrast() {
final int blueArgb = 0xff0000ff;
final DynamicColor dynamicColor = DynamicColor.fromArgb(blueArgb);
final SchemeTonalSpot standardContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false,
0.0);
assertThat(dynamicColor.getArgb(standardContrast)).isSameColorAs(blueArgb);
final SchemeTonalSpot minContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, -1.0);
assertThat(dynamicColor.getArgb(minContrast)).isSameColorAs(blueArgb);
final SchemeTonalSpot maxContrast = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, 1.0);
assertThat(dynamicColor.getArgb(maxContrast)).isSameColorAs(blueArgb);
}
@Test
public void toneDeltaConstraintNoPreference_evaluatesCorrectly() {
final int blueArgb = 0xff0000ff;
final int redArgb = 0xffff0000;
final DynamicColor otherDynamicColor = DynamicColor.fromArgb(redArgb);
final DynamicColor dynamicColor =
DynamicColor.fromArgb(
blueArgb,
(s) -> 30.0,
null,
(s) -> new ToneDeltaConstraint(30, otherDynamicColor,
TonePolarity.NO_PREFERENCE));
final SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(blueArgb), false, 0.0);
assertThat(dynamicColor.getArgb(scheme)).isSameColorAs(0xff0000ef);
}
@Test
public void dynamicColor_withOpacity() {
final DynamicColor dynamicColor =
new DynamicColor(
s -> 0.0,
s -> 0.0,
s -> s.isDark ? 100.0 : 0.0,
s -> s.isDark ? 0.20 : 0.12,
null,
scheme ->
DynamicColor.toneMinContrastDefault(
(s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
scheme ->
DynamicColor.toneMaxContrastDefault(
(s) -> s.isDark ? 100.0 : 0.0, null, scheme, null),
null);
final SchemeTonalSpot lightScheme = new SchemeTonalSpot(Hct.fromInt(0xff4285f4), false,
0.0);
assertThat(dynamicColor.getArgb(lightScheme)).isSameColorAs(0x1f000000);
final SchemeTonalSpot darkScheme = new SchemeTonalSpot(Hct.fromInt(0xff4285f4), true, 0.0);
assertThat(dynamicColor.getArgb(darkScheme)).isSameColorAs(0x33ffffff);
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.hct.Cam16;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.hct.ViewingConditions;
import com.android.systemui.monet.utils.ColorUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class HctTest extends SysuiTestCase {
private static final int RED = 0xffff0000;
private static final int GREEN = 0xff00ff00;
private static final int BLUE = 0xff0000ff;
private static final int WHITE = 0xffffffff;
private static final int BLACK = 0xff000000;
private static final int MIDGRAY = 0xff777777;
public static final ViewingConditions VIEWING_CONDITIONS_WHITE_BACKGROUND =
ViewingConditions.make(
new double[]{
ColorUtils.whitePointD65()[0],
ColorUtils.whitePointD65()[1],
ColorUtils.whitePointD65()[2]
},
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
100.0,
2.0,
false);
public static final ViewingConditions VIEWING_CONDITIONS_BLACK_BACKGROUND =
ViewingConditions.make(
new double[]{
ColorUtils.whitePointD65()[0],
ColorUtils.whitePointD65()[1],
ColorUtils.whitePointD65()[2]
},
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
0.0,
2.0,
false);
@Test
public void camFromArgb_red() {
Cam16 cam = Cam16.fromInt(RED);
assertEquals(27.408, cam.getHue(), 0.001);
assertEquals(113.357, cam.getChroma(), 0.001);
assertEquals(46.445, cam.getJ(), 0.001);
assertEquals(89.494, cam.getM(), 0.001);
assertEquals(91.889, cam.getS(), 0.001);
assertEquals(105.988, cam.getQ(), 0.001);
}
@Test
public void camFromArgb_green() {
Cam16 cam = Cam16.fromInt(GREEN);
assertEquals(142.139, cam.getHue(), 0.001);
assertEquals(108.410, cam.getChroma(), 0.001);
assertEquals(79.331, cam.getJ(), 0.001);
assertEquals(85.587, cam.getM(), 0.001);
assertEquals(78.604, cam.getS(), 0.001);
assertEquals(138.520, cam.getQ(), 0.001);
}
@Test
public void camFromArgb_blue() {
Cam16 cam = Cam16.fromInt(BLUE);
assertEquals(282.788, cam.getHue(), 0.001);
assertEquals(87.230, cam.getChroma(), 0.001);
assertEquals(25.465, cam.getJ(), 0.001);
assertEquals(68.867, cam.getM(), 0.001);
assertEquals(93.674, cam.getS(), 0.001);
assertEquals(78.481, cam.getQ(), 0.001);
}
@Test
public void camFromArgb_white() {
Cam16 cam = Cam16.fromInt(WHITE);
assertEquals(209.492, cam.getHue(), 0.001);
assertEquals(2.869, cam.getChroma(), 0.001);
assertEquals(100.0, cam.getJ(), 0.001);
assertEquals(2.265, cam.getM(), 0.001);
assertEquals(12.068, cam.getS(), 0.001);
assertEquals(155.521, cam.getQ(), 0.001);
}
@Test
public void camFromArgb_black() {
Cam16 cam = Cam16.fromInt(BLACK);
assertEquals(0.0, cam.getHue(), 0.001);
assertEquals(0.0, cam.getChroma(), 0.001);
assertEquals(0.0, cam.getJ(), 0.001);
assertEquals(0.0, cam.getM(), 0.001);
assertEquals(0.0, cam.getS(), 0.001);
assertEquals(0.0, cam.getQ(), 0.001);
}
@Test
public void camToArgbToCam_red() {
Cam16 cam = Cam16.fromInt(RED);
int argb = cam.toInt();
assertEquals(RED, argb);
}
@Test
public void camToArgbToCam_green() {
Cam16 cam = Cam16.fromInt(GREEN);
int argb = cam.toInt();
assertEquals(GREEN, argb);
}
@Test
public void camToArgbToCam_blue() {
Cam16 cam = Cam16.fromInt(BLUE);
int argb = cam.toInt();
assertEquals(BLUE, argb);
}
@Test
public void viewingConditions_default() {
ViewingConditions vc = ViewingConditions.DEFAULT;
assertEquals(0.184, vc.getN(), 0.001);
assertEquals(29.981, vc.getAw(), 0.001);
assertEquals(1.016, vc.getNbb(), 0.001);
assertEquals(1.021, vc.getRgbD()[0], 0.001);
assertEquals(0.986, vc.getRgbD()[1], 0.001);
assertEquals(0.933, vc.getRgbD()[2], 0.001);
assertEquals(0.789, vc.getFlRoot(), 0.001);
}
private boolean colorIsOnBoundary(int argb) {
return ColorUtils.redFromArgb(argb) == 0
|| ColorUtils.redFromArgb(argb) == 255
|| ColorUtils.greenFromArgb(argb) == 0
|| ColorUtils.greenFromArgb(argb) == 255
|| ColorUtils.blueFromArgb(argb) == 0
|| ColorUtils.blueFromArgb(argb) == 255;
}
@Test
public void correctness() {
for (int hue = 15; hue < 360; hue += 30) {
for (int chroma = 0; chroma <= 100; chroma += 10) {
for (int tone = 20; tone <= 80; tone += 10) {
Hct hctColor = Hct.from((double) hue, (double) chroma, (double) tone);
if (chroma > 0) {
assertEquals(
"Incorrect hue for H" + hue + " C" + chroma + " T" + tone,
hue,
hctColor.getHue(),
4.0);
}
assertTrue(
"Negative chroma for H" + hue + " C" + chroma + " T" + tone,
hctColor.getChroma() >= 0.0);
assertTrue(
"Chroma too high for H" + hue + " C" + chroma + " T" + tone,
hctColor.getChroma() <= chroma + 2.5);
if (hctColor.getChroma() < chroma - 2.5) {
assertTrue(
"Color not on boundary for non-sRGB color for H"
+ hue
+ " C"
+ chroma
+ " T"
+ tone,
colorIsOnBoundary(hctColor.toInt()));
}
assertEquals(tone, hctColor.getTone(), 0.5);
}
}
}
}
@Test
public void relativity_redInBlack() {
final int colorToTest = RED;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff9F5C51)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_redInWhite() {
final int colorToTest = RED;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xffFF5D48)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
@Test
public void relativity_greenInBlack() {
final int colorToTest = GREEN;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xffACD69D)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_greenInWhite() {
final int colorToTest = GREEN;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff8EFF77)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
@Test
public void relativity_blueInBlack() {
final int colorToTest = BLUE;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff343654)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_blueInWhite() {
final int colorToTest = BLUE;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff3F49FF)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
@Test
public void relativity_whiteInBlack() {
final int colorToTest = WHITE;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xffFFFFFF)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_whiteInWhite() {
final int colorToTest = WHITE;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xffFFFFFF)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
@Test
public void relativity_midgrayInBlack() {
final int colorToTest = MIDGRAY;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff605F5F)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_midgrayInWhite() {
final int colorToTest = MIDGRAY;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff8E8E8E)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
@Test
public void relativity_blackInBlack() {
final int colorToTest = BLACK;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff000000)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_BLACK_BACKGROUND).toInt());
}
@Test
public void relativity_blackInWhite() {
final int colorToTest = BLACK;
final Hct hct = Hct.fromInt(colorToTest);
assertThat(0xff000000)
.isSameColorAs(
hct.inViewingConditions(VIEWING_CONDITIONS_WHITE_BACKGROUND).toInt());
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.utils.MathUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class MathUtilsTest extends SysuiTestCase {
@Test
public void signum() {
assertThat(MathUtils.signum(1.5)).isEqualTo(1);
assertThat(MathUtils.signum(0)).isEqualTo(0);
assertThat(MathUtils.signum(-3.0)).isEqualTo(-1);
}
@Test
public void sanitizeDegreesInt() {
assertThat(MathUtils.sanitizeDegreesInt(0)).isEqualTo(0);
assertThat(MathUtils.sanitizeDegreesInt(30)).isEqualTo(30);
assertThat(MathUtils.sanitizeDegreesInt(150)).isEqualTo(150);
assertThat(MathUtils.sanitizeDegreesInt(360)).isEqualTo(0);
assertThat(MathUtils.sanitizeDegreesInt(450)).isEqualTo(90);
assertThat(MathUtils.sanitizeDegreesInt(1000000)).isEqualTo(280);
assertThat(MathUtils.sanitizeDegreesInt(-10)).isEqualTo(350);
assertThat(MathUtils.sanitizeDegreesInt(-90)).isEqualTo(270);
assertThat(MathUtils.sanitizeDegreesInt(-1000000)).isEqualTo(80);
}
@Test
public void sanitizeDegreesDouble() {
assertThat(MathUtils.sanitizeDegreesDouble(0.0)).isWithin(0.001).of(0.0);
assertThat(MathUtils.sanitizeDegreesDouble(30.0)).isWithin(0.001).of(30.0);
assertThat(MathUtils.sanitizeDegreesDouble(150.0)).isWithin(0.001).of(150.0);
assertThat(MathUtils.sanitizeDegreesDouble(360.0)).isWithin(0.001).of(0.0);
assertThat(MathUtils.sanitizeDegreesDouble(450.0)).isWithin(0.001).of(90.0);
assertThat(MathUtils.sanitizeDegreesDouble(1000000.0)).isWithin(0.001).of(280.0);
assertThat(MathUtils.sanitizeDegreesDouble(-10.0)).isWithin(0.001).of(350.0);
assertThat(MathUtils.sanitizeDegreesDouble(-90.0)).isWithin(0.001).of(270.0);
assertThat(MathUtils.sanitizeDegreesDouble(-1000000.0)).isWithin(0.001).of(80.0);
assertThat(MathUtils.sanitizeDegreesDouble(100.375)).isWithin(0.001).of(100.375);
assertThat(MathUtils.sanitizeDegreesDouble(123456.789)).isWithin(0.001).of(336.789);
assertThat(MathUtils.sanitizeDegreesDouble(-200.625)).isWithin(0.001).of(159.375);
assertThat(MathUtils.sanitizeDegreesDouble(-123456.789)).isWithin(0.001).of(23.211);
}
@Test
public void rotationDirection() {
for (double from = 0.0; from < 360.0; from += 15.0) {
for (double to = 7.5; to < 360.0; to += 15.0) {
double expectedAnswer = originalRotationDirection(from, to);
double actualAnswer = MathUtils.rotationDirection(from, to);
assertThat(actualAnswer).isEqualTo(expectedAnswer);
assertThat(Math.abs(actualAnswer)).isEqualTo(1.0);
}
}
}
// Original implementation for MathUtils.rotationDirection.
// Included here to test equivalence with new implementation.
private static double originalRotationDirection(double from, double to) {
double a = to - from;
double b = to - from + 360.0;
double c = to - from - 360.0;
double aAbs = Math.abs(a);
double bAbs = Math.abs(b);
double cAbs = Math.abs(c);
if (aAbs <= bAbs && aAbs <= cAbs) {
return a >= 0.0 ? 1.0 : -1.0;
} else if (bAbs <= aAbs && bAbs <= cAbs) {
return b >= 0.0 ? 1.0 : -1.0;
} else {
return c >= 0.0 ? 1.0 : -1.0;
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.hct.Cam16;
import com.android.systemui.monet.palettes.CorePalette;
import com.android.systemui.monet.palettes.TonalPalette;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class PalettesTest extends SysuiTestCase {
@Test
public void tones_ofBlue() {
int color = 0xff0000ff;
Cam16 cam = Cam16.fromInt(color);
TonalPalette tones = TonalPalette.fromHueAndChroma(cam.getHue(), cam.getChroma());
assertThat(tones.tone(100)).isEqualTo(0xFFFFFFFF);
assertThat(tones.tone(95)).isEqualTo(0xfff1efff);
assertThat(tones.tone(90)).isEqualTo(0xffe0e0ff);
assertThat(tones.tone(80)).isEqualTo(0xffbec2ff);
assertThat(tones.tone(70)).isEqualTo(0xff9da3ff);
assertThat(tones.tone(60)).isEqualTo(0xff7c84ff);
assertThat(tones.tone(50)).isEqualTo(0xff5a64ff);
assertThat(tones.tone(40)).isEqualTo(0xff343dff);
assertThat(tones.tone(30)).isEqualTo(0xff0000ef);
assertThat(tones.tone(20)).isEqualTo(0xff0001ac);
assertThat(tones.tone(10)).isEqualTo(0xff00006e);
assertThat(tones.tone(0)).isEqualTo(0xff000000);
}
@Test
public void keyTones_ofBlue() {
CorePalette core = CorePalette.of(0xff0000ff);
assertThat(core.a1.tone(100)).isEqualTo(0xFFFFFFFF);
assertThat(core.a1.tone(95)).isEqualTo(0xfff1efff);
assertThat(core.a1.tone(90)).isEqualTo(0xffe0e0ff);
assertThat(core.a1.tone(80)).isEqualTo(0xffbec2ff);
assertThat(core.a1.tone(70)).isEqualTo(0xff9da3ff);
assertThat(core.a1.tone(60)).isEqualTo(0xff7c84ff);
assertThat(core.a1.tone(50)).isEqualTo(0xff5a64ff);
assertThat(core.a1.tone(40)).isEqualTo(0xff343dff);
assertThat(core.a1.tone(30)).isEqualTo(0xff0000ef);
assertThat(core.a1.tone(20)).isEqualTo(0xff0001ac);
assertThat(core.a1.tone(10)).isEqualTo(0xff00006e);
assertThat(core.a1.tone(0)).isEqualTo(0xff000000);
assertThat(core.a2.tone(100)).isEqualTo(0xffffffff);
assertThat(core.a2.tone(95)).isEqualTo(0xfff1efff);
assertThat(core.a2.tone(90)).isEqualTo(0xffe1e0f9);
assertThat(core.a2.tone(80)).isEqualTo(0xffc5c4dd);
assertThat(core.a2.tone(70)).isEqualTo(0xffa9a9c1);
assertThat(core.a2.tone(60)).isEqualTo(0xff8f8fa6);
assertThat(core.a2.tone(50)).isEqualTo(0xff75758b);
assertThat(core.a2.tone(40)).isEqualTo(0xff5c5d72);
assertThat(core.a2.tone(30)).isEqualTo(0xff444559);
assertThat(core.a2.tone(20)).isEqualTo(0xff2e2f42);
assertThat(core.a2.tone(10)).isEqualTo(0xff191a2c);
assertThat(core.a2.tone(0)).isEqualTo(0xff000000);
}
@Test
public void keyTones_contentOfBlue() {
CorePalette core = CorePalette.contentOf(0xff0000ff);
assertThat(core.a1.tone(100)).isEqualTo(0xffffffff);
assertThat(core.a1.tone(95)).isEqualTo(0xfff1efff);
assertThat(core.a1.tone(90)).isEqualTo(0xffe0e0ff);
assertThat(core.a1.tone(80)).isEqualTo(0xffbec2ff);
assertThat(core.a1.tone(70)).isEqualTo(0xff9da3ff);
assertThat(core.a1.tone(60)).isEqualTo(0xff7c84ff);
assertThat(core.a1.tone(50)).isEqualTo(0xff5a64ff);
assertThat(core.a1.tone(40)).isEqualTo(0xff343dff);
assertThat(core.a1.tone(30)).isEqualTo(0xff0000ef);
assertThat(core.a1.tone(20)).isEqualTo(0xff0001ac);
assertThat(core.a1.tone(10)).isEqualTo(0xff00006e);
assertThat(core.a1.tone(0)).isEqualTo(0xff000000);
assertThat(core.a2.tone(100)).isEqualTo(0xffffffff);
assertThat(core.a2.tone(95)).isEqualTo(0xfff1efff);
assertThat(core.a2.tone(90)).isEqualTo(0xffe0e0ff);
assertThat(core.a2.tone(80)).isEqualTo(0xffc1c3f4);
assertThat(core.a2.tone(70)).isEqualTo(0xffa5a7d7);
assertThat(core.a2.tone(60)).isEqualTo(0xff8b8dbb);
assertThat(core.a2.tone(50)).isEqualTo(0xff7173a0);
assertThat(core.a2.tone(40)).isEqualTo(0xff585b86);
assertThat(core.a2.tone(30)).isEqualTo(0xff40436d);
assertThat(core.a2.tone(20)).isEqualTo(0xff2a2d55);
assertThat(core.a2.tone(10)).isEqualTo(0xff14173f);
assertThat(core.a2.tone(0)).isEqualTo(0xff000000);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.quantize.QuantizerMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Map;
@SmallTest
@RunWith(JUnit4.class)
public final class QuantizerMapTest extends SysuiTestCase {
private static final int RED = 0xffff0000;
private static final int GREEN = 0xff00ff00;
private static final int BLUE = 0xff0000ff;
private static final QuantizerMap quantizer = new QuantizerMap();
@Test
public void quantize_1R() {
Map<Integer, Integer> answer = quantizer.quantize(new int[]{RED}, 128).colorToCount;
assertThat(answer).hasSize(1);
assertThat(answer).containsEntry(RED, 1);
}
@Test
public void quantize_1G() {
Map<Integer, Integer> answer = quantizer.quantize(new int[]{GREEN}, 128).colorToCount;
assertThat(answer).hasSize(1);
assertThat(answer).containsEntry(GREEN, 1);
}
@Test
public void quantize_1B() {
Map<Integer, Integer> answer = quantizer.quantize(new int[]{BLUE}, 128).colorToCount;
assertThat(answer).hasSize(1);
assertThat(answer).containsEntry(BLUE, 1);
}
@Test
public void quantize_5B() {
Map<Integer, Integer> answer =
quantizer.quantize(new int[]{BLUE, BLUE, BLUE, BLUE, BLUE}, 128).colorToCount;
assertThat(answer).hasSize(1);
assertThat(answer).containsEntry(BLUE, 5);
}
@Test
public void quantize_2R3G() {
Map<Integer, Integer> answer =
quantizer.quantize(new int[]{RED, RED, GREEN, GREEN, GREEN}, 128).colorToCount;
assertThat(answer).hasSize(2);
assertThat(answer).containsEntry(RED, 2);
assertThat(answer).containsEntry(GREEN, 3);
}
@Test
public void quantize_1R1G1B() {
Map<Integer, Integer> answer =
quantizer.quantize(new int[]{RED, GREEN, BLUE}, 128).colorToCount;
assertThat(answer).hasSize(3);
assertThat(answer).containsEntry(RED, 1);
assertThat(answer).containsEntry(GREEN, 1);
assertThat(answer).containsEntry(BLUE, 1);
}
}

View File

@@ -0,0 +1,4 @@
The tests in this folder are imported from Google's internal source repository.
For instructions on importing, see:
frameworks/base/packages/SystemUI/tests/src/com/android/systemui/monet/README

View File

@@ -0,0 +1,254 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeContent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeContentTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF1218FF);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF0001C3);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF000181);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF5660FF);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF2D36FF);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF0000E3);
}
@Test
public void lightTheme_minContrast_tertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFFB042CC);
}
@Test
public void lightTheme_standardContrast_tertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFF9221AF);
}
@Test
public void lightTheme_maxContrast_tertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFF73008E);
}
@Test
public void lightTheme_minContrast_objectionableTertiaryContainerLightens() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFFD03A71);
}
@Test
public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFFAC1B57);
}
@Test
public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFF870040);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFFCBCDFF);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFFCECFFF);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFFD6D6FF);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF5660FF);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFBEC2FF);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFF6F4FF);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF0000E6);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF0000E6);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFFC4C6FF);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFF7A83FF);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFFA4AAFF);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xFF0001C6);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFFCF60EA);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFFEB8CFF);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFF63007B);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D);
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeExpressive;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeExpressiveTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff924b28);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff401400);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffffdbcc);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffffdbcc);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff6f3010);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff99512e);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff351000);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffffd0bc);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffffb595);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfffff3ee);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff743413);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff743413);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffffbb9e);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xfff99f75);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffffdbcc);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff622706);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
}

View File

@@ -0,0 +1,255 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeFidelity;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeFidelityTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff1218ff);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0001c3);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff5660ff);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff2d36ff);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000e3);
}
@Test
public void lightTheme_minContrast_tertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffd93628);
}
@Test
public void lightTheme_standardContrast_tertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffb31910);
}
@Test
public void lightTheme_maxContrast_tertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff8c0002);
}
@Test
public void lightTheme_minContrast_objectionableTertiaryContainerLightens() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffbcac5a);
}
@Test
public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffbcac5a);
}
@Test
public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff4d4300);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffcbcdff);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffcecfff);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffd6d6ff);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000e6);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000e6);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffc4c6ff);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff7a83ff);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffa4aaff);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff0001c6);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xfffe513e);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffFF9181);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff790001);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d);
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeFruitSalad;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeFruitSaladTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007ea7);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff006688);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff002635);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffC2E8FF);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffC2E8FF);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff004862);
}
@Test
public void lightTheme_minContrast_tertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffE0E0FF);
}
@Test
public void lightTheme_standardContrast_tertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffE0E0FF);
}
@Test
public void lightTheme_maxContrast_tertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xFF3A3E74);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff006C90);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff001E2B);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffACE1FF);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007EA7);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF76D1FF);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFECF7FF);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF004D67);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF004D67);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xFF83D5FF);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff55C0F2);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffC2E8FF);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff003E54);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffADB0EF);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff30346A);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c);
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeMonochrome;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeMonochromeTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5e5e5e);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff222222);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e2e2);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e2e2);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff434343);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff646464);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff1b1b1b);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffdadada);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c6c6);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff5f5f5);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff474747);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff474747);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffcbcbcb);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffb5b5b5);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e2e2);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff393939);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffb5b5b5);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e2e2);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff393939);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeNeutral;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeNeutralTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5d5d6c);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff21212e);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e1f3);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e1f3);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff414250);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff636372);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff1a1b27);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffd9d8ea);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c5d6);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff454654);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff454654);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffcac9da);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffb5b3c4);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffe2e1f3);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff373846);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffb3b3cb);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffe1e0f9);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff37374b);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315);
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeRainbow;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeRainbowTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5056A9);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0F136A);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffE0E0FF);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffE0E0FF);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff34398B);
}
@Test
public void lightTheme_minContrast_tertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffffd8ee);
}
@Test
public void lightTheme_standardContrast_tertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffffd8ee);
}
@Test
public void lightTheme_maxContrast_tertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff5A384E);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff565CB0);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff050865);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffd6d6ff);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff383E8F);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff383E8F);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffc4c6ff);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffa9afff);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff292f81);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffd5a8c3);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffffd8ee);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff4f2e44);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313);
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.scheme.Scheme;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeTest extends SysuiTestCase {
@Test
public void lightScheme_ofBlue() {
Scheme scheme = Scheme.light(0xff0000ff);
assertThat(scheme.getPrimary()).isSameColorAs(0xff343DFF);
}
@Test
public void darkScheme_ofBlue() {
Scheme scheme = Scheme.dark(0xff0000ff);
assertThat(scheme.getPrimary()).isSameColorAs(0xffBEC2FF);
}
@Test
public void thirdPartyScheme_light() {
Scheme scheme = Scheme.light(0xff6750A4);
assertThat(scheme.getPrimary()).isSameColorAs(0xff6750A4);
assertThat(scheme.getSecondary()).isSameColorAs(0xff625B71);
assertThat(scheme.getTertiary()).isSameColorAs(0xff7e5260);
assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
assertThat(scheme.getOnSurface()).isSameColorAs(0xff1c1b1e);
}
@Test
public void thirdPartyScheme_dark() {
Scheme scheme = Scheme.dark(0xff6750A4);
assertThat(scheme.getPrimary()).isSameColorAs(0xffcfbcff);
assertThat(scheme.getSecondary()).isSameColorAs(0xffcbc2db);
assertThat(scheme.getTertiary()).isSameColorAs(0xffefb8c8);
assertThat(scheme.getSurface()).isSameColorAs(0xff1c1b1e);
assertThat(scheme.getOnSurface()).isSameColorAs(0xffe6e1e6);
}
@Test
public void lightSchemeFromHighChromaColor() {
Scheme scheme = Scheme.light(0xfffa2bec);
assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2);
assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff);
assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035);
assertThat(scheme.getSecondary()).isSameColorAs(0xff6e5868);
assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff);
assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xfff8daee);
assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff271624);
assertThat(scheme.getTertiary()).isSameColorAs(0xff815343);
assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff);
assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0);
assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff321207);
assertThat(scheme.getError()).isSameColorAs(0xffba1a1a);
assertThat(scheme.getOnError()).isSameColorAs(0xffffffff);
assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6);
assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002);
assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff);
assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7);
assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getOutline()).isSameColorAs(0xff80747b);
assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb);
assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32);
assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2);
assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee);
}
@Test
public void darkSchemeFromHighChromaColor() {
Scheme scheme = Scheme.dark(0xfffa2bec);
assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee);
assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057);
assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b);
assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getSecondary()).isSameColorAs(0xffdbbed1);
assertThat(scheme.getOnSecondary()).isSameColorAs(0xff3e2a39);
assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff554050);
assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xfff8daee);
assertThat(scheme.getTertiary()).isSameColorAs(0xfff5b9a5);
assertThat(scheme.getOnTertiary()).isSameColorAs(0xff4c2619);
assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff663c2d);
assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0);
assertThat(scheme.getError()).isSameColorAs(0xffffb4ab);
assertThat(scheme.getOnError()).isSameColorAs(0xff690005);
assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a);
assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab);
assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb);
assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95);
assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32);
assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2);
}
@Test
public void lightContentSchemeFromHighChromaColor() {
Scheme scheme = Scheme.lightContent(0xfffa2bec);
assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2);
assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff);
assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035);
assertThat(scheme.getSecondary()).isSameColorAs(0xff7f4e75);
assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff);
assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff330b2f);
assertThat(scheme.getTertiary()).isSameColorAs(0xff9c4323);
assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff);
assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0);
assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff390c00);
assertThat(scheme.getError()).isSameColorAs(0xffba1a1a);
assertThat(scheme.getOnError()).isSameColorAs(0xffffffff);
assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6);
assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002);
assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff);
assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff);
assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7);
assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getOutline()).isSameColorAs(0xff80747b);
assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb);
assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32);
assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2);
assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee);
}
@Test
public void darkContentSchemeFromHighChromaColor() {
Scheme scheme = Scheme.darkContent(0xfffa2bec);
assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee);
assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057);
assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b);
assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getSecondary()).isSameColorAs(0xfff0b4e1);
assertThat(scheme.getOnSecondary()).isSameColorAs(0xff4b2145);
assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff64375c);
assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xffffd7f3);
assertThat(scheme.getTertiary()).isSameColorAs(0xffffb59c);
assertThat(scheme.getOnTertiary()).isSameColorAs(0xff5c1900);
assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff7d2c0d);
assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0);
assertThat(scheme.getError()).isSameColorAs(0xffffb4ab);
assertThat(scheme.getOnError()).isSameColorAs(0xff690005);
assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a);
assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab);
assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d);
assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb);
assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95);
assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b);
assertThat(scheme.getShadow()).isSameColorAs(0xff000000);
assertThat(scheme.getScrim()).isSameColorAs(0xff000000);
assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4);
assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32);
assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2);
}
}

View File

@@ -0,0 +1,356 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeTonalSpot;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeTonalSpotTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff545999);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff161a59);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff383c7c);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff5a5fa0);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff0e1253);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffd6d6ff);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_minContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff5f5e65);
}
@Test
public void lightTheme_standardContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1b1b21);
}
@Test
public void lightTheme_maxContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1a1a20);
}
@Test
public void lightTheme_minContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffcfcfe7);
}
@Test
public void lightTheme_standardContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffffffff);
}
@Test
public void lightTheme_maxContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffababc3);
}
@Test
public void lightTheme_minContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfff3c3df);
}
@Test
public void lightTheme_standardContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffffffff);
}
@Test
public void lightTheme_maxContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffcda0bb);
}
@Test
public void lightTheme_minContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffc2bb);
}
@Test
public void lightTheme_standardContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffffff);
}
@Test
public void lightTheme_maxContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffff8d80);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff3c4180);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff3c4180);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffc4c6ff);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffabaff7);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff2e3271);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffd5a8c3);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffffd8ee);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff4f2e44);
}
@Test
public void darkTheme_minContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xfffffbff);
}
@Test
public void darkTheme_standardContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff2e2f42);
}
@Test
public void darkTheme_maxContrast_onSecondary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff505165);
}
@Test
public void darkTheme_minContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfffffbff);
}
@Test
public void darkTheme_standardContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff46263b);
}
@Test
public void darkTheme_maxContrast_onTertiary() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff6b485f);
}
@Test
public void darkTheme_minContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xfffffbff);
}
@Test
public void darkTheme_standardContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xff690005);
}
@Test
public void darkTheme_maxContrast_onError() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffa80710);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318);
}
@Test
public void darkTheme_minContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffa4a2a9);
}
@Test
public void darkTheme_standardContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe4e1e9);
}
@Test
public void darkTheme_maxContrast_onSurface() {
SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe5e2ea);
}
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.scheme.SchemeVibrant;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@SmallTest
@RunWith(JUnit4.class)
public final class SchemeVibrantTest extends SysuiTestCase {
@Test
public void lightTheme_minContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
}
@Test
public void lightTheme_standardContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff343dff);
}
@Test
public void lightTheme_maxContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181);
}
@Test
public void lightTheme_minContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void lightTheme_standardContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void lightTheme_maxContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000e3);
}
@Test
public void lightTheme_minContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff3e47ff);
}
@Test
public void lightTheme_standardContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff00006e);
}
@Test
public void lightTheme_maxContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffd6d6ff);
}
@Test
public void lightTheme_minContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_standardContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void lightTheme_maxContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff);
}
@Test
public void darkTheme_minContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff);
}
@Test
public void darkTheme_standardContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff);
}
@Test
public void darkTheme_maxContrast_primary() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff);
}
@Test
public void darkTheme_minContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000ef);
}
@Test
public void darkTheme_standardContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xff0000ef);
}
@Test
public void darkTheme_maxContrast_primaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs(
0xffc4c6ff);
}
@Test
public void darkTheme_minContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffa9afff);
}
@Test
public void darkTheme_standardContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xffe0e0ff);
}
@Test
public void darkTheme_maxContrast_onPrimaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs(
0xff0001c6);
}
@Test
public void darkTheme_minContrast_onTertiaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xffc9a9df);
}
@Test
public void darkTheme_standardContrast_onTertiaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xfff2daff);
}
@Test
public void darkTheme_maxContrast_onTertiaryContainer() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs(
0xff472e5b);
}
@Test
public void darkTheme_minContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
@Test
public void darkTheme_standardContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
@Test
public void darkTheme_maxContrast_surface() {
SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0);
assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.score.Score;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SmallTest
@RunWith(JUnit4.class)
public final class ScoreTest extends SysuiTestCase {
@Test
public void score_picksMostChromatic() {
Map<Integer, Integer> colorsToPopulation = new HashMap<>();
colorsToPopulation.put(0xFF0000FF, 1);
colorsToPopulation.put(0xFFFF0000, 1);
colorsToPopulation.put(0xFF00FF00, 1);
List<Integer> scores = Score.score(colorsToPopulation);
assertThat(scores.get(0)).isEqualTo(0xffff0000);
assertThat(scores.get(1)).isEqualTo(0xff00ff00);
assertThat(scores.get(2)).isEqualTo(0xff0000ff);
}
@Test
public void score_usesGblueFallback() {
Map<Integer, Integer> colorsToPopulation = new HashMap<>();
colorsToPopulation.put(0xFF000000, 1);
List<Integer> scores = Score.score(colorsToPopulation);
assertThat(scores).containsExactly(0xff4285f4);
}
@Test
public void score_dedupesNearbyHues() {
Map<Integer, Integer> colorsToPopulation = new HashMap<>();
colorsToPopulation.put(0xff008772, 1);
colorsToPopulation.put(0xff318477, 1);
List<Integer> scores = Score.score(colorsToPopulation);
assertThat(scores).containsExactly(0xff008772);
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2023 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.android.systemui.monet;
import static com.android.systemui.monet.utils.ArgbSubject.assertThat;
import static org.junit.Assert.assertEquals;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.temperature.TemperatureCache;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@SmallTest
@RunWith(JUnit4.class)
public final class TemperatureCacheTest extends SysuiTestCase {
@Test
public void testRawTemperature() {
final Hct blueHct = Hct.fromInt(0xff0000ff);
final double blueTemp = TemperatureCache.rawTemperature(blueHct);
assertEquals(-1.393, blueTemp, 0.001);
final Hct redHct = Hct.fromInt(0xffff0000);
final double redTemp = TemperatureCache.rawTemperature(redHct);
assertEquals(2.351, redTemp, 0.001);
final Hct greenHct = Hct.fromInt(0xff00ff00);
final double greenTemp = TemperatureCache.rawTemperature(greenHct);
assertEquals(-0.267, greenTemp, 0.001);
final Hct whiteHct = Hct.fromInt(0xffffffff);
final double whiteTemp = TemperatureCache.rawTemperature(whiteHct);
assertEquals(-0.5, whiteTemp, 0.001);
final Hct blackHct = Hct.fromInt(0xff000000);
final double blackTemp = TemperatureCache.rawTemperature(blackHct);
assertEquals(-0.5, blackTemp, 0.001);
}
@Test
public void testComplement() {
final int blueComplement =
new TemperatureCache(Hct.fromInt(0xff0000ff)).getComplement().toInt();
assertThat(0xff9D0002).isSameColorAs(blueComplement);
final int redComplement = new TemperatureCache(
Hct.fromInt(0xffff0000)).getComplement().toInt();
assertThat(0xff007BFC).isSameColorAs(redComplement);
final int greenComplement =
new TemperatureCache(Hct.fromInt(0xff00ff00)).getComplement().toInt();
assertThat(0xffFFD2C9).isSameColorAs(greenComplement);
final int whiteComplement =
new TemperatureCache(Hct.fromInt(0xffffffff)).getComplement().toInt();
assertThat(0xffffffff).isSameColorAs(whiteComplement);
final int blackComplement =
new TemperatureCache(Hct.fromInt(0xff000000)).getComplement().toInt();
assertThat(0xff000000).isSameColorAs(blackComplement);
}
@Test
public void testAnalogous() {
final List<Hct> blueAnalogous =
new TemperatureCache(Hct.fromInt(0xff0000ff)).getAnalogousColors();
assertThat(0xff00590C).isSameColorAs(blueAnalogous.get(0).toInt());
assertThat(0xff00564E).isSameColorAs(blueAnalogous.get(1).toInt());
assertThat(0xff0000ff).isSameColorAs(blueAnalogous.get(2).toInt());
assertThat(0xff6700CC).isSameColorAs(blueAnalogous.get(3).toInt());
assertThat(0xff81009F).isSameColorAs(blueAnalogous.get(4).toInt());
final List<Hct> redAnalogous =
new TemperatureCache(Hct.fromInt(0xffff0000)).getAnalogousColors();
assertThat(0xffF60082).isSameColorAs(redAnalogous.get(0).toInt());
assertThat(0xffFC004C).isSameColorAs(redAnalogous.get(1).toInt());
assertThat(0xffff0000).isSameColorAs(redAnalogous.get(2).toInt());
assertThat(0xffD95500).isSameColorAs(redAnalogous.get(3).toInt());
assertThat(0xffAF7200).isSameColorAs(redAnalogous.get(4).toInt());
final List<Hct> greenAnalogous =
new TemperatureCache(Hct.fromInt(0xff00ff00)).getAnalogousColors();
assertThat(0xffCEE900).isSameColorAs(greenAnalogous.get(0).toInt());
assertThat(0xff92F500).isSameColorAs(greenAnalogous.get(1).toInt());
assertThat(0xff00ff00).isSameColorAs(greenAnalogous.get(2).toInt());
assertThat(0xff00FD6F).isSameColorAs(greenAnalogous.get(3).toInt());
assertThat(0xff00FAB3).isSameColorAs(greenAnalogous.get(4).toInt());
final List<Hct> blackAnalogous =
new TemperatureCache(Hct.fromInt(0xff000000)).getAnalogousColors();
assertThat(0xff000000).isSameColorAs(blackAnalogous.get(0).toInt());
assertThat(0xff000000).isSameColorAs(blackAnalogous.get(1).toInt());
assertThat(0xff000000).isSameColorAs(blackAnalogous.get(2).toInt());
assertThat(0xff000000).isSameColorAs(blackAnalogous.get(3).toInt());
assertThat(0xff000000).isSameColorAs(blackAnalogous.get(4).toInt());
final List<Hct> whiteAnalogous =
new TemperatureCache(Hct.fromInt(0xffffffff)).getAnalogousColors();
assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(0).toInt());
assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(1).toInt());
assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(2).toInt());
assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(3).toInt());
assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(4).toInt());
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 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.android.systemui.monet.utils;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
/** Enables assertions involving colors as integers to log as hex codes instead of raw integers. */
public final class ArgbSubject extends Subject {
public static ArgbSubject assertThat(int argb) {
return assertAbout(ArgbSubject::new).that(argb);
}
private final int actual;
private ArgbSubject(FailureMetadata failureMetadata, int subject) {
super(failureMetadata, subject);
this.actual = subject;
}
@Override
protected String actualCustomStringRepresentation() {
return userFriendlyHexCode(actual);
}
public void isSameColorAs(int otherArgb) {
if (otherArgb != actual) {
failWithActual("color", userFriendlyHexCode(otherArgb));
}
}
private static String userFriendlyHexCode(int integer) {
String hexString = Integer.toHexString(integer);
if (hexString.length() > 6) {
return String.format("#%s", hexString.substring(hexString.length() - 6));
} else if (hexString.length() == 6) {
return String.format("#%s", hexString);
} else {
return String.format("??? #%s", hexString);
}
}
}