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 {
// 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 {
* <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.
*/
public float getM() {
public double getM() {
return m;
}
@ -119,22 +119,22 @@ public final class Cam16 {
* <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.
*/
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);
}

View File

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

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. */
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;

View File

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

View File

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

View File

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

View File

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

View File

@ -102,6 +102,22 @@ public class MathUtils {
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. */
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};
}
}

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