Added new class Color

This commit is contained in:
Robert Vokac 2024-09-28 18:46:12 +02:00
parent 1b5e436d09
commit de90c9780b
Signed by: robertvokac
GPG Key ID: FB9CE8E20AADA55F
2 changed files with 501 additions and 4 deletions

View File

@ -17,13 +17,322 @@
// <https://www.gnu.org/licenses/> or write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
///////////////////////////////////////////////////////////////////////////////////////////////
package com.pixelgamelibrary.api.graphics;
import lombok.Data;
/**
* Represents a color in the RGBA format where each component is a float value
* between 0 and 1. This class provides methods to manipulate color values,
* including normalization, blending, and conversion to various formats (e.g.,
* hexadecimal, integer).
* <p>
* The color components are represented as:
* <ul>
* <li><b>red</b> - The intensity of the red component, ranging from 0 to
* 1.</li>
* <li><b>green</b> - The intensity of the green component, ranging from 0 to
* 1.</li>
* <li><b>blue</b> - The intensity of the blue component, ranging from 0 to
* 1.</li>
* <li><b>alpha</b> - The transparency level, where 0 is fully transparent and 1
* is fully opaque.</li>
* </ul>
* </p>
*
* @author robertvokac
* <p>
* This class is immutable. Each method that modifies a color returns a new
* instance instead of modifying the original.
* </p>
*
* <p>
* <b>Author:</b> robertvokac
* </p>
*/
public interface Color {
@Data
public final class Color {
private float red, green, blue, alpha;
/**
* Ensures that the color components are confined within the valid range [0,
* 1]. If any component exceeds this range, it will be clamped to fit within
* the specified boundaries. This method supports method chaining by
* returning the current instance.
*
* @return the current instance of this Color for chaining
*/
public Color normalize() {
red = Math.max(0, Math.min(red, 1));
green = Math.max(0, Math.min(green, 1));
blue = Math.max(0, Math.min(blue, 1));
alpha = Math.max(0, Math.min(alpha, 1));
return this;
}
// Default alpha value for colors
public static final float DEFAULT_ALPHA = 1f;
// Default alpha int value for colors
public static final int DEFAULT_ALPHA_INT = (int) DEFAULT_ALPHA;
// Default constructor initializes color to black with full opacity
public Color() {
this.red = 0;
this.green = 0;
this.blue = 0;
this.alpha = DEFAULT_ALPHA;
}
// Constructor initializes color with specified RGB values, defaulting alpha to full opacity
public Color(float red, float green, float blue) {
this(red, green, blue, DEFAULT_ALPHA);
}
// Constructor initializes color with specified RGBA values and normalizes them
public Color(float red, float green, float blue, float alpha) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
normalize();
}
// Constructor initializes color with specified RGB values, defaulting alpha to full opacity
public Color(int red, int green, int blue) {
this(red, green, blue, DEFAULT_ALPHA_INT);
}
// Constructor initializes color with specified RGBA values, converting from integers to floats
public Color(int red, int green, int blue, int alpha) {
this((float) red, (float) green, (float) blue, (float) alpha);
}
// Copy constructor creates a new Color instance from an existing one
public Color(Color color) {
set(color);
}
/**
* Sets the color's components to match the provided color.
*
* @param color the Color instance to copy from
* @return this color instance for method chaining
*/
public Color set(Color color) {
return set(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
}
/**
* Sets the color's components using the provided RGB values, defaulting
* alpha to full opacity.
*
* @param red the red component
* @param green the green component
* @param blue the blue component
* @return this color instance for method chaining
*/
public Color set(float red, float green, float blue) {
return set(red, green, blue, DEFAULT_ALPHA);
}
/**
* Sets the color's components using the provided RGBA values.
*
* @param red the red component
* @param green the green component
* @param blue the blue component
* @param alpha the alpha component
* @return this color instance for method chaining
*/
public Color set(float red, float green, float blue, float alpha) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
return normalize();
}
/**
* Sets the color's components using the provided RGB values, defaulting
* alpha to full opacity.
*
* @param red the red component
* @param green the green component
* @param blue the blue component
* @return this color instance for method chaining
*/
public Color set(int red, int green, int blue) {
return set(red, green, blue, DEFAULT_ALPHA_INT);
}
/**
* Sets the color's components using the provided RGBA values.
*
* @param red the red component
* @param green the green component
* @param blue the blue component
* @param alpha the alpha component
* @return this color instance for method chaining
*/
public Color set(int red, int green, int blue, int alpha) {
return set(
(float) red,
(float) green,
(float) blue,
(float) alpha);
}
/**
* Blends this color with the specified color by multiplying their
* respective components.
*
* @param other the color to blend with
* @return the updated color instance for method chaining
*/
public Color multiply(Color other) {
this.red *= other.red;
this.green *= other.green;
this.blue *= other.blue;
this.alpha *= other.alpha;
return normalize();
}
/**
* Scales the intensity of this color by multiplying each component by the
* specified factor.
*
* @param factor the scalar value to multiply with
* @return the updated color instance for method chaining
*/
public Color multiply(float factor) {
this.red *= factor;
this.green *= factor;
this.blue *= factor;
this.alpha *= factor;
return normalize();
}
/**
* Increases the intensity of this color by adding the corresponding values
* from another color.
*
* @param other the color whose components will be added
* @return the updated color instance for method chaining
*/
public Color add(Color other) {
this.red += other.red;
this.green += other.green;
this.blue += other.blue;
this.alpha += other.alpha;
return normalize();
}
/**
* Decreases the intensity of this color by subtracting the corresponding
* values from another color.
*
* @param other the color whose components will be subtracted
* @return the updated color instance for method chaining
*/
public Color subtract(Color other) {
this.red -= other.red;
this.green -= other.green;
this.blue -= other.blue;
this.alpha -= other.alpha;
return normalize();
}
@Override
public boolean equals(Object obj) {
// Check for reference equality
if (this == obj) {
return true;
}
// Ensure the other object is non-null and of the same class
if (obj == null || !(obj instanceof Color)) {
return false;
}
// Cast and compare the packed integer representations of the two colors
Color otherColor = (Color) obj;
return this.toInt() == otherColor.toInt();
}
@Override
public int hashCode() {
// Calculate a hash code based on the color's components
int hash = (red != 0.0f ? Float.floatToIntBits(red) : 0);
hash = 31 * hash + (green != 0.0f ? Float.floatToIntBits(green) : 0);
hash = 31 * hash + (blue != 0.0f ? Float.floatToIntBits(blue) : 0);
hash = 31 * hash + (alpha != 0.0f ? Float.floatToIntBits(alpha) : 0);
return hash;
}
/**
* Converts the color to an integer representation (0xAARRGGBB).
*
* @return the integer representation of this color
*/
public int toInt() {
int a = Math.round(alpha * 255);
int r = Math.round(red * 255);
int g = Math.round(green * 255);
int b = Math.round(blue * 255);
return (a << 24) | (r << 16) | (g << 8) | b;
}
public static int alpha(float alpha) {
return Math.round(alpha * 255);
}
/**
* Converts the color to a hexadecimal string representation (e.g.,
* "#AARRGGBB").
*
* @return the hexadecimal string representation of this color
*/
public String toHexString() {
return String.format("#%08X", toInt());
}
public static Color valueOf(String hexString) {
// Remove '#' if present
if (hexString.startsWith("#")) {
hexString = hexString.substring(1);
}
// Ensure the hex string is the correct length
if (hexString.length() != 8 && hexString.length() != 6) {
throw new IllegalArgumentException("Hex string must be in the format #AARRGGBB or RRGGBB");
}
int r, g, b, a = 255; // Default alpha is fully opaque
// Parse the hex values
if (hexString.length() == 8) { // Format: AARRGGBB
a = Integer.parseInt(hexString.substring(0, 2), 16);
r = Integer.parseInt(hexString.substring(2, 4), 16);
g = Integer.parseInt(hexString.substring(4, 6), 16);
b = Integer.parseInt(hexString.substring(6, 8), 16);
} else { // Format: RRGGBB
r = Integer.parseInt(hexString.substring(0, 2), 16);
g = Integer.parseInt(hexString.substring(2, 4), 16);
b = Integer.parseInt(hexString.substring(4, 6), 16);
}
return new Color(r, g, b, a);
}
public Color cpy() {
return new Color(this);
}
@Override
public String toString() {
// Provide a string representation of the color in RGBA format
return String.format("Color{red=%f, green=%f, blue=%f, alpha=%f}", red, green, blue, alpha);
}
}

