/*
* Copyright (C) 2010 Markus Echterhoff <tam@edu.uni-klu.ac.at>
*
* This file is part of EvoPaint.
*
* EvoPaint 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.
*
* This program 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 EvoPaint. If not, see <http://www.gnu.org/licenses/>.
*/
package evopaint.pixel;
import evopaint.interfaces.IRandomNumberGenerator;
import evopaint.pixel.rulebased.interfaces.IHTML;
import java.awt.Color;
import java.io.Serializable;
// hsb color space is a cylinder with
// radius = saturation, angle = hue and height = brightness
/**
*
* @author Markus Echterhoff <tam@edu.uni-klu.ac.at>
*/
public class PixelColor implements IHTML, Serializable {
private float hue;
private float saturation;
private float brightness;
public float[] getHSB() {
return new float [] {hue, saturation, brightness};
}
public float getBrightness() {
return brightness;
}
public float getHue() {
return hue;
}
public float getSaturation() {
return saturation;
}
public void setHSB(float hue, float saturation, float brightness) {
this.hue = hue;
this.saturation = saturation;
this.brightness = brightness;
}
public void setInteger(int integer) {
float [] hsb = null;
hsb = Color.RGBtoHSB((integer >> 16) & 0xFF, (integer >> 8) & 0xFF, integer & 0xFF, hsb);
this.hue = hsb[0];
this.saturation = hsb[1];
this.brightness = hsb[2];
}
public int getInteger() {
return Color.HSBtoRGB(hue, saturation, brightness);
}
public void setHSB(float[] hsb) {
this.hue = hsb[0];
this.saturation = hsb[1];
this.brightness = hsb[2];
}
public void setColor(PixelColor theirColor) {
hue = theirColor.hue;
saturation = theirColor.saturation;
brightness = theirColor.brightness;
}
@Override
public String toString() {
return "#" + Integer.toHexString(Color.HSBtoRGB(hue, saturation, brightness)).substring(2).toUpperCase();
}
public String toHTML() {
String s = toString();
return "<span style='background: " + s + ";" + (brightness < 0.5 ? "color: #FFFFFF;" : "") + "'>" + s + "</span>";
}
public double distanceTo(PixelColor theirColor, ColorDimensions dimensions) {
double distance = 0;
if (dimensions.hue && dimensions.saturation && dimensions.brightness) {
// this is difficult. we need to come up with a distance that
// is at least somewhat comparable
// to what we percieve as a "distance" between colors...
// rule out black and white for hue comparison for the default
// hue is red which will lead to a wrong distance (in most cases)
if (saturation == 0f || brightness == 0f ||
theirColor.saturation == 0f || theirColor.brightness == 0f) {
return (distanceLinear(saturation, theirColor.saturation) +
distanceLinear(brightness, theirColor.brightness)
) / 2;
}
// hue is everyting. except for when we have very bright
// or very dark colors
// the 1:4 division between saturation and brightness is because
// if you look at the SB colorspace of a given H, you will
// find that roughly 1/2 of it is "darker", while the other
// half is a gradient from the color to white giving each
// "colorful" and "colorless" roughly 1/4 of the total
// color space
double maxS = Math.max(saturation, theirColor.saturation);
double maxB = Math.max(brightness, theirColor.brightness);
double hueWeight = 1d - ((1d - maxS) * 0.25d + (1d - maxB) * 0.75d) / 2d;
hueWeight = hueWeight >= 0.15d ? hueWeight : 0.15d; // hue always counts a little
hueWeight = hueWeight <= 0.85d ? hueWeight : 0.85d; // but never too much
//System.out.println("hue weight is: " + hueWeight);
distance = distanceCyclic(hue, theirColor.hue)
* hueWeight;
//System.out.println("hue distance: " + distanceCyclic(hue, theirColor.hue));
distance = distance
+ distanceLinear(saturation, theirColor.saturation)
* (1d - hueWeight) / 2d;
distance = distance
+ distanceLinear(brightness, theirColor.brightness)
* (1d - hueWeight) / 2d;
//System.out.println("distance between " + Integer.toHexString(getInteger()).substring(2).toUpperCase() +
// " and " + Integer.toHexString(theirColor.getInteger()).substring(2).toUpperCase() +
// " is " + distance);
} else if (dimensions.hue && dimensions.saturation) {
// rule out black and white for hue comparison for the default
// hue is red which will lead to a wrong distance (in most cases)
if (saturation == 0f || brightness == 0f ||
theirColor.saturation == 0f || theirColor.brightness == 0f) {
return distanceLinear(saturation, theirColor.saturation);
}
// see HSB
double maxS = Math.max(saturation, theirColor.saturation);
double maxB = Math.max(brightness, theirColor.brightness);
double hueWeight = 1d - ((1d - maxS) * 0.25d + (1d - maxB) * 0.75d) / 2d;
hueWeight = hueWeight >= 0.15d ? hueWeight : 0.15d; // hue always counts a little
hueWeight = hueWeight <= 0.85d ? hueWeight : 0.85d; // but never too much
distance = distanceCyclic(hue, theirColor.hue)
* hueWeight;
distance = distance
+ distanceLinear(saturation, theirColor.saturation)
* (1d - hueWeight);
} else if (dimensions.hue && dimensions.brightness) {
// rule out black and white for hue comparison for the default
// hue is red which will lead to a wrong distance (in most cases)
if (saturation == 0f || brightness == 0f ||
theirColor.saturation == 0f || theirColor.brightness == 0f) {
return distanceLinear(brightness, theirColor.brightness);
}
// see HSB
double maxS = Math.max(saturation, theirColor.saturation);
double maxB = Math.max(brightness, theirColor.brightness);
double hueWeight = 1d - ((1d - maxS) * 0.25d + (1d - maxB) * 0.75d) / 2d;
hueWeight = hueWeight >= 0.15d ? hueWeight : 0.15d; // hue always counts a little
hueWeight = hueWeight <= 0.85d ? hueWeight : 0.85d; // but never too much
distance = distanceCyclic(hue, theirColor.hue)
* hueWeight;
distance = distance
+ distanceLinear(brightness, theirColor.brightness)
* (1d - hueWeight);
} else if (dimensions.saturation && dimensions.brightness) {
// finally, something easy (I hope)
distance = (distanceLinear(saturation, theirColor.saturation) +
distanceLinear(brightness, theirColor.brightness)
) / 2;
} else if (dimensions.hue) {
distance = distanceCyclic(hue, theirColor.hue);
} else if (dimensions.saturation) {
distance = distanceLinear(saturation, theirColor.saturation);
} else if (dimensions.brightness) {
distance = distanceLinear(brightness, theirColor.brightness);
} //else { // cannot assert anymore, because mutation will produce
// no-dimension comparisons. so.. a distance is always 0 in
// that case. worx4me
// assert(false);
//}
return distance > 1d ? 1d : distance; // we have some rounding problems
}
private static double distanceCyclic(double a, double b) {
//System.out.print("distance between " + a + " and " + b + " is ");
double delta = Math.abs(a - b);
//System.out.println(delta);
return Math.min(delta, 1 - delta) * 2; // * 2 to norm to max distance 1
}
private static double distanceLinear(double a, double b) {
return Math.abs(a - b);
}
public int countGenes() {
return 3;
}
public void mutate(IRandomNumberGenerator rng) {
switch (rng.nextPositiveInt(3)) {
case 0: hue = rng.nextFloat();
break;
case 1: saturation = rng.nextFloat();
break;
case 2: brightness = rng.nextFloat();
break;
}
}
public void mutate(int mutatedGene, IRandomNumberGenerator rng) {
assert (mutatedGene >= 0 && mutatedGene <= 2);
switch (mutatedGene) {
case 0: hue = rng.nextFloat();
break;
case 1: saturation = rng.nextFloat();
break;
case 2: brightness = rng.nextFloat();
break;
}
}
public void mixWith(PixelColor theirColor, float theirShare, ColorDimensions dimensions) {
//System.out.print(toString() + " + " + theirColor.toString() + " = ");
if (dimensions.hue && dimensions.saturation && dimensions.brightness) {
// if we are black, white or grey, just take their hue
if (saturation == 0f || brightness == 0f) {
hue = theirColor.hue;
}
// else if they also have hue, then we mix
else if (theirColor.saturation != 0f && theirColor.brightness != 0f) {
hue = (float)mixCyclic(hue, theirColor.hue, theirShare);
}
// else they are color less and we keep your hue
saturation = (float)mixLinear(saturation, theirColor.saturation, theirShare);
brightness = (float)mixLinear(brightness, theirColor.brightness, theirShare);
} else if (dimensions.hue && dimensions.saturation) {
// if we are black, white or grey, just take their hue
if (saturation == 0f || brightness == 0f) {
hue = theirColor.hue;
}
// else if they also have hue, then we mix
else if (theirColor.saturation != 0f && theirColor.brightness != 0f) {
hue = (float)mixCyclic(hue, theirColor.hue, theirShare);
}
// else they are color less and we keep your hue
saturation = (float)mixLinear(saturation, theirColor.saturation, theirShare);
} else if (dimensions.hue && dimensions.brightness) {
// if we are black, white or grey, just take their hue
if (saturation == 0f || brightness == 0f) {
hue = theirColor.hue;
}
// else if they also have hue, then we mix
else if (theirColor.saturation != 0f && theirColor.brightness != 0f) {
hue = (float)mixCyclic(hue, theirColor.hue, theirShare);
}
// else they are color less and we keep your hue
brightness = (float)mixLinear(brightness, theirColor.brightness, theirShare);
} else if (dimensions.saturation && dimensions.brightness) {
saturation = (float)mixLinear(saturation, theirColor.saturation, theirShare);
brightness = (float)mixLinear(brightness, theirColor.brightness, theirShare);
} else if (dimensions.hue) {
hue = (float)mixCyclic(hue, theirColor.hue, theirShare);
} else if (dimensions.saturation) {
saturation = (float)mixLinear(saturation, theirColor.saturation, theirShare);
} else if (dimensions.brightness) {
brightness = (float)mixLinear(brightness, theirColor.brightness, theirShare);
} // else { if we mix in no dimensions, we do not mix at all
// assert(false);
//}
//System.out.println(toString());
}
private static double mixCyclic(double a, double b, double shareOfB) {
double ret = 0.0f;
double min = Math.min(a, b);
double delta = Math.abs(a - b);
boolean isWrapped = false;
if (delta > 1 - delta) {
isWrapped = true;
delta = 1 - delta;
}
if (min == b) {
ret = isWrapped ? min - delta * (1 - shareOfB) : min + delta * (1 - shareOfB);
} else {
ret = isWrapped ? min - delta * shareOfB : min + delta * shareOfB;
}
if (ret < 0) {
ret = ret + 1;
}
return ret;
}
private static double mixLinear(double us, double them, double theirShare) {
double min = Math.min(us, them);
double delta = Math.abs(us - them);
if (min == them) {
return min + delta * (1 - theirShare);
} else {
return min + delta * theirShare;
}
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PixelColor other = (PixelColor) obj;
if (this.hue != other.hue) {
return false;
}
if (this.saturation != other.saturation) {
return false;
}
if (this.brightness != other.brightness) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 53 * hash + Float.floatToIntBits(this.hue);
hash = 53 * hash + Float.floatToIntBits(this.saturation);
hash = 53 * hash + Float.floatToIntBits(this.brightness);
return hash;
}
public PixelColor(float hue, float saturation, float brightness) {
this.hue = hue;
this.saturation = saturation;
this.brightness = brightness;
}
public PixelColor(int integer) {
setInteger(integer);
}
public PixelColor(PixelColor pixelColor) {
this.hue = pixelColor.hue;
this.saturation = pixelColor.saturation;
this.brightness = pixelColor.brightness;
}
public PixelColor(IRandomNumberGenerator rng) {
this(rng.nextPositiveInt());
}
}