SPIGOT-7292: Support for alpha channel in Color and a few improvements

By: Doc <nachito94@msn.com>
This commit is contained in:
Bukkit/Spigot 2023-03-16 19:27:53 +11:00
parent fff80f432d
commit f328857bac
2 changed files with 199 additions and 33 deletions

View file

@ -8,6 +8,7 @@ import java.util.Objects;
import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.SerializableAs; import org.bukkit.configuration.serialization.SerializableAs;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* A container for a color palette. This class is immutable; the set methods * A container for a color palette. This class is immutable; the set methods
@ -17,6 +18,7 @@ import org.jetbrains.annotations.NotNull;
@SerializableAs("Color") @SerializableAs("Color")
public final class Color implements ConfigurationSerializable { public final class Color implements ConfigurationSerializable {
private static final int BIT_MASK = 0xff; private static final int BIT_MASK = 0xff;
private static final int DEFAULT_ALPHA = 255;
/** /**
* White, or (0xFF,0xFF,0xFF) in (R,G,B) * White, or (0xFF,0xFF,0xFF) in (R,G,B)
@ -103,10 +105,26 @@ public final class Color implements ConfigurationSerializable {
*/ */
public static final Color ORANGE = fromRGB(0xFFA500); public static final Color ORANGE = fromRGB(0xFFA500);
private final byte alpha;
private final byte red; private final byte red;
private final byte green; private final byte green;
private final byte blue; private final byte blue;
/**
* Creates a new Color object from an alpha, red, green, and blue
*
* @param alpha integer from 0-255
* @param red integer from 0-255
* @param green integer from 0-255
* @param blue integer from 0-255
* @return a new Color object for the alpha, red, green, blue
* @throws IllegalArgumentException if any value is strictly {@literal >255 or <0}
*/
@NotNull
public static Color fromARGB(int alpha, int red, int green, int blue) throws IllegalArgumentException {
return new Color(alpha, red, green, blue);
}
/** /**
* Creates a new Color object from a red, green, and blue * Creates a new Color object from a red, green, and blue
* *
@ -118,7 +136,7 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public static Color fromRGB(int red, int green, int blue) throws IllegalArgumentException { public static Color fromRGB(int red, int green, int blue) throws IllegalArgumentException {
return new Color(red, green, blue); return new Color(DEFAULT_ALPHA, red, green, blue);
} }
/** /**
@ -132,7 +150,7 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public static Color fromBGR(int blue, int green, int red) throws IllegalArgumentException { public static Color fromBGR(int blue, int green, int red) throws IllegalArgumentException {
return new Color(red, green, blue); return new Color(DEFAULT_ALPHA, red, green, blue);
} }
/** /**
@ -146,8 +164,20 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public static Color fromRGB(int rgb) throws IllegalArgumentException { public static Color fromRGB(int rgb) throws IllegalArgumentException {
Preconditions.checkArgument((rgb >> 24) == 0, "Extrenuous data in: ", rgb); Preconditions.checkArgument((rgb >> 24) == 0, "Extraneous data in: %s", rgb);
return fromRGB(rgb >> 16 & BIT_MASK, rgb >> 8 & BIT_MASK, rgb >> 0 & BIT_MASK); return fromRGB(rgb >> 16 & BIT_MASK, rgb >> 8 & BIT_MASK, rgb & BIT_MASK);
}
/**
* Creates a new color object from an integer that contains the alpha, red,
* green, and blue bytes.
*
* @param argb the integer storing the alpha, red, green, and blue values
* @return a new color object for specified values
*/
@NotNull
public static Color fromARGB(int argb) {
return fromARGB(argb >> 24 & BIT_MASK, argb >> 16 & BIT_MASK, argb >> 8 & BIT_MASK, argb & BIT_MASK);
} }
/** /**
@ -161,20 +191,46 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public static Color fromBGR(int bgr) throws IllegalArgumentException { public static Color fromBGR(int bgr) throws IllegalArgumentException {
Preconditions.checkArgument((bgr >> 24) == 0, "Extrenuous data in: ", bgr); Preconditions.checkArgument((bgr >> 24) == 0, "Extrenuous data in: %s", bgr);
return fromBGR(bgr >> 16 & BIT_MASK, bgr >> 8 & BIT_MASK, bgr >> 0 & BIT_MASK); return fromBGR(bgr >> 16 & BIT_MASK, bgr >> 8 & BIT_MASK, bgr & BIT_MASK);
} }
private Color(int red, int green, int blue) { private Color(int red, int green, int blue) {
Preconditions.checkArgument(red >= 0 && red <= BIT_MASK, "Red is not between 0-255: ", red); this(DEFAULT_ALPHA, red, green, blue);
Preconditions.checkArgument(green >= 0 && green <= BIT_MASK, "Green is not between 0-255: ", green); }
Preconditions.checkArgument(blue >= 0 && blue <= BIT_MASK, "Blue is not between 0-255: ", blue);
private Color(int alpha, int red, int green, int blue) {
Preconditions.checkArgument(alpha >= 0 && alpha <= BIT_MASK, "Alpha[%s] is not between 0-255", alpha);
Preconditions.checkArgument(red >= 0 && red <= BIT_MASK, "Red[%s] is not between 0-255", red);
Preconditions.checkArgument(green >= 0 && green <= BIT_MASK, "Green[%s] is not between 0-255", green);
Preconditions.checkArgument(blue >= 0 && blue <= BIT_MASK, "Blue[%s] is not between 0-255", blue);
this.alpha = (byte) alpha;
this.red = (byte) red; this.red = (byte) red;
this.green = (byte) green; this.green = (byte) green;
this.blue = (byte) blue; this.blue = (byte) blue;
} }
/**
* Gets the alpha component
*
* @return alpha component, from 0 to 255
*/
public int getAlpha() {
return BIT_MASK & alpha;
}
/**
* Creates a new Color object with specified component
*
* @param alpha the alpha component, from 0 to 255
* @return a new color object with the red component
*/
@NotNull
public Color setAlpha(int alpha) {
return fromARGB(alpha, getRed(), getGreen(), getBlue());
}
/** /**
* Gets the red component * Gets the red component
* *
@ -192,7 +248,7 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public Color setRed(int red) { public Color setRed(int red) {
return fromRGB(red, getGreen(), getBlue()); return fromARGB(getAlpha(), red, getGreen(), getBlue());
} }
/** /**
@ -212,7 +268,7 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public Color setGreen(int green) { public Color setGreen(int green) {
return fromRGB(getRed(), green, getBlue()); return fromARGB(getAlpha(), getRed(), green, getBlue());
} }
/** /**
@ -232,7 +288,7 @@ public final class Color implements ConfigurationSerializable {
*/ */
@NotNull @NotNull
public Color setBlue(int blue) { public Color setBlue(int blue) {
return fromRGB(getRed(), getGreen(), blue); return fromARGB(getAlpha(), getRed(), getGreen(), blue);
} }
/** /**
@ -241,7 +297,16 @@ public final class Color implements ConfigurationSerializable {
* @return An integer representation of this color, as 0xRRGGBB * @return An integer representation of this color, as 0xRRGGBB
*/ */
public int asRGB() { public int asRGB() {
return getRed() << 16 | getGreen() << 8 | getBlue() << 0; return getRed() << 16 | getGreen() << 8 | getBlue();
}
/**
* Gets the color as an ARGB integer.
*
* @return An integer representation of this color, as 0xAARRGGBB
*/
public int asARGB() {
return getAlpha() << 24 | getRed() << 16 | getGreen() << 8 | getBlue();
} }
/** /**
@ -250,7 +315,7 @@ public final class Color implements ConfigurationSerializable {
* @return An integer representation of this color, as 0xBBGGRR * @return An integer representation of this color, as 0xBBGGRR
*/ */
public int asBGR() { public int asBGR() {
return getBlue() << 16 | getGreen() << 8 | getRed() << 0; return getBlue() << 16 | getGreen() << 8 | getRed();
} }
/** /**
@ -263,7 +328,7 @@ public final class Color implements ConfigurationSerializable {
// TODO: Javadoc what this method does, not what it mimics. API != Implementation // TODO: Javadoc what this method does, not what it mimics. API != Implementation
@NotNull @NotNull
public Color mixDyes(@NotNull DyeColor... colors) { public Color mixDyes(@NotNull DyeColor... colors) {
Preconditions.checkArgument(colors != null && Arrays.stream(colors).allMatch(Objects::nonNull), "Colors cannot be null"); Preconditions.checkArgument(colors != null && Arrays.stream(colors).allMatch(Objects::nonNull), "DyeColor cannot be null or contain null values");
Color[] toPass = new Color[colors.length]; Color[] toPass = new Color[colors.length];
for (int i = 0; i < colors.length; i++) { for (int i = 0; i < colors.length; i++) {
@ -275,7 +340,10 @@ public final class Color implements ConfigurationSerializable {
/** /**
* Creates a new color with its RGB components changed as if it was dyed * Creates a new color with its RGB components changed as if it was dyed
* with the colors passed in, replicating vanilla workbench dyeing * with the colors passed in, replicating vanilla workbench dyeing.
*
* <b>Note that this method does not currently take into account alpha
* components.</b>
* *
* @param colors The colors to dye with * @param colors The colors to dye with
* @return A new color with the changed rgb components * @return A new color with the changed rgb components
@ -313,36 +381,42 @@ public final class Color implements ConfigurationSerializable {
return false; return false;
} }
final Color that = (Color) o; final Color that = (Color) o;
return this.blue == that.blue && this.green == that.green && this.red == that.red; return this.alpha == that.alpha && this.blue == that.blue && this.green == that.green && this.red == that.red;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return asRGB() ^ Color.class.hashCode(); return asARGB() ^ Color.class.hashCode();
} }
@Override @Override
@NotNull @NotNull
public Map<String, Object> serialize() { public Map<String, Object> serialize() {
return ImmutableMap.<String, Object>of( return ImmutableMap.of(
"RED", getRed(), "ALPHA", getAlpha(),
"BLUE", getBlue(), "RED", getRed(),
"GREEN", getGreen() "BLUE", getBlue(),
"GREEN", getGreen()
); );
} }
@SuppressWarnings("javadoc") @SuppressWarnings("javadoc")
@NotNull @NotNull
public static Color deserialize(@NotNull Map<String, Object> map) { public static Color deserialize(@NotNull Map<String, Object> map) {
return fromRGB( return fromARGB(
asInt("RED", map), asInt("ALPHA", map, DEFAULT_ALPHA),
asInt("GREEN", map), asInt("RED", map),
asInt("BLUE", map) asInt("GREEN", map),
asInt("BLUE", map)
); );
} }
private static int asInt(@NotNull String string, @NotNull Map<String, Object> map) { private static int asInt(@NotNull String string, @NotNull Map<String, Object> map) {
Object value = map.get(string); return asInt(string, map, null);
}
private static int asInt(@NotNull String string, @NotNull Map<String, Object> map, @Nullable Object defaultValue) {
Object value = map.getOrDefault(string, defaultValue);
if (value == null) { if (value == null) {
throw new IllegalArgumentException(string + " not in map " + map); throw new IllegalArgumentException(string + " not in map " + map);
} }
@ -354,6 +428,6 @@ public final class Color implements ConfigurationSerializable {
@Override @Override
public String toString() { public String toString() {
return "Color:[rgb0x" + Integer.toHexString(getRed()).toUpperCase() + Integer.toHexString(getGreen()).toUpperCase() + Integer.toHexString(getBlue()).toUpperCase() + "]"; return "Color:[argb0x" + Integer.toHexString(asARGB()).toUpperCase() + "]";
} }
} }

View file

@ -10,19 +10,27 @@ public class ColorTest {
static class TestColor { static class TestColor {
static int id = 0; static int id = 0;
final String name; final String name;
final int argb;
final int rgb; final int rgb;
final int bgr; final int bgr;
final int a;
final int r; final int r;
final int g; final int g;
final int b; final int b;
TestColor(int rgb, int bgr, int r, int g, int b) { TestColor(int rgb, int bgr, int r, int g, int b) {
this((255 << 24 | r << 16 | g << 8 | b), rgb, bgr, 255, r, g, b);
}
TestColor(int argb, int rgb, int bgr, int a, int r, int g, int b) {
this.argb = argb;
this.rgb = rgb; this.rgb = rgb;
this.bgr = bgr; this.bgr = bgr;
this.a = a;
this.r = r; this.r = r;
this.g = g; this.g = g;
this.b = b; this.b = b;
this.name = id + ":" + Integer.toHexString(rgb).toUpperCase() + "_" + Integer.toHexString(bgr).toUpperCase() + "-r" + Integer.toHexString(r).toUpperCase() + "-g" + Integer.toHexString(g).toUpperCase() + "-b" + Integer.toHexString(b).toUpperCase(); this.name = id + ":" + Integer.toHexString(argb).toUpperCase() + "_" + Integer.toHexString(rgb).toUpperCase() + "_" + Integer.toHexString(bgr).toUpperCase() + "-a" + Integer.toHexString(a).toUpperCase() + "-r" + Integer.toHexString(r).toUpperCase() + "-g" + Integer.toHexString(g).toUpperCase() + "-b" + Integer.toHexString(b).toUpperCase();
} }
} }
@ -32,7 +40,10 @@ public class ColorTest {
new TestColor(0xFFFFAA, 0xAAFFFF, 0xFF, 0xFF, 0xAA), new TestColor(0xFFFFAA, 0xAAFFFF, 0xFF, 0xFF, 0xAA),
new TestColor(0xFF00FF, 0xFF00FF, 0xFF, 0x00, 0xFF), new TestColor(0xFF00FF, 0xFF00FF, 0xFF, 0x00, 0xFF),
new TestColor(0x67FF22, 0x22FF67, 0x67, 0xFF, 0x22), new TestColor(0x67FF22, 0x22FF67, 0x67, 0xFF, 0x22),
new TestColor(0x000000, 0x000000, 0x00, 0x00, 0x00) new TestColor(0x000000, 0x000000, 0x00, 0x00, 0x00),
/* 0xAARRGGBB, 0xRRGGBB, 0xBBGGRR, 0xAA, 0xRR, 0xGG, 0xBB */
new TestColor(0xFF559922, 0x559922, 0x229955, 0xFF, 0x55, 0x99, 0x22),
new TestColor(0x00000000, 0x000000, 0x000000, 0x00, 0x00, 0x00, 0x00)
}; };
@Test @Test
@ -55,11 +66,15 @@ public class ColorTest {
@Test @Test
public void testEqualities() { public void testEqualities() {
for (TestColor testColor : examples) { for (TestColor testColor : examples) {
Color fromARGB = Color.fromARGB(testColor.argb);
Color fromARGBs = Color.fromARGB(testColor.a, testColor.r, testColor.g, testColor.b);
Color fromRGB = Color.fromRGB(testColor.rgb); Color fromRGB = Color.fromRGB(testColor.rgb);
Color fromBGR = Color.fromBGR(testColor.bgr); Color fromBGR = Color.fromBGR(testColor.bgr);
Color fromRGBs = Color.fromRGB(testColor.r, testColor.g, testColor.b); Color fromRGBs = Color.fromRGB(testColor.r, testColor.g, testColor.b);
Color fromBGRs = Color.fromBGR(testColor.b, testColor.g, testColor.r); Color fromBGRs = Color.fromBGR(testColor.b, testColor.g, testColor.r);
assertThat(testColor.name, fromARGB, is(fromARGB));
assertThat(testColor.name, fromARGBs, is(fromARGBs));
assertThat(testColor.name, fromRGB, is(fromRGBs)); assertThat(testColor.name, fromRGB, is(fromRGBs));
assertThat(testColor.name, fromRGB, is(fromBGR)); assertThat(testColor.name, fromRGB, is(fromBGR));
assertThat(testColor.name, fromRGB, is(fromBGRs)); assertThat(testColor.name, fromRGB, is(fromBGRs));
@ -73,20 +88,29 @@ public class ColorTest {
public void testInequalities() { public void testInequalities() {
for (int i = 1; i < examples.length; i++) { for (int i = 1; i < examples.length; i++) {
TestColor testFrom = examples[i]; TestColor testFrom = examples[i];
Color from = Color.fromRGB(testFrom.rgb); Color from = Color.fromARGB(testFrom.argb);
for (int j = i - 1; j >= 0; j--) { for (int j = i - 1; j >= 0; j--) {
TestColor testTo = examples[j]; TestColor testTo = examples[j];
Color to = Color.fromRGB(testTo.rgb); Color to = Color.fromARGB(testTo.argb);
String name = testFrom.name + " to " + testTo.name; String name = testFrom.name + " to " + testTo.name;
assertThat(name, from, is(not(to))); assertThat(name, from, is(not(to)));
Color transform = from.setRed(testTo.r).setBlue(testTo.b).setGreen(testTo.g); Color transform = from.setAlpha(testTo.a).setRed(testTo.r).setBlue(testTo.b).setGreen(testTo.g);
assertThat(name, transform, is(not(sameInstance(from)))); assertThat(name, transform, is(not(sameInstance(from))));
assertThat(name, transform, is(to)); assertThat(name, transform, is(to));
} }
} }
} }
// ARGB tests
@Test
public void testARGB() {
for (TestColor testColor : examples) {
assertThat(testColor.name, Color.fromARGB(testColor.argb).asARGB(), is(testColor.argb));
assertThat(testColor.name, Color.fromARGB(testColor.a, testColor.r, testColor.g, testColor.b).asARGB(), is(testColor.argb));
}
}
// RGB tests // RGB tests
@Test @Test
public void testRGB() { public void testRGB() {
@ -149,6 +173,74 @@ public class ColorTest {
Color.fromBGR(-1); Color.fromBGR(-1);
} }
// Alpha tests
@Test
public void testAlpha() {
for (TestColor testColor : examples) {
assertThat(testColor.name, Color.fromARGB(testColor.argb).getAlpha(), is(testColor.a));
}
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA01() {
Color.fromARGB(-1, 0x00, 0x00, 0x00);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA02() {
Color.fromARGB(Integer.MAX_VALUE, 0x00, 0x00, 0x00);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA03() {
Color.fromARGB(Integer.MIN_VALUE, 0x00, 0x00, 0x00);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA04() {
Color.fromARGB(0x100, 0x00, 0x00, 0x00);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA05() {
Color.fromBGR(0x00, 0x00, 0x00).setAlpha(-1);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA06() {
Color.fromBGR(0x00, 0x00, 0x00).setAlpha(Integer.MAX_VALUE);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA07() {
Color.fromBGR(0x00, 0x00, 0x00).setAlpha(Integer.MIN_VALUE);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA08() {
Color.fromBGR(0x00, 0x00, 0x00).setAlpha(0x100);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA09() {
Color.WHITE.setAlpha(-1);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA10() {
Color.WHITE.setAlpha(Integer.MAX_VALUE);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA11() {
Color.WHITE.setAlpha(Integer.MIN_VALUE);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidA12() {
Color.WHITE.setAlpha(0x100);
}
// Red tests // Red tests
@Test @Test
public void testRed() { public void testRed() {