From 8bb37374604c9f1a34a17161e0c9d56b11712293 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 12 Apr 2022 18:28:08 +0200 Subject: [PATCH] Add wallpaper color scheme --- .../launcher2/licenses/OpenSourceLicenses.kt | 8 + build.gradle.kts | 1 - i18n/src/main/res/values/strings.xml | 1 + material-color-utilities/.gitignore | 1 + material-color-utilities/build.gradle.kts | 42 + material-color-utilities/consumer-rules.pro | 0 material-color-utilities/proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 5 + .../src/main/java/hct/Cam16.java | 435 +++++++++++ .../src/main/java/hct/Hct.java | 262 +++++++ .../src/main/java/hct/ViewingConditions.java | 198 +++++ .../src/main/java/palettes/CorePalette.java | 54 ++ .../src/main/java/palettes/TonalPalette.java | 75 ++ .../src/main/java/scheme/Scheme.java | 717 ++++++++++++++++++ .../src/main/java/utils/ColorUtils.java | 265 +++++++ .../src/main/java/utils/MathUtils.java | 119 +++ .../src/main/java/utils/StringUtils.java | 34 + preferences/src/main/proto/settings.proto | 1 + settings.gradle.kts | 1 + ui/build.gradle.kts | 2 + .../appearance/AppearanceSettingsScreen.kt | 1 + .../colorscheme/ColorSchemeSettingsScreen.kt | 119 +-- .../mm20/launcher2/ui/theme/LauncherTheme.kt | 70 +- .../launcher2/ui/theme/WallpaperColors.kt | 1 + .../ui/theme/colorscheme/Wallpaper.kt | 69 ++ 25 files changed, 2414 insertions(+), 88 deletions(-) create mode 100644 material-color-utilities/.gitignore create mode 100644 material-color-utilities/build.gradle.kts create mode 100644 material-color-utilities/consumer-rules.pro create mode 100644 material-color-utilities/proguard-rules.pro create mode 100644 material-color-utilities/src/main/AndroidManifest.xml create mode 100644 material-color-utilities/src/main/java/hct/Cam16.java create mode 100644 material-color-utilities/src/main/java/hct/Hct.java create mode 100644 material-color-utilities/src/main/java/hct/ViewingConditions.java create mode 100644 material-color-utilities/src/main/java/palettes/CorePalette.java create mode 100644 material-color-utilities/src/main/java/palettes/TonalPalette.java create mode 100644 material-color-utilities/src/main/java/scheme/Scheme.java create mode 100644 material-color-utilities/src/main/java/utils/ColorUtils.java create mode 100644 material-color-utilities/src/main/java/utils/MathUtils.java create mode 100644 material-color-utilities/src/main/java/utils/StringUtils.java create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Wallpaper.kt 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 @@ Color scheme Default Black and White + From wallpaper About Version Links diff --git a/material-color-utilities/.gitignore b/material-color-utilities/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/material-color-utilities/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/material-color-utilities/build.gradle.kts b/material-color-utilities/build.gradle.kts new file mode 100644 index 00000000..34fc4180 --- /dev/null +++ b/material-color-utilities/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + + implementation(libs.koin.android) + +} \ No newline at end of file diff --git a/material-color-utilities/consumer-rules.pro b/material-color-utilities/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/material-color-utilities/proguard-rules.pro b/material-color-utilities/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/material-color-utilities/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/material-color-utilities/src/main/AndroidManifest.xml b/material-color-utilities/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3a283795 --- /dev/null +++ b/material-color-utilities/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/material-color-utilities/src/main/java/hct/Cam16.java b/material-color-utilities/src/main/java/hct/Cam16.java new file mode 100644 index 00000000..c847a716 --- /dev/null +++ b/material-color-utilities/src/main/java/hct/Cam16.java @@ -0,0 +1,435 @@ +/* + * 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 static java.lang.Math.max; + +import 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 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 cache; + float hue; + float 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 final TonalPalette fromInt(int argb) { + Hct hct = Hct.fromInt(argb); + 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 final TonalPalette fromHueAndChroma(float hue, float chroma) { + return new TonalPalette(hue, chroma); + } + + private TonalPalette(float hue, float 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; + } +} diff --git a/material-color-utilities/src/main/java/scheme/Scheme.java b/material-color-utilities/src/main/java/scheme/Scheme.java new file mode 100644 index 00000000..b861de05 --- /dev/null +++ b/material-color-utilities/src/main/java/scheme/Scheme.java @@ -0,0 +1,717 @@ +/* + * 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 scheme; + +import 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 shadow; + 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 shadow, + 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.shadow = shadow; + this.inverseSurface = inverseSurface; + this.inverseOnSurface = inverseOnSurface; + this.inversePrimary = inversePrimary; + } + + public static Scheme light(int argb) { + CorePalette core = CorePalette.of(argb); + 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)) + .withShadow(core.n1.tone(0)) + .withInverseSurface(core.n1.tone(20)) + .withInverseOnSurface(core.n1.tone(95)) + .withInversePrimary(core.a1.tone(80)); + } + + public static Scheme dark(int argb) { + CorePalette core = CorePalette.of(argb); + 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)) + .withShadow(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 getShadow() { + return shadow; + } + + public void setShadow(int shadow) { + this.shadow = shadow; + } + + public Scheme withShadow(int shadow) { + this.shadow = shadow; + 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 + + ", shadow=" + + shadow + + ", 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 (shadow != scheme.shadow) { + 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 + shadow; + result = 31 * result + inverseSurface; + result = 31 * result + inverseOnSurface; + result = 31 * result + inversePrimary; + return result; + } +} diff --git a/material-color-utilities/src/main/java/utils/ColorUtils.java b/material-color-utilities/src/main/java/utils/ColorUtils.java new file mode 100644 index 00000000..f80338f9 --- /dev/null +++ b/material-color-utilities/src/main/java/utils/ColorUtils.java @@ -0,0 +1,265 @@ +/* + * 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; + +/** + * 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); + } + + /** 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 { + val context = LocalContext.current + val dataStore: LauncherDataStore by inject() + + val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState( + Theme.System + ) + val darkTheme = + themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme() + + val state = remember(colorScheme, darkTheme) { + mutableStateOf( + 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 + } + } } + ) + } + + if (colorScheme == AppearanceSettings.ColorScheme.Wallpaper && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + val wallpaperColors by wallpaperColorsAsState() + LaunchedEffect(wallpaperColors, darkTheme) { + state.value = WallpaperColorScheme(wallpaperColors, darkTheme) } } -} \ No newline at end of file + + return state +} diff --git a/ui/src/main/java/de/mm20/launcher2/ui/theme/WallpaperColors.kt b/ui/src/main/java/de/mm20/launcher2/ui/theme/WallpaperColors.kt index 2133c3e0..3e6b62cf 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/theme/WallpaperColors.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/theme/WallpaperColors.kt @@ -4,6 +4,7 @@ import android.app.WallpaperManager import android.os.Build import android.os.Handler import android.os.Looper +import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color diff --git a/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Wallpaper.kt b/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Wallpaper.kt new file mode 100644 index 00000000..780954cb --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Wallpaper.kt @@ -0,0 +1,69 @@ +package de.mm20.launcher2.ui.theme.colorscheme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import de.mm20.launcher2.ui.theme.WallpaperColors +import palettes.TonalPalette + +fun WallpaperColorScheme(wallpaperColors: WallpaperColors, darkTheme: Boolean): ColorScheme { + val primary = TonalPalette.fromInt(wallpaperColors.primary.toArgb()) + val secondary = TonalPalette.fromInt((wallpaperColors.secondary ?: wallpaperColors.primary).toArgb()) + val tertiary = TonalPalette.fromInt((wallpaperColors.tertiary ?: wallpaperColors.primary).toArgb()) + + val neutral1 = TonalPalette.fromInt(Color.Black.toArgb()) + val neutral2 = TonalPalette.fromInt(Color.Black.toArgb()) + + return if(darkTheme) { + darkColorScheme( + primary = Color(primary.tone(80)), + onPrimary = Color(primary.tone(20)), + primaryContainer = Color(primary.tone(30)), + onPrimaryContainer = Color(primary.tone(90)), + secondary = Color(secondary.tone(80)), + onSecondary = Color(secondary.tone(20)), + secondaryContainer = Color(secondary.tone(30)), + onSecondaryContainer = Color(secondary.tone(90)), + tertiary = Color(tertiary.tone(80)), + onTertiary = Color(tertiary.tone(20)), + tertiaryContainer = Color(tertiary.tone(30)), + onTertiaryContainer = Color(tertiary.tone(90)), + background = Color(neutral1.tone(10)), + onBackground = Color(neutral1.tone(90)), + surface = Color(neutral1.tone(10)), + onSurface = Color(neutral1.tone(90)), + surfaceVariant = Color(neutral2.tone(30)), + onSurfaceVariant = Color(neutral2.tone(80)), + outline = Color(neutral2.tone(60)), + inverseSurface = Color(neutral1.tone(90)), + inverseOnSurface = Color(neutral1.tone(20)), + inversePrimary = Color(primary.tone(40)), + ) + } else { + lightColorScheme( + primary = Color(primary.tone(40)), + onPrimary = Color(primary.tone(100)), + primaryContainer = Color(primary.tone(90)), + onPrimaryContainer = Color(primary.tone(10)), + secondary = Color(secondary.tone(40)), + onSecondary = Color(secondary.tone(100)), + secondaryContainer = Color(secondary.tone(90)), + onSecondaryContainer = Color(secondary.tone(10)), + tertiary = Color(tertiary.tone(40)), + onTertiary = Color(tertiary.tone(100)), + tertiaryContainer = Color(tertiary.tone(90)), + onTertiaryContainer = Color(tertiary.tone(10)), + background = Color(neutral1.tone(99)), + onBackground = Color(neutral1.tone(10)), + surface = Color(neutral1.tone(99)), + onSurface = Color(neutral1.tone(10)), + surfaceVariant = Color(neutral2.tone(90)), + onSurfaceVariant = Color(neutral2.tone(30)), + outline = Color(neutral2.tone(50)), + inverseSurface = Color(neutral1.tone(20)), + inverseOnSurface = Color(neutral1.tone(95)), + inversePrimary = Color(primary.tone(80)),) + } +} \ No newline at end of file