From de90c9780b916790cba74631464ae758ee397a35 Mon Sep 17 00:00:00 2001
From: Robert Vokac
+ * The color components are represented as:
+ *
+ *
+ *
+ * This class is immutable. Each method that modifies a color returns a new + * instance instead of modifying the original. + *
+ * + *+ * Author: robertvokac + *
*/ -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); + } + } diff --git a/src/test/java/com/pixelgamelibrary/api/graphics/ColorTest.java b/src/test/java/com/pixelgamelibrary/api/graphics/ColorTest.java new file mode 100644 index 0000000..2d176ad --- /dev/null +++ b/src/test/java/com/pixelgamelibrary/api/graphics/ColorTest.java @@ -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); + } +}