From c5091ee4cab16e388ceddde260d743fe9cef48b6 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 15 Oct 2022 12:07:22 +0200 Subject: [PATCH] Update material-color-utilities --- .../src/main/java/hct/Cam16.java | 318 ++++----- .../src/main/java/hct/Hct.java | 179 +---- .../src/main/java/hct/HctSolver.java | 672 ++++++++++++++++++ .../src/main/java/hct/ViewingConditions.java | 157 ++-- .../src/main/java/palettes/CorePalette.java | 37 +- .../src/main/java/palettes/TonalPalette.java | 8 +- .../src/main/java/scheme/Scheme.java | 70 +- .../src/main/java/utils/ColorUtils.java | 41 +- .../src/main/java/utils/MathUtils.java | 17 +- .../src/main/java/utils/StringUtils.java | 34 - 10 files changed, 1052 insertions(+), 481 deletions(-) create mode 100644 material-color-utilities/src/main/java/hct/HctSolver.java delete mode 100644 material-color-utilities/src/main/java/utils/StringUtils.java diff --git a/material-color-utilities/src/main/java/hct/Cam16.java b/material-color-utilities/src/main/java/hct/Cam16.java index c847a716..9dfdb09e 100644 --- a/material-color-utilities/src/main/java/hct/Cam16.java +++ b/material-color-utilities/src/main/java/hct/Cam16.java @@ -37,58 +37,58 @@ import utils.ColorUtils; */ 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} + static final double[][] XYZ_TO_CAM16RGB = { + {0.401288, 0.650173, -0.051461}, + {-0.250268, 1.204414, 0.045854}, + {-0.002079, 0.048952, 0.953127} }; // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. - static final float[][] CAM16RGB_TO_XYZ = { - {1.8620678f, -1.0112547f, 0.14918678f}, - {0.38752654f, 0.62144744f, -0.00897398f}, - {-0.01584150f, -0.03412294f, 1.0499644f} + static final double[][] CAM16RGB_TO_XYZ = { + {1.8620678, -1.0112547, 0.14918678}, + {0.38752654, 0.62144744, -0.00897398}, + {-0.01584150, -0.03412294, 1.0499644} }; // CAM16 color dimensions, see getters for documentation. - private final float hue; - private final float chroma; - private final float j; - private final float q; - private final float m; - private final float s; + private final double hue; + private final double chroma; + private final double j; + private final double q; + private final double m; + private final double s; // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*. - private final float jstar; - private final float astar; - private final float bstar; + private final double jstar; + private final double astar; + private final double 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 distance(Cam16 other) { + double dJ = getJstar() - other.getJstar(); + double dA = getAstar() - other.getAstar(); + double dB = getBstar() - other.getBstar(); double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); double dE = 1.41 * Math.pow(dEPrime, 0.63); - return (float) dE; + return dE; } /** Hue in CAM16 */ - public float getHue() { + public double getHue() { return hue; } /** Chroma in CAM16 */ - public float getChroma() { + public double getChroma() { return chroma; } /** Lightness in CAM16 */ - public float getJ() { + public double getJ() { return j; } @@ -99,7 +99,7 @@ public final class Cam16 { * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any * lighting. */ - public float getQ() { + public double getQ() { return q; } @@ -109,7 +109,7 @@ public final class 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() { + public double getM() { return m; } @@ -119,22 +119,22 @@ public final class 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() {
+ public double getS() {
return s;
}
/** Lightness coordinate in CAM16-UCS */
- public float getJStar() {
+ public double getJstar() {
return jstar;
}
/** a* coordinate in CAM16-UCS */
- public float getAStar() {
+ public double getAstar() {
return astar;
}
/** b* coordinate in CAM16-UCS */
- public float getBStar() {
+ public double getBstar() {
return bstar;
}
@@ -156,15 +156,15 @@ public final class Cam16 {
* @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) {
+ double hue,
+ double chroma,
+ double j,
+ double q,
+ double m,
+ double s,
+ double jstar,
+ double astar,
+ double bstar) {
this.hue = hue;
this.chroma = chroma;
this.j = j;
@@ -200,88 +200,84 @@ public final class Cam16 {
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;
+ double redL = ColorUtils.linearized(red);
+ double greenL = ColorUtils.linearized(green);
+ double blueL = ColorUtils.linearized(blue);
+ double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
+ double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
+ double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
// 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]);
+ double[][] matrix = XYZ_TO_CAM16RGB;
+ double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
+ double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
+ double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
// Discount illuminant
- float rD = viewingConditions.getRgbD()[0] * rT;
- float gD = viewingConditions.getRgbD()[1] * gT;
- float bD = viewingConditions.getRgbD()[2] * bT;
+ double rD = viewingConditions.getRgbD()[0] * rT;
+ double gD = viewingConditions.getRgbD()[1] * gT;
+ double 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);
+ double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
+ double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
+ double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
+ double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
+ double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
+ double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
// redness-greenness
- float a = (float) (11.0 * rA + -12.0 * gA + bA) / 11.0f;
+ double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
// yellowness-blueness
- float b = (float) (rA + gA - 2.0 * bA) / 9.0f;
+ double b = (rA + gA - 2.0 * bA) / 9.0;
// 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;
+ double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
+ double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
// hue
- float atan2 = (float) Math.atan2(b, a);
- float atanDegrees = atan2 * 180.0f / (float) Math.PI;
- float hue =
+ double atan2 = Math.atan2(b, a);
+ double atanDegrees = Math.toDegrees(atan2);
+ double hue =
atanDegrees < 0
- ? atanDegrees + 360.0f
- : atanDegrees >= 360 ? atanDegrees - 360.0f : atanDegrees;
- float hueRadians = hue * (float) Math.PI / 180.0f;
+ ? atanDegrees + 360.0
+ : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
+ double hueRadians = Math.toRadians(hue);
// achromatic response to color
- float ac = p2 * viewingConditions.getNbb();
+ double 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
+ double j =
+ 100.0
+ * Math.pow(
+ ac / viewingConditions.getAw(),
+ viewingConditions.getC() * viewingConditions.getZ());
+ double q =
+ 4.0
/ viewingConditions.getC()
- * (float) Math.sqrt(j / 100.0f)
- * (viewingConditions.getAw() + 4.0f)
+ * Math.sqrt(j / 100.0)
+ * (viewingConditions.getAw() + 4.0)
* 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);
+ double huePrime = (hue < 20.14) ? hue + 360 : hue;
+ double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
+ double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
+ double t = p1 * Math.hypot(a, b) / (u + 0.305);
+ double alpha =
+ Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
// CAM16 chroma, colorfulness, saturation
- 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));
+ double c = alpha * Math.sqrt(j / 100.0);
+ double m = c * viewingConditions.getFlRoot();
+ double s =
+ 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
// CAM16-UCS components
- 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);
+ double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
+ double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
+ double astar = mstar * Math.cos(hueRadians);
+ double bstar = mstar * Math.sin(hueRadians);
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
}
@@ -291,7 +287,7 @@ public final class Cam16 {
* @param c CAM16 chroma
* @param h CAM16 hue
*/
- static Cam16 fromJch(float j, float c, float h) {
+ static Cam16 fromJch(double j, double c, double h) {
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
}
@@ -302,25 +298,23 @@ public final class Cam16 {
* @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
+ double j, double c, double h, ViewingConditions viewingConditions) {
+ double q =
+ 4.0
/ viewingConditions.getC()
- * (float) Math.sqrt(j / 100.0)
- * (viewingConditions.getAw() + 4.0f)
+ * Math.sqrt(j / 100.0)
+ * (viewingConditions.getAw() + 4.0)
* 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));
+ double m = c * viewingConditions.getFlRoot();
+ double alpha = c / Math.sqrt(j / 100.0);
+ double s =
+ 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
- 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);
+ double hueRadians = Math.toRadians(h);
+ double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
+ double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
+ double astar = mstar * Math.cos(hueRadians);
+ double bstar = mstar * Math.sin(hueRadians);
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
}
@@ -333,7 +327,7 @@ public final class Cam16 {
* @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) {
+ public static Cam16 fromUcs(double jstar, double astar, double bstar) {
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
}
@@ -349,24 +343,24 @@ public final class Cam16 {
* @param viewingConditions Information about the environment where the color was observed.
*/
public static Cam16 fromUcsInViewingConditions(
- float jstar, float astar, float bstar, ViewingConditions viewingConditions) {
+ double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
double m = Math.hypot(astar, bstar);
- double m2 = Math.expm1(m * 0.0228f) / 0.0228f;
+ double m2 = Math.expm1(m * 0.0228) / 0.0228;
double c = m2 / viewingConditions.getFlRoot();
- double h = Math.atan2(bstar, astar) * (180.0f / Math.PI);
+ double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
if (h < 0.0) {
- h += 360.0f;
+ h += 360.0;
}
- float j = jstar / (1f - (jstar - 100f) * 0.007f);
- return fromJchInViewingConditions(j, (float) c, (float) h, viewingConditions);
+ double j = jstar / (1. - (jstar - 100.) * 0.007);
+ return fromJchInViewingConditions(j, c, h, viewingConditions);
}
/**
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
* which are near-identical to the default viewing conditions for sRGB.
*/
- public int getInt() {
+ public int toInt() {
return viewed(ViewingConditions.DEFAULT);
}
@@ -377,58 +371,48 @@ public final class Cam16 {
* @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);
+ double alpha =
+ (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / 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;
+ double t =
+ Math.pow(
+ alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
+ double hRad = Math.toRadians(getHue());
- float eHue = 0.25f * (float) (Math.cos(hRad + 2.0) + 3.8);
- float ac =
+ double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
+ double 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());
+ * Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
+ double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
+ double p2 = (ac / viewingConditions.getNbb());
- float hSin = (float) Math.sin(hRad);
- float hCos = (float) Math.cos(hRad);
+ double hSin = Math.sin(hRad);
+ double hCos = 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;
+ double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
+ double a = gamma * hCos;
+ double b = gamma * hSin;
+ double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
+ double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
+ double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
- 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];
+ double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
+ double rC =
+ Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
+ double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
+ double gC =
+ Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
+ double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
+ double bC =
+ Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
+ double rF = rC / viewingConditions.getRgbD()[0];
+ double gF = gC / viewingConditions.getRgbD()[1];
+ double bF = bC / viewingConditions.getRgbD()[2];
- 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]);
+ double[][] matrix = CAM16RGB_TO_XYZ;
+ double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
+ double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
+ double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
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
index f9b8d747..4bcdb781 100644
--- a/material-color-utilities/src/main/java/hct/Hct.java
+++ b/material-color-utilities/src/main/java/hct/Hct.java
@@ -17,7 +17,6 @@
package hct;
import utils.ColorUtils;
-import utils.MathUtils;
/**
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
@@ -39,9 +38,10 @@ import utils.MathUtils;
* lighting environments.
*/
public final class Hct {
- private float hue;
- private float chroma;
- private float tone;
+ private double hue;
+ private double chroma;
+ private double tone;
+ private int argb;
/**
* Create an HCT color from hue, chroma, and tone.
@@ -52,8 +52,9 @@ public final class Hct {
* @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);
+ public static Hct from(double hue, double chroma, double tone) {
+ int argb = HctSolver.solveToInt(hue, chroma, tone);
+ return new Hct(argb);
}
/**
@@ -63,28 +64,27 @@ public final class Hct {
* @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));
+ return new Hct(argb);
}
- private Hct(float hue, float chroma, float tone) {
- setInternalState(gamutMap(hue, chroma, tone));
+ private Hct(int argb) {
+ setInternalState(argb);
}
- public float getHue() {
+ public double getHue() {
return hue;
}
- public float getChroma() {
+ public double getChroma() {
return chroma;
}
- public float getTone() {
+ public double getTone() {
return tone;
}
public int toInt() {
- return gamutMap(hue, chroma, tone);
+ return argb;
}
/**
@@ -93,8 +93,8 @@ public final class Hct {
*
* @param newHue 0 <= newHue < 360; invalid values are corrected.
*/
- public void setHue(float newHue) {
- setInternalState(gamutMap((float) MathUtils.sanitizeDegreesDouble(newHue), chroma, tone));
+ public void setHue(double newHue) {
+ setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
}
/**
@@ -103,8 +103,8 @@ public final class Hct {
*
* @param newChroma 0 <= newChroma < ?
*/
- public void setChroma(float newChroma) {
- setInternalState(gamutMap(hue, newChroma, tone));
+ public void setChroma(double newChroma) {
+ setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
}
/**
@@ -113,150 +113,15 @@ public final class Hct {
*
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
*/
- public void setTone(float newTone) {
- setInternalState(gamutMap(hue, chroma, newTone));
+ public void setTone(double newTone) {
+ setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
}
private void setInternalState(int argb) {
+ this.argb = 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;
+ this.tone = ColorUtils.lstarFromArgb(argb);
}
}
diff --git a/material-color-utilities/src/main/java/hct/HctSolver.java b/material-color-utilities/src/main/java/hct/HctSolver.java
new file mode 100644
index 00000000..955080c1
--- /dev/null
+++ b/material-color-utilities/src/main/java/hct/HctSolver.java
@@ -0,0 +1,672 @@
+/*
+ * 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 hct;
+
+import utils.ColorUtils;
+import utils.MathUtils;
+
+/** A class that solves the HCT equation. */
+public class HctSolver {
+ private HctSolver() {}
+
+ static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
+ new double[][] {
+ new double[] {
+ 0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
+ },
+ new double[] {
+ 0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
+ },
+ new double[] {
+ 0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
+ },
+ };
+
+ static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
+ new double[][] {
+ new double[] {
+ 1373.2198709594231, -1100.4251190754821, -7.278681089101213,
+ },
+ new double[] {
+ -271.815969077903, 559.6580465940733, -32.46047482791194,
+ },
+ new double[] {
+ 1.9622899599665666, -57.173814538844006, 308.7233197812385,
+ },
+ };
+
+ static final double[] Y_FROM_LINRGB = new double[] {0.2126, 0.7152, 0.0722};
+
+ static final double[] CRITICAL_PLANES =
+ new double[] {
+ 0.015176349177441876,
+ 0.045529047532325624,
+ 0.07588174588720938,
+ 0.10623444424209313,
+ 0.13658714259697685,
+ 0.16693984095186062,
+ 0.19729253930674434,
+ 0.2276452376616281,
+ 0.2579979360165119,
+ 0.28835063437139563,
+ 0.3188300904430532,
+ 0.350925934958123,
+ 0.3848314933096426,
+ 0.42057480301049466,
+ 0.458183274052838,
+ 0.4976837250274023,
+ 0.5391024159806381,
+ 0.5824650784040898,
+ 0.6277969426914107,
+ 0.6751227633498623,
+ 0.7244668422128921,
+ 0.775853049866786,
+ 0.829304845476233,
+ 0.8848452951698498,
+ 0.942497089126609,
+ 1.0022825574869039,
+ 1.0642236851973577,
+ 1.1283421258858297,
+ 1.1946592148522128,
+ 1.2631959812511864,
+ 1.3339731595349034,
+ 1.407011200216447,
+ 1.4823302800086415,
+ 1.5599503113873272,
+ 1.6398909516233677,
+ 1.7221716113234105,
+ 1.8068114625156377,
+ 1.8938294463134073,
+ 1.9832442801866852,
+ 2.075074464868551,
+ 2.1693382909216234,
+ 2.2660538449872063,
+ 2.36523901573795,
+ 2.4669114995532007,
+ 2.5710888059345764,
+ 2.6777882626779785,
+ 2.7870270208169257,
+ 2.898822059350997,
+ 3.0131901897720907,
+ 3.1301480604002863,
+ 3.2497121605402226,
+ 3.3718988244681087,
+ 3.4967242352587946,
+ 3.624204428461639,
+ 3.754355295633311,
+ 3.887192587735158,
+ 4.022731918402185,
+ 4.160988767090289,
+ 4.301978482107941,
+ 4.445716283538092,
+ 4.592217266055746,
+ 4.741496401646282,
+ 4.893568542229298,
+ 5.048448422192488,
+ 5.20615066083972,
+ 5.3666897647573375,
+ 5.5300801301023865,
+ 5.696336044816294,
+ 5.865471690767354,
+ 6.037501145825082,
+ 6.212438385869475,
+ 6.390297286737924,
+ 6.571091626112461,
+ 6.7548350853498045,
+ 6.941541251256611,
+ 7.131223617812143,
+ 7.323895587840543,
+ 7.5195704746346665,
+ 7.7182615035334345,
+ 7.919981813454504,
+ 8.124744458384042,
+ 8.332562408825165,
+ 8.543448553206703,
+ 8.757415699253682,
+ 8.974476575321063,
+ 9.194643831691977,
+ 9.417930041841839,
+ 9.644347703669503,
+ 9.873909240696694,
+ 10.106627003236781,
+ 10.342513269534024,
+ 10.58158024687427,
+ 10.8238400726681,
+ 11.069304815507364,
+ 11.317986476196008,
+ 11.569896988756009,
+ 11.825048221409341,
+ 12.083451977536606,
+ 12.345119996613247,
+ 12.610063955123938,
+ 12.878295467455942,
+ 13.149826086772048,
+ 13.42466730586372,
+ 13.702830557985108,
+ 13.984327217668513,
+ 14.269168601521828,
+ 14.55736596900856,
+ 14.848930523210871,
+ 15.143873411576273,
+ 15.44220572664832,
+ 15.743938506781891,
+ 16.04908273684337,
+ 16.35764934889634,
+ 16.66964922287304,
+ 16.985093187232053,
+ 17.30399201960269,
+ 17.62635644741625,
+ 17.95219714852476,
+ 18.281524751807332,
+ 18.614349837764564,
+ 18.95068293910138,
+ 19.290534541298456,
+ 19.633915083172692,
+ 19.98083495742689,
+ 20.331304511189067,
+ 20.685334046541502,
+ 21.042933821039977,
+ 21.404114048223256,
+ 21.76888489811322,
+ 22.137256497705877,
+ 22.50923893145328,
+ 22.884842241736916,
+ 23.264076429332462,
+ 23.6469514538663,
+ 24.033477234264016,
+ 24.42366364919083,
+ 24.817520537484558,
+ 25.21505769858089,
+ 25.61628489293138,
+ 26.021211842414342,
+ 26.429848230738664,
+ 26.842203703840827,
+ 27.258287870275353,
+ 27.678110301598522,
+ 28.10168053274597,
+ 28.529008062403893,
+ 28.96010235337422,
+ 29.39497283293396,
+ 29.83362889318845,
+ 30.276079891419332,
+ 30.722335150426627,
+ 31.172403958865512,
+ 31.62629557157785,
+ 32.08401920991837,
+ 32.54558406207592,
+ 33.010999283389665,
+ 33.4802739966603,
+ 33.953417292456834,
+ 34.430438229418264,
+ 34.911345834551085,
+ 35.39614910352207,
+ 35.88485700094671,
+ 36.37747846067349,
+ 36.87402238606382,
+ 37.37449765026789,
+ 37.87891309649659,
+ 38.38727753828926,
+ 38.89959975977785,
+ 39.41588851594697,
+ 39.93615253289054,
+ 40.460400508064545,
+ 40.98864111053629,
+ 41.520882981230194,
+ 42.05713473317016,
+ 42.597404951718396,
+ 43.141702194811224,
+ 43.6900349931913,
+ 44.24241185063697,
+ 44.798841244188324,
+ 45.35933162437017,
+ 45.92389141541209,
+ 46.49252901546552,
+ 47.065252796817916,
+ 47.64207110610409,
+ 48.22299226451468,
+ 48.808024568002054,
+ 49.3971762874833,
+ 49.9904556690408,
+ 50.587870934119984,
+ 51.189430279724725,
+ 51.79514187861014,
+ 52.40501387947288,
+ 53.0190544071392,
+ 53.637271562750364,
+ 54.259673423945976,
+ 54.88626804504493,
+ 55.517063457223934,
+ 56.15206766869424,
+ 56.79128866487574,
+ 57.43473440856916,
+ 58.08241284012621,
+ 58.734331877617365,
+ 59.39049941699807,
+ 60.05092333227251,
+ 60.715611475655585,
+ 61.38457167773311,
+ 62.057811747619894,
+ 62.7353394731159,
+ 63.417162620860914,
+ 64.10328893648692,
+ 64.79372614476921,
+ 65.48848194977529,
+ 66.18756403501224,
+ 66.89098006357258,
+ 67.59873767827808,
+ 68.31084450182222,
+ 69.02730813691093,
+ 69.74813616640164,
+ 70.47333615344107,
+ 71.20291564160104,
+ 71.93688215501312,
+ 72.67524319850172,
+ 73.41800625771542,
+ 74.16517879925733,
+ 74.9167682708136,
+ 75.67278210128072,
+ 76.43322770089146,
+ 77.1981124613393,
+ 77.96744375590167,
+ 78.74122893956174,
+ 79.51947534912904,
+ 80.30219030335869,
+ 81.08938110306934,
+ 81.88105503125999,
+ 82.67721935322541,
+ 83.4778813166706,
+ 84.28304815182372,
+ 85.09272707154808,
+ 85.90692527145302,
+ 86.72564993000343,
+ 87.54890820862819,
+ 88.3767072518277,
+ 89.2090541872801,
+ 90.04595612594655,
+ 90.88742016217518,
+ 91.73345337380438,
+ 92.58406282226491,
+ 93.43925555268066,
+ 94.29903859396902,
+ 95.16341895893969,
+ 96.03240364439274,
+ 96.9059996312159,
+ 97.78421388448044,
+ 98.6670533535366,
+ 99.55452497210776,
+ };
+
+ /**
+ * Sanitizes a small enough angle in radians.
+ *
+ * @param angle An angle in radians; must not deviate too much from 0.
+ * @return A coterminal angle between 0 and 2pi.
+ */
+ static double sanitizeRadians(double angle) {
+ return (angle + Math.PI * 8) % (Math.PI * 2);
+ }
+
+ /**
+ * Delinearizes an RGB component, returning a floating-point number.
+ *
+ * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
+ * @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
+ */
+ static double trueDelinearized(double rgbComponent) {
+ double normalized = rgbComponent / 100.0;
+ double delinearized = 0.0;
+ if (normalized <= 0.0031308) {
+ delinearized = normalized * 12.92;
+ } else {
+ delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
+ }
+ return delinearized * 255.0;
+ }
+
+ static double chromaticAdaptation(double component) {
+ double af = Math.pow(Math.abs(component), 0.42);
+ return MathUtils.signum(component) * 400.0 * af / (af + 27.13);
+ }
+
+ /**
+ * Returns the hue of a linear RGB color in CAM16.
+ *
+ * @param linrgb The linear RGB coordinates of a color.
+ * @return The hue of the color in CAM16, in radians.
+ */
+ static double hueOf(double[] linrgb) {
+ double[] scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
+ double rA = chromaticAdaptation(scaledDiscount[0]);
+ double gA = chromaticAdaptation(scaledDiscount[1]);
+ double bA = chromaticAdaptation(scaledDiscount[2]);
+ // redness-greenness
+ double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
+ // yellowness-blueness
+ double b = (rA + gA - 2.0 * bA) / 9.0;
+ return Math.atan2(b, a);
+ }
+
+ static boolean areInCyclicOrder(double a, double b, double c) {
+ double deltaAB = sanitizeRadians(b - a);
+ double deltaAC = sanitizeRadians(c - a);
+ return deltaAB < deltaAC;
+ }
+
+ /**
+ * Solves the lerp equation.
+ *
+ * @param source The starting number.
+ * @param mid The number in the middle.
+ * @param target The ending number.
+ * @return A number t such that lerp(source, target, t) = mid.
+ */
+ static double intercept(double source, double mid, double target) {
+ return (mid - source) / (target - source);
+ }
+
+ static double[] lerpPoint(double[] source, double t, double[] target) {
+ return new double[] {
+ source[0] + (target[0] - source[0]) * t,
+ source[1] + (target[1] - source[1]) * t,
+ source[2] + (target[2] - source[2]) * t,
+ };
+ }
+
+ /**
+ * Intersects a segment with a plane.
+ *
+ * @param source The coordinates of point A.
+ * @param coordinate The R-, G-, or B-coordinate of the plane.
+ * @param target The coordinates of point B.
+ * @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
+ * @return The intersection point of the segment AB with the plane R=coordinate, G=coordinate, or
+ * B=coordinate
+ */
+ static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
+ double t = intercept(source[axis], coordinate, target[axis]);
+ return lerpPoint(source, t, target);
+ }
+
+ static boolean isBounded(double x) {
+ return 0.0 <= x && x <= 100.0;
+ }
+
+ /**
+ * Returns the nth possible vertex of the polygonal intersection.
+ *
+ * @param y The Y value of the plane.
+ * @param n The zero-based index of the point. 0 <= n <= 11.
+ * @return The nth possible vertex of the polygonal intersection of the y plane and the RGB cube,
+ * in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
+ * [-1.0, -1.0, -1.0] is returned.
+ */
+ static double[] nthVertex(double y, int n) {
+ double kR = Y_FROM_LINRGB[0];
+ double kG = Y_FROM_LINRGB[1];
+ double kB = Y_FROM_LINRGB[2];
+ double coordA = n % 4 <= 1 ? 0.0 : 100.0;
+ double coordB = n % 2 == 0 ? 0.0 : 100.0;
+ if (n < 4) {
+ double g = coordA;
+ double b = coordB;
+ double r = (y - g * kG - b * kB) / kR;
+ if (isBounded(r)) {
+ return new double[] {r, g, b};
+ } else {
+ return new double[] {-1.0, -1.0, -1.0};
+ }
+ } else if (n < 8) {
+ double b = coordA;
+ double r = coordB;
+ double g = (y - r * kR - b * kB) / kG;
+ if (isBounded(g)) {
+ return new double[] {r, g, b};
+ } else {
+ return new double[] {-1.0, -1.0, -1.0};
+ }
+ } else {
+ double r = coordA;
+ double g = coordB;
+ double b = (y - r * kR - g * kG) / kB;
+ if (isBounded(b)) {
+ return new double[] {r, g, b};
+ } else {
+ return new double[] {-1.0, -1.0, -1.0};
+ }
+ }
+ }
+
+ /**
+ * Finds the segment containing the desired color.
+ *
+ * @param y The Y value of the color.
+ * @param targetHue The hue of the color.
+ * @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of the
+ * segment containing the desired color.
+ */
+ static double[][] bisectToSegment(double y, double targetHue) {
+ double[] left = new double[] {-1.0, -1.0, -1.0};
+ double[] right = left;
+ double leftHue = 0.0;
+ double rightHue = 0.0;
+ boolean initialized = false;
+ boolean uncut = true;
+ for (int n = 0; n < 12; n++) {
+ double[] mid = nthVertex(y, n);
+ if (mid[0] < 0) {
+ continue;
+ }
+ double midHue = hueOf(mid);
+ if (!initialized) {
+ left = mid;
+ right = mid;
+ leftHue = midHue;
+ rightHue = midHue;
+ initialized = true;
+ continue;
+ }
+ if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
+ uncut = false;
+ if (areInCyclicOrder(leftHue, targetHue, midHue)) {
+ right = mid;
+ rightHue = midHue;
+ } else {
+ left = mid;
+ leftHue = midHue;
+ }
+ }
+ }
+ return new double[][] {left, right};
+ }
+
+ static double[] midpoint(double[] a, double[] b) {
+ return new double[] {
+ (a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2,
+ };
+ }
+
+ static int criticalPlaneBelow(double x) {
+ return (int) Math.floor(x - 0.5);
+ }
+
+ static int criticalPlaneAbove(double x) {
+ return (int) Math.ceil(x - 0.5);
+ }
+
+ /**
+ * Finds a color with the given Y and hue on the boundary of the cube.
+ *
+ * @param y The Y value of the color.
+ * @param targetHue The hue of the color.
+ * @return The desired color, in linear RGB coordinates.
+ */
+ static double[] bisectToLimit(double y, double targetHue) {
+ double[][] segment = bisectToSegment(y, targetHue);
+ double[] left = segment[0];
+ double leftHue = hueOf(left);
+ double[] right = segment[1];
+ for (int axis = 0; axis < 3; axis++) {
+ if (left[axis] != right[axis]) {
+ int lPlane = -1;
+ int rPlane = 255;
+ if (left[axis] < right[axis]) {
+ lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
+ rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
+ } else {
+ lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
+ rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
+ }
+ for (int i = 0; i < 8; i++) {
+ if (Math.abs(rPlane - lPlane) <= 1) {
+ break;
+ } else {
+ int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
+ double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
+ double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
+ double midHue = hueOf(mid);
+ if (areInCyclicOrder(leftHue, targetHue, midHue)) {
+ right = mid;
+ rPlane = mPlane;
+ } else {
+ left = mid;
+ leftHue = midHue;
+ lPlane = mPlane;
+ }
+ }
+ }
+ }
+ }
+ return midpoint(left, right);
+ }
+
+ static double inverseChromaticAdaptation(double adapted) {
+ double adaptedAbs = Math.abs(adapted);
+ double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
+ return MathUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
+ }
+
+ /**
+ * Finds a color with the given hue, chroma, and Y.
+ *
+ * @param hueRadians The desired hue in radians.
+ * @param chroma The desired chroma.
+ * @param y The desired Y.
+ * @return The desired color as a hexadecimal integer, if found; 0 otherwise.
+ */
+ static int findResultByJ(double hueRadians, double chroma, double y) {
+ // Initial estimate of j.
+ double j = Math.sqrt(y) * 11.0;
+ // ===========================================================
+ // Operations inlined from Cam16 to avoid repeated calculation
+ // ===========================================================
+ ViewingConditions viewingConditions = ViewingConditions.DEFAULT;
+ double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
+ double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
+ double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
+ double hSin = Math.sin(hueRadians);
+ double hCos = Math.cos(hueRadians);
+ for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
+ // ===========================================================
+ // Operations inlined from Cam16 to avoid repeated calculation
+ // ===========================================================
+ double jNormalized = j / 100.0;
+ double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
+ double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
+ double ac =
+ viewingConditions.getAw()
+ * Math.pow(jNormalized, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
+ double p2 = ac / viewingConditions.getNbb();
+ double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
+ double a = gamma * hCos;
+ double b = gamma * hSin;
+ double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
+ double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
+ double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
+ double rCScaled = inverseChromaticAdaptation(rA);
+ double gCScaled = inverseChromaticAdaptation(gA);
+ double bCScaled = inverseChromaticAdaptation(bA);
+ double[] linrgb =
+ MathUtils.matrixMultiply(
+ new double[] {rCScaled, gCScaled, bCScaled}, LINRGB_FROM_SCALED_DISCOUNT);
+ // ===========================================================
+ // Operations inlined from Cam16 to avoid repeated calculation
+ // ===========================================================
+ if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
+ return 0;
+ }
+ double kR = Y_FROM_LINRGB[0];
+ double kG = Y_FROM_LINRGB[1];
+ double kB = Y_FROM_LINRGB[2];
+ double fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
+ if (fnj <= 0) {
+ return 0;
+ }
+ if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
+ if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
+ return 0;
+ }
+ return ColorUtils.argbFromLinrgb(linrgb);
+ }
+ // Iterates with Newton method,
+ // Using 2 * fn(j) / j as the approximation of fn'(j)
+ j = j - (fnj - y) * j / (2 * fnj);
+ }
+ return 0;
+ }
+
+ /**
+ * Finds an sRGB color with the given hue, chroma, and L*, if possible.
+ *
+ * @param hueDegrees The desired hue, in degrees.
+ * @param chroma The desired chroma.
+ * @param lstar The desired L*.
+ * @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
+ * chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
+ * sufficiently close, and chroma will be maximized.
+ */
+ public static int solveToInt(double hueDegrees, double chroma, double lstar) {
+ if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
+ return ColorUtils.argbFromLstar(lstar);
+ }
+ hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees);
+ double hueRadians = hueDegrees / 180 * Math.PI;
+ double y = ColorUtils.yFromLstar(lstar);
+ int exactAnswer = findResultByJ(hueRadians, chroma, y);
+ if (exactAnswer != 0) {
+ return exactAnswer;
+ }
+ double[] linrgb = bisectToLimit(y, hueRadians);
+ return ColorUtils.argbFromLinrgb(linrgb);
+ }
+
+ /**
+ * Finds an sRGB color with the given hue, chroma, and L*, if possible.
+ *
+ * @param hueDegrees The desired hue, in degrees.
+ * @param chroma The desired chroma.
+ * @param lstar The desired L*.
+ * @return An CAM16 object representing the sRGB color. The color has sufficiently close hue,
+ * chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
+ * sufficiently close, and chroma will be maximized.
+ */
+ public static Cam16 solveToCam(double hueDegrees, double chroma, double lstar) {
+ return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar));
+ }
+}
+
diff --git a/material-color-utilities/src/main/java/hct/ViewingConditions.java b/material-color-utilities/src/main/java/hct/ViewingConditions.java
index e42dbe11..93a3a6bb 100644
--- a/material-color-utilities/src/main/java/hct/ViewingConditions.java
+++ b/material-color-utilities/src/main/java/hct/ViewingConditions.java
@@ -34,64 +34,64 @@ 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]
+ new double[] {
+ ColorUtils.whitePointD65()[0],
+ ColorUtils.whitePointD65()[1],
+ ColorUtils.whitePointD65()[2]
},
- (float) (200.0f / Math.PI * ColorUtils.yFromLstar(50.0f) / 100.f),
- 50.0f,
- 2.0f,
+ (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
+ 50.0,
+ 2.0,
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;
+ private final double aw;
+ private final double nbb;
+ private final double ncb;
+ private final double c;
+ private final double nc;
+ private final double n;
+ private final double[] rgbD;
+ private final double fl;
+ private final double flRoot;
+ private final double z;
- public float getAw() {
+ public double getAw() {
return aw;
}
- public float getN() {
+ public double getN() {
return n;
}
- public float getNbb() {
+ public double getNbb() {
return nbb;
}
- float getNcb() {
+ double getNcb() {
return ncb;
}
- float getC() {
+ double getC() {
return c;
}
- float getNc() {
+ double getNc() {
return nc;
}
- public float[] getRgbD() {
+ public double[] getRgbD() {
return rgbD;
}
- float getFl() {
+ double getFl() {
return fl;
}
- public float getFlRoot() {
+ public double getFlRoot() {
return flRoot;
}
- float getZ() {
+ double getZ() {
return z;
}
@@ -114,57 +114,56 @@ public final class ViewingConditions {
* perform this process on self-luminous objects like displays.
*/
static ViewingConditions make(
- float[] whitePoint,
- float adaptingLuminance,
- float backgroundLstar,
- float surround,
+ double[] whitePoint,
+ double adaptingLuminance,
+ double backgroundLstar,
+ double 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 =
+ double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
+ double[] xyz = whitePoint;
+ double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
+ double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
+ double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
+ double f = 0.8 + (surround / 10.0);
+ double c =
(f >= 0.9)
- ? (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 =
+ ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
+ : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
+ double d =
discountingIlluminant
- ? 1.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
+ ? 1.0
+ : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
+ d = MathUtils.clampDouble(0.0, 1.0, d);
+ double nc = f;
+ double[] rgbD =
+ new double[] {
+ d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d
};
- 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)
+ double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
+ double k4 = k * k * k * k;
+ double k4F = 1.0 - k4;
+ double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance));
+ double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
+ double z = 1.48 + Math.sqrt(n);
+ double nbb = 0.725 / Math.pow(n, 0.2);
+ double ncb = nbb;
+ double[] rgbAFactors =
+ new double[] {
+ Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
+ Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
+ Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
};
- 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)
+ double[] rgbA =
+ new double[] {
+ (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
+ (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
+ (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
};
- 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);
+ double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
+ return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
}
/**
@@ -174,16 +173,16 @@ public final class ViewingConditions {
* 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) {
+ double n,
+ double aw,
+ double nbb,
+ double ncb,
+ double c,
+ double nc,
+ double[] rgbD,
+ double fl,
+ double flRoot,
+ double z) {
this.n = n;
this.aw = aw;
this.nbb = nbb;
diff --git a/material-color-utilities/src/main/java/palettes/CorePalette.java b/material-color-utilities/src/main/java/palettes/CorePalette.java
index 32e35b79..9a47ee79 100644
--- a/material-color-utilities/src/main/java/palettes/CorePalette.java
+++ b/material-color-utilities/src/main/java/palettes/CorePalette.java
@@ -17,6 +17,7 @@
package palettes;
import static java.lang.Math.max;
+import static java.lang.Math.min;
import hct.Hct;
@@ -38,17 +39,35 @@ public final class CorePalette {
* @param argb ARGB representation of a color
*/
public static CorePalette of(int argb) {
- return new CorePalette(argb);
+ return new CorePalette(argb, false);
}
- private CorePalette(int argb) {
+ /**
+ * Create content key tones from a color.
+ *
+ * @param argb ARGB representation of a color
+ */
+ public static CorePalette contentOf(int argb) {
+ return new CorePalette(argb, true);
+ }
+
+ private CorePalette(int argb, boolean isContent) {
Hct hct = Hct.fromInt(argb);
- 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);
+ double hue = hct.getHue();
+ double chroma = hct.getChroma();
+ if (isContent) {
+ this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
+ this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
+ this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
+ this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.));
+ this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.));
+ } else {
+ this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma));
+ this.a2 = TonalPalette.fromHueAndChroma(hue, 16.);
+ this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.);
+ this.n1 = TonalPalette.fromHueAndChroma(hue, 4.);
+ this.n2 = TonalPalette.fromHueAndChroma(hue, 8.);
+ }
+ this.error = TonalPalette.fromHueAndChroma(25, 84.);
}
}
diff --git a/material-color-utilities/src/main/java/palettes/TonalPalette.java b/material-color-utilities/src/main/java/palettes/TonalPalette.java
index 06b24857..fdcdd2de 100644
--- a/material-color-utilities/src/main/java/palettes/TonalPalette.java
+++ b/material-color-utilities/src/main/java/palettes/TonalPalette.java
@@ -25,8 +25,8 @@ import java.util.Map;
*/
public final class TonalPalette {
Map For angles that are 180 degrees apart from each other, both directions have the same travel
+ * distance, so either direction is shortest. The value 1.0 is returned in this case.
+ *
+ * @param from The angle travel starts from, in degrees.
+ * @param to The angle travel ends at, in degrees.
+ * @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from leads
+ * to the shortest travel distance.
+ */
+ public static double rotationDirection(double from, double to) {
+ double increasingDifference = sanitizeDegreesDouble(to - from);
+ return increasingDifference <= 180.0 ? 1.0 : -1.0;
+ }
+
/** Distance of two points on a circle, represented using degrees. */
public static double differenceDegrees(double a, double b) {
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
@@ -114,6 +130,5 @@ public class MathUtils {
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
deleted file mode 100644
index 75aa5b1b..00000000
--- a/material-color-utilities/src/main/java/utils/StringUtils.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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);
- }
-}