From bf07f0cc8eceb823410b89bee78d81466dec00d0 Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Sun, 29 Jan 2023 12:43:33 -0500 Subject: [PATCH] Import libmonet update from internal repo Bug: 266731167 Test: atest com.android.systemui.monet, 380 pass. Original test count was 13. Library has 90%+ coverage overall and is maintaining 90%+ coverage as CLs are added, per stats from internal source repository. Change-Id: I922b352facb5da89d77583602381291ac571842a --- .../src/com/android/systemui/monet/README.md | 26 + .../android/systemui/monet/blend/Blend.java | 92 ++ .../systemui/monet/contrast/Contrast.java | 215 +++++ .../monet/dislike/DislikeAnalyzer.java | 58 ++ .../monet/dynamiccolor/DynamicColor.java | 646 +++++++++++++++ .../dynamiccolor/MaterialDynamicColors.java | 415 +++++++++ .../dynamiccolor/ToneDeltaConstraint.java | 39 + .../monet/dynamiccolor/TonePolarity.java | 24 + .../com/android/systemui/monet/hct/Cam16.java | 454 ++++++++++ .../com/android/systemui/monet/hct/Hct.java | 164 ++++ .../android/systemui/monet/hct/HctSolver.java | 677 +++++++++++++++ .../systemui/monet/hct/ViewingConditions.java | 218 +++++ .../systemui/monet/palettes/CorePalette.java | 73 ++ .../systemui/monet/palettes/TonalPalette.java | 101 +++ .../monet/quantize/PointProvider.java | 32 + .../monet/quantize/PointProviderLab.java | 56 ++ .../systemui/monet/quantize/Quantizer.java | 21 + .../monet/quantize/QuantizerCelebi.java | 61 ++ .../systemui/monet/quantize/QuantizerMap.java | 41 + .../monet/quantize/QuantizerResult.java | 28 + .../monet/quantize/QuantizerWsmeans.java | 233 ++++++ .../systemui/monet/quantize/QuantizerWu.java | 407 +++++++++ .../systemui/monet/scheme/DynamicScheme.java | 93 +++ .../android/systemui/monet/scheme/Scheme.java | 784 ++++++++++++++++++ .../systemui/monet/scheme/SchemeContent.java | 59 ++ .../monet/scheme/SchemeExpressive.java | 47 ++ .../systemui/monet/scheme/SchemeFidelity.java | 54 ++ .../monet/scheme/SchemeFruitSalad.java | 39 + .../monet/scheme/SchemeMonochrome.java | 36 + .../systemui/monet/scheme/SchemeNeutral.java | 36 + .../systemui/monet/scheme/SchemeRainbow.java | 38 + .../monet/scheme/SchemeTonalSpot.java | 38 + .../systemui/monet/scheme/SchemeVibrant.java | 44 + .../systemui/monet/scheme/Variant.java | 30 + .../android/systemui/monet/score/Score.java | 184 ++++ .../monet/temperature/TemperatureCache.java | 337 ++++++++ .../systemui/monet/utils/ColorUtils.java | 266 ++++++ .../systemui/monet/utils/MathUtils.java | 135 +++ .../systemui/monet/utils/StringUtils.java | 35 + .../com/android/systemui/monet/BlendTest.java | 109 +++ .../systemui/monet/ColorUtilsTest.java | 207 +++++ .../android/systemui/monet/ContrastTest.java | 73 ++ .../android/systemui/monet/DislikeTest.java | 81 ++ .../systemui/monet/DynamicColorTest.java | 93 +++ .../com/android/systemui/monet/HctTest.java | 320 +++++++ .../android/systemui/monet/MathUtilsTest.java | 99 +++ .../android/systemui/monet/PalettesTest.java | 116 +++ .../systemui/monet/QuantizerMapTest.java | 93 +++ .../src/com/android/systemui/monet/README.md | 4 + .../systemui/monet/SchemeContentTest.java | 254 ++++++ .../systemui/monet/SchemeExpressiveTest.java | 191 +++++ .../systemui/monet/SchemeFidelityTest.java | 255 ++++++ .../systemui/monet/SchemeFruitSaladTest.java | 234 ++++++ .../systemui/monet/SchemeMonochromeTest.java | 212 +++++ .../systemui/monet/SchemeNeutralTest.java | 212 +++++ .../systemui/monet/SchemeRainbowTest.java | 234 ++++++ .../android/systemui/monet/SchemeTest.java | 201 +++++ .../systemui/monet/SchemeTonalSpotTest.java | 356 ++++++++ .../systemui/monet/SchemeVibrantTest.java | 212 +++++ .../com/android/systemui/monet/ScoreTest.java | 72 ++ .../systemui/monet/TemperatureCacheTest.java | 127 +++ .../systemui/monet/utils/ArgbSubject.java | 59 ++ 62 files changed, 10150 insertions(+) create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/README.md create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/blend/Blend.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/contrast/Contrast.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/dislike/DislikeAnalyzer.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/DynamicColor.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/MaterialDynamicColors.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/ToneDeltaConstraint.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/dynamiccolor/TonePolarity.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/hct/Cam16.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/hct/Hct.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/hct/HctSolver.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/hct/ViewingConditions.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/palettes/CorePalette.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/palettes/TonalPalette.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/Quantizer.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerCelebi.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeExpressive.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFidelity.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeFruitSalad.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeMonochrome.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeNeutral.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeRainbow.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeTonalSpot.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeVibrant.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Variant.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/score/Score.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/utils/MathUtils.java create mode 100644 packages/SystemUI/monet/src/com/android/systemui/monet/utils/StringUtils.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/BlendTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/ColorUtilsTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/ContrastTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/DislikeTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/DynamicColorTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/HctTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/MathUtilsTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/PalettesTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/QuantizerMapTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/README.md create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java 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 hue; + public final Function chroma; + public final Function tone; + public final Function opacity; + + public final Function background; + public final Function toneMinContrast; + public final Function toneMaxContrast; + public final Function toneDeltaConstraint; + + private final HashMap hctCache = new HashMap<>(); + + /** + * The base constructor for DynamicColor. + * + *

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.
+ * 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 hue, + Function chroma, + Function tone, + Function opacity, + Function background, + Function toneMinContrast, + Function toneMaxContrast, + Function toneDeltaConstraint) { + this.hue = hue; + this.chroma = chroma; + this.tone = tone; + this.opacity = opacity; + this.background = background; + this.toneMinContrast = toneMinContrast; + this.toneMaxContrast = toneMaxContrast; + this.toneDeltaConstraint = toneDeltaConstraint; + } + + /** + * Create a DynamicColor from a hex code. + * + *

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 tone) { + return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone); + } + + /** + * Create a DynamicColor. + * + *

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 tone, + Function background) { + return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone, background); + } + + /** + * Create a DynamicColor from: + * + * @param argb A hex code. + * @param tone Function that provides a tone given DynamicScheme. (useful for + * dark vs. light mode) + * @param background Function that provides background DynamicColor given + * DynamicScheme. Useful + * for contrast, given a background, colors can adjust to + * increase/decrease contrast. + * @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme. + * Useful for ensuring lightness difference between colors that don't + * _require_ contrast or + * have a formal background/foreground relationship. + */ + public static DynamicColor fromArgb( + int argb, + Function tone, + Function background, + Function toneDeltaConstraint) { + return DynamicColor.fromPalette( + (s) -> TonalPalette.fromInt(argb), tone, background, toneDeltaConstraint); + } + + /** + * Create a DynamicColor. + * + * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is + * defined by a hue and chroma, so this replaces the need to specify hue/chroma. + * By providing + * a tonal palette, when contrast adjustments are made, intended chroma can be + * preserved. For + * example, at T/L* 90, there is a significant limit to the amount of chroma. + * There is no + * colorful red, a red that light is pink. By preserving the _intended_ chroma if + * lightness + * lowers for contrast adjustments, the intended chroma is restored. + * @param tone Function that provides a tone given DynamicScheme. (useful for dark vs. light + * mode) + */ + public static DynamicColor fromPalette( + Function palette, Function tone) { + return fromPalette(palette, tone, null, null); + } + + /** + * Create a DynamicColor. + * + * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette + * is + * defined by a hue and chroma, so this replaces the need to specify + * hue/chroma. By providing + * a tonal palette, when contrast adjustments are made, intended chroma can be + * preserved. For + * example, at T/L* 90, there is a significant limit to the amount of chroma. + * There is no + * colorful red, a red that light is pink. By preserving the _intended_ chroma + * if lightness + * lowers for contrast adjustments, the intended chroma is restored. + * @param tone Function that provides a tone given DynamicScheme. (useful for dark vs. + * light mode) + * @param background Function that provides background DynamicColor given DynamicScheme. Useful + * for contrast, given a background, colors can adjust to increase/decrease + * contrast. + */ + public static DynamicColor fromPalette( + Function palette, + Function tone, + Function background) { + return fromPalette(palette, tone, background, null); + } + + /** + * Create a DynamicColor. + * + * @param palette Function that provides a TonalPalette given DynamicScheme. A + * TonalPalette is + * defined by a hue and chroma, so this replaces the need to specify + * hue/chroma. By providing + * a tonal palette, when contrast adjustments are made, intended + * chroma can be preserved. For + * example, at T/L* 90, there is a significant limit to the amount of + * chroma. There is no + * colorful red, a red that light is pink. By preserving the + * _intended_ chroma if lightness + * lowers for contrast adjustments, the intended chroma is restored. + * @param tone Function that provides a tone given DynamicScheme. (useful for + * dark vs. light mode) + * @param background Function that provides background DynamicColor given + * DynamicScheme. Useful + * for contrast, given a background, colors can adjust to + * increase/decrease contrast. + * @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme. + * Useful for ensuring lightness difference between colors that don't + * _require_ contrast or + * have a formal background/foreground relationship. + */ + public static DynamicColor fromPalette( + Function palette, + Function tone, + Function background, + Function toneDeltaConstraint) { + return new DynamicColor( + scheme -> palette.apply(scheme).getHue(), + scheme -> palette.apply(scheme).getChroma(), + tone, + null, + background, + scheme -> toneMinContrastDefault(tone, background, scheme, toneDeltaConstraint), + scheme -> toneMaxContrastDefault(tone, background, scheme, toneDeltaConstraint), + toneDeltaConstraint); + } + + /** Returns the ARGB (i.e. hex code) representation of the resolved color given scheme. */ + public int getArgb(DynamicScheme scheme) { + final int argb = getHct(scheme).toInt(); + if (opacity == null) { + return argb; + } + final double percentage = opacity.apply(scheme); + final int alpha = MathUtils.clampInt(0, 255, (int) Math.round(percentage * 255)); + return (argb & 0x00ffffff) | (alpha << 24); + } + + /** Returns the HCT representation of the resolved color given scheme. */ + public Hct getHct(DynamicScheme scheme) { + final Hct cachedAnswer = hctCache.get(scheme); + if (cachedAnswer != null) { + return cachedAnswer; + } + // This is crucial for aesthetics: we aren't simply the taking the standard color + // and changing its tone for contrast. Rather, we find the tone for contrast, then + // use the specified chroma from the palette to construct a new color. + // + // For example, this enables colors with standard tone of T90, which has limited chroma, to + // "recover" intended chroma as contrast increases. + final Hct answer = Hct.from(hue.apply(scheme), chroma.apply(scheme), getTone(scheme)); + // NOMUTANTS--trivial test with onerous dependency injection requirement. + if (hctCache.size() > 4) { + hctCache.clear(); + } + // NOMUTANTS--trivial test with onerous dependency injection requirement. + hctCache.put(scheme, answer); + return answer; + } + + /** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */ + public double getTone(DynamicScheme scheme) { + double answer = tone.apply(scheme); + + final boolean decreasingContrast = scheme.contrastLevel < 0.0; + if (scheme.contrastLevel != 0.0) { + final double startTone = tone.apply(scheme); + final double endTone = + decreasingContrast ? toneMinContrast.apply(scheme) : toneMaxContrast.apply( + scheme); + final double delta = (endTone - startTone) * Math.abs(scheme.contrastLevel); + answer = delta + startTone; + } + + final DynamicColor bgDynamicColor = background == null ? null : background.apply(scheme); + double minRatio = Contrast.RATIO_MIN; + double maxRatio = Contrast.RATIO_MAX; + if (bgDynamicColor != null) { + final boolean bgHasBg = + bgDynamicColor.background != null && bgDynamicColor.background.apply(scheme) + == null; + final double standardRatio = + Contrast.ratioOfTones(tone.apply(scheme), bgDynamicColor.tone.apply(scheme)); + if (decreasingContrast) { + final double minContrastRatio = + Contrast.ratioOfTones( + toneMinContrast.apply(scheme), + bgDynamicColor.toneMinContrast.apply(scheme)); + minRatio = bgHasBg ? 1.0 : minContrastRatio; + maxRatio = standardRatio; + } else { + final double maxContrastRatio = + Contrast.ratioOfTones( + toneMaxContrast.apply(scheme), + bgDynamicColor.toneMaxContrast.apply(scheme)); + minRatio = !bgHasBg ? 1.0 : min(maxContrastRatio, standardRatio); + maxRatio = !bgHasBg ? 21.0 : max(maxContrastRatio, standardRatio); + } + } + + final double finalMinRatio = minRatio; + final double finalMaxRatio = maxRatio; + final double finalAnswer = answer; + answer = + calculateDynamicTone( + scheme, + this.tone, + (dynamicColor) -> dynamicColor.getTone(scheme), + (a, b) -> finalAnswer, + (s) -> bgDynamicColor, + toneDeltaConstraint, + (s) -> finalMinRatio, + (s) -> finalMaxRatio); + + return answer; + } + + /** + * The default algorithm for calculating the tone of a color at minimum contrast.
+ * 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 tone, + Function background, + DynamicScheme scheme, + Function toneDeltaConstraint) { + return DynamicColor.calculateDynamicTone( + scheme, + tone, + c -> c.toneMinContrast.apply(scheme), + (stdRatio, bgTone) -> { + double answer = tone.apply(scheme); + if (stdRatio >= Contrast.RATIO_70) { + answer = contrastingTone(bgTone, Contrast.RATIO_45); + } else if (stdRatio >= Contrast.RATIO_30) { + answer = contrastingTone(bgTone, Contrast.RATIO_30); + } else { + final boolean backgroundHasBackground = + background != null + && background.apply(scheme) != null + && background.apply(scheme).background != null + && background.apply(scheme).background.apply(scheme) + != null; + if (backgroundHasBackground) { + answer = contrastingTone(bgTone, stdRatio); + } + } + return answer; + }, + background, + toneDeltaConstraint, + null, + standardRatio -> standardRatio); + } + + /** + * The default algorithm for calculating the tone of a color at maximum contrast.
+ * If the color's background has a background, reach contrast 7.0.
+ * If it doesn't, maintain the original contrast ratio.
+ * + *

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 tone, + Function background, + DynamicScheme scheme, + Function toneDeltaConstraint) { + return DynamicColor.calculateDynamicTone( + scheme, + tone, + c -> c.toneMaxContrast.apply(scheme), + (stdRatio, bgTone) -> { + final boolean backgroundHasBackground = + background != null + && background.apply(scheme) != null + && background.apply(scheme).background != null + && background.apply(scheme).background.apply(scheme) != null; + if (backgroundHasBackground) { + return contrastingTone(bgTone, Contrast.RATIO_70); + } else { + return contrastingTone(bgTone, max(Contrast.RATIO_70, stdRatio)); + } + }, + background, + toneDeltaConstraint, + null, + null); + } + + /** + * Core method for calculating a tone for under dynamic contrast. + * + *

It enforces important properties:
+ * #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. + * + *

#2. Enable light foregrounds on midtones.
+ * 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. + * + *

#3. Ensure tone delta with another color.
+ * 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 toneStandard, + Function toneToJudge, + BiFunction desiredTone, + Function background, + Function toneDeltaConstraint, + Function minRatio, + Function maxRatio) { + // Start with the tone with no adjustment for contrast. + // If there is no background, don't perform any adjustment, return immediately. + final double toneStd = toneStandard.apply(scheme); + double answer = toneStd; + final DynamicColor bgDynamic = background == null ? null : background.apply(scheme); + if (bgDynamic == null) { + return answer; + } + final double bgToneStd = bgDynamic.tone.apply(scheme); + final double stdRatio = Contrast.ratioOfTones(toneStd, bgToneStd); + + // If there is a background, determine its tone after contrast adjustment. + // Then, calculate the foreground tone that ensures the caller's desired contrast ratio + // is met. + final double bgTone = toneToJudge.apply(bgDynamic); + final double myDesiredTone = desiredTone.apply(stdRatio, bgTone); + final double currentRatio = Contrast.ratioOfTones(bgTone, myDesiredTone); + final double minRatioRealized = + minRatio == null + ? Contrast.RATIO_MIN + : minRatio.apply(stdRatio) == null ? Contrast.RATIO_MIN : minRatio.apply( + stdRatio); + final double maxRatioRealized = + maxRatio == null + ? Contrast.RATIO_MAX + : maxRatio.apply(stdRatio) == null ? Contrast.RATIO_MAX : maxRatio.apply( + stdRatio); + final double desiredRatio = + MathUtils.clampDouble(minRatioRealized, maxRatioRealized, currentRatio); + if (desiredRatio == currentRatio) { + answer = myDesiredTone; + } else { + answer = DynamicColor.contrastingTone(bgTone, desiredRatio); + } + + // If the background has no background, adjust the foreground tone to ensure that + // it is dark enough to have a light foreground. + if (bgDynamic.background == null || bgDynamic.background.apply(scheme) == null) { + answer = DynamicColor.enableLightForeground(answer); + } + + // If the caller has specified a constraint where it must have a certain tone distance from + // another color, enforce that constraint. + answer = ensureToneDelta(answer, toneStd, scheme, toneDeltaConstraint, toneToJudge); + + return answer; + } + + static double ensureToneDelta( + double tone, + double toneStandard, + DynamicScheme scheme, + Function toneDeltaConstraint, + Function toneToDistanceFrom) { + final ToneDeltaConstraint constraint = + (toneDeltaConstraint == null ? null : toneDeltaConstraint.apply(scheme)); + if (constraint == null) { + return tone; + } + + final double requiredDelta = constraint.delta; + final double keepAwayTone = toneToDistanceFrom.apply(constraint.keepAway); + final double delta = Math.abs(tone - keepAwayTone); + if (delta >= requiredDelta) { + return tone; + } + switch (constraint.keepAwayPolarity) { + case DARKER: + return MathUtils.clampDouble(0, 100, keepAwayTone + requiredDelta); + case LIGHTER: + return MathUtils.clampDouble(0, 100, keepAwayTone - requiredDelta); + case NO_PREFERENCE: + final double keepAwayToneStandard = constraint.keepAway.tone.apply(scheme); + final boolean preferLighten = toneStandard > keepAwayToneStandard; + final double alterAmount = Math.abs(delta - requiredDelta); + final boolean lighten = + preferLighten ? (tone + alterAmount <= 100.0) : tone < alterAmount; + return lighten ? tone + alterAmount : tone - alterAmount; + } + return tone; + } + + /** + * Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio + * that is as close to ratio as possible. + */ + public static double contrastingTone(double bgTone, double ratio) { + final double lighterTone = Contrast.lighterUnsafe(bgTone, ratio); + final double darkerTone = Contrast.darkerUnsafe(bgTone, ratio); + final double lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone); + final double darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone); + final boolean preferLighter = tonePrefersLightForeground(bgTone); + + if (preferLighter) { + // "Negligible difference" handles an edge case where the initial contrast ratio is high + // (ex. 13.0), and the ratio passed to the function is that high ratio, and both the + // lighter and darker ratio fails to pass that ratio. + // + // This was observed with Tonal Spot's On Primary Container turning black momentarily + // between high and max contrast in light mode. PC's standard tone was T90, OPC's was + // T10, it was light mode, and the contrast level was 0.6568521221032331. + final boolean negligibleDifference = + Math.abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio + && darkerRatio < ratio; + if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) { + return lighterTone; + } else { + return darkerTone; + } + } else { + return darkerRatio >= ratio || darkerRatio >= lighterRatio ? darkerTone : lighterTone; + } + } + + /** + * Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to + * supporting it. + */ + public static double enableLightForeground(double tone) { + if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) { + return 49.0; + } + return tone; + } + + /** + * People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers + * during research for APCA. + * + *

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 cache; + double hue; + double chroma; + + /** + * Create tones using the HCT hue and chroma from a color. + * + * @param argb ARGB representation of a color + * @return Tones matching that color's hue and chroma. + */ + public static TonalPalette fromInt(int argb) { + return fromHct(Hct.fromInt(argb)); + } + + /** + * Create tones using a HCT color. + * + * @param hct HCT representation of a color. + * @return Tones matching that color's hue and chroma. + */ + public static TonalPalette fromHct(Hct hct) { + return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma()); + } + + /** + * Create tones from a defined HCT hue and chroma. + * + * @param hue HCT hue + * @param chroma HCT chroma + * @return Tones matching hue and chroma. + */ + public static TonalPalette fromHueAndChroma(double hue, double chroma) { + return new TonalPalette(hue, chroma); + } + + private TonalPalette(double hue, double chroma) { + cache = new HashMap<>(); + this.hue = hue; + this.chroma = chroma; + } + + /** + * Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT + * tone. + * + * @param tone HCT tone, measured from 0 to 100. + * @return ARGB representation of a color with that tone. + */ + // AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923) + @SuppressWarnings("ComputeIfAbsentUseValue") + public int tone(int tone) { + Integer color = cache.get(tone); + if (color == null) { + color = Hct.from(this.hue, this.chroma, tone).toInt(); + cache.put(tone, color); + } + return color; + } + + /** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */ + public Hct getHct(double tone) { + return Hct.from(this.hue, this.chroma, tone); + } + + /** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */ + public double getChroma() { + return this.chroma; + } + + /** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */ + public double getHue() { + return this.hue; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java new file mode 100644 index 0000000000000..99ee0d4d7c68a --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +/** An interface to allow use of different color spaces by quantizers. */ +public interface PointProvider { + /** The three components in the color space of an sRGB color. */ + double[] fromInt(int argb); + + /** The ARGB (i.e. hex code) representation of this color. */ + int toInt(double[] point); + + /** + * Squared distance between two colors. + * Distance is defined by scientific color spaces and referred to as delta E. + */ + double distance(double[] a, double[] b); +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java new file mode 100644 index 0000000000000..e852d5ad9c60a --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/PointProviderLab.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +import com.android.systemui.monet.utils.ColorUtils; + +/** + * Provides conversions needed for K-Means quantization. Converting input to points, and converting + * the final state of the K-Means algorithm to colors. + */ +public final class PointProviderLab implements PointProvider { + /** + * Convert a color represented in ARGB to a 3-element array of L*a*b* coordinates of the color. + */ + @Override + public double[] fromInt(int argb) { + double[] lab = ColorUtils.labFromArgb(argb); + return new double[]{lab[0], lab[1], lab[2]}; + } + + /** Convert a 3-element array to a color represented in ARGB. */ + @Override + public int toInt(double[] lab) { + return ColorUtils.argbFromLab(lab[0], lab[1], lab[2]); + } + + /** + * Standard CIE 1976 delta E formula also takes the square root, unneeded here. This method is + * used by quantization algorithms to compare distance, and the relative ordering is the same, + * with or without a square root. + * + *

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 quantize(int[] pixels, int maxColors) { + QuantizerWu wu = new QuantizerWu(); + QuantizerResult wuResult = wu.quantize(pixels, maxColors); + + Set wuClustersAsObjects = wuResult.colorToCount.keySet(); + int index = 0; + int[] wuClusters = new int[wuClustersAsObjects.size()]; + for (Integer argb : wuClustersAsObjects) { + wuClusters[index++] = argb; + } + + return QuantizerWsmeans.quantize(pixels, wuClusters, maxColors); + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java new file mode 100644 index 0000000000000..9dae8ffa2c413 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerMap.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Creates a dictionary with keys of colors, and values of count of the color */ +public final class QuantizerMap implements Quantizer { + Map colorToCount; + + @Override + public QuantizerResult quantize(int[] pixels, int colorCount) { + final Map pixelByCount = new LinkedHashMap<>(); + for (int pixel : pixels) { + final Integer currentPixelCount = pixelByCount.get(pixel); + final int newPixelCount = currentPixelCount == null ? 1 : currentPixelCount + 1; + pixelByCount.put(pixel, newPixelCount); + } + colorToCount = pixelByCount; + return new QuantizerResult(pixelByCount); + } + + public Map getColorToCount() { + return colorToCount; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java new file mode 100644 index 0000000000000..a2d06aff3773b --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerResult.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +import java.util.Map; + +/** Represents result of a quantizer run */ +public final class QuantizerResult { + public final Map colorToCount; + + QuantizerResult(Map colorToCount) { + this.colorToCount = colorToCount; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java new file mode 100644 index 0000000000000..75dd75bd518b0 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWsmeans.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +import static java.lang.Math.min; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +/** + * An image quantizer that improves on the speed of a standard K-Means algorithm by implementing + * several optimizations, including deduping identical pixels and a triangle inequality rule that + * reduces the number of comparisons needed to identify which cluster a point should be moved to. + * + *

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 { + int index; + double distance; + + Distance() { + this.index = -1; + this.distance = -1; + } + + @Override + public int compareTo(Distance other) { + return ((Double) this.distance).compareTo(other.distance); + } + } + + private static final int MAX_ITERATIONS = 10; + private static final double MIN_MOVEMENT_DISTANCE = 3.0; + + /** + * Reduce the number of colors needed to represented the input, minimizing the difference + * between + * the original image and the recolored image. + * + * @param inputPixels Colors in ARGB format. + * @param startingClusters Defines the initial state of the quantizer. Passing an empty array + * is + * fine, the implementation will create its own initial state that leads + * to reproducible + * results for the same inputs. Passing an array that is the result of + * Wu quantization leads + * to higher quality results. + * @param maxColors The number of colors to divide the image into. A lower number of + * colors may be + * returned. + * @return Map with keys of colors in ARGB format, values of how many of the input pixels belong + * to the color. + */ + public static Map quantize( + int[] inputPixels, int[] startingClusters, int maxColors) { + // Uses a seeded random number generator to ensure consistent results. + Random random = new Random(0x42688); + + Map pixelToCount = new LinkedHashMap<>(); + double[][] points = new double[inputPixels.length][]; + int[] pixels = new int[inputPixels.length]; + PointProvider pointProvider = new PointProviderLab(); + + int pointCount = 0; + for (int i = 0; i < inputPixels.length; i++) { + int inputPixel = inputPixels[i]; + Integer pixelCount = pixelToCount.get(inputPixel); + if (pixelCount == null) { + points[pointCount] = pointProvider.fromInt(inputPixel); + pixels[pointCount] = inputPixel; + pointCount++; + + pixelToCount.put(inputPixel, 1); + } else { + pixelToCount.put(inputPixel, pixelCount + 1); + } + } + + int[] counts = new int[pointCount]; + for (int i = 0; i < pointCount; i++) { + int pixel = pixels[i]; + int count = pixelToCount.get(pixel); + counts[i] = count; + } + + int clusterCount = min(maxColors, pointCount); + if (startingClusters.length != 0) { + clusterCount = min(clusterCount, startingClusters.length); + } + + double[][] clusters = new double[clusterCount][]; + for (int i = 0; i < startingClusters.length; i++) { + clusters[i] = pointProvider.fromInt(startingClusters[i]); + } + + int[] clusterIndices = new int[pointCount]; + for (int i = 0; i < pointCount; i++) { + clusterIndices[i] = random.nextInt(clusterCount); + } + + int[][] indexMatrix = new int[clusterCount][]; + for (int i = 0; i < clusterCount; i++) { + indexMatrix[i] = new int[clusterCount]; + } + + Distance[][] distanceToIndexMatrix = new Distance[clusterCount][]; + for (int i = 0; i < clusterCount; i++) { + distanceToIndexMatrix[i] = new Distance[clusterCount]; + for (int j = 0; j < clusterCount; j++) { + distanceToIndexMatrix[i][j] = new Distance(); + } + } + + int[] pixelCountSums = new int[clusterCount]; + for (int iteration = 0; iteration < MAX_ITERATIONS; iteration++) { + for (int i = 0; i < clusterCount; i++) { + for (int j = i + 1; j < clusterCount; j++) { + double distance = pointProvider.distance(clusters[i], clusters[j]); + distanceToIndexMatrix[j][i].distance = distance; + distanceToIndexMatrix[j][i].index = i; + distanceToIndexMatrix[i][j].distance = distance; + distanceToIndexMatrix[i][j].index = j; + } + Arrays.sort(distanceToIndexMatrix[i]); + for (int j = 0; j < clusterCount; j++) { + indexMatrix[i][j] = distanceToIndexMatrix[i][j].index; + } + } + + int pointsMoved = 0; + for (int i = 0; i < pointCount; i++) { + double[] point = points[i]; + int previousClusterIndex = clusterIndices[i]; + double[] previousCluster = clusters[previousClusterIndex]; + double previousDistance = pointProvider.distance(point, previousCluster); + + double minimumDistance = previousDistance; + int newClusterIndex = -1; + for (int j = 0; j < clusterCount; j++) { + if (distanceToIndexMatrix[previousClusterIndex][j].distance + >= 4 * previousDistance) { + continue; + } + double distance = pointProvider.distance(point, clusters[j]); + if (distance < minimumDistance) { + minimumDistance = distance; + newClusterIndex = j; + } + } + if (newClusterIndex != -1) { + double distanceChange = + Math.abs(Math.sqrt(minimumDistance) - Math.sqrt(previousDistance)); + if (distanceChange > MIN_MOVEMENT_DISTANCE) { + pointsMoved++; + clusterIndices[i] = newClusterIndex; + } + } + } + + if (pointsMoved == 0 && iteration != 0) { + break; + } + + double[] componentASums = new double[clusterCount]; + double[] componentBSums = new double[clusterCount]; + double[] componentCSums = new double[clusterCount]; + Arrays.fill(pixelCountSums, 0); + for (int i = 0; i < pointCount; i++) { + int clusterIndex = clusterIndices[i]; + double[] point = points[i]; + int count = counts[i]; + pixelCountSums[clusterIndex] += count; + componentASums[clusterIndex] += (point[0] * count); + componentBSums[clusterIndex] += (point[1] * count); + componentCSums[clusterIndex] += (point[2] * count); + } + + for (int i = 0; i < clusterCount; i++) { + int count = pixelCountSums[i]; + if (count == 0) { + clusters[i] = new double[]{0., 0., 0.}; + continue; + } + double a = componentASums[i] / count; + double b = componentBSums[i] / count; + double c = componentCSums[i] / count; + clusters[i][0] = a; + clusters[i][1] = b; + clusters[i][2] = c; + } + } + + Map argbToPopulation = new LinkedHashMap<>(); + for (int i = 0; i < clusterCount; i++) { + int count = pixelCountSums[i]; + if (count == 0) { + continue; + } + + int possibleNewCluster = pointProvider.toInt(clusters[i]); + if (argbToPopulation.containsKey(possibleNewCluster)) { + continue; + } + + argbToPopulation.put(possibleNewCluster, count); + } + + return argbToPopulation; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java new file mode 100644 index 0000000000000..f788d4583529d --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/quantize/QuantizerWu.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.quantize; + +import com.android.systemui.monet.utils.ColorUtils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An image quantizer that divides the image's pixels into clusters by recursively cutting an RGB + * cube, based on the weight of pixels in each area of the cube. + * + *

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 colors = createResult(createBoxesResult.resultCount); + Map resultMap = new LinkedHashMap<>(); + for (int color : colors) { + resultMap.put(color, 0); + } + return new QuantizerResult(resultMap); + } + + static int getIndex(int r, int g, int b) { + return (r << (INDEX_BITS * 2)) + (r << (INDEX_BITS + 1)) + r + (g << INDEX_BITS) + g + b; + } + + void constructHistogram(Map pixels) { + weights = new int[TOTAL_SIZE]; + momentsR = new int[TOTAL_SIZE]; + momentsG = new int[TOTAL_SIZE]; + momentsB = new int[TOTAL_SIZE]; + moments = new double[TOTAL_SIZE]; + + for (Map.Entry pair : pixels.entrySet()) { + int pixel = pair.getKey(); + int count = pair.getValue(); + int red = ColorUtils.redFromArgb(pixel); + int green = ColorUtils.greenFromArgb(pixel); + int blue = ColorUtils.blueFromArgb(pixel); + int bitsToRemove = 8 - INDEX_BITS; + int iR = (red >> bitsToRemove) + 1; + int iG = (green >> bitsToRemove) + 1; + int iB = (blue >> bitsToRemove) + 1; + int index = getIndex(iR, iG, iB); + weights[index] += count; + momentsR[index] += (red * count); + momentsG[index] += (green * count); + momentsB[index] += (blue * count); + moments[index] += (count * ((red * red) + (green * green) + (blue * blue))); + } + } + + void createMoments() { + for (int r = 1; r < INDEX_COUNT; ++r) { + int[] area = new int[INDEX_COUNT]; + int[] areaR = new int[INDEX_COUNT]; + int[] areaG = new int[INDEX_COUNT]; + int[] areaB = new int[INDEX_COUNT]; + double[] area2 = new double[INDEX_COUNT]; + + for (int g = 1; g < INDEX_COUNT; ++g) { + int line = 0; + int lineR = 0; + int lineG = 0; + int lineB = 0; + double line2 = 0.0; + for (int b = 1; b < INDEX_COUNT; ++b) { + int index = getIndex(r, g, b); + line += weights[index]; + lineR += momentsR[index]; + lineG += momentsG[index]; + lineB += momentsB[index]; + line2 += moments[index]; + + area[b] += line; + areaR[b] += lineR; + areaG[b] += lineG; + areaB[b] += lineB; + area2[b] += line2; + + int previousIndex = getIndex(r - 1, g, b); + weights[index] = weights[previousIndex] + area[b]; + momentsR[index] = momentsR[previousIndex] + areaR[b]; + momentsG[index] = momentsG[previousIndex] + areaG[b]; + momentsB[index] = momentsB[previousIndex] + areaB[b]; + moments[index] = moments[previousIndex] + area2[b]; + } + } + } + } + + CreateBoxesResult createBoxes(int maxColorCount) { + cubes = new Box[maxColorCount]; + for (int i = 0; i < maxColorCount; i++) { + cubes[i] = new Box(); + } + double[] volumeVariance = new double[maxColorCount]; + Box firstBox = cubes[0]; + firstBox.r1 = INDEX_COUNT - 1; + firstBox.g1 = INDEX_COUNT - 1; + firstBox.b1 = INDEX_COUNT - 1; + + int generatedColorCount = maxColorCount; + int next = 0; + for (int i = 1; i < maxColorCount; i++) { + if (cut(cubes[next], cubes[i])) { + volumeVariance[next] = (cubes[next].vol > 1) ? variance(cubes[next]) : 0.0; + volumeVariance[i] = (cubes[i].vol > 1) ? variance(cubes[i]) : 0.0; + } else { + volumeVariance[next] = 0.0; + i--; + } + + next = 0; + + double temp = volumeVariance[0]; + for (int j = 1; j <= i; j++) { + if (volumeVariance[j] > temp) { + temp = volumeVariance[j]; + next = j; + } + } + if (temp <= 0.0) { + generatedColorCount = i + 1; + break; + } + } + + return new CreateBoxesResult(maxColorCount, generatedColorCount); + } + + List createResult(int colorCount) { + List colors = new ArrayList<>(); + for (int i = 0; i < colorCount; ++i) { + Box cube = cubes[i]; + int weight = volume(cube, weights); + if (weight > 0) { + int r = volume(cube, momentsR) / weight; + int g = volume(cube, momentsG) / weight; + int b = volume(cube, momentsB) / weight; + int color = (255 << 24) | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff); + colors.add(color); + } + } + return colors; + } + + double variance(Box cube) { + int dr = volume(cube, momentsR); + int dg = volume(cube, momentsG); + int db = volume(cube, momentsB); + double xx = + moments[getIndex(cube.r1, cube.g1, cube.b1)] + - moments[getIndex(cube.r1, cube.g1, cube.b0)] + - moments[getIndex(cube.r1, cube.g0, cube.b1)] + + moments[getIndex(cube.r1, cube.g0, cube.b0)] + - moments[getIndex(cube.r0, cube.g1, cube.b1)] + + moments[getIndex(cube.r0, cube.g1, cube.b0)] + + moments[getIndex(cube.r0, cube.g0, cube.b1)] + - moments[getIndex(cube.r0, cube.g0, cube.b0)]; + + int hypotenuse = dr * dr + dg * dg + db * db; + int volume = volume(cube, weights); + return xx - hypotenuse / ((double) volume); + } + + Boolean cut(Box one, Box two) { + int wholeR = volume(one, momentsR); + int wholeG = volume(one, momentsG); + int wholeB = volume(one, momentsB); + int wholeW = volume(one, weights); + + MaximizeResult maxRResult = + maximize(one, Direction.RED, one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW); + MaximizeResult maxGResult = + maximize(one, Direction.GREEN, one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW); + MaximizeResult maxBResult = + maximize(one, Direction.BLUE, one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW); + Direction cutDirection; + double maxR = maxRResult.maximum; + double maxG = maxGResult.maximum; + double maxB = maxBResult.maximum; + if (maxR >= maxG && maxR >= maxB) { + if (maxRResult.cutLocation < 0) { + return false; + } + cutDirection = Direction.RED; + } else if (maxG >= maxR && maxG >= maxB) { + cutDirection = Direction.GREEN; + } else { + cutDirection = Direction.BLUE; + } + + two.r1 = one.r1; + two.g1 = one.g1; + two.b1 = one.b1; + + switch (cutDirection) { + case RED: + one.r1 = maxRResult.cutLocation; + two.r0 = one.r1; + two.g0 = one.g0; + two.b0 = one.b0; + break; + case GREEN: + one.g1 = maxGResult.cutLocation; + two.r0 = one.r0; + two.g0 = one.g1; + two.b0 = one.b0; + break; + case BLUE: + one.b1 = maxBResult.cutLocation; + two.r0 = one.r0; + two.g0 = one.g0; + two.b0 = one.b1; + break; + } + + one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0); + two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0); + + return true; + } + + MaximizeResult maximize( + Box cube, + Direction direction, + int first, + int last, + int wholeR, + int wholeG, + int wholeB, + int wholeW) { + int bottomR = bottom(cube, direction, momentsR); + int bottomG = bottom(cube, direction, momentsG); + int bottomB = bottom(cube, direction, momentsB); + int bottomW = bottom(cube, direction, weights); + + double max = 0.0; + int cut = -1; + + int halfR = 0; + int halfG = 0; + int halfB = 0; + int halfW = 0; + for (int i = first; i < last; i++) { + halfR = bottomR + top(cube, direction, i, momentsR); + halfG = bottomG + top(cube, direction, i, momentsG); + halfB = bottomB + top(cube, direction, i, momentsB); + halfW = bottomW + top(cube, direction, i, weights); + if (halfW == 0) { + continue; + } + + double tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB; + double tempDenominator = halfW; + double temp = tempNumerator / tempDenominator; + + halfR = wholeR - halfR; + halfG = wholeG - halfG; + halfB = wholeB - halfB; + halfW = wholeW - halfW; + if (halfW == 0) { + continue; + } + + tempNumerator = halfR * halfR + halfG * halfG + halfB * halfB; + tempDenominator = halfW; + temp += (tempNumerator / tempDenominator); + + if (temp > max) { + max = temp; + cut = i; + } + } + return new MaximizeResult(cut, max); + } + + static int volume(Box cube, int[] moment) { + return (moment[getIndex(cube.r1, cube.g1, cube.b1)] + - moment[getIndex(cube.r1, cube.g1, cube.b0)] + - moment[getIndex(cube.r1, cube.g0, cube.b1)] + + moment[getIndex(cube.r1, cube.g0, cube.b0)] + - moment[getIndex(cube.r0, cube.g1, cube.b1)] + + moment[getIndex(cube.r0, cube.g1, cube.b0)] + + moment[getIndex(cube.r0, cube.g0, cube.b1)] + - moment[getIndex(cube.r0, cube.g0, cube.b0)]); + } + + static int bottom(Box cube, Direction direction, int[] moment) { + switch (direction) { + case RED: + return -moment[getIndex(cube.r0, cube.g1, cube.b1)] + + moment[getIndex(cube.r0, cube.g1, cube.b0)] + + moment[getIndex(cube.r0, cube.g0, cube.b1)] + - moment[getIndex(cube.r0, cube.g0, cube.b0)]; + case GREEN: + return -moment[getIndex(cube.r1, cube.g0, cube.b1)] + + moment[getIndex(cube.r1, cube.g0, cube.b0)] + + moment[getIndex(cube.r0, cube.g0, cube.b1)] + - moment[getIndex(cube.r0, cube.g0, cube.b0)]; + case BLUE: + return -moment[getIndex(cube.r1, cube.g1, cube.b0)] + + moment[getIndex(cube.r1, cube.g0, cube.b0)] + + moment[getIndex(cube.r0, cube.g1, cube.b0)] + - moment[getIndex(cube.r0, cube.g0, cube.b0)]; + } + throw new IllegalArgumentException("unexpected direction " + direction); + } + + static int top(Box cube, Direction direction, int position, int[] moment) { + switch (direction) { + case RED: + return (moment[getIndex(position, cube.g1, cube.b1)] + - moment[getIndex(position, cube.g1, cube.b0)] + - moment[getIndex(position, cube.g0, cube.b1)] + + moment[getIndex(position, cube.g0, cube.b0)]); + case GREEN: + return (moment[getIndex(cube.r1, position, cube.b1)] + - moment[getIndex(cube.r1, position, cube.b0)] + - moment[getIndex(cube.r0, position, cube.b1)] + + moment[getIndex(cube.r0, position, cube.b0)]); + case BLUE: + return (moment[getIndex(cube.r1, cube.g1, position)] + - moment[getIndex(cube.r1, cube.g0, position)] + - moment[getIndex(cube.r0, cube.g1, position)] + + moment[getIndex(cube.r0, cube.g0, position)]); + } + throw new IllegalArgumentException("unexpected direction " + direction); + } + + private enum Direction { + RED, + GREEN, + BLUE + } + + private static final class MaximizeResult { + // < 0 if cut impossible + int cutLocation; + double maximum; + + MaximizeResult(int cut, double max) { + this.cutLocation = cut; + this.maximum = max; + } + } + + private static final class CreateBoxesResult { + int requestedCount; + int resultCount; + + CreateBoxesResult(int requestedCount, int resultCount) { + this.requestedCount = requestedCount; + this.resultCount = resultCount; + } + } + + private static final class Box { + int r0 = 0; + int r1 = 0; + int g0 = 0; + int g1 = 0; + int b0 = 0; + int b1 = 0; + int vol = 0; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.java new file mode 100644 index 0000000000000..e1cbfb123a4ab --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/DynamicScheme.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.scheme; + +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.palettes.TonalPalette; +import com.android.systemui.monet.utils.MathUtils; + +/** + * Provides important settings for creating colors dynamically, and 6 color palettes. Requires: 1. A + * color. (source color) 2. A theme. (Variant) 3. Whether or not its dark mode. 4. Contrast level. + * (-1 to 1, currently contrast ratio 3.0 and 7.0) + */ +public class DynamicScheme { + public final int sourceColorArgb; + public final Hct sourceColorHct; + public final Variant variant; + public final boolean isDark; + public final double contrastLevel; + + public final TonalPalette primaryPalette; + public final TonalPalette secondaryPalette; + public final TonalPalette tertiaryPalette; + public final TonalPalette neutralPalette; + public final TonalPalette neutralVariantPalette; + public final TonalPalette errorPalette; + + public DynamicScheme( + Hct sourceColorHct, + Variant variant, + boolean isDark, + double contrastLevel, + TonalPalette primaryPalette, + TonalPalette secondaryPalette, + TonalPalette tertiaryPalette, + TonalPalette neutralPalette, + TonalPalette neutralVariantPalette) { + this.sourceColorArgb = sourceColorHct.toInt(); + this.sourceColorHct = sourceColorHct; + this.variant = variant; + this.isDark = isDark; + this.contrastLevel = contrastLevel; + + this.primaryPalette = primaryPalette; + this.secondaryPalette = secondaryPalette; + this.tertiaryPalette = tertiaryPalette; + this.neutralPalette = neutralPalette; + this.neutralVariantPalette = neutralVariantPalette; + this.errorPalette = TonalPalette.fromHueAndChroma(25.0, 84.0); + } + + /** + * Given a set of hues and set of hue rotations, locate which hues the source color's hue is + * between, apply the rotation at the same index as the first hue in the range, and return the + * rotated hue. + * + * @param sourceColorHct The color whose hue should be rotated. + * @param hues A set of hues. + * @param rotations A set of hue rotations. + * @return Color's hue with a rotation applied. + */ + public static double getRotatedHue(Hct sourceColorHct, double[] hues, double[] rotations) { + final double sourceHue = sourceColorHct.getHue(); + if (rotations.length == 1) { + return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[0]); + } + final int size = hues.length; + for (int i = 0; i <= (size - 2); i++) { + final double thisHue = hues[i]; + final double nextHue = hues[i + 1]; + if (thisHue < sourceHue && sourceHue < nextHue) { + return MathUtils.sanitizeDegreesDouble(sourceHue + rotations[i]); + } + } + // If this statement executes, something is wrong, there should have been a rotation + // found using the arrays. + return sourceHue; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java new file mode 100644 index 0000000000000..fa736cc88e291 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/Scheme.java @@ -0,0 +1,784 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.scheme; + +import com.android.systemui.monet.palettes.CorePalette; + +/** Represents a Material color scheme, a mapping of color roles to colors. */ +public class Scheme { + private int primary; + private int onPrimary; + private int primaryContainer; + private int onPrimaryContainer; + private int secondary; + private int onSecondary; + private int secondaryContainer; + private int onSecondaryContainer; + private int tertiary; + private int onTertiary; + private int tertiaryContainer; + private int onTertiaryContainer; + private int error; + private int onError; + private int errorContainer; + private int onErrorContainer; + private int background; + private int onBackground; + private int surface; + private int onSurface; + private int surfaceVariant; + private int onSurfaceVariant; + private int outline; + private int outlineVariant; + private int shadow; + private int scrim; + private int inverseSurface; + private int inverseOnSurface; + private int inversePrimary; + + public Scheme() { + } + + public Scheme( + int primary, + int onPrimary, + int primaryContainer, + int onPrimaryContainer, + int secondary, + int onSecondary, + int secondaryContainer, + int onSecondaryContainer, + int tertiary, + int onTertiary, + int tertiaryContainer, + int onTertiaryContainer, + int error, + int onError, + int errorContainer, + int onErrorContainer, + int background, + int onBackground, + int surface, + int onSurface, + int surfaceVariant, + int onSurfaceVariant, + int outline, + int outlineVariant, + int shadow, + int scrim, + int inverseSurface, + int inverseOnSurface, + int inversePrimary) { + super(); + this.primary = primary; + this.onPrimary = onPrimary; + this.primaryContainer = primaryContainer; + this.onPrimaryContainer = onPrimaryContainer; + this.secondary = secondary; + this.onSecondary = onSecondary; + this.secondaryContainer = secondaryContainer; + this.onSecondaryContainer = onSecondaryContainer; + this.tertiary = tertiary; + this.onTertiary = onTertiary; + this.tertiaryContainer = tertiaryContainer; + this.onTertiaryContainer = onTertiaryContainer; + this.error = error; + this.onError = onError; + this.errorContainer = errorContainer; + this.onErrorContainer = onErrorContainer; + this.background = background; + this.onBackground = onBackground; + this.surface = surface; + this.onSurface = onSurface; + this.surfaceVariant = surfaceVariant; + this.onSurfaceVariant = onSurfaceVariant; + this.outline = outline; + this.outlineVariant = outlineVariant; + this.shadow = shadow; + this.scrim = scrim; + this.inverseSurface = inverseSurface; + this.inverseOnSurface = inverseOnSurface; + this.inversePrimary = inversePrimary; + } + + /** Create a light theme Scheme from a source color in ARGB, i.e. a hex code. */ + public static Scheme light(int argb) { + return lightFromCorePalette(CorePalette.of(argb)); + } + + /** Create a dark theme Scheme from a source color in ARGB, i.e. a hex code. */ + public static Scheme dark(int argb) { + return darkFromCorePalette(CorePalette.of(argb)); + } + + /** Create a light theme content-based Scheme from a source color in ARGB, i.e. a hex code. */ + public static Scheme lightContent(int argb) { + return lightFromCorePalette(CorePalette.contentOf(argb)); + } + + /** Create a dark theme content-based Scheme from a source color in ARGB, i.e. a hex code. */ + public static Scheme darkContent(int argb) { + return darkFromCorePalette(CorePalette.contentOf(argb)); + } + + private static Scheme lightFromCorePalette(CorePalette core) { + return new Scheme() + .withPrimary(core.a1.tone(40)) + .withOnPrimary(core.a1.tone(100)) + .withPrimaryContainer(core.a1.tone(90)) + .withOnPrimaryContainer(core.a1.tone(10)) + .withSecondary(core.a2.tone(40)) + .withOnSecondary(core.a2.tone(100)) + .withSecondaryContainer(core.a2.tone(90)) + .withOnSecondaryContainer(core.a2.tone(10)) + .withTertiary(core.a3.tone(40)) + .withOnTertiary(core.a3.tone(100)) + .withTertiaryContainer(core.a3.tone(90)) + .withOnTertiaryContainer(core.a3.tone(10)) + .withError(core.error.tone(40)) + .withOnError(core.error.tone(100)) + .withErrorContainer(core.error.tone(90)) + .withOnErrorContainer(core.error.tone(10)) + .withBackground(core.n1.tone(99)) + .withOnBackground(core.n1.tone(10)) + .withSurface(core.n1.tone(99)) + .withOnSurface(core.n1.tone(10)) + .withSurfaceVariant(core.n2.tone(90)) + .withOnSurfaceVariant(core.n2.tone(30)) + .withOutline(core.n2.tone(50)) + .withOutlineVariant(core.n2.tone(80)) + .withShadow(core.n1.tone(0)) + .withScrim(core.n1.tone(0)) + .withInverseSurface(core.n1.tone(20)) + .withInverseOnSurface(core.n1.tone(95)) + .withInversePrimary(core.a1.tone(80)); + } + + private static Scheme darkFromCorePalette(CorePalette core) { + return new Scheme() + .withPrimary(core.a1.tone(80)) + .withOnPrimary(core.a1.tone(20)) + .withPrimaryContainer(core.a1.tone(30)) + .withOnPrimaryContainer(core.a1.tone(90)) + .withSecondary(core.a2.tone(80)) + .withOnSecondary(core.a2.tone(20)) + .withSecondaryContainer(core.a2.tone(30)) + .withOnSecondaryContainer(core.a2.tone(90)) + .withTertiary(core.a3.tone(80)) + .withOnTertiary(core.a3.tone(20)) + .withTertiaryContainer(core.a3.tone(30)) + .withOnTertiaryContainer(core.a3.tone(90)) + .withError(core.error.tone(80)) + .withOnError(core.error.tone(20)) + .withErrorContainer(core.error.tone(30)) + .withOnErrorContainer(core.error.tone(80)) + .withBackground(core.n1.tone(10)) + .withOnBackground(core.n1.tone(90)) + .withSurface(core.n1.tone(10)) + .withOnSurface(core.n1.tone(90)) + .withSurfaceVariant(core.n2.tone(30)) + .withOnSurfaceVariant(core.n2.tone(80)) + .withOutline(core.n2.tone(60)) + .withOutlineVariant(core.n2.tone(30)) + .withShadow(core.n1.tone(0)) + .withScrim(core.n1.tone(0)) + .withInverseSurface(core.n1.tone(90)) + .withInverseOnSurface(core.n1.tone(20)) + .withInversePrimary(core.a1.tone(40)); + } + + public int getPrimary() { + return primary; + } + + public void setPrimary(int primary) { + this.primary = primary; + } + + public Scheme withPrimary(int primary) { + this.primary = primary; + return this; + } + + public int getOnPrimary() { + return onPrimary; + } + + public void setOnPrimary(int onPrimary) { + this.onPrimary = onPrimary; + } + + public Scheme withOnPrimary(int onPrimary) { + this.onPrimary = onPrimary; + return this; + } + + public int getPrimaryContainer() { + return primaryContainer; + } + + public void setPrimaryContainer(int primaryContainer) { + this.primaryContainer = primaryContainer; + } + + public Scheme withPrimaryContainer(int primaryContainer) { + this.primaryContainer = primaryContainer; + return this; + } + + public int getOnPrimaryContainer() { + return onPrimaryContainer; + } + + public void setOnPrimaryContainer(int onPrimaryContainer) { + this.onPrimaryContainer = onPrimaryContainer; + } + + public Scheme withOnPrimaryContainer(int onPrimaryContainer) { + this.onPrimaryContainer = onPrimaryContainer; + return this; + } + + public int getSecondary() { + return secondary; + } + + public void setSecondary(int secondary) { + this.secondary = secondary; + } + + public Scheme withSecondary(int secondary) { + this.secondary = secondary; + return this; + } + + public int getOnSecondary() { + return onSecondary; + } + + public void setOnSecondary(int onSecondary) { + this.onSecondary = onSecondary; + } + + public Scheme withOnSecondary(int onSecondary) { + this.onSecondary = onSecondary; + return this; + } + + public int getSecondaryContainer() { + return secondaryContainer; + } + + public void setSecondaryContainer(int secondaryContainer) { + this.secondaryContainer = secondaryContainer; + } + + public Scheme withSecondaryContainer(int secondaryContainer) { + this.secondaryContainer = secondaryContainer; + return this; + } + + public int getOnSecondaryContainer() { + return onSecondaryContainer; + } + + public void setOnSecondaryContainer(int onSecondaryContainer) { + this.onSecondaryContainer = onSecondaryContainer; + } + + public Scheme withOnSecondaryContainer(int onSecondaryContainer) { + this.onSecondaryContainer = onSecondaryContainer; + return this; + } + + public int getTertiary() { + return tertiary; + } + + public void setTertiary(int tertiary) { + this.tertiary = tertiary; + } + + public Scheme withTertiary(int tertiary) { + this.tertiary = tertiary; + return this; + } + + public int getOnTertiary() { + return onTertiary; + } + + public void setOnTertiary(int onTertiary) { + this.onTertiary = onTertiary; + } + + public Scheme withOnTertiary(int onTertiary) { + this.onTertiary = onTertiary; + return this; + } + + public int getTertiaryContainer() { + return tertiaryContainer; + } + + public void setTertiaryContainer(int tertiaryContainer) { + this.tertiaryContainer = tertiaryContainer; + } + + public Scheme withTertiaryContainer(int tertiaryContainer) { + this.tertiaryContainer = tertiaryContainer; + return this; + } + + public int getOnTertiaryContainer() { + return onTertiaryContainer; + } + + public void setOnTertiaryContainer(int onTertiaryContainer) { + this.onTertiaryContainer = onTertiaryContainer; + } + + public Scheme withOnTertiaryContainer(int onTertiaryContainer) { + this.onTertiaryContainer = onTertiaryContainer; + return this; + } + + public int getError() { + return error; + } + + public void setError(int error) { + this.error = error; + } + + public Scheme withError(int error) { + this.error = error; + return this; + } + + public int getOnError() { + return onError; + } + + public void setOnError(int onError) { + this.onError = onError; + } + + public Scheme withOnError(int onError) { + this.onError = onError; + return this; + } + + public int getErrorContainer() { + return errorContainer; + } + + public void setErrorContainer(int errorContainer) { + this.errorContainer = errorContainer; + } + + public Scheme withErrorContainer(int errorContainer) { + this.errorContainer = errorContainer; + return this; + } + + public int getOnErrorContainer() { + return onErrorContainer; + } + + public void setOnErrorContainer(int onErrorContainer) { + this.onErrorContainer = onErrorContainer; + } + + public Scheme withOnErrorContainer(int onErrorContainer) { + this.onErrorContainer = onErrorContainer; + return this; + } + + public int getBackground() { + return background; + } + + public void setBackground(int background) { + this.background = background; + } + + public Scheme withBackground(int background) { + this.background = background; + return this; + } + + public int getOnBackground() { + return onBackground; + } + + public void setOnBackground(int onBackground) { + this.onBackground = onBackground; + } + + public Scheme withOnBackground(int onBackground) { + this.onBackground = onBackground; + return this; + } + + public int getSurface() { + return surface; + } + + public void setSurface(int surface) { + this.surface = surface; + } + + public Scheme withSurface(int surface) { + this.surface = surface; + return this; + } + + public int getOnSurface() { + return onSurface; + } + + public void setOnSurface(int onSurface) { + this.onSurface = onSurface; + } + + public Scheme withOnSurface(int onSurface) { + this.onSurface = onSurface; + return this; + } + + public int getSurfaceVariant() { + return surfaceVariant; + } + + public void setSurfaceVariant(int surfaceVariant) { + this.surfaceVariant = surfaceVariant; + } + + public Scheme withSurfaceVariant(int surfaceVariant) { + this.surfaceVariant = surfaceVariant; + return this; + } + + public int getOnSurfaceVariant() { + return onSurfaceVariant; + } + + public void setOnSurfaceVariant(int onSurfaceVariant) { + this.onSurfaceVariant = onSurfaceVariant; + } + + public Scheme withOnSurfaceVariant(int onSurfaceVariant) { + this.onSurfaceVariant = onSurfaceVariant; + return this; + } + + public int getOutline() { + return outline; + } + + public void setOutline(int outline) { + this.outline = outline; + } + + public Scheme withOutline(int outline) { + this.outline = outline; + return this; + } + + public int getOutlineVariant() { + return outlineVariant; + } + + public void setOutlineVariant(int outlineVariant) { + this.outlineVariant = outlineVariant; + } + + public Scheme withOutlineVariant(int outlineVariant) { + this.outlineVariant = outlineVariant; + return this; + } + + public int getShadow() { + return shadow; + } + + public void setShadow(int shadow) { + this.shadow = shadow; + } + + public Scheme withShadow(int shadow) { + this.shadow = shadow; + return this; + } + + public int getScrim() { + return scrim; + } + + public void setScrim(int scrim) { + this.scrim = scrim; + } + + public Scheme withScrim(int scrim) { + this.scrim = scrim; + return this; + } + + public int getInverseSurface() { + return inverseSurface; + } + + public void setInverseSurface(int inverseSurface) { + this.inverseSurface = inverseSurface; + } + + public Scheme withInverseSurface(int inverseSurface) { + this.inverseSurface = inverseSurface; + return this; + } + + public int getInverseOnSurface() { + return inverseOnSurface; + } + + public void setInverseOnSurface(int inverseOnSurface) { + this.inverseOnSurface = inverseOnSurface; + } + + public Scheme withInverseOnSurface(int inverseOnSurface) { + this.inverseOnSurface = inverseOnSurface; + return this; + } + + public int getInversePrimary() { + return inversePrimary; + } + + public void setInversePrimary(int inversePrimary) { + this.inversePrimary = inversePrimary; + } + + public Scheme withInversePrimary(int inversePrimary) { + this.inversePrimary = inversePrimary; + return this; + } + + @Override + public String toString() { + return "Scheme{" + + "primary=" + + primary + + ", onPrimary=" + + onPrimary + + ", primaryContainer=" + + primaryContainer + + ", onPrimaryContainer=" + + onPrimaryContainer + + ", secondary=" + + secondary + + ", onSecondary=" + + onSecondary + + ", secondaryContainer=" + + secondaryContainer + + ", onSecondaryContainer=" + + onSecondaryContainer + + ", tertiary=" + + tertiary + + ", onTertiary=" + + onTertiary + + ", tertiaryContainer=" + + tertiaryContainer + + ", onTertiaryContainer=" + + onTertiaryContainer + + ", error=" + + error + + ", onError=" + + onError + + ", errorContainer=" + + errorContainer + + ", onErrorContainer=" + + onErrorContainer + + ", background=" + + background + + ", onBackground=" + + onBackground + + ", surface=" + + surface + + ", onSurface=" + + onSurface + + ", surfaceVariant=" + + surfaceVariant + + ", onSurfaceVariant=" + + onSurfaceVariant + + ", outline=" + + outline + + ", outlineVariant=" + + outlineVariant + + ", shadow=" + + shadow + + ", scrim=" + + scrim + + ", inverseSurface=" + + inverseSurface + + ", inverseOnSurface=" + + inverseOnSurface + + ", inversePrimary=" + + inversePrimary + + '}'; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Scheme)) { + return false; + } + if (!super.equals(object)) { + return false; + } + + Scheme scheme = (Scheme) object; + + if (primary != scheme.primary) { + return false; + } + if (onPrimary != scheme.onPrimary) { + return false; + } + if (primaryContainer != scheme.primaryContainer) { + return false; + } + if (onPrimaryContainer != scheme.onPrimaryContainer) { + return false; + } + if (secondary != scheme.secondary) { + return false; + } + if (onSecondary != scheme.onSecondary) { + return false; + } + if (secondaryContainer != scheme.secondaryContainer) { + return false; + } + if (onSecondaryContainer != scheme.onSecondaryContainer) { + return false; + } + if (tertiary != scheme.tertiary) { + return false; + } + if (onTertiary != scheme.onTertiary) { + return false; + } + if (tertiaryContainer != scheme.tertiaryContainer) { + return false; + } + if (onTertiaryContainer != scheme.onTertiaryContainer) { + return false; + } + if (error != scheme.error) { + return false; + } + if (onError != scheme.onError) { + return false; + } + if (errorContainer != scheme.errorContainer) { + return false; + } + if (onErrorContainer != scheme.onErrorContainer) { + return false; + } + if (background != scheme.background) { + return false; + } + if (onBackground != scheme.onBackground) { + return false; + } + if (surface != scheme.surface) { + return false; + } + if (onSurface != scheme.onSurface) { + return false; + } + if (surfaceVariant != scheme.surfaceVariant) { + return false; + } + if (onSurfaceVariant != scheme.onSurfaceVariant) { + return false; + } + if (outline != scheme.outline) { + return false; + } + if (outlineVariant != scheme.outlineVariant) { + return false; + } + if (shadow != scheme.shadow) { + return false; + } + if (scrim != scheme.scrim) { + return false; + } + if (inverseSurface != scheme.inverseSurface) { + return false; + } + if (inverseOnSurface != scheme.inverseOnSurface) { + return false; + } + if (inversePrimary != scheme.inversePrimary) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + primary; + result = 31 * result + onPrimary; + result = 31 * result + primaryContainer; + result = 31 * result + onPrimaryContainer; + result = 31 * result + secondary; + result = 31 * result + onSecondary; + result = 31 * result + secondaryContainer; + result = 31 * result + onSecondaryContainer; + result = 31 * result + tertiary; + result = 31 * result + onTertiary; + result = 31 * result + tertiaryContainer; + result = 31 * result + onTertiaryContainer; + result = 31 * result + error; + result = 31 * result + onError; + result = 31 * result + errorContainer; + result = 31 * result + onErrorContainer; + result = 31 * result + background; + result = 31 * result + onBackground; + result = 31 * result + surface; + result = 31 * result + onSurface; + result = 31 * result + surfaceVariant; + result = 31 * result + onSurfaceVariant; + result = 31 * result + outline; + result = 31 * result + outlineVariant; + result = 31 * result + shadow; + result = 31 * result + scrim; + result = 31 * result + inverseSurface; + result = 31 * result + inverseOnSurface; + result = 31 * result + inversePrimary; + return result; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java new file mode 100644 index 0000000000000..4c3c07c512405 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/scheme/SchemeContent.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.scheme; + +import static java.lang.Math.max; + +import com.android.systemui.monet.dislike.DislikeAnalyzer; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.palettes.TonalPalette; +import com.android.systemui.monet.temperature.TemperatureCache; + +/** + * A scheme that places the source color in Scheme.primaryContainer. + * + *

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 score(Map colorsToPopulation) { + // Determine the total count of all colors. + double populationSum = 0.; + for (Map.Entry entry : colorsToPopulation.entrySet()) { + populationSum += entry.getValue(); + } + + // Turn the count of each color into a proportion by dividing by the total + // count. Also, fill a cache of CAM16 colors representing each color, and + // record the proportion of colors for each CAM16 hue. + Map colorsToCam = new HashMap<>(); + double[] hueProportions = new double[361]; + for (Map.Entry entry : colorsToPopulation.entrySet()) { + int color = entry.getKey(); + double population = entry.getValue(); + double proportion = population / populationSum; + + Cam16 cam = Cam16.fromInt(color); + colorsToCam.put(color, cam); + + int hue = (int) Math.round(cam.getHue()); + hueProportions[hue] += proportion; + } + + // Determine the proportion of the colors around each color, by summing the + // proportions around each color's hue. + Map colorsToExcitedProportion = new HashMap<>(); + for (Map.Entry entry : colorsToCam.entrySet()) { + int color = entry.getKey(); + Cam16 cam = entry.getValue(); + int hue = (int) Math.round(cam.getHue()); + + double excitedProportion = 0.; + for (int j = (hue - 15); j < (hue + 15); j++) { + int neighborHue = MathUtils.sanitizeDegreesInt(j); + excitedProportion += hueProportions[neighborHue]; + } + + colorsToExcitedProportion.put(color, excitedProportion); + } + + // Score the colors by their proportion, as well as how chromatic they are. + Map colorsToScore = new HashMap<>(); + for (Map.Entry entry : colorsToCam.entrySet()) { + int color = entry.getKey(); + Cam16 cam = entry.getValue(); + + double proportion = colorsToExcitedProportion.get(color); + double proportionScore = proportion * 100.0 * WEIGHT_PROPORTION; + + double chromaWeight = + cam.getChroma() < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE; + double chromaScore = (cam.getChroma() - TARGET_CHROMA) * chromaWeight; + + double score = proportionScore + chromaScore; + colorsToScore.put(color, score); + } + + // Remove colors that are unsuitable, ex. very dark or unchromatic colors. + // Also, remove colors that are very similar in hue. + List filteredColors = filter(colorsToExcitedProportion, colorsToCam); + Map filteredColorsToScore = new HashMap<>(); + for (int color : filteredColors) { + filteredColorsToScore.put(color, colorsToScore.get(color)); + } + + // Ensure the list of colors returned is sorted such that the first in the + // list is the most suitable, and the last is the least suitable. + List> entryList = new ArrayList<>( + filteredColorsToScore.entrySet()); + Collections.sort(entryList, new ScoredComparator()); + List colorsByScoreDescending = new ArrayList<>(); + for (Map.Entry entry : entryList) { + int color = entry.getKey(); + Cam16 cam = colorsToCam.get(color); + boolean duplicateHue = false; + + for (Integer alreadyChosenColor : colorsByScoreDescending) { + Cam16 alreadyChosenCam = colorsToCam.get(alreadyChosenColor); + if (MathUtils.differenceDegrees(cam.getHue(), alreadyChosenCam.getHue()) < 15) { + duplicateHue = true; + break; + } + } + + if (duplicateHue) { + continue; + } + colorsByScoreDescending.add(entry.getKey()); + } + + // Ensure that at least one color is returned. + if (colorsByScoreDescending.isEmpty()) { + colorsByScoreDescending.add(0xff4285F4); + } + return colorsByScoreDescending; + } + + private static List filter( + Map colorsToExcitedProportion, Map colorsToCam) { + List filtered = new ArrayList<>(); + for (Map.Entry entry : colorsToCam.entrySet()) { + int color = entry.getKey(); + Cam16 cam = entry.getValue(); + double proportion = colorsToExcitedProportion.get(color); + + if (cam.getChroma() >= CUTOFF_CHROMA + && ColorUtils.lstarFromArgb(color) >= CUTOFF_TONE + && proportion >= CUTOFF_EXCITED_PROPORTION) { + filtered.add(color); + } + } + return filtered; + } + + static class ScoredComparator implements Comparator> { + ScoredComparator() { + } + + @Override + public int compare(Map.Entry entry1, Map.Entry entry2) { + return -entry1.getValue().compareTo(entry2.getValue()); + } + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java b/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java new file mode 100644 index 0000000000000..2f2022b289c80 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/temperature/TemperatureCache.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.temperature; + +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.utils.ColorUtils; +import com.android.systemui.monet.utils.MathUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Design utilities using color temperature theory. + * + *

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 precomputedHctsByTemp; + private List precomputedHctsByHue; + private Map precomputedTempsByHct; + + private TemperatureCache() { + throw new UnsupportedOperationException(); + } + + /** + * Create a cache that allows calculation of ex. complementary and analogous colors. + * + * @param input Color to find complement/analogous colors of. Any colors will have the same + * tone, + * and chroma as the input color, modulo any restrictions due to the other hues + * having lower + * limits on chroma. + */ + public TemperatureCache(Hct input) { + this.input = input; + } + + /** + * A color that complements the input color aesthetically. + * + *

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 getAnalogousColors() { + return getAnalogousColors(5, 12); + } + + /** + * A set of colors with differing hues, equidistant in temperature. + * + *

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 getAnalogousColors(int count, int divisions) { + // The starting hue is the hue of the input color. + int startHue = (int) Math.round(input.getHue()); + Hct startHct = getHctsByHue().get(startHue); + double lastTemp = getRelativeTemperature(startHct); + + List allColors = new ArrayList<>(); + allColors.add(startHct); + + double absoluteTotalTempDelta = 0.f; + for (int i = 0; i < 360; i++) { + int hue = MathUtils.sanitizeDegreesInt(startHue + i); + Hct hct = getHctsByHue().get(hue); + double temp = getRelativeTemperature(hct); + double tempDelta = Math.abs(temp - lastTemp); + lastTemp = temp; + absoluteTotalTempDelta += tempDelta; + } + + int hueAddend = 1; + double tempStep = absoluteTotalTempDelta / (double) divisions; + double totalTempDelta = 0.0; + lastTemp = getRelativeTemperature(startHct); + while (allColors.size() < divisions) { + int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend); + Hct hct = getHctsByHue().get(hue); + double temp = getRelativeTemperature(hct); + double tempDelta = Math.abs(temp - lastTemp); + totalTempDelta += tempDelta; + + double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep); + boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex; + int indexAddend = 1; + // Keep adding this hue to the answers until its temperature is + // insufficient. This ensures consistent behavior when there aren't + // `divisions` discrete steps between 0 and 360 in hue with `tempStep` + // delta in temperature between them. + // + // For example, white and black have no analogues: there are no other + // colors at T100/T0. Therefore, they should just be added to the array + // as answers. + while (indexSatisfied && allColors.size() < divisions) { + allColors.add(hct); + desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep); + indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex; + indexAddend++; + } + lastTemp = temp; + hueAddend++; + + if (hueAddend > 360) { + while (allColors.size() < divisions) { + allColors.add(hct); + } + break; + } + } + + List answers = new ArrayList<>(); + answers.add(input); + + int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0); + for (int i = 1; i < (ccwCount + 1); i++) { + int index = 0 - i; + while (index < 0) { + index = allColors.size() + index; + } + if (index >= allColors.size()) { + index = index % allColors.size(); + } + answers.add(0, allColors.get(index)); + } + + int cwCount = count - ccwCount - 1; + for (int i = 1; i < (cwCount + 1); i++) { + int index = i; + while (index < 0) { + index = allColors.size() + index; + } + if (index >= allColors.size()) { + index = index % allColors.size(); + } + answers.add(allColors.get(index)); + } + + return answers; + } + + /** + * Temperature relative to all colors with the same chroma and tone. + * + * @param hct HCT to find the relative temperature of. + * @return Value on a scale from 0 to 1. + */ + public double getRelativeTemperature(Hct hct) { + double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest()); + double differenceFromColdest = + getTempsByHct().get(hct) - getTempsByHct().get(getColdest()); + // Handle when there's no difference in temperature between warmest and + // coldest: for example, at T100, only one color is available, white. + if (range == 0.) { + return 0.5; + } + return differenceFromColdest / range; + } + + /** + * Value representing cool-warm factor of a color. Values below 0 are considered cool, above, + * warm. + * + *

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:
+ * - 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. + * + *

Sorted by hue, ex. index 0 is hue 0. + */ + private List getHctsByHue() { + if (precomputedHctsByHue != null) { + return precomputedHctsByHue; + } + List hcts = new ArrayList<>(); + for (double hue = 0.; hue <= 360.; hue += 1.) { + Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone()); + hcts.add(colorAtHue); + } + precomputedHctsByHue = Collections.unmodifiableList(hcts); + return precomputedHctsByHue; + } + + /** + * HCTs for all colors with the same chroma/tone as the input. + * + *

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 getHctsByTemp() { + if (precomputedHctsByTemp != null) { + return precomputedHctsByTemp; + } + + List hcts = new ArrayList<>(getHctsByHue()); + hcts.add(input); + Comparator temperaturesComparator = + Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo); + Collections.sort(hcts, temperaturesComparator); + precomputedHctsByTemp = hcts; + return precomputedHctsByTemp; + } + + /** Keys of HCTs in getHctsByTemp, values of raw temperature. */ + private Map getTempsByHct() { + if (precomputedTempsByHct != null) { + return precomputedTempsByHct; + } + + List allHcts = new ArrayList<>(getHctsByHue()); + allHcts.add(input); + + Map temperaturesByHct = new HashMap<>(); + for (Hct hct : allHcts) { + temperaturesByHct.put(hct, rawTemperature(hct)); + } + + precomputedTempsByHct = temperaturesByHct; + return precomputedTempsByHct; + } + + /** Warmest color with same chroma and tone as input. */ + private Hct getWarmest() { + return getHctsByTemp().get(getHctsByTemp().size() - 1); + } + + /** Determines if an angle is between two other angles, rotating clockwise. */ + private static boolean isBetween(double angle, double a, double b) { + if (a < b) { + return a <= angle && angle <= b; + } + return a <= angle || angle <= b; + } +} diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java new file mode 100644 index 0000000000000..5a4ced0a64524 --- /dev/null +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/utils/ColorUtils.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.utils; + +/** + * Color science utilities. + * + *

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 answer = quantizer.quantize(new int[]{RED}, 128).colorToCount; + + assertThat(answer).hasSize(1); + assertThat(answer).containsEntry(RED, 1); + } + + @Test + public void quantize_1G() { + Map answer = quantizer.quantize(new int[]{GREEN}, 128).colorToCount; + + assertThat(answer).hasSize(1); + assertThat(answer).containsEntry(GREEN, 1); + } + + @Test + public void quantize_1B() { + Map answer = quantizer.quantize(new int[]{BLUE}, 128).colorToCount; + + assertThat(answer).hasSize(1); + assertThat(answer).containsEntry(BLUE, 1); + } + + @Test + public void quantize_5B() { + Map answer = + quantizer.quantize(new int[]{BLUE, BLUE, BLUE, BLUE, BLUE}, 128).colorToCount; + + assertThat(answer).hasSize(1); + assertThat(answer).containsEntry(BLUE, 5); + } + + @Test + public void quantize_2R3G() { + Map answer = + quantizer.quantize(new int[]{RED, RED, GREEN, GREEN, GREEN}, 128).colorToCount; + + assertThat(answer).hasSize(2); + assertThat(answer).containsEntry(RED, 2); + assertThat(answer).containsEntry(GREEN, 3); + } + + @Test + public void quantize_1R1G1B() { + Map answer = + quantizer.quantize(new int[]{RED, GREEN, BLUE}, 128).colorToCount; + + assertThat(answer).hasSize(3); + assertThat(answer).containsEntry(RED, 1); + assertThat(answer).containsEntry(GREEN, 1); + assertThat(answer).containsEntry(BLUE, 1); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/README.md b/packages/SystemUI/tests/src/com/android/systemui/monet/README.md new file mode 100644 index 0000000000000..a3e025700a26d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/README.md @@ -0,0 +1,4 @@ +The tests in this folder are imported from Google's internal source repository. + +For instructions on importing, see: + frameworks/base/packages/SystemUI/tests/src/com/android/systemui/monet/README \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java new file mode 100644 index 0000000000000..9de55b176f2a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeContentTest.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeContent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeContentTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF1218FF); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF0001C3); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF000181); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF5660FF); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF2D36FF); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF0000E3); + } + + @Test + public void lightTheme_minContrast_tertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFB042CC); + } + + @Test + public void lightTheme_standardContrast_tertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF9221AF); + } + + @Test + public void lightTheme_maxContrast_tertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF73008E); + } + + @Test + public void lightTheme_minContrast_objectionableTertiaryContainerLightens() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFD03A71); + } + + @Test + public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFAC1B57); + } + + @Test + public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF850096), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF870040); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFCBCDFF); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFCECFFF); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFD6D6FF); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, -1); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFFFBF8FF); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF5660FF); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFBEC2FF); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFF6F4FF); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF0000E6); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF0000E6); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFC4C6FF); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF7A83FF); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFA4AAFF); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF0001C6); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFCF60EA); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFFEB8CFF); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF63007B); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeContent scheme = new SchemeContent(Hct.fromInt(0xFF0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xFF12121D); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java new file mode 100644 index 0000000000000..3ef92804cb837 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeExpressiveTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeExpressive; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeExpressiveTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff924b28); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff401400); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffdbcc); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffdbcc); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff6f3010); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff99512e); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff351000); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffd0bc); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffad603c); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffffb595); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfffff3ee); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff743413); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff743413); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffbb9e); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xfff99f75); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffdbcc); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff622706); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeExpressive scheme = new SchemeExpressive(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java new file mode 100644 index 0000000000000..cfd05184b8184 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFidelityTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeFidelity; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeFidelityTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff1218ff); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0001c3); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff5660ff); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff2d36ff); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000e3); + } + + @Test + public void lightTheme_minContrast_tertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd93628); + } + + @Test + public void lightTheme_standardContrast_tertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffb31910); + } + + @Test + public void lightTheme_maxContrast_tertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff8c0002); + } + + @Test + public void lightTheme_minContrast_objectionableTertiaryContainerLightens() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffbcac5a); + } + + @Test + public void lightTheme_standardContrast_objectionableTertiaryContainerLightens() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffbcac5a); + } + + @Test + public void lightTheme_maxContrast_objectionableTertiaryContainerDarkens() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff850096), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff4d4300); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffcbcdff); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffcecfff); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd6d6ff); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000e6); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000e6); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffc4c6ff); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff7a83ff); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffa4aaff); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0001c6); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xfffe513e); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffFF9181); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff790001); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeFidelity scheme = new SchemeFidelity(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12121d); + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java new file mode 100644 index 0000000000000..ce6f6d96a1a55 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeFruitSaladTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeFruitSalad; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeFruitSaladTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007ea7); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff006688); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff002635); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffC2E8FF); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffC2E8FF); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff004862); + } + + @Test + public void lightTheme_minContrast_tertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffE0E0FF); + } + + @Test + public void lightTheme_standardContrast_tertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffE0E0FF); + } + + @Test + public void lightTheme_maxContrast_tertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF3A3E74); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff006C90); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff001E2B); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffACE1FF); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff007EA7); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFF76D1FF); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xFFECF7FF); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF004D67); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF004D67); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xFF83D5FF); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff55C0F2); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffC2E8FF); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff003E54); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffADB0EF); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff30346A); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeFruitSalad scheme = new SchemeFruitSalad(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131c); + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java new file mode 100644 index 0000000000000..9ffc52384e64a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeMonochromeTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeMonochrome; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeMonochromeTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5e5e5e); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff222222); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e2e2); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e2e2); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff434343); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff646464); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff1b1b1b); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffdadada); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff747474); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c6c6); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff5f5f5); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff474747); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff474747); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffcbcbcb); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffb5b5b5); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e2e2); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff393939); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffb5b5b5); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e2e2); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff393939); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeMonochrome scheme = new SchemeMonochrome(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java new file mode 100644 index 0000000000000..c6892f948a17f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeNeutralTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeNeutral; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeNeutralTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5d5d6c); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff21212e); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e1f3); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e1f3); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff414250); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff636372); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff1a1b27); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd9d8ea); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffcf8fa); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff737383); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffc6c5d6); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff454654); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff454654); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffcac9da); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffb5b3c4); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe2e1f3); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff373846); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffb3b3cb); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe1e0f9); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff37374b); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeNeutral scheme = new SchemeNeutral(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131315); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java new file mode 100644 index 0000000000000..a0836220da975 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeRainbowTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeRainbow; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeRainbowTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5056A9); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff0F136A); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffE0E0FF); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffE0E0FF); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff34398B); + } + + @Test + public void lightTheme_minContrast_tertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffd8ee); + } + + @Test + public void lightTheme_standardContrast_tertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffd8ee); + } + + @Test + public void lightTheme_maxContrast_tertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff5A384E); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff565CB0); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff050865); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd6d6ff); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfff9f9f9); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff676DC1); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff383E8F); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff383E8F); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffc4c6ff); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffa9afff); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff292f81); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd5a8c3); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffd8ee); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff4f2e44); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeRainbow scheme = new SchemeRainbow(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131313); + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java new file mode 100644 index 0000000000000..859b65ba408e5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.scheme.Scheme; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeTest extends SysuiTestCase { + + @Test + public void lightScheme_ofBlue() { + Scheme scheme = Scheme.light(0xff0000ff); + assertThat(scheme.getPrimary()).isSameColorAs(0xff343DFF); + } + + @Test + public void darkScheme_ofBlue() { + Scheme scheme = Scheme.dark(0xff0000ff); + assertThat(scheme.getPrimary()).isSameColorAs(0xffBEC2FF); + } + + @Test + public void thirdPartyScheme_light() { + Scheme scheme = Scheme.light(0xff6750A4); + assertThat(scheme.getPrimary()).isSameColorAs(0xff6750A4); + assertThat(scheme.getSecondary()).isSameColorAs(0xff625B71); + assertThat(scheme.getTertiary()).isSameColorAs(0xff7e5260); + assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff); + assertThat(scheme.getOnSurface()).isSameColorAs(0xff1c1b1e); + } + + @Test + public void thirdPartyScheme_dark() { + Scheme scheme = Scheme.dark(0xff6750A4); + assertThat(scheme.getPrimary()).isSameColorAs(0xffcfbcff); + assertThat(scheme.getSecondary()).isSameColorAs(0xffcbc2db); + assertThat(scheme.getTertiary()).isSameColorAs(0xffefb8c8); + assertThat(scheme.getSurface()).isSameColorAs(0xff1c1b1e); + assertThat(scheme.getOnSurface()).isSameColorAs(0xffe6e1e6); + } + + @Test + public void lightSchemeFromHighChromaColor() { + Scheme scheme = Scheme.light(0xfffa2bec); + assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2); + assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff); + assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035); + assertThat(scheme.getSecondary()).isSameColorAs(0xff6e5868); + assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff); + assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xfff8daee); + assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff271624); + assertThat(scheme.getTertiary()).isSameColorAs(0xff815343); + assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff); + assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0); + assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff321207); + assertThat(scheme.getError()).isSameColorAs(0xffba1a1a); + assertThat(scheme.getOnError()).isSameColorAs(0xffffffff); + assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6); + assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002); + assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff); + assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff); + assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7); + assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getOutline()).isSameColorAs(0xff80747b); + assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb); + assertThat(scheme.getShadow()).isSameColorAs(0xff000000); + assertThat(scheme.getScrim()).isSameColorAs(0xff000000); + assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32); + assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2); + assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee); + } + + @Test + public void darkSchemeFromHighChromaColor() { + Scheme scheme = Scheme.dark(0xfffa2bec); + assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee); + assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057); + assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b); + assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getSecondary()).isSameColorAs(0xffdbbed1); + assertThat(scheme.getOnSecondary()).isSameColorAs(0xff3e2a39); + assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff554050); + assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xfff8daee); + assertThat(scheme.getTertiary()).isSameColorAs(0xfff5b9a5); + assertThat(scheme.getOnTertiary()).isSameColorAs(0xff4c2619); + assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff663c2d); + assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0); + assertThat(scheme.getError()).isSameColorAs(0xffffb4ab); + assertThat(scheme.getOnError()).isSameColorAs(0xff690005); + assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a); + assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab); + assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb); + assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95); + assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getShadow()).isSameColorAs(0xff000000); + assertThat(scheme.getScrim()).isSameColorAs(0xff000000); + assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32); + assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2); + } + + @Test + public void lightContentSchemeFromHighChromaColor() { + Scheme scheme = Scheme.lightContent(0xfffa2bec); + assertThat(scheme.getPrimary()).isSameColorAs(0xffab00a2); + assertThat(scheme.getOnPrimary()).isSameColorAs(0xffffffff); + assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xff390035); + assertThat(scheme.getSecondary()).isSameColorAs(0xff7f4e75); + assertThat(scheme.getOnSecondary()).isSameColorAs(0xffffffff); + assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xff330b2f); + assertThat(scheme.getTertiary()).isSameColorAs(0xff9c4323); + assertThat(scheme.getOnTertiary()).isSameColorAs(0xffffffff); + assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xffffdbd0); + assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xff390c00); + assertThat(scheme.getError()).isSameColorAs(0xffba1a1a); + assertThat(scheme.getOnError()).isSameColorAs(0xffffffff); + assertThat(scheme.getErrorContainer()).isSameColorAs(0xffffdad6); + assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xff410002); + assertThat(scheme.getBackground()).isSameColorAs(0xfffffbff); + assertThat(scheme.getOnBackground()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getSurface()).isSameColorAs(0xfffffbff); + assertThat(scheme.getOnSurface()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xffeedee7); + assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getOutline()).isSameColorAs(0xff80747b); + assertThat(scheme.getOutlineVariant()).isSameColorAs(0xffd2c2cb); + assertThat(scheme.getShadow()).isSameColorAs(0xff000000); + assertThat(scheme.getScrim()).isSameColorAs(0xff000000); + assertThat(scheme.getInverseSurface()).isSameColorAs(0xff342f32); + assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xfff8eef2); + assertThat(scheme.getInversePrimary()).isSameColorAs(0xffffabee); + } + + @Test + public void darkContentSchemeFromHighChromaColor() { + Scheme scheme = Scheme.darkContent(0xfffa2bec); + assertThat(scheme.getPrimary()).isSameColorAs(0xffffabee); + assertThat(scheme.getOnPrimary()).isSameColorAs(0xff5c0057); + assertThat(scheme.getPrimaryContainer()).isSameColorAs(0xff83007b); + assertThat(scheme.getOnPrimaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getSecondary()).isSameColorAs(0xfff0b4e1); + assertThat(scheme.getOnSecondary()).isSameColorAs(0xff4b2145); + assertThat(scheme.getSecondaryContainer()).isSameColorAs(0xff64375c); + assertThat(scheme.getOnSecondaryContainer()).isSameColorAs(0xffffd7f3); + assertThat(scheme.getTertiary()).isSameColorAs(0xffffb59c); + assertThat(scheme.getOnTertiary()).isSameColorAs(0xff5c1900); + assertThat(scheme.getTertiaryContainer()).isSameColorAs(0xff7d2c0d); + assertThat(scheme.getOnTertiaryContainer()).isSameColorAs(0xffffdbd0); + assertThat(scheme.getError()).isSameColorAs(0xffffb4ab); + assertThat(scheme.getOnError()).isSameColorAs(0xff690005); + assertThat(scheme.getErrorContainer()).isSameColorAs(0xff93000a); + assertThat(scheme.getOnErrorContainer()).isSameColorAs(0xffffb4ab); + assertThat(scheme.getBackground()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getOnBackground()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getSurface()).isSameColorAs(0xff1f1a1d); + assertThat(scheme.getOnSurface()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getSurfaceVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getOnSurfaceVariant()).isSameColorAs(0xffd2c2cb); + assertThat(scheme.getOutline()).isSameColorAs(0xff9a8d95); + assertThat(scheme.getOutlineVariant()).isSameColorAs(0xff4e444b); + assertThat(scheme.getShadow()).isSameColorAs(0xff000000); + assertThat(scheme.getScrim()).isSameColorAs(0xff000000); + assertThat(scheme.getInverseSurface()).isSameColorAs(0xffeae0e4); + assertThat(scheme.getInverseOnSurface()).isSameColorAs(0xff342f32); + assertThat(scheme.getInversePrimary()).isSameColorAs(0xffab00a2); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java new file mode 100644 index 0000000000000..b0c7f10496bfb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeTonalSpotTest.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeTonalSpot; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeTonalSpotTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff545999); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff161a59); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff383c7c); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff5a5fa0); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0e1253); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd6d6ff); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_minContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff5f5e65); + } + + @Test + public void lightTheme_standardContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1b1b21); + } + + @Test + public void lightTheme_maxContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xff1a1a20); + } + + @Test + public void lightTheme_minContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffcfcfe7); + } + + @Test + public void lightTheme_standardContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffffffff); + } + + @Test + public void lightTheme_maxContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xffababc3); + } + + @Test + public void lightTheme_minContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfff3c3df); + } + + @Test + public void lightTheme_standardContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffffffff); + } + + @Test + public void lightTheme_maxContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xffcda0bb); + } + + @Test + public void lightTheme_minContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffc2bb); + } + + @Test + public void lightTheme_standardContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffffffff); + } + + @Test + public void lightTheme_maxContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffff8d80); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff6a6fb1); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff3c4180); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff3c4180); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffc4c6ff); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffabaff7); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff2e3271); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd5a8c3); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffffd8ee); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff4f2e44); + } + + @Test + public void darkTheme_minContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xfffffbff); + } + + @Test + public void darkTheme_standardContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff2e2f42); + } + + @Test + public void darkTheme_maxContrast_onSecondary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onSecondary.getArgb(scheme)).isSameColorAs(0xff505165); + } + + @Test + public void darkTheme_minContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xfffffbff); + } + + @Test + public void darkTheme_standardContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff46263b); + } + + @Test + public void darkTheme_maxContrast_onTertiary() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiary.getArgb(scheme)).isSameColorAs(0xff6b485f); + } + + @Test + public void darkTheme_minContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xfffffbff); + } + + @Test + public void darkTheme_standardContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xff690005); + } + + @Test + public void darkTheme_maxContrast_onError() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onError.getArgb(scheme)).isSameColorAs(0xffa80710); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff131318); + } + + @Test + public void darkTheme_minContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffa4a2a9); + } + + @Test + public void darkTheme_standardContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe4e1e9); + } + + @Test + public void darkTheme_maxContrast_onSurface() { + SchemeTonalSpot scheme = new SchemeTonalSpot(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onSurface.getArgb(scheme)).isSameColorAs(0xffe5e2ea); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java new file mode 100644 index 0000000000000..f056a16717665 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/SchemeVibrantTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.dynamiccolor.MaterialDynamicColors; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.scheme.SchemeVibrant; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SmallTest +@RunWith(JUnit4.class) +public final class SchemeVibrantTest extends SysuiTestCase { + + @Test + public void lightTheme_minContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff); + } + + @Test + public void lightTheme_standardContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff343dff); + } + + @Test + public void lightTheme_maxContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff000181); + } + + @Test + public void lightTheme_minContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void lightTheme_standardContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void lightTheme_maxContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000e3); + } + + @Test + public void lightTheme_minContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff3e47ff); + } + + @Test + public void lightTheme_standardContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff00006e); + } + + @Test + public void lightTheme_maxContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffd6d6ff); + } + + @Test + public void lightTheme_minContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_standardContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void lightTheme_maxContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), false, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xfffbf8ff); + } + + @Test + public void darkTheme_minContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xff5660ff); + } + + @Test + public void darkTheme_standardContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xffbec2ff); + } + + @Test + public void darkTheme_maxContrast_primary() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primary.getArgb(scheme)).isSameColorAs(0xfff6f4ff); + } + + @Test + public void darkTheme_minContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000ef); + } + + @Test + public void darkTheme_standardContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0000ef); + } + + @Test + public void darkTheme_maxContrast_primaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.primaryContainer.getArgb(scheme)).isSameColorAs( + 0xffc4c6ff); + } + + @Test + public void darkTheme_minContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffa9afff); + } + + @Test + public void darkTheme_standardContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xffe0e0ff); + } + + @Test + public void darkTheme_maxContrast_onPrimaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)).isSameColorAs( + 0xff0001c6); + } + + @Test + public void darkTheme_minContrast_onTertiaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xffc9a9df); + } + + @Test + public void darkTheme_standardContrast_onTertiaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xfff2daff); + } + + @Test + public void darkTheme_maxContrast_onTertiaryContainer() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)).isSameColorAs( + 0xff472e5b); + } + + @Test + public void darkTheme_minContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, -1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } + + @Test + public void darkTheme_standardContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 0.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } + + @Test + public void darkTheme_maxContrast_surface() { + SchemeVibrant scheme = new SchemeVibrant(Hct.fromInt(0xff0000ff), true, 1.0); + assertThat(MaterialDynamicColors.surface.getArgb(scheme)).isSameColorAs(0xff12131a); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java new file mode 100644 index 0000000000000..47ad43c87a388 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ScoreTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.score.Score; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SmallTest +@RunWith(JUnit4.class) +public final class ScoreTest extends SysuiTestCase { + + @Test + public void score_picksMostChromatic() { + Map colorsToPopulation = new HashMap<>(); + colorsToPopulation.put(0xFF0000FF, 1); + colorsToPopulation.put(0xFFFF0000, 1); + colorsToPopulation.put(0xFF00FF00, 1); + + List scores = Score.score(colorsToPopulation); + + assertThat(scores.get(0)).isEqualTo(0xffff0000); + assertThat(scores.get(1)).isEqualTo(0xff00ff00); + assertThat(scores.get(2)).isEqualTo(0xff0000ff); + } + + @Test + public void score_usesGblueFallback() { + Map colorsToPopulation = new HashMap<>(); + colorsToPopulation.put(0xFF000000, 1); + + List scores = Score.score(colorsToPopulation); + + assertThat(scores).containsExactly(0xff4285f4); + } + + @Test + public void score_dedupesNearbyHues() { + Map colorsToPopulation = new HashMap<>(); + colorsToPopulation.put(0xff008772, 1); + colorsToPopulation.put(0xff318477, 1); + + List scores = Score.score(colorsToPopulation); + + assertThat(scores).containsExactly(0xff008772); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java new file mode 100644 index 0000000000000..c304e92e445e2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/TemperatureCacheTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet; + +import static com.android.systemui.monet.utils.ArgbSubject.assertThat; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.monet.hct.Hct; +import com.android.systemui.monet.temperature.TemperatureCache; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.List; + +@SmallTest +@RunWith(JUnit4.class) +public final class TemperatureCacheTest extends SysuiTestCase { + + @Test + public void testRawTemperature() { + final Hct blueHct = Hct.fromInt(0xff0000ff); + final double blueTemp = TemperatureCache.rawTemperature(blueHct); + assertEquals(-1.393, blueTemp, 0.001); + + final Hct redHct = Hct.fromInt(0xffff0000); + final double redTemp = TemperatureCache.rawTemperature(redHct); + assertEquals(2.351, redTemp, 0.001); + + final Hct greenHct = Hct.fromInt(0xff00ff00); + final double greenTemp = TemperatureCache.rawTemperature(greenHct); + assertEquals(-0.267, greenTemp, 0.001); + + final Hct whiteHct = Hct.fromInt(0xffffffff); + final double whiteTemp = TemperatureCache.rawTemperature(whiteHct); + assertEquals(-0.5, whiteTemp, 0.001); + + final Hct blackHct = Hct.fromInt(0xff000000); + final double blackTemp = TemperatureCache.rawTemperature(blackHct); + assertEquals(-0.5, blackTemp, 0.001); + } + + @Test + public void testComplement() { + final int blueComplement = + new TemperatureCache(Hct.fromInt(0xff0000ff)).getComplement().toInt(); + assertThat(0xff9D0002).isSameColorAs(blueComplement); + + final int redComplement = new TemperatureCache( + Hct.fromInt(0xffff0000)).getComplement().toInt(); + assertThat(0xff007BFC).isSameColorAs(redComplement); + + final int greenComplement = + new TemperatureCache(Hct.fromInt(0xff00ff00)).getComplement().toInt(); + assertThat(0xffFFD2C9).isSameColorAs(greenComplement); + + final int whiteComplement = + new TemperatureCache(Hct.fromInt(0xffffffff)).getComplement().toInt(); + assertThat(0xffffffff).isSameColorAs(whiteComplement); + + final int blackComplement = + new TemperatureCache(Hct.fromInt(0xff000000)).getComplement().toInt(); + assertThat(0xff000000).isSameColorAs(blackComplement); + } + + @Test + public void testAnalogous() { + final List blueAnalogous = + new TemperatureCache(Hct.fromInt(0xff0000ff)).getAnalogousColors(); + assertThat(0xff00590C).isSameColorAs(blueAnalogous.get(0).toInt()); + assertThat(0xff00564E).isSameColorAs(blueAnalogous.get(1).toInt()); + assertThat(0xff0000ff).isSameColorAs(blueAnalogous.get(2).toInt()); + assertThat(0xff6700CC).isSameColorAs(blueAnalogous.get(3).toInt()); + assertThat(0xff81009F).isSameColorAs(blueAnalogous.get(4).toInt()); + + final List redAnalogous = + new TemperatureCache(Hct.fromInt(0xffff0000)).getAnalogousColors(); + assertThat(0xffF60082).isSameColorAs(redAnalogous.get(0).toInt()); + assertThat(0xffFC004C).isSameColorAs(redAnalogous.get(1).toInt()); + assertThat(0xffff0000).isSameColorAs(redAnalogous.get(2).toInt()); + assertThat(0xffD95500).isSameColorAs(redAnalogous.get(3).toInt()); + assertThat(0xffAF7200).isSameColorAs(redAnalogous.get(4).toInt()); + + final List greenAnalogous = + new TemperatureCache(Hct.fromInt(0xff00ff00)).getAnalogousColors(); + assertThat(0xffCEE900).isSameColorAs(greenAnalogous.get(0).toInt()); + assertThat(0xff92F500).isSameColorAs(greenAnalogous.get(1).toInt()); + assertThat(0xff00ff00).isSameColorAs(greenAnalogous.get(2).toInt()); + assertThat(0xff00FD6F).isSameColorAs(greenAnalogous.get(3).toInt()); + assertThat(0xff00FAB3).isSameColorAs(greenAnalogous.get(4).toInt()); + + final List blackAnalogous = + new TemperatureCache(Hct.fromInt(0xff000000)).getAnalogousColors(); + assertThat(0xff000000).isSameColorAs(blackAnalogous.get(0).toInt()); + assertThat(0xff000000).isSameColorAs(blackAnalogous.get(1).toInt()); + assertThat(0xff000000).isSameColorAs(blackAnalogous.get(2).toInt()); + assertThat(0xff000000).isSameColorAs(blackAnalogous.get(3).toInt()); + assertThat(0xff000000).isSameColorAs(blackAnalogous.get(4).toInt()); + + final List whiteAnalogous = + new TemperatureCache(Hct.fromInt(0xffffffff)).getAnalogousColors(); + assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(0).toInt()); + assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(1).toInt()); + assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(2).toInt()); + assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(3).toInt()); + assertThat(0xffffffff).isSameColorAs(whiteAnalogous.get(4).toInt()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java b/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java new file mode 100644 index 0000000000000..f3040c815f0e0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/utils/ArgbSubject.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.monet.utils; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; + +/** Enables assertions involving colors as integers to log as hex codes instead of raw integers. */ +public final class ArgbSubject extends Subject { + + public static ArgbSubject assertThat(int argb) { + return assertAbout(ArgbSubject::new).that(argb); + } + + private final int actual; + + private ArgbSubject(FailureMetadata failureMetadata, int subject) { + super(failureMetadata, subject); + this.actual = subject; + } + + @Override + protected String actualCustomStringRepresentation() { + return userFriendlyHexCode(actual); + } + + public void isSameColorAs(int otherArgb) { + if (otherArgb != actual) { + failWithActual("color", userFriendlyHexCode(otherArgb)); + } + } + + private static String userFriendlyHexCode(int integer) { + String hexString = Integer.toHexString(integer); + if (hexString.length() > 6) { + return String.format("#%s", hexString.substring(hexString.length() - 6)); + } else if (hexString.length() == 6) { + return String.format("#%s", hexString); + } else { + return String.format("??? #%s", hexString); + } + } +}