diff --git a/base/src/main/java/de/mm20/launcher2/licenses/OpenSourceLicenses.kt b/base/src/main/java/de/mm20/launcher2/licenses/OpenSourceLicenses.kt
index 1d719a03..61887281 100644
--- a/base/src/main/java/de/mm20/launcher2/licenses/OpenSourceLicenses.kt
+++ b/base/src/main/java/de/mm20/launcher2/licenses/OpenSourceLicenses.kt
@@ -201,4 +201,12 @@ val OpenSourceLicenses = arrayOf(
licenseText = R.raw.license_apache_2,
url = "https://github.com/promeG/TinyPinyin"
),
+ OpenSourceLibrary(
+ name = "material-color-utilities",
+ copyrightNote = "Copyright 2021 Google LLC",
+ description = "Algorithms and utilities that power the Material Design 3 (M3) color system, including choosing theme colors from images and creating tones of colors; all in a new color space.",
+ licenseName = R.string.apache_license_name,
+ licenseText = R.raw.license_apache_2,
+ url = "https://github.com/material-foundation/material-color-utilities"
+ ),
)
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 782ae81a..24464e24 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,6 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.1.3")
classpath(libs.kotlin.gradle)
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml
index 97b16574..89676ed8 100644
--- a/i18n/src/main/res/values/strings.xml
+++ b/i18n/src/main/res/values/strings.xml
@@ -387,6 +387,7 @@
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 float[][] XYZ_TO_CAM16RGB = { + {0.401288f, 0.650173f, -0.051461f}, + {-0.250268f, 1.204414f, 0.045854f}, + {-0.002079f, 0.048952f, 0.953127f} + }; + + // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. + static final float[][] CAM16RGB_TO_XYZ = { + {1.8620678f, -1.0112547f, 0.14918678f}, + {0.38752654f, 0.62144744f, -0.00897398f}, + {-0.01584150f, -0.03412294f, 1.0499644f} + }; + + // CAM16 color dimensions, see getters for documentation. + private final float hue; + private final float chroma; + private final float j; + private final float q; + private final float m; + private final float s; + + // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*. + private final float jstar; + private final float astar; + private final float bstar; + + /** + * 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. + */ + float distance(Cam16 other) { + float dJ = getJStar() - other.getJStar(); + float dA = getAStar() - other.getAStar(); + float dB = getBStar() - other.getBStar(); + double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); + double dE = 1.41 * Math.pow(dEPrime, 0.63); + return (float) dE; + } + + /** Hue in CAM16 */ + public float getHue() { + return hue; + } + + /** Chroma in CAM16 */ + public float getChroma() { + return chroma; + } + + /** Lightness in CAM16 */ + public float 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 float 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 float 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 float getS() { + return s; + } + + /** Lightness coordinate in CAM16-UCS */ + public float getJStar() { + return jstar; + } + + /** a* coordinate in CAM16-UCS */ + public float getAStar() { + return astar; + } + + /** b* coordinate in CAM16-UCS */ + public float 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( + float hue, + float chroma, + float j, + float q, + float m, + float s, + float jstar, + float astar, + float 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); + float redL = (float) ColorUtils.linearized(red); + float greenL = (float) ColorUtils.linearized(green); + float blueL = (float) ColorUtils.linearized(blue); + float x = 0.41233895f * redL + 0.35762064f * greenL + 0.18051042f * blueL; + float y = 0.2126f * redL + 0.7152f * greenL + 0.0722f * blueL; + float z = 0.01932141f * redL + 0.11916382f * greenL + 0.95034478f * blueL; + + // Transform XYZ to 'cone'/'rgb' responses + float[][] matrix = XYZ_TO_CAM16RGB; + float rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]); + float gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]); + float bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]); + + // Discount illuminant + float rD = viewingConditions.getRgbD()[0] * rT; + float gD = viewingConditions.getRgbD()[1] * gT; + float bD = viewingConditions.getRgbD()[2] * bT; + + // Chromatic adaptation + float rAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42); + float gAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42); + float bAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42); + float rA = Math.signum(rD) * 400.0f * rAF / (rAF + 27.13f); + float gA = Math.signum(gD) * 400.0f * gAF / (gAF + 27.13f); + float bA = Math.signum(bD) * 400.0f * bAF / (bAF + 27.13f); + + // redness-greenness + float a = (float) (11.0 * rA + -12.0 * gA + bA) / 11.0f; + // yellowness-blueness + float b = (float) (rA + gA - 2.0 * bA) / 9.0f; + + // auxiliary components + float u = (20.0f * rA + 20.0f * gA + 21.0f * bA) / 20.0f; + float p2 = (40.0f * rA + 20.0f * gA + bA) / 20.0f; + + // hue + float atan2 = (float) Math.atan2(b, a); + float atanDegrees = atan2 * 180.0f / (float) Math.PI; + float hue = + atanDegrees < 0 + ? atanDegrees + 360.0f + : atanDegrees >= 360 ? atanDegrees - 360.0f : atanDegrees; + float hueRadians = hue * (float) Math.PI / 180.0f; + + // achromatic response to color + float ac = p2 * viewingConditions.getNbb(); + + // CAM16 lightness and brightness + float j = + 100.0f + * (float) + Math.pow( + ac / viewingConditions.getAw(), + viewingConditions.getC() * viewingConditions.getZ()); + float q = + 4.0f + / viewingConditions.getC() + * (float) Math.sqrt(j / 100.0f) + * (viewingConditions.getAw() + 4.0f) + * viewingConditions.getFlRoot(); + + // CAM16 chroma, colorfulness, and saturation. + float huePrime = (hue < 20.14) ? hue + 360 : hue; + float eHue = 0.25f * (float) (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8); + float p1 = 50000.0f / 13.0f * eHue * viewingConditions.getNc() * viewingConditions.getNcb(); + float t = p1 * (float) Math.hypot(a, b) / (u + 0.305f); + float alpha = + (float) Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) + * (float) Math.pow(t, 0.9); + // CAM16 chroma, colorfulness, saturation + float c = alpha * (float) Math.sqrt(j / 100.0); + float m = c * viewingConditions.getFlRoot(); + float s = + 50.0f + * (float) + Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f)); + + // CAM16-UCS components + float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j); + float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228f * m); + float astar = mstar * (float) Math.cos(hueRadians); + float bstar = mstar * (float) 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(float j, float c, float 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( + float j, float c, float h, ViewingConditions viewingConditions) { + float q = + 4.0f + / viewingConditions.getC() + * (float) Math.sqrt(j / 100.0) + * (viewingConditions.getAw() + 4.0f) + * viewingConditions.getFlRoot(); + float m = c * viewingConditions.getFlRoot(); + float alpha = c / (float) Math.sqrt(j / 100.0); + float s = + 50.0f + * (float) + Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f)); + + float hueRadians = h * (float) Math.PI / 180.0f; + float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j); + float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228 * m); + float astar = mstar * (float) Math.cos(hueRadians); + float bstar = mstar * (float) 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(float jstar, float astar, float 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( + float jstar, float astar, float bstar, ViewingConditions viewingConditions) { + + double m = Math.hypot(astar, bstar); + double m2 = Math.expm1(m * 0.0228f) / 0.0228f; + double c = m2 / viewingConditions.getFlRoot(); + double h = Math.atan2(bstar, astar) * (180.0f / Math.PI); + if (h < 0.0) { + h += 360.0f; + } + float j = jstar / (1f - (jstar - 100f) * 0.007f); + return fromJchInViewingConditions(j, (float) c, (float) 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 getInt() { + 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) { + float alpha = + (getChroma() == 0.0 || getJ() == 0.0) + ? 0.0f + : getChroma() / (float) Math.sqrt(getJ() / 100.0); + + float t = + (float) + Math.pow( + alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9); + float hRad = getHue() * (float) Math.PI / 180.0f; + + float eHue = 0.25f * (float) (Math.cos(hRad + 2.0) + 3.8); + float ac = + viewingConditions.getAw() + * (float) + Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ()); + float p1 = eHue * (50000.0f / 13.0f) * viewingConditions.getNc() * viewingConditions.getNcb(); + float p2 = (ac / viewingConditions.getNbb()); + + float hSin = (float) Math.sin(hRad); + float hCos = (float) Math.cos(hRad); + + float gamma = 23.0f * (p2 + 0.305f) * t / (23.0f * p1 + 11.0f * t * hCos + 108.0f * t * hSin); + float a = gamma * hCos; + float b = gamma * hSin; + float rA = (460.0f * p2 + 451.0f * a + 288.0f * b) / 1403.0f; + float gA = (460.0f * p2 - 891.0f * a - 261.0f * b) / 1403.0f; + float bA = (460.0f * p2 - 220.0f * a - 6300.0f * b) / 1403.0f; + + float rCBase = (float) max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA))); + float rC = + Math.signum(rA) + * (100.0f / viewingConditions.getFl()) + * (float) Math.pow(rCBase, 1.0 / 0.42); + float gCBase = (float) max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA))); + float gC = + Math.signum(gA) + * (100.0f / viewingConditions.getFl()) + * (float) Math.pow(gCBase, 1.0 / 0.42); + float bCBase = (float) max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA))); + float bC = + Math.signum(bA) + * (100.0f / viewingConditions.getFl()) + * (float) Math.pow(bCBase, 1.0 / 0.42); + float rF = rC / viewingConditions.getRgbD()[0]; + float gF = gC / viewingConditions.getRgbD()[1]; + float bF = bC / viewingConditions.getRgbD()[2]; + + float[][] matrix = CAM16RGB_TO_XYZ; + float x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]); + float y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]); + float z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]); + + return ColorUtils.argbFromXyz(x, y, z); + } +} diff --git a/material-color-utilities/src/main/java/hct/Hct.java b/material-color-utilities/src/main/java/hct/Hct.java new file mode 100644 index 00000000..f9b8d747 --- /dev/null +++ b/material-color-utilities/src/main/java/hct/Hct.java @@ -0,0 +1,262 @@ +/* + * Copyright 2021 Google LLC + * + * 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 hct; + +import utils.ColorUtils; +import utils.MathUtils; + +/** + * 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 float hue; + private float chroma; + private float tone; + + /** + * 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(float hue, float chroma, float tone) { + return new Hct(hue, chroma, tone); + } + + /** + * 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) { + Cam16 cam = Cam16.fromInt(argb); + return new Hct(cam.getHue(), cam.getChroma(), (float) ColorUtils.lstarFromArgb(argb)); + } + + private Hct(float hue, float chroma, float tone) { + setInternalState(gamutMap(hue, chroma, tone)); + } + + public float getHue() { + return hue; + } + + public float getChroma() { + return chroma; + } + + public float getTone() { + return tone; + } + + public int toInt() { + return gamutMap(hue, chroma, tone); + } + + /** + * 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(float newHue) { + setInternalState(gamutMap((float) MathUtils.sanitizeDegreesDouble(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(float newChroma) { + setInternalState(gamutMap(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(float newTone) { + setInternalState(gamutMap(hue, chroma, newTone)); + } + + private void setInternalState(int argb) { + Cam16 cam = Cam16.fromInt(argb); + float tone = (float) ColorUtils.lstarFromArgb(argb); + hue = cam.getHue(); + chroma = cam.getChroma(); + this.tone = tone; + } + + /** + * When the delta between the floor & ceiling of a binary search for maximum chroma at a hue and + * tone is less than this, the binary search terminates. + */ + private static final float CHROMA_SEARCH_ENDPOINT = 0.4f; + + /** The maximum color distance, in CAM16-UCS, between a requested color and the color returned. */ + private static final float DE_MAX = 1.0f; + + /** The maximum difference between the requested L* and the L* returned. */ + private static final float DL_MAX = 0.2f; + + /** + * The minimum color distance, in CAM16-UCS, between a requested color and an 'exact' match. This + * allows the binary search during gamut mapping to terminate much earlier when the error is + * infinitesimal. + */ + private static final float DE_MAX_ERROR = 0.000000001f; + + /** + * When the delta between the floor & ceiling of a binary search for J, lightness in CAM16, is + * less than this, the binary search terminates. + */ + private static final float LIGHTNESS_SEARCH_ENDPOINT = 0.01f; + + /** + * @param hue a number, in degrees, representing ex. red, orange, yellow, etc. Ranges from 0 <= + * hue < 360. + * @param chroma Informally, colorfulness. Ranges from 0 to roughly 150. Like all perceptually + * accurate color systems, chroma has a different maximum for any given hue and tone, so the + * color returned may be lower than the requested chroma. + * @param tone Lightness. Ranges from 0 to 100. + * @return ARGB representation of a color in default viewing conditions + */ + private static int gamutMap(float hue, float chroma, float tone) { + return gamutMapInViewingConditions(hue, chroma, tone, ViewingConditions.DEFAULT); + } + + /** + * @param hue CAM16 hue. + * @param chroma CAM16 chroma. + * @param tone L*a*b* lightness. + * @param viewingConditions Information about the environment where the color was observed. + */ + static int gamutMapInViewingConditions( + float hue, float chroma, float tone, ViewingConditions viewingConditions) { + + if (chroma < 1.0 || Math.round(tone) <= 0.0 || Math.round(tone) >= 100.0) { + return ColorUtils.argbFromLstar(tone); + } + + hue = (float) MathUtils.sanitizeDegreesDouble(hue); + + float high = chroma; + float mid = chroma; + float low = 0.0f; + boolean isFirstLoop = true; + + Cam16 answer = null; + while (Math.abs(low - high) >= CHROMA_SEARCH_ENDPOINT) { + Cam16 possibleAnswer = findCamByJ(hue, mid, tone); + + if (isFirstLoop) { + if (possibleAnswer != null) { + return possibleAnswer.viewed(viewingConditions); + } else { + isFirstLoop = false; + mid = low + (high - low) / 2.0f; + continue; + } + } + + if (possibleAnswer == null) { + high = mid; + } else { + answer = possibleAnswer; + low = mid; + } + + mid = low + (high - low) / 2.0f; + } + + if (answer == null) { + return ColorUtils.argbFromLstar(tone); + } + + return answer.viewed(viewingConditions); + } + + /** + * @param hue CAM16 hue + * @param chroma CAM16 chroma + * @param tone L*a*b* lightness + * @return CAM16 instance within error tolerance of the provided dimensions, or null. + */ + private static Cam16 findCamByJ(float hue, float chroma, float tone) { + float low = 0.0f; + float high = 100.0f; + float mid = 0.0f; + float bestdL = 1000.0f; + float bestdE = 1000.0f; + + Cam16 bestCam = null; + while (Math.abs(low - high) > LIGHTNESS_SEARCH_ENDPOINT) { + mid = low + (high - low) / 2; + Cam16 camBeforeClip = Cam16.fromJch(mid, chroma, hue); + int clipped = camBeforeClip.getInt(); + float clippedLstar = (float) ColorUtils.lstarFromArgb(clipped); + float dL = Math.abs(tone - clippedLstar); + + if (dL < DL_MAX) { + Cam16 camClipped = Cam16.fromInt(clipped); + float dE = + camClipped.distance(Cam16.fromJch(camClipped.getJ(), camClipped.getChroma(), hue)); + if (dE <= DE_MAX && dE <= bestdE) { + bestdL = dL; + bestdE = dE; + bestCam = camClipped; + } + } + + if (bestdL == 0 && bestdE < DE_MAX_ERROR) { + break; + } + + if (clippedLstar < tone) { + low = mid; + } else { + high = mid; + } + } + + return bestCam; + } +} diff --git a/material-color-utilities/src/main/java/hct/ViewingConditions.java b/material-color-utilities/src/main/java/hct/ViewingConditions.java new file mode 100644 index 00000000..e42dbe11 --- /dev/null +++ b/material-color-utilities/src/main/java/hct/ViewingConditions.java @@ -0,0 +1,198 @@ +/* + * Copyright 2021 Google LLC + * + * 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 hct; + +import utils.ColorUtils; +import 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.make(
+ new float[] {
+ (float) ColorUtils.whitePointD65()[0],
+ (float) ColorUtils.whitePointD65()[1],
+ (float) ColorUtils.whitePointD65()[2]
+ },
+ (float) (200.0f / Math.PI * ColorUtils.yFromLstar(50.0f) / 100.f),
+ 50.0f,
+ 2.0f,
+ false);
+
+ private final float aw;
+ private final float nbb;
+ private final float ncb;
+ private final float c;
+ private final float nc;
+ private final float n;
+ private final float[] rgbD;
+ private final float fl;
+ private final float flRoot;
+ private final float z;
+
+ public float getAw() {
+ return aw;
+ }
+
+ public float getN() {
+ return n;
+ }
+
+ public float getNbb() {
+ return nbb;
+ }
+
+ float getNcb() {
+ return ncb;
+ }
+
+ float getC() {
+ return c;
+ }
+
+ float getNc() {
+ return nc;
+ }
+
+ public float[] getRgbD() {
+ return rgbD;
+ }
+
+ float getFl() {
+ return fl;
+ }
+
+ public float getFlRoot() {
+ return flRoot;
+ }
+
+ float 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.
+ */
+ static ViewingConditions make(
+ float[] whitePoint,
+ float adaptingLuminance,
+ float backgroundLstar,
+ float surround,
+ boolean discountingIlluminant) {
+ // Transform white point XYZ to 'cone'/'rgb' responses
+ float[][] matrix = Cam16.XYZ_TO_CAM16RGB;
+ float[] xyz = whitePoint;
+ float rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
+ float gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
+ float bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
+ float f = 0.8f + (surround / 10.0f);
+ float c =
+ (f >= 0.9)
+ ? (float) MathUtils.lerp(0.59f, 0.69f, ((f - 0.9f) * 10.0f))
+ : (float) MathUtils.lerp(0.525f, 0.59f, ((f - 0.8f) * 10.0f));
+ float d =
+ discountingIlluminant
+ ? 1.0f
+ : f * (1.0f - ((1.0f / 3.6f) * (float) Math.exp((-adaptingLuminance - 42.0f) / 92.0f)));
+ d = (d > 1.0) ? 1.0f : (d < 0.0) ? 0.0f : d;
+ float nc = f;
+ float[] rgbD =
+ new float[] {
+ d * (100.0f / rW) + 1.0f - d, d * (100.0f / gW) + 1.0f - d, d * (100.0f / bW) + 1.0f - d
+ };
+ float k = 1.0f / (5.0f * adaptingLuminance + 1.0f);
+ float k4 = k * k * k * k;
+ float k4F = 1.0f - k4;
+ float fl =
+ (k4 * adaptingLuminance) + (0.1f * k4F * k4F * (float) Math.cbrt(5.0 * adaptingLuminance));
+ float n = (float) (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
+ float z = 1.48f + (float) Math.sqrt(n);
+ float nbb = 0.725f / (float) Math.pow(n, 0.2);
+ float ncb = nbb;
+ float[] rgbAFactors =
+ new float[] {
+ (float) Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
+ (float) Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
+ (float) Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
+ };
+
+ float[] rgbA =
+ new float[] {
+ (400.0f * rgbAFactors[0]) / (rgbAFactors[0] + 27.13f),
+ (400.0f * rgbAFactors[1]) / (rgbAFactors[1] + 27.13f),
+ (400.0f * rgbAFactors[2]) / (rgbAFactors[2] + 27.13f)
+ };
+
+ float aw = ((2.0f * rgbA[0]) + rgbA[1] + (0.05f * rgbA[2])) * nbb;
+ return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, (float) Math.pow(fl, 0.25), z);
+ }
+
+ /**
+ * 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(
+ float n,
+ float aw,
+ float nbb,
+ float ncb,
+ float c,
+ float nc,
+ float[] rgbD,
+ float fl,
+ float flRoot,
+ float 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/material-color-utilities/src/main/java/palettes/CorePalette.java b/material-color-utilities/src/main/java/palettes/CorePalette.java
new file mode 100644
index 00000000..32e35b79
--- /dev/null
+++ b/material-color-utilities/src/main/java/palettes/CorePalette.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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 palettes;
+
+import static java.lang.Math.max;
+
+import 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);
+ }
+
+ private CorePalette(int argb) {
+ Hct hct = Hct.fromInt(argb);
+ float hue = hct.getHue();
+ this.a1 = TonalPalette.fromHueAndChroma(hue, max(48f, hct.getChroma()));
+ this.a2 = TonalPalette.fromHueAndChroma(hue, 16f);
+ this.a3 = TonalPalette.fromHueAndChroma(hue + 60f, 24f);
+ this.n1 = TonalPalette.fromHueAndChroma(hue, 4f);
+ this.n2 = TonalPalette.fromHueAndChroma(hue, 8f);
+ this.error = TonalPalette.fromHueAndChroma(25, 84f);
+ }
+}
diff --git a/material-color-utilities/src/main/java/palettes/TonalPalette.java b/material-color-utilities/src/main/java/palettes/TonalPalette.java
new file mode 100644
index 00000000..06b24857
--- /dev/null
+++ b/material-color-utilities/src/main/java/palettes/TonalPalette.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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 palettes;
+
+import 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 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);
+ }
+
+ /** 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 fy = (lstar + 16.0) / 116.0;
+ double fz = fy;
+ double fx = fy;
+ double kappa = 24389.0 / 27.0;
+ double epsilon = 216.0 / 24389.0;
+ boolean lExceedsEpsilonKappa = lstar > 8.0;
+ double y = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa;
+ boolean cubeExceedEpsilon = fy * fy * fy > epsilon;
+ double x = cubeExceedEpsilon ? fx * fx * fx : lstar / kappa;
+ double z = cubeExceedEpsilon ? fz * fz * fz : lstar / kappa;
+ double[] whitePoint = WHITE_POINT_D65;
+ return argbFromXyz(x * whitePoint[0], y * whitePoint[1], z * whitePoint[2]);
+ }
+
+ /**
+ * 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] / 100.0;
+ double e = 216.0 / 24389.0;
+ if (y <= e) {
+ return 24389.0 / 27.0 * y;
+ } else {
+ double yIntermediate = Math.pow(y, 1.0 / 3.0);
+ return 116.0 * yIntermediate - 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) {
+ double ke = 8.0;
+ if (lstar > ke) {
+ return Math.pow((lstar + 16.0) / 116.0, 3.0) * 100.0;
+ } else {
+ return lstar / 24389.0 / 27.0 * 100.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/material-color-utilities/src/main/java/utils/MathUtils.java b/material-color-utilities/src/main/java/utils/MathUtils.java
new file mode 100644
index 00000000..a4e07bf8
--- /dev/null
+++ b/material-color-utilities/src/main/java/utils/MathUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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.
+ */
+
+// This file is automatically generated. Do not modify it.
+
+package 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;
+ }
+
+ /** 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/material-color-utilities/src/main/java/utils/StringUtils.java b/material-color-utilities/src/main/java/utils/StringUtils.java
new file mode 100644
index 00000000..75aa5b1b
--- /dev/null
+++ b/material-color-utilities/src/main/java/utils/StringUtils.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * 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 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/preferences/src/main/proto/settings.proto b/preferences/src/main/proto/settings.proto
index 6d7e52d0..0fb45030 100644
--- a/preferences/src/main/proto/settings.proto
+++ b/preferences/src/main/proto/settings.proto
@@ -15,6 +15,7 @@ message Settings {
enum ColorScheme {
Default = 0;
BlackAndWhite = 1;
+ Wallpaper = 2;
}
ColorScheme color_scheme = 6;
bool dim_wallpaper = 7;
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 84b9db84..4441df0c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -385,3 +385,4 @@ dependencyResolutionManagement {
include(":notifications")
include(":accounts")
include(":appshortcuts")
+include(":material-color-utilities")
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index 5f459ebd..3aaae275 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -104,6 +104,8 @@ dependencies {
implementation(libs.coil.core)
implementation(libs.coil.compose)
+ implementation(project(":material-color-utilities"))
+
implementation(project(":base"))
implementation(project(":i18n"))
implementation(project(":compat"))
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt
index 7a6b160a..2049709b 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt
@@ -71,6 +71,7 @@ fun AppearanceSettingsScreen() {
summary = when (colorScheme) {
ColorScheme.Default -> stringResource(R.string.preference_colors_default)
ColorScheme.BlackAndWhite -> stringResource(R.string.preference_colors_bw)
+ ColorScheme.Wallpaper -> stringResource(R.string.preference_colors_wallpaper)
else -> null
},
onClick = {
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt
index c1b16fa6..0d47d8ea 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt
@@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings.colorscheme
+import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
@@ -7,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -17,44 +19,42 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
-import de.mm20.launcher2.ui.theme.getColorScheme
+import de.mm20.launcher2.ui.theme.colorSchemeAsState
@Composable
fun ColorSchemeSettingsScreen() {
val viewModel: ColorSchemeSettingsScreenVM = viewModel()
- val context = LocalContext.current
PreferenceScreen(title = stringResource(R.string.preference_screen_colors)) {
item {
PreferenceCategory {
- val theme by viewModel.theme.observeAsState()
- val darkTheme =
- theme == AppearanceSettings.Theme.Dark || theme == AppearanceSettings.Theme.System && isSystemInDarkTheme()
val colorScheme by viewModel.colorScheme.observeAsState()
- val items = listOf(
+ val items = mutableListOf(
AppearanceSettings.ColorScheme.Default to R.string.preference_colors_default,
- AppearanceSettings.ColorScheme.BlackAndWhite to R.string.preference_colors_bw
+ AppearanceSettings.ColorScheme.BlackAndWhite to R.string.preference_colors_bw,
)
+ if (isAtLeastApiLevel(Build.VERSION_CODES.O_MR1)) {
+ items.add(
+ AppearanceSettings.ColorScheme.Wallpaper to R.string.preference_colors_wallpaper
+ )
+ }
+
for (cs in items) {
+ val scheme by colorSchemeAsState(cs.first)
Preference(
title = stringResource(cs.second),
icon = if (colorScheme == cs.first) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
onClick = { viewModel.setColorScheme(cs.first) },
controls = {
- ColorSchemePreview(
- getColorScheme(
- LocalContext.current,
- cs.first,
- darkTheme,
- )
- )
+ ColorSchemePreview(scheme)
}
)
}
@@ -65,51 +65,54 @@ fun ColorSchemeSettingsScreen() {
@Composable
fun ColorSchemePreview(colorScheme: ColorScheme) {
- Box(
- modifier = Modifier
- .padding(vertical = 12.dp)
- .width(72.dp)
- .height(36.dp),
- contentAlignment = Alignment.Center
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
+ MaterialTheme(colorScheme = colorScheme) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 12.dp)
+ .width(72.dp)
+ .height(36.dp),
+ contentAlignment = Alignment.Center
) {
- Surface(
- tonalElevation = 1.dp,
- color = colorScheme.surface,
- modifier = Modifier
- .size(36.dp)
- ) {}
- Surface(
- tonalElevation = 1.dp,
- color = colorScheme.surfaceVariant,
- modifier = Modifier
- .size(36.dp)
- ) {}
- }
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Surface(
- tonalElevation = 1.dp,
- color = colorScheme.primary,
- modifier = Modifier
- .size(16.dp)
- ) {}
- Surface(
- tonalElevation = 1.dp,
- color = colorScheme.secondary,
- modifier = Modifier
- .padding(horizontal = 8.dp)
- .size(16.dp)
- ) {}
- Surface(
- tonalElevation = 1.dp,
- color = colorScheme.tertiary,
- modifier = Modifier
- .size(16.dp)
- ) {}
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ tonalElevation = 1.dp,
+ color = MaterialTheme.colorScheme.surface,
+ modifier = Modifier
+ .size(36.dp)
+ ) {}
+ Surface(
+ tonalElevation = 1.dp,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier
+ .size(36.dp)
+ ) {}
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ tonalElevation = 1.dp,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .size(16.dp)
+ ) {}
+ Surface(
+ tonalElevation = 1.dp,
+ color = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .size(16.dp)
+ ) {}
+ Surface(
+ tonalElevation = 1.dp,
+ color = MaterialTheme.colorScheme.tertiary,
+ modifier = Modifier
+ .size(16.dp)
+ ) {}
+ }
}
}
+
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt b/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt
index 5c58979e..c13405f8 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt
@@ -1,24 +1,18 @@
package de.mm20.launcher2.ui.theme
-import android.content.Context
+import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
-import de.mm20.launcher2.ui.theme.colorscheme.DarkBlackAndWhiteColorScheme
-import de.mm20.launcher2.ui.theme.colorscheme.DarkPre31DefaultColorScheme
-import de.mm20.launcher2.ui.theme.colorscheme.LightBlackAndWhiteColorScheme
-import de.mm20.launcher2.ui.theme.colorscheme.LightPre31DefaultColorScheme
+import de.mm20.launcher2.ui.theme.colorscheme.*
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
import kotlinx.coroutines.flow.map
import org.koin.androidx.compose.inject
@@ -34,34 +28,52 @@ fun LauncherTheme(
val colorSchemePreference by remember { dataStore.data.map { it.appearance.colorScheme } }.collectAsState(
AppearanceSettings.ColorScheme.Default
)
- val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
- Theme.System
- )
- val darkTheme =
- themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme()
+ val colorScheme by colorSchemeAsState(colorSchemePreference)
MaterialTheme(
- colorScheme = getColorScheme(LocalContext.current, colorSchemePreference, darkTheme),
+ colorScheme = colorScheme,
typography = DefaultTypography,
content = content
)
}
-fun getColorScheme(context: Context, colorScheme: AppearanceSettings.ColorScheme, darkTheme: Boolean): ColorScheme {
- return when (colorScheme) {
- AppearanceSettings.ColorScheme.BlackAndWhite -> {
- if (darkTheme) DarkBlackAndWhiteColorScheme
- else LightBlackAndWhiteColorScheme
- }
- else -> {
- if (darkTheme) {
- if (isAtLeastApiLevel(31)) dynamicDarkColorScheme(context)
- else DarkPre31DefaultColorScheme
- } else {
- if (isAtLeastApiLevel(31)) dynamicLightColorScheme(context)
- else LightPre31DefaultColorScheme
+@Composable
+fun colorSchemeAsState(colorScheme: AppearanceSettings.ColorScheme): MutableState