Update material-color-utilities
This commit is contained in:
parent
3f1a04ad81
commit
4841fcfc69
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user