Update material-color-utilities

This commit is contained in:
MM20 2022-10-15 12:07:22 +02:00
parent 44ea0acf9b
commit c5091ee4ca
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
10 changed files with 1052 additions and 481 deletions

View File

@ -37,58 +37,58 @@ import utils.ColorUtils;
*/ */
public final class Cam16 { public final class Cam16 {
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16. // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
static final float[][] XYZ_TO_CAM16RGB = { static final double[][] XYZ_TO_CAM16RGB = {
{0.401288f, 0.650173f, -0.051461f}, {0.401288, 0.650173, -0.051461},
{-0.250268f, 1.204414f, 0.045854f}, {-0.250268, 1.204414, 0.045854},
{-0.002079f, 0.048952f, 0.953127f} {-0.002079, 0.048952, 0.953127}
}; };
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
static final float[][] CAM16RGB_TO_XYZ = { static final double[][] CAM16RGB_TO_XYZ = {
{1.8620678f, -1.0112547f, 0.14918678f}, {1.8620678, -1.0112547, 0.14918678},
{0.38752654f, 0.62144744f, -0.00897398f}, {0.38752654, 0.62144744, -0.00897398},
{-0.01584150f, -0.03412294f, 1.0499644f} {-0.01584150, -0.03412294, 1.0499644}
}; };
// CAM16 color dimensions, see getters for documentation. // CAM16 color dimensions, see getters for documentation.
private final float hue; private final double hue;
private final float chroma; private final double chroma;
private final float j; private final double j;
private final float q; private final double q;
private final float m; private final double m;
private final float s; private final double s;
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*. // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
private final float jstar; private final double jstar;
private final float astar; private final double astar;
private final float bstar; private final double bstar;
/** /**
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, * 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 * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
* distances between colors. * distances between colors.
*/ */
float distance(Cam16 other) { double distance(Cam16 other) {
float dJ = getJStar() - other.getJStar(); double dJ = getJstar() - other.getJstar();
float dA = getAStar() - other.getAStar(); double dA = getAstar() - other.getAstar();
float dB = getBStar() - other.getBStar(); double dB = getBstar() - other.getBstar();
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
double dE = 1.41 * Math.pow(dEPrime, 0.63); double dE = 1.41 * Math.pow(dEPrime, 0.63);
return (float) dE; return dE;
} }
/** Hue in CAM16 */ /** Hue in CAM16 */
public float getHue() { public double getHue() {
return hue; return hue;
} }
/** Chroma in CAM16 */ /** Chroma in CAM16 */
public float getChroma() { public double getChroma() {
return chroma; return chroma;
} }
/** Lightness in CAM16 */ /** Lightness in CAM16 */
public float getJ() { public double getJ() {
return j; 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 * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
* lighting. * lighting.
*/ */
public float getQ() { public double getQ() {
return q; return q;
} }
@ -109,7 +109,7 @@ public final class Cam16 {
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much * <p>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. * more colorful outside than inside, but it has the same chroma in both environments.
*/ */
public float getM() { public double getM() {
return m; return m;
} }
@ -119,22 +119,22 @@ public final class Cam16 {
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness * <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
* relative to the color's own brightness, where chroma is colorfulness relative to white. * relative to the color's own brightness, where chroma is colorfulness relative to white.
*/ */
public float getS() { public double getS() {
return s; return s;
} }
/** Lightness coordinate in CAM16-UCS */ /** Lightness coordinate in CAM16-UCS */
public float getJStar() { public double getJstar() {
return jstar; return jstar;
} }
/** a* coordinate in CAM16-UCS */ /** a* coordinate in CAM16-UCS */
public float getAStar() { public double getAstar() {
return astar; return astar;
} }
/** b* coordinate in CAM16-UCS */ /** b* coordinate in CAM16-UCS */
public float getBStar() { public double getBstar() {
return bstar; return bstar;
} }
@ -156,15 +156,15 @@ public final class Cam16 {
* @param bstar CAM16-UCS b coordinate * @param bstar CAM16-UCS b coordinate
*/ */
private Cam16( private Cam16(
float hue, double hue,
float chroma, double chroma,
float j, double j,
float q, double q,
float m, double m,
float s, double s,
float jstar, double jstar,
float astar, double astar,
float bstar) { double bstar) {
this.hue = hue; this.hue = hue;
this.chroma = chroma; this.chroma = chroma;
this.j = j; this.j = j;
@ -200,88 +200,84 @@ public final class Cam16 {
int red = (argb & 0x00ff0000) >> 16; int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8; int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff); int blue = (argb & 0x000000ff);
float redL = (float) ColorUtils.linearized(red); double redL = ColorUtils.linearized(red);
float greenL = (float) ColorUtils.linearized(green); double greenL = ColorUtils.linearized(green);
float blueL = (float) ColorUtils.linearized(blue); double blueL = ColorUtils.linearized(blue);
float x = 0.41233895f * redL + 0.35762064f * greenL + 0.18051042f * blueL; double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
float y = 0.2126f * redL + 0.7152f * greenL + 0.0722f * blueL; double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
float z = 0.01932141f * redL + 0.11916382f * greenL + 0.95034478f * blueL; double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
// Transform XYZ to 'cone'/'rgb' responses // Transform XYZ to 'cone'/'rgb' responses
float[][] matrix = XYZ_TO_CAM16RGB; double[][] matrix = XYZ_TO_CAM16RGB;
float rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]); double 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]); double 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 bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
// Discount illuminant // Discount illuminant
float rD = viewingConditions.getRgbD()[0] * rT; double rD = viewingConditions.getRgbD()[0] * rT;
float gD = viewingConditions.getRgbD()[1] * gT; double gD = viewingConditions.getRgbD()[1] * gT;
float bD = viewingConditions.getRgbD()[2] * bT; double bD = viewingConditions.getRgbD()[2] * bT;
// Chromatic adaptation // Chromatic adaptation
float rAF = (float) Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42); double rAF = 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); double gAF = 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); double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
float rA = Math.signum(rD) * 400.0f * rAF / (rAF + 27.13f); double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
float gA = Math.signum(gD) * 400.0f * gAF / (gAF + 27.13f); double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
float bA = Math.signum(bD) * 400.0f * bAF / (bAF + 27.13f); double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
// redness-greenness // 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 // yellowness-blueness
float b = (float) (rA + gA - 2.0 * bA) / 9.0f; double b = (rA + gA - 2.0 * bA) / 9.0;
// auxiliary components // auxiliary components
float u = (20.0f * rA + 20.0f * gA + 21.0f * bA) / 20.0f; double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
float p2 = (40.0f * rA + 20.0f * gA + bA) / 20.0f; double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
// hue // hue
float atan2 = (float) Math.atan2(b, a); double atan2 = Math.atan2(b, a);
float atanDegrees = atan2 * 180.0f / (float) Math.PI; double atanDegrees = Math.toDegrees(atan2);
float hue = double hue =
atanDegrees < 0 atanDegrees < 0
? atanDegrees + 360.0f ? atanDegrees + 360.0
: atanDegrees >= 360 ? atanDegrees - 360.0f : atanDegrees; : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
float hueRadians = hue * (float) Math.PI / 180.0f; double hueRadians = Math.toRadians(hue);
// achromatic response to color // achromatic response to color
float ac = p2 * viewingConditions.getNbb(); double ac = p2 * viewingConditions.getNbb();
// CAM16 lightness and brightness // CAM16 lightness and brightness
float j = double j =
100.0f 100.0
* (float) * Math.pow(
Math.pow( ac / viewingConditions.getAw(),
ac / viewingConditions.getAw(), viewingConditions.getC() * viewingConditions.getZ());
viewingConditions.getC() * viewingConditions.getZ()); double q =
float q = 4.0
4.0f
/ viewingConditions.getC() / viewingConditions.getC()
* (float) Math.sqrt(j / 100.0f) * Math.sqrt(j / 100.0)
* (viewingConditions.getAw() + 4.0f) * (viewingConditions.getAw() + 4.0)
* viewingConditions.getFlRoot(); * viewingConditions.getFlRoot();
// CAM16 chroma, colorfulness, and saturation. // CAM16 chroma, colorfulness, and saturation.
float huePrime = (hue < 20.14) ? hue + 360 : hue; double huePrime = (hue < 20.14) ? hue + 360 : hue;
float eHue = 0.25f * (float) (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8); double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
float p1 = 50000.0f / 13.0f * eHue * viewingConditions.getNc() * viewingConditions.getNcb(); double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
float t = p1 * (float) Math.hypot(a, b) / (u + 0.305f); double t = p1 * Math.hypot(a, b) / (u + 0.305);
float alpha = double alpha =
(float) Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
* (float) Math.pow(t, 0.9);
// CAM16 chroma, colorfulness, saturation // CAM16 chroma, colorfulness, saturation
float c = alpha * (float) Math.sqrt(j / 100.0); double c = alpha * Math.sqrt(j / 100.0);
float m = c * viewingConditions.getFlRoot(); double m = c * viewingConditions.getFlRoot();
float s = double s =
50.0f 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
* (float)
Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f));
// CAM16-UCS components // CAM16-UCS components
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j); double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228f * m); double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
float astar = mstar * (float) Math.cos(hueRadians); double astar = mstar * Math.cos(hueRadians);
float bstar = mstar * (float) Math.sin(hueRadians); double bstar = mstar * Math.sin(hueRadians);
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar); 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 c CAM16 chroma
* @param h CAM16 hue * @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); 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. * @param viewingConditions Information about the environment where the color was observed.
*/ */
private static Cam16 fromJchInViewingConditions( private static Cam16 fromJchInViewingConditions(
float j, float c, float h, ViewingConditions viewingConditions) { double j, double c, double h, ViewingConditions viewingConditions) {
float q = double q =
4.0f 4.0
/ viewingConditions.getC() / viewingConditions.getC()
* (float) Math.sqrt(j / 100.0) * Math.sqrt(j / 100.0)
* (viewingConditions.getAw() + 4.0f) * (viewingConditions.getAw() + 4.0)
* viewingConditions.getFlRoot(); * viewingConditions.getFlRoot();
float m = c * viewingConditions.getFlRoot(); double m = c * viewingConditions.getFlRoot();
float alpha = c / (float) Math.sqrt(j / 100.0); double alpha = c / Math.sqrt(j / 100.0);
float s = double s =
50.0f 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
* (float)
Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0f));
float hueRadians = h * (float) Math.PI / 180.0f; double hueRadians = Math.toRadians(h);
float jstar = (1.0f + 100.0f * 0.007f) * j / (1.0f + 0.007f * j); double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
float mstar = 1.0f / 0.0228f * (float) Math.log1p(0.0228 * m); double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
float astar = mstar * (float) Math.cos(hueRadians); double astar = mstar * Math.cos(hueRadians);
float bstar = mstar * (float) Math.sin(hueRadians); double bstar = mstar * Math.sin(hueRadians);
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar); 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 * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
* axis. * 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); 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. * @param viewingConditions Information about the environment where the color was observed.
*/ */
public static Cam16 fromUcsInViewingConditions( 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 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 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) { if (h < 0.0) {
h += 360.0f; h += 360.0;
} }
float j = jstar / (1f - (jstar - 100f) * 0.007f); double j = jstar / (1. - (jstar - 100.) * 0.007);
return fromJchInViewingConditions(j, (float) c, (float) h, viewingConditions); return fromJchInViewingConditions(j, c, h, viewingConditions);
} }
/** /**
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions, * 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. * which are near-identical to the default viewing conditions for sRGB.
*/ */
public int getInt() { public int toInt() {
return viewed(ViewingConditions.DEFAULT); return viewed(ViewingConditions.DEFAULT);
} }
@ -377,58 +371,48 @@ public final class Cam16 {
* @return ARGB representation of color * @return ARGB representation of color
*/ */
int viewed(ViewingConditions viewingConditions) { int viewed(ViewingConditions viewingConditions) {
float alpha = double alpha =
(getChroma() == 0.0 || getJ() == 0.0) (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
? 0.0f
: getChroma() / (float) Math.sqrt(getJ() / 100.0);
float t = double t =
(float) Math.pow(
Math.pow( alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9); double hRad = Math.toRadians(getHue());
float hRad = getHue() * (float) Math.PI / 180.0f;
float eHue = 0.25f * (float) (Math.cos(hRad + 2.0) + 3.8); double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
float ac = double ac =
viewingConditions.getAw() viewingConditions.getAw()
* (float) * Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ()); double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
float p1 = eHue * (50000.0f / 13.0f) * viewingConditions.getNc() * viewingConditions.getNcb(); double p2 = (ac / viewingConditions.getNbb());
float p2 = (ac / viewingConditions.getNbb());
float hSin = (float) Math.sin(hRad); double hSin = Math.sin(hRad);
float hCos = (float) Math.cos(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); double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
float a = gamma * hCos; double a = gamma * hCos;
float b = gamma * hSin; double b = gamma * hSin;
float rA = (460.0f * p2 + 451.0f * a + 288.0f * b) / 1403.0f; double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
float gA = (460.0f * p2 - 891.0f * a - 261.0f * b) / 1403.0f; double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
float bA = (460.0f * p2 - 220.0f * a - 6300.0f * b) / 1403.0f; 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))); double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
float rC = double rC =
Math.signum(rA) Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
* (100.0f / viewingConditions.getFl()) double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
* (float) Math.pow(rCBase, 1.0 / 0.42); double gC =
float gCBase = (float) max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA))); Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
float gC = double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
Math.signum(gA) double bC =
* (100.0f / viewingConditions.getFl()) Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
* (float) Math.pow(gCBase, 1.0 / 0.42); double rF = rC / viewingConditions.getRgbD()[0];
float bCBase = (float) max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA))); double gF = gC / viewingConditions.getRgbD()[1];
float bC = double bF = bC / viewingConditions.getRgbD()[2];
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; double[][] matrix = CAM16RGB_TO_XYZ;
float x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]); double 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]); double 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 z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
return ColorUtils.argbFromXyz(x, y, z); return ColorUtils.argbFromXyz(x, y, z);
} }

