Update material-color-utilities

This commit is contained in:
MM20 2023-06-28 18:57:37 +02:00
parent 3f1a04ad81
commit 4841fcfc69
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 221 additions and 78 deletions

View File

@ -25,66 +25,67 @@ import utils.MathUtils;
/** Functions for blending in HCT and CAM16. */ /** Functions for blending in HCT and CAM16. */
public class Blend { public class Blend {
private Blend() {} private Blend() {}
/** /**
* Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the * Blend the design color's HCT hue towards the key color's HCT hue, in a way that leaves the
* original color recognizable and recognizably shifted towards the key color. * original color recognizable and recognizably shifted towards the key color.
* *
* @param designColor ARGB representation of an arbitrary color. * @param designColor ARGB representation of an arbitrary color.
* @param sourceColor ARGB representation of the main theme color. * @param sourceColor ARGB representation of the main theme color.
* @return The design color with a hue shifted towards the system's color, a slightly * @return The design color with a hue shifted towards the system's color, a slightly
* warmer/cooler variant of the design color's hue. * warmer/cooler variant of the design color's hue.
*/ */
public static int harmonize(int designColor, int sourceColor) { public static int harmonize(int designColor, int sourceColor) {
Hct fromHct = Hct.fromInt(designColor); Hct fromHct = Hct.fromInt(designColor);
Hct toHct = Hct.fromInt(sourceColor); Hct toHct = Hct.fromInt(sourceColor);
double differenceDegrees = MathUtils.differenceDegrees(fromHct.getHue(), toHct.getHue()); double differenceDegrees = MathUtils.differenceDegrees(fromHct.getHue(), toHct.getHue());
double rotationDegrees = Math.min(differenceDegrees * 0.5, 15.0); double rotationDegrees = Math.min(differenceDegrees * 0.5, 15.0);
double outputHue = double outputHue =
MathUtils.sanitizeDegreesDouble( MathUtils.sanitizeDegreesDouble(
fromHct.getHue() fromHct.getHue()
+ rotationDegrees * MathUtils.rotationDirection(fromHct.getHue(), toHct.getHue())); + rotationDegrees * MathUtils.rotationDirection(fromHct.getHue(), toHct.getHue()));
return Hct.from(outputHue, fromHct.getChroma(), fromHct.getTone()).toInt(); return Hct.from(outputHue, fromHct.getChroma(), fromHct.getTone()).toInt();
} }
/** /**
* Blends hue from one color into another. The chroma and tone of the original color are * Blends hue from one color into another. The chroma and tone of the original color are
* maintained. * maintained.
* *
* @param from ARGB representation of color * @param from ARGB representation of color
* @param to ARGB representation of color * @param to ARGB representation of color
* @param amount how much blending to perform; 0.0 >= and <= 1.0 * @param amount how much blending to perform; 0.0 >= and <= 1.0
* @return from, with a hue blended towards to. Chroma and tone are constant. * @return from, with a hue blended towards to. Chroma and tone are constant.
*/ */
public static int hctHue(int from, int to, double amount) { public static int hctHue(int from, int to, double amount) {
int ucs = cam16Ucs(from, to, amount); int ucs = cam16Ucs(from, to, amount);
Cam16 ucsCam = Cam16.fromInt(ucs); Cam16 ucsCam = Cam16.fromInt(ucs);
Cam16 fromCam = Cam16.fromInt(from); Cam16 fromCam = Cam16.fromInt(from);
Hct blended = Hct.from(ucsCam.getHue(), fromCam.getChroma(), ColorUtils.lstarFromArgb(from)); Hct blended = Hct.from(ucsCam.getHue(), fromCam.getChroma(), ColorUtils.lstarFromArgb(from));
return blended.toInt(); return blended.toInt();
} }
/** /**
* Blend in CAM16-UCS space. * Blend in CAM16-UCS space.
* *
* @param from ARGB representation of color * @param from ARGB representation of color
* @param to ARGB representation of color * @param to ARGB representation of color
* @param amount how much blending to perform; 0.0 >= and <= 1.0 * @param amount how much blending to perform; 0.0 >= and <= 1.0
* @return from, blended towards to. Hue, chroma, and tone will change. * @return from, blended towards to. Hue, chroma, and tone will change.
*/ */
public static int cam16Ucs(int from, int to, double amount) { public static int cam16Ucs(int from, int to, double amount) {
Cam16 fromCam = Cam16.fromInt(from); Cam16 fromCam = Cam16.fromInt(from);
Cam16 toCam = Cam16.fromInt(to); Cam16 toCam = Cam16.fromInt(to);
double fromJ = fromCam.getJstar(); double fromJ = fromCam.getJstar();
double fromA = fromCam.getAstar(); double fromA = fromCam.getAstar();
double fromB = fromCam.getBstar(); double fromB = fromCam.getBstar();
double toJ = toCam.getJstar(); double toJ = toCam.getJstar();
double toA = toCam.getAstar(); double toA = toCam.getAstar();
double toB = toCam.getBstar(); double toB = toCam.getBstar();
double jstar = fromJ + (toJ - fromJ) * amount; double jstar = fromJ + (toJ - fromJ) * amount;
double astar = fromA + (toA - fromA) * amount; double astar = fromA + (toA - fromA) * amount;
double bstar = fromB + (toB - fromB) * amount; double bstar = fromB + (toB - fromB) * amount;
return Cam16.fromUcs(jstar, astar, bstar).toInt(); return Cam16.fromUcs(jstar, astar, bstar).toInt();
} }
} }