View File

@ -0,0 +1,188 @@
package com.pixelgamelibrary.api.graphics;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ColorTest {
@Test
public void testDefaultConstructor() {
Color color = new Color();
assertEquals(0.0f, color.getRed(), 0.001);
assertEquals(0.0f, color.getGreen(), 0.001);
assertEquals(0.0f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testRgbConstructor() {
Color color = new Color(0.5f, 0.5f, 0.5f);
assertEquals(0.5f, color.getRed(), 0.001);
assertEquals(0.5f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testRgbaConstructor() {
Color color = new Color(0.5f, 0.5f, 0.5f, 0.5f);
assertEquals(0.5f, color.getRed(), 0.001);
assertEquals(0.5f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(0.5f, color.getAlpha(), 0.001);
}
@Test
public void testIntConstructor() {
Color color = new Color(128, 64, 32);
assertEquals(128 / 255f, color.getRed(), 0.001);
assertEquals(64 / 255f, color.getGreen(), 0.001);
assertEquals(32 / 255f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testIntRgbaConstructor() {
Color color = new Color(128, 64, 32, 128);
assertEquals(128 / 255f, color.getRed(), 0.001);
assertEquals(64 / 255f, color.getGreen(), 0.001);
assertEquals(32 / 255f, color.getBlue(), 0.001);
assertEquals(128 / 255f, color.getAlpha(), 0.001);
}
@Test
public void testNormalization() {
Color color = new Color(1.2f, -0.2f, 0.5f, 1.5f);
color.normalize();
assertEquals(1.0f, color.getRed(), 0.001);
assertEquals(0.0f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testMultiplyWithAnotherColor() {
Color color1 = new Color(0.5f, 0.5f, 0.5f, 1.0f);
Color color2 = new Color(0.2f, 0.4f, 0.6f, 1.0f);
color1.multiply(color2);
assertEquals(0.1f, color1.getRed(), 0.001);
assertEquals(0.2f, color1.getGreen(), 0.001);
assertEquals(0.3f, color1.getBlue(), 0.001);
assertEquals(1.0f, color1.getAlpha(), 0.001);
}
@Test
public void testMultiplyWithFactor() {
Color color = new Color(0.5f, 0.5f, 0.5f, 1.0f);
color.multiply(2.0f);
assertEquals(1.0f, color.getRed(), 0.001);
assertEquals(1.0f, color.getGreen(), 0.001);
assertEquals(1.0f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testAdd() {
Color color1 = new Color(0.5f, 0.5f, 0.5f, 1.0f);
Color color2 = new Color(0.3f, 0.3f, 0.3f, 1.0f);
color1.add(color2);
assertEquals(0.8f, color1.getRed(), 0.001);
assertEquals(0.8f, color1.getGreen(), 0.001);
assertEquals(0.8f, color1.getBlue(), 0.001);
assertEquals(1.0f, color1.getAlpha(), 0.001);
}
@Test
public void testSubtract() {
Color color1 = new Color(0.5f, 0.5f, 0.5f, 1.0f);
Color color2 = new Color(0.2f, 0.2f, 0.2f, 1.0f);
color1.subtract(color2);
assertEquals(0.3f, color1.getRed(), 0.001);
assertEquals(0.3f, color1.getGreen(), 0.001);
assertEquals(0.3f, color1.getBlue(), 0.001);
assertEquals(1.0f, color1.getAlpha(), 0.001);
}
@Test
public void testEqualsAndHashCode() {
Color color1 = new Color(0.5f, 0.5f, 0.5f, 1.0f);
Color color2 = new Color(0.5f, 0.5f, 0.5f, 1.0f);
assertEquals(color1, color2);
assertEquals(color1.hashCode(), color2.hashCode());
Color color3 = new Color(0.5f, 0.5f, 0.5f, 0.8f);
assertNotEquals(color1, color3);
}
@Test
public void testToInt() {
Color color = new Color(1.0f, 0.5f, 0.25f, 1.0f);
int expected = 0xFFFF7F40; // (0xAARRGGBB) with alpha = 255, red = 255, green = 127, blue = 64
assertEquals(expected, color.toInt());
}
@Test
public void testToHexString() {
Color color = new Color(0.5f, 0.5f, 0.5f, 1.0f);
assertEquals("#FF7F7F7F", color.toString());
}
@Test
public void testValueOfHex() {
Color color = Color.valueOf("#FF7F7F7F");
assertEquals(0.5f, color.getRed(), 0.001);
assertEquals(0.5f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testValueOfHexWithoutHash() {
Color color = Color.valueOf("FF7F7F7F");
assertEquals(0.5f, color.getRed(), 0.001);
assertEquals(0.5f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(1.0f, color.getAlpha(), 0.001);
}
@Test
public void testValueOfHexWithAlpha() {
Color color = Color.valueOf("#80FF7F7F");
assertEquals(0.5f, color.getRed(), 0.001);
assertEquals(0.5f, color.getGreen(), 0.001);
assertEquals(0.5f, color.getBlue(), 0.001);
assertEquals(0.5f, color.getAlpha(), 0.001);
}
@Test
public void testAlphaConversion() {
assertEquals(255, Color.alpha(1.0f));
assertEquals(128, Color.alpha(0.5f));
assertEquals(0, Color.alpha(0.0f));
}
@Test
public void testCopy() {
Color color = new Color(0.5f, 0.5f, 0.5f, 1.0f);
Color copy = color.copy();
assertEquals(color, copy);
assertNotSame(color, copy); // Ensure they are different objects
}
@Test
public void testToString() {
Color color = new Color(0.5f, 0.5f, 0.5f, 0.75f);
assertEquals("Color{red=0.500000, green=0.500000, blue=0.500000, alpha=0.750000}", color.toString());
}
@Test
public void testDifferentColors() {
Color color1 = new Color(1.0f, 0.0f, 0.0f, 1.0f); // Red
Color color2 = new Color(0.0f, 1.0f, 0.0f, 1.0f); // Green
Color color3 = new Color(0.0f, 0.0f, 1.0f, 1.0f); // Blue
assertNotEquals(color1, color2);
assertNotEquals(color2, color3);
assertNotEquals(color3, color1);
}
}