diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/README.md b/packages/SystemUI/monet/src/com/android/systemui/monet/README.md new file mode 100644 index 0000000000000..e4818f9c8d81a --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/README.md @@ -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. diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java b/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java new file mode 100644 index 0000000000000..927417a57c036 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java @@ -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(); + } +} + diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java b/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java new file mode 100644 index 0000000000000..ada8cedf30d80 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java @@ -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. + * + *
Utility methods for calculating contrast given two colors, or calculating a color given one + * color and a contrast ratio. + * + *
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. + * + *
To compare lightness, the colors are expressed in the XYZ color space, where Y is + * lightness, + * also known as relative luminance. + * + *
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. + * + *
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. + * + *
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. + * + *
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. + * + *
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); + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java new file mode 100644 index 0000000000000..31feee3b02deb --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java @@ -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. + * + *
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. + * + *
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. + * + *
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; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java new file mode 100644 index 0000000000000..3a6b3628e9371 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java @@ -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. + * + *
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. + * + *
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. + * + *
Prefer the static constructors. They provide a much more simple interface, such as requiring + * just a hexcode, or just a hexcode and a background. + * + *
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 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.
+ *
+ * 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. 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.
+ *
+ * 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 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.
+ *
+ * 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 This ensures text on surfaces maintains its original, often detrimentally excessive,
+ * contrast ratio. But, text on buttons can soften to not have excessive contrast.
+ *
+ * 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 It enforces important properties: #2. Enable light foregrounds on midtones. #3. Ensure tone delta with another color. 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;
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
new file mode 100644
index 0000000000000..813b22a411a63
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java
@@ -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.
+ *
+ * * 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());
+ }
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java
new file mode 100644
index 0000000000000..5fd1210fdb352
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java
@@ -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;
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java
new file mode 100644
index 0000000000000..bb5649ca3c4ee
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java
@@ -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;
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java
new file mode 100644
index 0000000000000..9ccc50307a597
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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};
+ }
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java
new file mode 100644
index 0000000000000..6cff99e9083ca
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java
@@ -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*.
+ *
+ * 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.
+ *
+ * Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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);
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java
new file mode 100644
index 0000000000000..7083222489c7f
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java
@@ -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));
+ }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java
new file mode 100644
index 0000000000000..a0ba7a6781cc6
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java
@@ -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.
+ *
+ * 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)
+ *
+ * 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.
+ *
+ * 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;
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java
new file mode 100644
index 0000000000000..37f6042045f93
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java
@@ -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.);
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
new file mode 100644
index 0000000000000..6911666bce551
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java
@@ -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 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);
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java
new file mode 100644
index 0000000000000..136e5a55c1914
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java
@@ -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);
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java
new file mode 100644
index 0000000000000..d448dd0e4a96b
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java
@@ -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.
+ *
+ * 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 Wsmeans stands for Weighted Square Means.
+ *
+ * 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 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 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.
+ *
+ * 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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
new file mode 100644
index 0000000000000..e7c6f79021e8b
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java
new file mode 100644
index 0000000000000..dfbf56d4e5efa
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java
new file mode 100644
index 0000000000000..aa2c979bf854c
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java
new file mode 100644
index 0000000000000..509ea2a1a8b32
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java
new file mode 100644
index 0000000000000..fdbde9def79db
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java
new file mode 100644
index 0000000000000..92eb27fe620c8
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
new file mode 100644
index 0000000000000..cd74ac382c91c
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
new file mode 100644
index 0000000000000..89337cac771a9
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java
@@ -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));
+ }
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java
new file mode 100644
index 0000000000000..99aa2deb5a60a
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java
@@ -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
+}
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java b/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java
new file mode 100644
index 0000000000000..09ea7a39bbea4
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java
@@ -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.
+ *
+ * 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 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 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.
+ *
+ * The colors are equidistant in temperature and adjacent in hue.
+ */
+ public List 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.
+ *
+ * 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 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.
+ *
+ * Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
+ * Return value has these properties: Sorted by hue, ex. index 0 is hue 0.
+ */
+ private List 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 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.
+ *
+ * L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+ *
+ * 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.
+ *
+ * L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
+ *
+ * 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;
+ }
+ }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java
new file mode 100644
index 0000000000000..a7622efc8567f
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java
@@ -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.
+ *
+ * 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};
+ }
+}
+
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java
new file mode 100644
index 0000000000000..cb2b6940ab1ee
--- /dev/null
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java
new file mode 100644
index 0000000000000..5b3419d838f6b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java
new file mode 100644
index 0000000000000..6a7f104a52d99
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java
@@ -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);
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java
new file mode 100644
index 0000000000000..8d3e401469004
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java
new file mode 100644
index 0000000000000..bda7fcc8a5950
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java
@@ -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));
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java
new file mode 100644
index 0000000000000..41ac3213d4025
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java
new file mode 100644
index 0000000000000..e9ca8a653e41b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java
@@ -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());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java
new file mode 100644
index 0000000000000..6b8437a6e46e7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java
@@ -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;
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java
new file mode 100644
index 0000000000000..0b65fdfa740b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java
@@ -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);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java
new file mode 100644
index 0000000000000..6a282a1fdb7c9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java
@@ -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
+ * 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
+ * If the original contrast ratio was >= 7.0, reach contrast 4.5.
+ * If the original contrast ratio was >= 3.0, reach contrast 3.0.
+ * If the original contrast ratio was < 3.0, reach that ratio.
+ */
+ public static double toneMinContrastDefault(
+ Function
+ * If the color's background has a background, reach contrast 7.0.
+ * If it doesn't, maintain the original contrast ratio.
+ *
+ *
+ * #1. Desired contrast ratio is reached.
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * 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
+ * - Values below 0 are cool, above 0 are warm.
+ * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.
+ * - 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.
+ *
+ *