View File

@ -17,7 +17,6 @@
package hct; package hct;
import utils.ColorUtils; import utils.ColorUtils;
import utils.MathUtils;
/** /**
* A color system built using CAM16 hue and chroma, and L* from L*a*b*. * A color system built using CAM16 hue and chroma, and L* from L*a*b*.
@ -39,9 +38,10 @@ import utils.MathUtils;
* lighting environments. * lighting environments.
*/ */
public final class Hct { public final class Hct {
private float hue; private double hue;
private float chroma; private double chroma;
private float tone; private double tone;
private int argb;
/** /**
* Create an HCT color from hue, chroma, and tone. * 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. * @param tone 0 <= tone <= 100; invalid values are corrected.
* @return HCT representation of a color in default viewing conditions. * @return HCT representation of a color in default viewing conditions.
*/ */
public static Hct from(float hue, float chroma, float tone) { public static Hct from(double hue, double chroma, double tone) {
return new Hct(hue, chroma, 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 * @return HCT representation of a color in default viewing conditions
*/ */
public static Hct fromInt(int argb) { public static Hct fromInt(int argb) {
Cam16 cam = Cam16.fromInt(argb); return new Hct(argb);
return new Hct(cam.getHue(), cam.getChroma(), (float) ColorUtils.lstarFromArgb(argb));
} }
private Hct(float hue, float chroma, float tone) { private Hct(int argb) {
setInternalState(gamutMap(hue, chroma, tone)); setInternalState(argb);
} }
public float getHue() { public double getHue() {
return hue; return hue;
} }
public float getChroma() { public double getChroma() {
return chroma; return chroma;
} }
public float getTone() { public double getTone() {
return tone; return tone;
} }
public int toInt() { 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. * @param newHue 0 <= newHue < 360; invalid values are corrected.
*/ */
public void setHue(float newHue) { public void setHue(double newHue) {
setInternalState(gamutMap((float) MathUtils.sanitizeDegreesDouble(newHue), chroma, tone)); setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
} }
/** /**
@ -103,8 +103,8 @@ public final class Hct {
* *
* @param newChroma 0 <= newChroma < ? * @param newChroma 0 <= newChroma < ?
*/ */
public void setChroma(float newChroma) { public void setChroma(double newChroma) {
setInternalState(gamutMap(hue, newChroma, tone)); setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
} }
/** /**
@ -113,150 +113,15 @@ public final class Hct {
* *
* @param newTone 0 <= newTone <= 100; invalid valids are corrected. * @param newTone 0 <= newTone <= 100; invalid valids are corrected.
*/ */
public void setTone(float newTone) { public void setTone(double newTone) {
setInternalState(gamutMap(hue, chroma, newTone)); setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
} }
private void setInternalState(int argb) { private void setInternalState(int argb) {
this.argb = argb;
Cam16 cam = Cam16.fromInt(argb); Cam16 cam = Cam16.fromInt(argb);
float tone = (float) ColorUtils.lstarFromArgb(argb);
hue = cam.getHue(); hue = cam.getHue();
chroma = cam.getChroma(); chroma = cam.getChroma();
this.tone = tone; this.tone = ColorUtils.lstarFromArgb(argb);
}
/**
* 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;
} }
} }

View File

@ -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));
}
}

View File

@ -34,64 +34,64 @@ public final class ViewingConditions {
/** sRGB-like viewing conditions. */ /** sRGB-like viewing conditions. */
public static final ViewingConditions DEFAULT = public static final ViewingConditions DEFAULT =
ViewingConditions.make( ViewingConditions.make(
new float[] { new double[] {
(float) ColorUtils.whitePointD65()[0], ColorUtils.whitePointD65()[0],
(float) ColorUtils.whitePointD65()[1], ColorUtils.whitePointD65()[1],
(float) ColorUtils.whitePointD65()[2] ColorUtils.whitePointD65()[2]
}, },
(float) (200.0f / Math.PI * ColorUtils.yFromLstar(50.0f) / 100.f), (200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
50.0f, 50.0,
2.0f, 2.0,
false); false);
private final float aw; private final double aw;
private final float nbb; private final double nbb;
private final float ncb; private final double ncb;
private final float c; private final double c;
private final float nc; private final double nc;
private final float n; private final double n;
private final float[] rgbD; private final double[] rgbD;
private final float fl; private final double fl;
private final float flRoot; private final double flRoot;
private final float z; private final double z;
public float getAw() { public double getAw() {
return aw; return aw;
} }
public float getN() { public double getN() {
return n; return n;
} }
public float getNbb() { public double getNbb() {
return nbb; return nbb;
} }
float getNcb() { double getNcb() {
return ncb; return ncb;
} }
float getC() { double getC() {
return c; return c;
} }
float getNc() { double getNc() {
return nc; return nc;
} }
public float[] getRgbD() { public double[] getRgbD() {
return rgbD; return rgbD;
} }
float getFl() { double getFl() {
return fl; return fl;
} }
public float getFlRoot() { public double getFlRoot() {
return flRoot; return flRoot;
} }
float getZ() { double getZ() {
return z; return z;
} }
@ -114,57 +114,56 @@ public final class ViewingConditions {
* perform this process on self-luminous objects like displays. * perform this process on self-luminous objects like displays.
*/ */
static ViewingConditions make( static ViewingConditions make(
float[] whitePoint, double[] whitePoint,
float adaptingLuminance, double adaptingLuminance,
float backgroundLstar, double backgroundLstar,
float surround, double surround,
boolean discountingIlluminant) { boolean discountingIlluminant) {
// Transform white point XYZ to 'cone'/'rgb' responses // Transform white point XYZ to 'cone'/'rgb' responses
float[][] matrix = Cam16.XYZ_TO_CAM16RGB; double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
float[] xyz = whitePoint; double[] xyz = whitePoint;
float rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]); double 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]); double 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]); double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
float f = 0.8f + (surround / 10.0f); double f = 0.8 + (surround / 10.0);
float c = double c =
(f >= 0.9) (f >= 0.9)
? (float) MathUtils.lerp(0.59f, 0.69f, ((f - 0.9f) * 10.0f)) ? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
: (float) MathUtils.lerp(0.525f, 0.59f, ((f - 0.8f) * 10.0f)); : MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
float d = double d =
discountingIlluminant discountingIlluminant
? 1.0f ? 1.0
: f * (1.0f - ((1.0f / 3.6f) * (float) Math.exp((-adaptingLuminance - 42.0f) / 92.0f))); : f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
d = (d > 1.0) ? 1.0f : (d < 0.0) ? 0.0f : d; d = MathUtils.clampDouble(0.0, 1.0, d);
float nc = f; double nc = f;
float[] rgbD = double[] rgbD =
new float[] { new double[] {
d * (100.0f / rW) + 1.0f - d, d * (100.0f / gW) + 1.0f - d, d * (100.0f / bW) + 1.0f - d 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); double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
float k4 = k * k * k * k; double k4 = k * k * k * k;
float k4F = 1.0f - k4; double k4F = 1.0 - k4;
float fl = double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance));
(k4 * adaptingLuminance) + (0.1f * k4F * k4F * (float) Math.cbrt(5.0 * adaptingLuminance)); double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
float n = (float) (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]); double z = 1.48 + Math.sqrt(n);
float z = 1.48f + (float) Math.sqrt(n); double nbb = 0.725 / Math.pow(n, 0.2);
float nbb = 0.725f / (float) Math.pow(n, 0.2); double ncb = nbb;
float ncb = nbb; double[] rgbAFactors =
float[] rgbAFactors = new double[] {
new float[] { Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
(float) Math.pow(fl * rgbD[0] * rW / 100.0, 0.42), Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
(float) Math.pow(fl * rgbD[1] * gW / 100.0, 0.42), Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
(float) Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
}; };
float[] rgbA = double[] rgbA =
new float[] { new double[] {
(400.0f * rgbAFactors[0]) / (rgbAFactors[0] + 27.13f), (400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
(400.0f * rgbAFactors[1]) / (rgbAFactors[1] + 27.13f), (400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
(400.0f * rgbAFactors[2]) / (rgbAFactors[2] + 27.13f) (400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
}; };
float aw = ((2.0f * rgbA[0]) + rgbA[1] + (0.05f * rgbA[2])) * nbb; double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, (float) Math.pow(fl, 0.25), z); 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. * requires a color science textbook, such as Fairchild's Color Appearance Models.
*/ */
private ViewingConditions( private ViewingConditions(
float n, double n,
float aw, double aw,
float nbb, double nbb,
float ncb, double ncb,
float c, double c,
float nc, double nc,
float[] rgbD, double[] rgbD,
float fl, double fl,
float flRoot, double flRoot,
float z) { double z) {
this.n = n; this.n = n;
this.aw = aw; this.aw = aw;
this.nbb = nbb; this.nbb = nbb;

View File

@ -17,6 +17,7 @@
package palettes; package palettes;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import hct.Hct; import hct.Hct;
@ -38,17 +39,35 @@ public final class CorePalette {
* @param argb ARGB representation of a color * @param argb ARGB representation of a color
*/ */
public static CorePalette of(int argb) { 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); Hct hct = Hct.fromInt(argb);
float hue = hct.getHue(); double hue = hct.getHue();
this.a1 = TonalPalette.fromHueAndChroma(hue, max(48f, hct.getChroma())); double chroma = hct.getChroma();
this.a2 = TonalPalette.fromHueAndChroma(hue, 16f); if (isContent) {
this.a3 = TonalPalette.fromHueAndChroma(hue + 60f, 24f); this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
this.n1 = TonalPalette.fromHueAndChroma(hue, 4f); this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
this.n2 = TonalPalette.fromHueAndChroma(hue, 8f); this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
this.error = TonalPalette.fromHueAndChroma(25, 84f); 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.);
} }
} }

View File

@ -25,8 +25,8 @@ import java.util.Map;
*/ */
public final class TonalPalette { public final class TonalPalette {
Map<Integer, Integer> cache; Map<Integer, Integer> cache;
float hue; double hue;
float chroma; double chroma;
/** /**
* Create tones using the HCT hue and chroma from a color. * Create tones using the HCT hue and chroma from a color.
@ -46,11 +46,11 @@ public final class TonalPalette {
* @param chroma HCT chroma * @param chroma HCT chroma
* @return Tones matching hue and chroma. * @return Tones matching hue and chroma.
*/ */
public static final TonalPalette fromHueAndChroma(float hue, float chroma) { public static final TonalPalette fromHueAndChroma(double hue, double chroma) {
return new TonalPalette(hue, chroma); return new TonalPalette(hue, chroma);
} }
private TonalPalette(float hue, float chroma) { private TonalPalette(double hue, double chroma) {
cache = new HashMap<>(); cache = new HashMap<>();
this.hue = hue; this.hue = hue;
this.chroma = chroma; this.chroma = chroma;

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
// This file is automatically generated. Do not modify it.
package scheme; package scheme;
import palettes.CorePalette; import palettes.CorePalette;
@ -43,7 +45,9 @@ public class Scheme {
private int surfaceVariant; private int surfaceVariant;
private int onSurfaceVariant; private int onSurfaceVariant;
private int outline; private int outline;
private int outlineVariant;
private int shadow; private int shadow;
private int scrim;
private int inverseSurface; private int inverseSurface;
private int inverseOnSurface; private int inverseOnSurface;
private int inversePrimary; private int inversePrimary;
@ -74,7 +78,9 @@ public class Scheme {
int surfaceVariant, int surfaceVariant,
int onSurfaceVariant, int onSurfaceVariant,
int outline, int outline,
int outlineVariant,
int shadow, int shadow,
int scrim,
int inverseSurface, int inverseSurface,
int inverseOnSurface, int inverseOnSurface,
int inversePrimary) { int inversePrimary) {
@ -102,14 +108,31 @@ public class Scheme {
this.surfaceVariant = surfaceVariant; this.surfaceVariant = surfaceVariant;
this.onSurfaceVariant = onSurfaceVariant; this.onSurfaceVariant = onSurfaceVariant;
this.outline = outline; this.outline = outline;
this.outlineVariant = outlineVariant;
this.shadow = shadow; this.shadow = shadow;
this.scrim = scrim;
this.inverseSurface = inverseSurface; this.inverseSurface = inverseSurface;
this.inverseOnSurface = inverseOnSurface; this.inverseOnSurface = inverseOnSurface;
this.inversePrimary = inversePrimary; this.inversePrimary = inversePrimary;
} }
public static Scheme light(int argb) { public static Scheme light(int argb) {
CorePalette core = CorePalette.of(argb); return lightFromCorePalette(CorePalette.of(argb));
}
public static Scheme dark(int argb) {
return darkFromCorePalette(CorePalette.of(argb));
}
public static Scheme lightContent(int argb) {
return lightFromCorePalette(CorePalette.contentOf(argb));
}
public static Scheme darkContent(int argb) {
return darkFromCorePalette(CorePalette.contentOf(argb));
}
private static Scheme lightFromCorePalette(CorePalette core) {
return new Scheme() return new Scheme()
.withPrimary(core.a1.tone(40)) .withPrimary(core.a1.tone(40))
.withOnPrimary(core.a1.tone(100)) .withOnPrimary(core.a1.tone(100))
@ -134,14 +157,15 @@ public class Scheme {
.withSurfaceVariant(core.n2.tone(90)) .withSurfaceVariant(core.n2.tone(90))
.withOnSurfaceVariant(core.n2.tone(30)) .withOnSurfaceVariant(core.n2.tone(30))
.withOutline(core.n2.tone(50)) .withOutline(core.n2.tone(50))
.withOutlineVariant(core.n2.tone(80))
.withShadow(core.n1.tone(0)) .withShadow(core.n1.tone(0))
.withScrim(core.n1.tone(0))
.withInverseSurface(core.n1.tone(20)) .withInverseSurface(core.n1.tone(20))
.withInverseOnSurface(core.n1.tone(95)) .withInverseOnSurface(core.n1.tone(95))
.withInversePrimary(core.a1.tone(80)); .withInversePrimary(core.a1.tone(80));
} }
public static Scheme dark(int argb) { private static Scheme darkFromCorePalette(CorePalette core) {
CorePalette core = CorePalette.of(argb);
return new Scheme() return new Scheme()
.withPrimary(core.a1.tone(80)) .withPrimary(core.a1.tone(80))
.withOnPrimary(core.a1.tone(20)) .withOnPrimary(core.a1.tone(20))
@ -166,7 +190,9 @@ public class Scheme {
.withSurfaceVariant(core.n2.tone(30)) .withSurfaceVariant(core.n2.tone(30))
.withOnSurfaceVariant(core.n2.tone(80)) .withOnSurfaceVariant(core.n2.tone(80))
.withOutline(core.n2.tone(60)) .withOutline(core.n2.tone(60))
.withOutlineVariant(core.n2.tone(30))
.withShadow(core.n1.tone(0)) .withShadow(core.n1.tone(0))
.withScrim(core.n1.tone(0))
.withInverseSurface(core.n1.tone(90)) .withInverseSurface(core.n1.tone(90))
.withInverseOnSurface(core.n1.tone(20)) .withInverseOnSurface(core.n1.tone(20))
.withInversePrimary(core.a1.tone(40)); .withInversePrimary(core.a1.tone(40));
@ -471,6 +497,19 @@ public class Scheme {
return this; return this;
} }
public int getOutlineVariant() {
return outlineVariant;
}
public void setOutlineVariant(int outlineVariant) {
this.outlineVariant = outlineVariant;
}
public Scheme withOutlineVariant(int outlineVariant) {
this.outlineVariant = outlineVariant;
return this;
}
public int getShadow() { public int getShadow() {
return shadow; return shadow;
} }
@ -484,6 +523,19 @@ public class Scheme {
return this; return this;
} }
public int getScrim() {
return scrim;
}
public void setScrim(int scrim) {
this.scrim = scrim;
}
public Scheme withScrim(int scrim) {
this.scrim = scrim;
return this;
}
public int getInverseSurface() { public int getInverseSurface() {
return inverseSurface; return inverseSurface;
} }
@ -572,8 +624,12 @@ public class Scheme {
+ onSurfaceVariant + onSurfaceVariant
+ ", outline=" + ", outline="
+ outline + outline
+ ", outlineVariant="
+ outlineVariant
+ ", shadow=" + ", shadow="
+ shadow + shadow
+ ", scrim="
+ scrim
+ ", inverseSurface=" + ", inverseSurface="
+ inverseSurface + inverseSurface
+ ", inverseOnSurface=" + ", inverseOnSurface="
@ -666,9 +722,15 @@ public class Scheme {
if (outline != scheme.outline) { if (outline != scheme.outline) {
return false; return false;
} }
if (outlineVariant != scheme.outlineVariant) {
return false;
}
if (shadow != scheme.shadow) { if (shadow != scheme.shadow) {
return false; return false;
} }
if (scrim != scheme.scrim) {
return false;
}
if (inverseSurface != scheme.inverseSurface) { if (inverseSurface != scheme.inverseSurface) {
return false; return false;
} }
@ -708,7 +770,9 @@ public class Scheme {
result = 31 * result + surfaceVariant; result = 31 * result + surfaceVariant;
result = 31 * result + onSurfaceVariant; result = 31 * result + onSurfaceVariant;
result = 31 * result + outline; result = 31 * result + outline;
result = 31 * result + outlineVariant;
result = 31 * result + shadow; result = 31 * result + shadow;
result = 31 * result + scrim;
result = 31 * result + inverseSurface; result = 31 * result + inverseSurface;
result = 31 * result + inverseOnSurface; result = 31 * result + inverseOnSurface;
result = 31 * result + inversePrimary; result = 31 * result + inversePrimary;

View File

@ -54,6 +54,14 @@ public class ColorUtils {
return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255); return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
} }
/** Converts a color from linear RGB components to ARGB format. */
public static int argbFromLinrgb(double[] linrgb) {
int r = delinearized(linrgb[0]);
int g = delinearized(linrgb[1]);
int b = delinearized(linrgb[2]);
return argbFromRgb(r, g, b);
}
/** Returns the alpha component of a color in ARGB format. */ /** Returns the alpha component of a color in ARGB format. */
public static int alphaFromArgb(int argb) { public static int alphaFromArgb(int argb) {
return (argb >> 24) & 255; return (argb >> 24) & 255;
@ -148,18 +156,9 @@ public class ColorUtils {
* @return ARGB representation of grayscale color with lightness matching L* * @return ARGB representation of grayscale color with lightness matching L*
*/ */
public static int argbFromLstar(double lstar) { public static int argbFromLstar(double lstar) {
double fy = (lstar + 16.0) / 116.0; double y = yFromLstar(lstar);
double fz = fy; int component = delinearized(y);
double fx = fy; return argbFromRgb(component, component, component);
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]);
} }
/** /**
@ -169,14 +168,8 @@ public class ColorUtils {
* @return L*, from L*a*b*, coordinate of the color * @return L*, from L*a*b*, coordinate of the color
*/ */
public static double lstarFromArgb(int argb) { public static double lstarFromArgb(int argb) {
double y = xyzFromArgb(argb)[1] / 100.0; double y = xyzFromArgb(argb)[1];
double e = 216.0 / 24389.0; return 116.0 * labF(y / 100.0) - 16.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;
}
} }
/** /**
@ -191,12 +184,7 @@ public class ColorUtils {
* @return Y in XYZ * @return Y in XYZ
*/ */
public static double yFromLstar(double lstar) { public static double yFromLstar(double lstar) {
double ke = 8.0; return 100.0 * labInvf((lstar + 16.0) / 116.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;
}
} }
/** /**
@ -260,6 +248,5 @@ public class ColorUtils {
return (116 * ft - 16) / kappa; return (116 * ft - 16) / kappa;
} }
} }
} }

View File

@ -102,6 +102,22 @@ public class MathUtils {
return degrees; return degrees;
} }
/**
* Sign of direction change needed to travel from one angle to another.
*
* <p>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. */ /** Distance of two points on a circle, represented using degrees. */
public static double differenceDegrees(double a, double b) { public static double differenceDegrees(double a, double b) {
return 180.0 - Math.abs(Math.abs(a - b) - 180.0); 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]; double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
return new double[] {a, b, c}; return new double[] {a, b, c};
} }
} }

View File

@ -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);
}
}