View File

@ -63,6 +63,9 @@ public final class Cam16 {
private final double astar; private final double astar;
private final double bstar; private final double bstar;
// Avoid allocations during conversion by pre-allocating an array.
private final double[] tempArray = new double[] {0.0, 0.0, 0.0};
/** /**
* 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
@ -207,6 +210,11 @@ public final class Cam16 {
double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL; double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL; double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
return fromXyzInViewingConditions(x, y, z, viewingConditions);
}
static Cam16 fromXyzInViewingConditions(
double x, double y, double z, ViewingConditions viewingConditions) {
// Transform XYZ to 'cone'/'rgb' responses // Transform XYZ to 'cone'/'rgb' responses
double[][] matrix = XYZ_TO_CAM16RGB; double[][] matrix = XYZ_TO_CAM16RGB;
double 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]);
@ -371,6 +379,11 @@ public final class Cam16 {
* @return ARGB representation of color * @return ARGB representation of color
*/ */
int viewed(ViewingConditions viewingConditions) { int viewed(ViewingConditions viewingConditions) {
double[] xyz = xyzInViewingConditions(viewingConditions, tempArray);
return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]);
}
double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) {
double alpha = double alpha =
(getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0); (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
@ -414,6 +427,13 @@ public final class Cam16 {
double 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]);
double 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); if (returnArray != null) {
returnArray[0] = x;
returnArray[1] = y;
returnArray[2] = z;
return returnArray;
} else {
return new double[] {x, y, z};
}
} }
} }

View File

