/*
* This file is part of NodeBox.
*
* Copyright (C) 2008 Frederik De Bleser (frederik@pandora.be)
*
* NodeBox is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NodeBox is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NodeBox. If not, see <http://www.gnu.org/licenses/>.
*/
package nodebox.graphics;
import java.awt.*;
import static com.google.common.base.Preconditions.checkArgument;
import static nodebox.graphics.MathUtils.clamp;
public final class Color implements Cloneable {
public enum Mode {
RGB, HSB, CMYK
}
public static final Color BLACK = new Color(0);
public static final Color WHITE = new Color(1);
private final double r, g, b, a;
private final double h, s, v;
private transient java.awt.Color awtColor = null;
public static Color fromHSB(double hue, double saturation, double brightness) {
return new Color(hue, saturation, brightness, Mode.HSB);
}
public static Color fromHSB(double hue, double saturation, double brightness, double alpha) {
return new Color(hue, saturation, brightness, alpha, Mode.HSB);
}
public static Color valueOf(String hex) {
return new Color(hex);
}
/**
* Create an empty (black) color object.
*/
public Color() {
this(0, 0, 0, 1.0, Mode.RGB);
}
/**
* Create a new color with the given grayscale value.
*
* @param v the gray component.
*/
public Color(double v) {
this(v, v, v, 1.0, Mode.RGB);
}
/**
* Create a new color with the given grayscale and alpha value.
*
* @param v the grayscale value.
* @param a the alpha value.
*/
public Color(double v, double a) {
this(v, v, v, a, Mode.RGB);
}
/**
* Create a new color with the the given R/G/B value.
*
* @param x the red or hue component.
* @param y the green or saturation component.
* @param z the blue or brightness component.
* @param m the specified color mode.
*/
public Color(double x, double y, double z, Mode m) {
this(x, y, z, 1.0, m);
}
/**
* Create a new color with the the given R/G/B value.
*
* @param r the red component.
* @param g the green component.
* @param b the blue component.
*/
public Color(double r, double g, double b) {
this(r, g, b, 1.0, Mode.RGB);
}
/**
* Create a new color with the the given R/G/B/A or H/S/B/A value.
*
* @param r the red component.
* @param g the green component.
* @param b the blue component.
* @param a the alpha component.
*/
public Color(double r, double g, double b, double a) {
this(r, g, b, a, Mode.RGB);
}
/**
* Create a new color with the the given R/G/B/A or H/S/B/A value.
*
* @param x the red or hue component.
* @param y the green or saturation component.
* @param z the blue or brightness component.
* @param a the alpha component.
* @param m the specified color mode.
*/
public Color(double x, double y, double z, double a, Mode m) {
checkArgument(m != Mode.CMYK, "CMYK not supported");
switch (m) {
case CMYK:
throw new RuntimeException("CMYK color mode is not implemented yet.");
case HSB:
this.h = clamp(x);
this.s = clamp(y);
this.v = clamp(z);
this.a = clamp(a);
double[] rgb = updateRGB();
this.r = rgb[0];
this.g = rgb[1];
this.b = rgb[2];
//updateCMYK();
break;
case RGB:
default:
this.r = clamp(x);
this.g = clamp(y);
this.b = clamp(z);
this.a = clamp(a);
double[] hsb = updateHSB();
this.h = hsb[0];
this.s = hsb[1];
this.v = hsb[2];
//updateCMYK();
break;
}
}
public Color(String colorName) {
if (!colorName.startsWith("#")) {
throw new IllegalArgumentException("The given value '" + colorName + "' is not of the format #112233.");
}
int r255, g255, b255, a255 = 255;
if (colorName.length() == 4) { // #123
r255 = Integer.parseInt(colorName.substring(1, 2) + colorName.substring(1, 2), 16);
g255 = Integer.parseInt(colorName.substring(2, 3) + colorName.substring(2, 3), 16);
b255 = Integer.parseInt(colorName.substring(3, 4) + colorName.substring(3, 4), 16);
} else if (colorName.length() == 5) { // #123f
r255 = Integer.parseInt(colorName.substring(1, 2) + colorName.substring(1, 2), 16);
g255 = Integer.parseInt(colorName.substring(2, 3) + colorName.substring(2, 3), 16);
b255 = Integer.parseInt(colorName.substring(3, 4) + colorName.substring(3, 4), 16);
a255 = Integer.parseInt(colorName.substring(4, 5) + colorName.substring(4, 5), 16);
} else if (colorName.length() == 7) { // #112233
r255 = Integer.parseInt(colorName.substring(1, 3), 16);
g255 = Integer.parseInt(colorName.substring(3, 5), 16);
b255 = Integer.parseInt(colorName.substring(5, 7), 16);
} else if (colorName.length() == 9) { // #112233ff
r255 = Integer.parseInt(colorName.substring(1, 3), 16);
g255 = Integer.parseInt(colorName.substring(3, 5), 16);
b255 = Integer.parseInt(colorName.substring(5, 7), 16);
a255 = Integer.parseInt(colorName.substring(7, 9), 16);
} else {
throw new IllegalArgumentException("The given value '" + colorName + "' is not of the format #112233.");
}
this.r = r255 / 255.0;
this.g = g255 / 255.0;
this.b = b255 / 255.0;
this.a = a255 / 255.0;
double[] hsb = updateHSB();
this.h = hsb[0];
this.s = hsb[1];
this.v = hsb[2];
//updateCMYK();
}
/**
* Create a new color with the the given color.
* <p/>
* The color object is cloned; you can change the original afterwards.
* If the color object is null, the new color is turned off (same as nocolor).
*
* @param color the color object.
*/
public Color(java.awt.Color color) {
this(color.getRed() / 255.0, color.getGreen() / 255.0, color.getBlue() / 255.0, color.getAlpha() / 255.0);
}
/**
* Create a new color with the the given color.
* <p/>
* The color object is cloned; you can change the original afterwards.
* If the color object is null, the new color is turned off (same as nocolor).
*
* @param other the color object.
*/
public Color(Color other) {
this(other.r, other.g, other.b, other.a);
}
public double getRed() {
return r;
}
public double getR() {
return r;
}
public double getGreen() {
return g;
}
public double getG() {
return g;
}
public double getBlue() {
return b;
}
public double getB() {
return b;
}
public double getAlpha() {
return a;
}
public double getA() {
return a;
}
public boolean isVisible() {
return a > 0.0;
}
public double getHue() {
return h;
}
public double getH() {
return h;
}
public double getSaturation() {
return s;
}
public double getS() {
return s;
}
public double getBrightness() {
return v;
}
public double getV() {
return v;
}
private double[] updateRGB() {
if (s == 0)
return new double[]{this.v, this.v, this.v};
else {
double h = this.h;
if (this.h == 1.0)
h = 0.999998;
double s = this.s;
double v = this.v;
double f, p, q, t;
h = h / (60.0 / 360);
int i = (int) Math.floor(h);
f = h - i;
p = v * (1 - s);
q = v * (1 - s * f);
t = v * (1 - s * (1 - f));
double rgb[];
if (i == 0)
rgb = new double[]{v, t, p};
else if (i == 1)
rgb = new double[]{q, v, p};
else if (i == 2)
rgb = new double[]{p, v, t};
else if (i == 3)
rgb = new double[]{p, q, v};
else if (i == 4)
rgb = new double[]{t, p, v};
else
rgb = new double[]{v, p, q};
return new double[]{rgb[0], rgb[1], rgb[2]};
}
}
private double[] updateHSB() {
double h = 0;
double s = 0;
double v = Math.max(Math.max(r, g), b);
double d = v - Math.min(Math.min(r, g), b);
if (v != 0)
s = d / v;
if (s != 0) {
if (r == v)
h = 0 + (g - b) / d;
else if (g == v)
h = 2 + (b - r) / d;
else
h = 4 + (r - g) / d;
}
h = h * (60.0 / 360);
if (h < 0)
h = h + 1;
return new double[]{h, s, v};
}
private void updateCMYK() {
// TODO: implement
}
public java.awt.Color getAwtColor() {
// We don't return the cached awtColor here, since java.awt.Color is mutable.
return new java.awt.Color((float) getRed(), (float) getGreen(), (float) getBlue(), (float) getAlpha());
}
public void set(Graphics2D g) {
if (awtColor == null) {
awtColor = new java.awt.Color((float) getRed(), (float) getGreen(), (float) getBlue(), (float) getAlpha());
}
g.setColor(awtColor);
}
@Override
public Color clone() {
return new Color(this);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Color)) return false;
Color other = (Color) obj;
// Because of the conversion to/from hex, we can have rounding errors.
// Therefore, we only compare what we can store, i.e. values in the 0-255 range.
return Math.round(r * 255) == Math.round(other.r * 255)
&& Math.round(g * 255) == Math.round(other.g * 255)
&& Math.round(b * 255) == Math.round(other.b * 255)
&& Math.round(a * 255) == Math.round(other.a * 255);
}
/**
* Parse a hexadecimal value and return a Color object.
* <p/>
* The value needs to have four components. (R,G,B,A)
*
* @param value the hexadecimal color value, e.g. #995423ff
* @return a Color object.
*/
public static Color parseColor(String value) {
return new Color(value);
}
private String paddedHexString(int v) {
String s = Integer.toHexString(v);
if (s.length() == 1) {
return "0" + s;
} else if (s.length() == 2) {
return s;
} else {
throw new AssertionError("Value too large (must be between 0-255, was " + v + ").");
}
}
/**
* Returns the color as a 8-bit hexadecimal value, e.g. #ae45cdff
*
* @return the color as a 8-bit hexadecimal value
*/
@Override
public String toString() {
int r256 = (int) Math.round(r * 255);
int g256 = (int) Math.round(g * 255);
int b256 = (int) Math.round(b * 255);
int a256 = (int) Math.round(a * 255);
return "#"
+ paddedHexString(r256)
+ paddedHexString(g256)
+ paddedHexString(b256)
+ paddedHexString(a256);
}
public String toCSS() {
if (!isVisible()) {
return "none";
}
StringBuilder sb = new StringBuilder();
int r256 = (int) Math.round(r * 255);
int g256 = (int) Math.round(g * 255);
int b256 = (int) Math.round(b * 255);
if (a == 1.0) {
return "#"
+ paddedHexString(r256)
+ paddedHexString(g256)
+ paddedHexString(b256);
} else {
return String.format("rgba(%d,%d,%d,%.2f)", r256, g256, b256, a);
}
}
}