@ -117,6 +117,36 @@ public final class Hct {
setInternalState(HctSolver.solveToInt(hue, chroma, newTone)); setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
} }
/**
* Translate a color into different ViewingConditions.
*
* <p>Colors change appearance. They look different with lights on versus off, the same color, as
* in hex code, on white looks different when on black. This is called color relativity, most
* famously explicated by Josef Albers in Interaction of Color.
*
* <p>In color science, color appearance models can account for this and calculate the appearance
* of a color in different settings. HCT is based on CAM16, a color appearance model, and uses it
* to make these calculations.
*
* <p>See ViewingConditions.make for parameters affecting color appearance.
*/
public Hct inViewingConditions(ViewingConditions vc) {
// 1. Use CAM16 to find XYZ coordinates of color in specified VC.
Cam16 cam16 = Cam16.fromInt(toInt());
double[] viewedInVc = cam16.xyzInViewingConditions(vc, null);
// 2. Create CAM16 of those XYZ coordinates in default VC.
Cam16 recastInVc =
Cam16.fromXyzInViewingConditions(
viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT);
// 3. Create HCT from:
// - CAM16 using default VC with XYZ coordinates in specified VC.
// - L* converted from Y in XYZ coordinates in specified VC.
return Hct.from(
recastInVc.getHue(), recastInVc.getChroma(), ColorUtils.lstarFromY(viewedInVc[1]));
}
private void setInternalState(int argb) { private void setInternalState(int argb) {
this.argb = argb; this.argb = argb;
Cam16 cam = Cam16.fromInt(argb); Cam16 cam = Cam16.fromInt(argb);

View File

@ -33,16 +33,7 @@ import utils.MathUtils;
public final class ViewingConditions { 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.defaultWithBackgroundLstar(50.0);
new double[] {
ColorUtils.whitePointD65()[0],
ColorUtils.whitePointD65()[1],
ColorUtils.whitePointD65()[2]
},
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
50.0,
2.0,
false);
private final double aw; private final double aw;
private final double nbb; private final double nbb;
@ -113,12 +104,15 @@ public final class ViewingConditions {
* such as knowing an apple is still red in green light. default = false, the eye does not * such as knowing an apple is still red in green light. default = false, the eye does not
* perform this process on self-luminous objects like displays. * perform this process on self-luminous objects like displays.
*/ */
static ViewingConditions make( public static ViewingConditions make(
double[] whitePoint, double[] whitePoint,
double adaptingLuminance, double adaptingLuminance,
double backgroundLstar, double backgroundLstar,
double surround, double surround,
boolean discountingIlluminant) { boolean discountingIlluminant) {
// A background of pure black is non-physical and leads to infinities that represent the idea
// that any color viewed in pure black can't be seen.
backgroundLstar = Math.max(0.1, backgroundLstar);
// Transform white point XYZ to 'cone'/'rgb' responses // Transform white point XYZ to 'cone'/'rgb' responses
double[][] matrix = Cam16.XYZ_TO_CAM16RGB; double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
double[] xyz = whitePoint; double[] xyz = whitePoint;
@ -166,6 +160,20 @@ public final class ViewingConditions {
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z); return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
} }
/**
* Create sRGB-like viewing conditions with a custom background lstar.
*
* <p>Default viewing conditions have a lstar of 50, midgray.
*/
public static ViewingConditions defaultWithBackgroundLstar(double lstar) {
return ViewingConditions.make(
ColorUtils.whitePointD65(),
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
lstar,
2.0,
false);
}
/** /**
* Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand * Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
* for technical color science terminology, this class would not benefit from documenting them * for technical color science terminology, this class would not benefit from documenting them

View File

@ -25,6 +25,7 @@ import java.util.Map;
*/ */
public final class TonalPalette { public final class TonalPalette {
Map<Integer, Integer> cache; Map<Integer, Integer> cache;
Hct keyColor;
double hue; double hue;
double chroma; double chroma;
@ -34,9 +35,18 @@ public final class TonalPalette {
* @param argb ARGB representation of a color * @param argb ARGB representation of a color
* @return Tones matching that color's hue and chroma. * @return Tones matching that color's hue and chroma.
*/ */
public static final TonalPalette fromInt(int argb) { public static TonalPalette fromInt(int argb) {
Hct hct = Hct.fromInt(argb); return fromHct(Hct.fromInt(argb));
return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma()); }
/**
* Create tones using a HCT color.
*
* @param hct HCT representation of a color.
* @return Tones matching that color's hue and chroma.
*/
public static TonalPalette fromHct(Hct hct) {
return new TonalPalette(hct.getHue(), hct.getChroma(), hct);
} }
/** /**
@ -46,14 +56,53 @@ 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(double hue, double chroma) { public static TonalPalette fromHueAndChroma(double hue, double chroma) {
return new TonalPalette(hue, chroma); return new TonalPalette(hue, chroma, createKeyColor(hue, chroma));
} }
private TonalPalette(double hue, double chroma) { private TonalPalette(double hue, double chroma, Hct keyColor) {
cache = new HashMap<>(); cache = new HashMap<>();
this.hue = hue; this.hue = hue;
this.chroma = chroma; this.chroma = chroma;
this.keyColor = keyColor;
}
/** The key color is the first tone, starting from T50, matching the given hue and chroma. */
private static Hct createKeyColor(double hue, double chroma) {
double startTone = 50.0;
Hct smallestDeltaHct = Hct.from(hue, chroma, startTone);
double smallestDelta = Math.abs(smallestDeltaHct.getChroma() - chroma);
// Starting from T50, check T+/-delta to see if they match the requested
// chroma.
//
// Starts from T50 because T50 has the most chroma available, on
// average. Thus it is most likely to have a direct answer and minimize
// iteration.
for (double delta = 1.0; delta < 50.0; delta += 1.0) {
// Termination condition rounding instead of minimizing delta to avoid
// case where requested chroma is 16.51, and the closest chroma is 16.49.
// Error is minimized, but when rounded and displayed, requested chroma
// is 17, key color's chroma is 16.
if (Math.round(chroma) == Math.round(smallestDeltaHct.getChroma())) {
return smallestDeltaHct;
}
final Hct hctAdd = Hct.from(hue, chroma, startTone + delta);
final double hctAddDelta = Math.abs(hctAdd.getChroma() - chroma);
if (hctAddDelta < smallestDelta) {
smallestDelta = hctAddDelta;
smallestDeltaHct = hctAdd;
}
final Hct hctSubtract = Hct.from(hue, chroma, startTone - delta);
final double hctSubtractDelta = Math.abs(hctSubtract.getChroma() - chroma);
if (hctSubtractDelta < smallestDelta) {
smallestDelta = hctSubtractDelta;
smallestDeltaHct = hctSubtract;
}
}
return smallestDeltaHct;
} }
/** /**
@ -72,4 +121,24 @@ public final class TonalPalette {
} }
return color; return color;
} }
/** Given a tone, use hue and chroma of palette to create a color, and return it as HCT. */
public Hct getHct(double tone) {
return Hct.from(this.hue, this.chroma, tone);
}
/** The chroma of the Tonal Palette, in HCT. Ranges from 0 to ~130 (for sRGB gamut). */
public double getChroma() {
return this.chroma;
}
/** The hue of the Tonal Palette, in HCT. Ranges from 0 to 360. */
public double getHue() {
return this.hue;
}
/** The key color is the first tone, starting from T50, that matches the palette's chroma. */
public Hct getKeyColor() {
return this.keyColor;
}
} }

View File

@ -187,6 +187,21 @@ public class ColorUtils {
return 100.0 * labInvf((lstar + 16.0) / 116.0); return 100.0 * labInvf((lstar + 16.0) / 116.0);
} }
/**
* Converts a Y value to an L* value.
*
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
* logarithmic scale.
*
* @param y Y in XYZ
* @return L* in L*a*b*
*/
public static double lstarFromY(double y) {
return labF(y / 100.0) * 116.0 - 16.0;
}
/** /**
* Linearizes an RGB component. * Linearizes an RGB component.
* *