package squidpony;
import squidpony.panel.IColoredString;
import squidpony.squidmath.RNG;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* How to manage colors, making sure that a color is allocated at most once.
*
* <p>
* If you aren't using squidlib's gdx part, you should use this interface (and
* the {@link Skeleton} implementation), because it caches instances.
* </p>
*
* <p>
* If you are using squidlib's gdx part, you should use this interface (and the
* {@code SquidColorCenter} implementation) if:
*
* <ul>
* <li>You don't want to use preallocated instances (if you do, check out
* {@code squidpony.squidgrid.gui.Colors})</li>
* <li>You don't want to use named colors (if you do, check out
* {@code com.badlogic.gdx.graphics.Colors})</li>
* <li>You don't like libgdx's Color representation (components as floats
* in-between 0 and 1) but prefer components within 0 (inclusive) and 256
* (exclusive); and don't mind the overhead of switching the representations. My
* personal opinion is that the overhead doesn't matter w.r.t other intensive
* operations that we have in roguelikes (path finding).</li>
* </ul>
*
* @author smelC
*
* @param <T>
* The concrete type of colors
*/
public interface IColorCenter<T> {
/**
* @param red
* The red component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @param green
* The green component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @param blue
* The blue component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @param opacity
* The alpha component. In-between 0 (inclusive) and 256
* (exclusive). Larger values mean more opacity; 0 is clear.
* @return A possibly transparent color.
*/
T get(int red, int green, int blue, int opacity);
/**
* @param red
* The red component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @param green
* The green component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @param blue
* The blue component. For screen colors, in-between 0 (inclusive)
* and 256 (exclusive).
* @return An opaque color.
*/
T get(int red, int green, int blue);
/**
*
* @param hue The hue of the desired color from 0.0 (red, inclusive) towards orange, then
* yellow, and eventually to purple before looping back to almost the same red
* (1.0, exclusive)
* @param saturation the saturation of the color from 0.0 (a grayscale color; inclusive)
* to 1.0 (a bright color, exclusive)
* @param value the value (essentially lightness) of the color from 0.0 (black,
* inclusive) to 1.0 (inclusive) for screen colors or arbitrarily high
* for HDR colors.
* @param opacity the alpha component as a float; 0.0f is clear, 1.0f is opaque.
* @return a possibly transparent color
*/
T getHSV(float hue, float saturation, float value, float opacity);
/**
*
* @param hue The hue of the desired color from 0.0 (red, inclusive) towards orange, then
* yellow, and eventually to purple before looping back to almost the same red
* (1.0, exclusive)
* @param saturation the saturation of the color from 0.0 (a grayscale color; inclusive)
* to 1.0 (a bright color, exclusive)
* @param value the value (essentially lightness) of the color from 0.0 (black,
* inclusive) to 1.0 (inclusive) for screen colors or arbitrarily high
* for HDR colors.
* @return an opaque color
*/
T getHSV(float hue, float saturation, float value);
/**
* @return Opaque white.
*/
T getWhite();
/**
* @return Opaque black.
*/
T getBlack();
/**
* @return The fully transparent color.
*/
T getTransparent();
/**
* @param rng an RNG from SquidLib.
* @param opacity
* The alpha component. In-between 0 (inclusive) and 256
* (exclusive). Larger values mean more opacity; 0 is clear.
* @return A random color, except for the alpha component.
*/
T getRandom(RNG rng, int opacity);
/**
* @param c a concrete color
* @return The red component. For screen colors, in-between 0 (inclusive) and 256 (exclusive).
*/
int getRed(T c);
/**
* @param c a concrete color
* @return The green component. For screen colors, in-between 0 (inclusive) and 256
* (exclusive).
*/
int getGreen(T c);
/**
* @param c a concrete color
* @return The blue component. For screen colors, in-between 0 (inclusive) and 256 (exclusive).
*/
int getBlue(T c);
/**
* @param c a concrete color
* @return The alpha component. In-between 0 (inclusive) and 256
* (exclusive).
*/
int getAlpha(T c);
/**
*
* @param c a concrete color
* @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and
* eventually to purple before looping back to almost the same red (1.0, exclusive)
*/
float getHue(T c);
/**
*
* @param c a concrete color
* @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a
* bright color, exclusive)
*/
float getSaturation(T c);
/**
*
* @param c a concrete color
* @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to
* 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors.
*/
float getValue(T c);
/**
* @param c
* @return The color that {@code this} shows when {@code c} is requested. May be {@code c} itself.
*/
T filter(T c);
/**
* @param ics
* @return {@code ics} filtered according to {@link #filter(Object)}. May be
* {@code ics} itself if unchanged.
*/
IColoredString<T> filter(IColoredString<T> ics);
/**
* Gets a copy of t and modifies it to make a shade of gray with the same brightness.
* The doAlpha parameter causes the alpha to be considered in the calculation of brightness and also changes the
* returned alpha of the color.
* Not related to reified types or any usage of "reify."
* @param t a T to copy; only the copy will be modified
* @param doAlpha
* Whether to include (and hereby change) the alpha component.
* @return A monochromatic variation of {@code t}.
*/
T greify(/*@Nullable*/ T t, boolean doAlpha);
/**
* Gets the linear interpolation from Color start to Color end, changing by the fraction given by change.
* @param start the initial color T
* @param end the "target" color T
* @param change the degree to change closer to end; a change of 0.0f produces start, 1.0f produces end
* @return a new T between start and end
*/
T lerp(T start, T end, float change);
/**
* Gets a fully-desaturated version of the given color (keeping its brightness, but making it grayscale).
* Keeps alpha the same; if you want alpha to be considered (and brightness to be calculated differently), then
* you can use greify() in this class instead.
* @param color the color T to desaturate (will not be modified)
* @return the grayscale version of color
*/
T desaturated(T color);
/**
* Brings a color closer to grayscale by the specified degree and returns the new color (desaturated somewhat).
* Alpha is left unchanged.
* @param color the color T to desaturate
* @param degree a float between 0.0f and 1.0f; more makes it less colorful
* @return the desaturated (and if a filter is used, also filtered) new color T
*/
T desaturate(T color, float degree);
/**
* Fully saturates color (makes it a vivid color like red or green and less gray) and returns the modified copy.
* Leaves alpha unchanged.
* @param color the color T to saturate (will not be modified)
* @return the saturated version of color
*/
T saturated(T color);
/**
* Saturates color (makes it closer to a vivid color like red or green and less gray) by the specified degree and
* returns the new color (saturated somewhat). If this is called on a color that is very close to gray, this does
* not necessarily return a specific color, but most implementations will treat a hue of 0 as red.
* @param color the color T to saturate
* @param degree a float between 0.0f and 1.0f; more makes it more colorful
* @return the saturated (and if a filter is used, also filtered) new color
*/
public T saturate(T color, float degree);
/**
* Finds a gradient with 16 steps going from fromColor to toColor,
* both included in the gradient.
* @param fromColor the color to start with, included in the gradient
* @param toColor the color to end on, included in the gradient
* @return an ArrayList composed of the blending steps from fromColor to toColor, with length equal to steps
*/
public ArrayList<T> gradient(T fromColor, T toColor);
/**
* Finds a gradient with the specified number of steps going from fromColor to toColor,
* both included in the gradient.
* @param fromColor the color to start with, included in the gradient
* @param toColor the color to end on, included in the gradient
* @param steps the number of elements to use in the gradient
* @return an ArrayList composed of the blending steps from fromColor to toColor, with length equal to steps
*/
public ArrayList<T> gradient(T fromColor, T toColor, int steps);
/**
* A skeletal implementation of {@link IColorCenter}.
*
* @author smelC
*
* @param <T> a concrete color type
*/
abstract class Skeleton<T> implements IColorCenter<T> {
private final Map<Long, T> cache = new HashMap<>(256);
protected /*Nullable*/ IFilter<T> filter;
/**
* @param filter
* The filter to use, or {@code null} for no filter.
*/
protected Skeleton(/*Nullable*/ IFilter<T> filter) {
this.filter = filter;
}
/**
* It clears the cache. You may need to do this to limit the cache to the colors used in a specific section.
* This is also useful if a Filter changes what colors it should return on a frame-by-frame basis; in that case,
* you can call clearCache() at the start or end of a frame to ensure the next frame gets different colors.
*/
public void clearCache()
{
cache.clear();
}
/**
* The actual cache is not public, but there are cases where you may want to know how many different colors are
* actually used in a frame or a section of the game. If the cache was emptied (which might be from calling
* {@link #clearCache()}), some colors were requested, then this is called, the returned int should be the
* count of distinct colors this IColorCenter had created and cached; duplicates won't be counted twice.
* @return
*/
public int cacheSize()
{
return cache.size();
}
/**
* You may want to copy colors between IColorCenter instances that have different create() methods -- and as
* such, will have different values for the same keys in the cache. This allows you to copy the cache from other
* into this Skeleton, but using this Skeleton's create() method.
* @param other another Skeleton of the same type that will have its cache copied into this Skeleton
*/
public void copyCache(Skeleton<T> other)
{
for (Map.Entry<Long, T> k : other.cache.entrySet())
{
cache.put(k.getKey(), create(getRed(k.getValue()), getGreen(k.getValue()), getBlue(k.getValue()),
getAlpha(k.getValue())));
}
}
/**
* If you're changing the filter, you should likely call
* {@link #clearCache()}.
*
* @param filter
* The filter to use, or {@code null} to turn filtering OFF.
* @return {@code this}
*/
public Skeleton<T> setFilter(IFilter<T> filter) {
this.filter = filter;
return this;
}
protected transient Long tempValue;
@Override
public T get(int red, int green, int blue, int opacity) {
tempValue = getUniqueIdentifier(red, green, blue, opacity);
T t = cache.get(tempValue);
if (t == null) {
/* Miss */
t = create(red, green, blue, opacity);
/* Put in cache */
cache.put(tempValue, t);
}
return t;
}
@Override
public T get(int red, int green, int blue) {
return get(red, green, blue, 255);
}
@Override
public T getHSV(float hue, float saturation, float value, float opacity) {
if ( saturation < 0.0001 ) //HSV from 0 to 1
{
return get(Math.round(value * 255), Math.round(value * 255), Math.round(value * 255),
Math.round(opacity * 255));
}
else
{
float h = hue * 6f;
if ( h >= 6 ) h = 0; //H must be < 1
int i = (int)h; //Or ... var_i = floor( var_h )
float a = value * ( 1 - saturation );
float b = value * ( 1 - saturation * ( h - i ) );
float c = value * ( 1 - saturation * ( 1 - ( h - i ) ) );
switch (i)
{
case 0: return get(Math.round(value * 255), Math.round(c * 255), Math.round(a * 255),
Math.round(opacity * 255));
case 1: return get(Math.round(b * 255), Math.round(value * 255), Math.round(a * 255),
Math.round(opacity * 255));
case 2: return get(Math.round(a * 255), Math.round(value * 255), Math.round(c * 255),
Math.round(opacity * 255));
case 3: return get(Math.round(a * 255), Math.round(b * 255), Math.round(value * 255),
Math.round(opacity * 255));
case 4: return get(Math.round(c * 255), Math.round(a * 255), Math.round(value * 255),
Math.round(opacity * 255));
default: return get(Math.round(value * 255), Math.round(a * 255), Math.round(b * 255),
Math.round(opacity * 255));
}
}
}
@Override
public T getHSV(float hue, float saturation, float value) {
return getHSV(hue, saturation, value, 1.0f);
}
@Override
public T getWhite() {
return get(255, 255, 255, 255);
}
@Override
public T getBlack() {
return get(0, 0, 0, 255);
}
@Override
public T getTransparent() {
return get(0, 0, 0, 0);
}
@Override
public T getRandom(RNG rng, int opacity) {
return get(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256), opacity);
}
/**
* @param r the red component in 0.0 to 1.0 range, typically
* @param g the green component in 0.0 to 1.0 range, typically
* @param b the blue component in 0.0 to 1.0 range, typically
* @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a
* bright color, exclusive)
*/
public float getSaturation(float r, float g, float b) {
float min = Math.min(Math.min(r, g ), b); //Min. value of RGB
float max = Math.max(Math.max(r, g), b); //Min. value of RGB
float delta = max - min; //Delta RGB value
float saturation;
if ( delta < 0.0001f ) //This is a gray, no chroma...
{
saturation = 0;
}
else //Chromatic data...
{
saturation = delta / max;
}
return saturation;
}
/**
* @param c a concrete color
* @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a
* bright color, exclusive)
*/
@Override
public float getSaturation(T c) {
return getSaturation(getRed(c) / 255f, getGreen(c) / 255f, getBlue(c) / 255f);
}
/**
* @param r the red component in 0.0 to 1.0 range, typically
* @param g the green component in 0.0 to 1.0 range, typically
* @param b the blue component in 0.0 to 1.0 range, typically
* @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to
* 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors.
*/
public float getValue(float r, float g, float b)
{
return Math.max(Math.max(r, g), b);
}
/**
* @param c a concrete color
* @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to
* 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors.
*/
@Override
public float getValue(T c) {
float r = getRed(c) / 255f; //RGB from 0 to 255
float g = getGreen(c) / 255f;
float b = getBlue(c) / 255f;
return Math.max(Math.max(r, g), b);
}
/**
* @param r the red component in 0.0 to 1.0 range, typically
* @param g the green component in 0.0 to 1.0 range, typically
* @param b the blue component in 0.0 to 1.0 range, typically
* @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and
* eventually to purple before looping back to almost the same red (1.0, exclusive)
*/
public float getHue(float r, float g, float b) {
float min = Math.min(Math.min(r, g ), b); //Min. value of RGB
float max = Math.max(Math.max(r, g), b); //Min. value of RGB
float delta = max - min; //Delta RGB value
float hue;
if ( delta < 0.0001f ) //This is a gray, no chroma...
{
hue = 0; //HSV results from 0 to 1
}
else //Chromatic data...
{
float rDelta = ( ( ( max - r ) / 6f ) + ( delta / 2f ) ) / delta;
float gDelta = ( ( ( max - g ) / 6f ) + ( delta / 2f ) ) / delta;
float bDelta = ( ( ( max - b ) / 6f ) + ( delta / 2f ) ) / delta;
if ( r == max ) hue = bDelta - gDelta;
else if ( g == max ) hue = ( 1f / 3f ) + rDelta - bDelta;
else hue = ( 2f / 3f ) + gDelta - rDelta;
if ( hue < 0 ) hue += 1f;
else if ( hue > 1 ) hue -= 1;
}
return hue;
}
/**
* @param c a concrete color
* @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and
* eventually to purple before looping back to almost the same red (1.0, exclusive)
*/
@Override
public float getHue(T c) {
return getHue(getRed(c) / 255f, getGreen(c) / 255f, getBlue(c) / 255f);
}
@Override
public T filter(T c)
{
return c == null ? c : get(getRed(c), getGreen(c), getBlue(c), getAlpha(c));
}
@Override
public IColoredString<T> filter(IColoredString<T> ics) {
/*
* It is common not to have a filter or to have the identity one. To
* avoid always copying strings in this case, we first roll over the
* string to see if there'll be a change.
*
* This is clearly a subjective design choice but my industry
* experience is that minimizing allocations is the thing to do for
* performances, hence I prefer iterating twice to do that.
*/
boolean change = false;
for (IColoredString.Bucket<T> bucket : ics) {
final T in = bucket.getColor();
if (in == null)
continue;
final T out = filter(in);
if (in != out) {
change = true;
break;
}
}
if (change) {
final IColoredString<T> result = IColoredString.Impl.create();
for (IColoredString.Bucket<T> bucket : ics)
result.append(bucket.getText(), filter(bucket.getColor()));
return result;
} else
/* Only one allocation: the iterator, yay \o/ */
return ics;
}
/**
* Gets a copy of t and modifies it to make a shade of gray with the same brightness.
* The doAlpha parameter causes the alpha to be considered in the calculation of brightness and also changes the
* returned alpha of the color.
* Not related to reified types or any usage of "reify."
* @param t a T to copy; only the copy will be modified
* @param doAlpha
* Whether to include (and hereby change) the alpha component.
* @return A monochromatic variation of {@code t}.
*/
@Override
public T greify(T t, boolean doAlpha) {
if (t == null)
/* Cannot do */
return null;
final int red = getRed(t);
final int green = getGreen(t);
final int blue = getBlue(t);
final int alpha = getAlpha(t);
final int rgb = red + green + blue;
final int mean;
final int newAlpha;
if (doAlpha) {
mean = (rgb + alpha) / 4;
newAlpha = mean;
} else {
mean = rgb / 3;
/* No change */
newAlpha = alpha;
}
return get(mean, mean, mean, newAlpha);
}
/**
* Gets the linear interpolation from Color start to Color end, changing by the fraction given by change.
* This implementation tries to work with colors in a way that is as general as possible, using getRed() instead
* of some specific detail that depends on how a color is implemented. Other implementations that specialize in
* a specific type of color may be able to be more efficient.
* @param start the initial color T
* @param end the "target" color T
* @param change the degree to change closer to end; a change of 0.0f produces start, 1.0f produces end
* @return a new T between start and end
*/
public T lerp(T start, T end, float change)
{
if(start == null || end == null)
return null;
final int sr = getRed(start), sg = getGreen(start), sb = getBlue(start), sa = getAlpha(start),
er = getRed(end), eg = getGreen(end), eb = getBlue(end), ea = getAlpha(end);
return get(
(int)(sr + change * (er - sr)),
(int)(sg + change
* (eg - sg)),
(int)(sb + change * (eb - sb)),
(int)(sa + change * (ea - sa))
);
}
/**
* Gets a fully-desaturated version of the given color (keeping its brightness, but making it grayscale).
* Keeps alpha the same; if you want alpha to be considered (and brightness to be calculated differently), then
* you can use greify() in this class instead.
* @param color the color T to desaturate (will not be modified)
* @return the grayscale version of color
*/
public T desaturated(T color)
{
int f = (int)Math.min(255, getRed(color) * 0.299f + getGreen(color) * 0.587f + getBlue(color) * 0.114f);
return get(f, f, f, getAlpha(color));
}
/**
* Brings a color closer to grayscale by the specified degree and returns the new color (desaturated somewhat).
* Alpha is left unchanged.
* @param color the color T to desaturate
* @param degree a float between 0.0f and 1.0f; more makes it less colorful
* @return the desaturated (and if a filter is used, also filtered) new color T
*/
public T desaturate(T color, float degree)
{
return lerp(color, desaturated(color), degree);
}
/**
* Fully saturates color (makes it a vivid color like red or green and less gray) and returns the modified copy.
* Leaves alpha unchanged.
* @param color the color T to saturate (will not be modified)
* @return the saturated version of color
*/
public T saturated(T color)
{
return getHSV(getHue(color), 1f, getValue(color), getAlpha(color));
}
/**
* Saturates color (makes it closer to a vivid color like red or green and less gray) by the specified degree and
* returns the new color (saturated somewhat). If this is called on a color that is very close to gray, this is
* likely to produce a red hue by default (if there's no hue to make vivid, it needs to choose something).
* @param color the color T to saturate
* @param degree a float between 0.0f and 1.0f; more makes it more colorful
* @return the saturated (and if a filter is used, also filtered) new color
*/
public T saturate(T color, float degree)
{
return lerp(color, saturated(color), degree);
}
@Override
public ArrayList<T> gradient(T fromColor, T toColor) {
return gradient(fromColor, toColor, 16);
}
@Override
public ArrayList<T> gradient(T fromColor, T toColor, int steps) {
ArrayList<T> colors = new ArrayList<>((steps > 1) ? steps : 1);
colors.add(filter(fromColor));
if(steps < 2)
return colors;
for (float i = 1; i < steps; i++) {
colors.add(lerp(fromColor, toColor, i / (steps - 1f)));
}
return colors;
}
/**
* Create a concrete instance of the color type given as a type parameter. That's the
* place to use the {@link #filter}.
*
* @param red the red component of the desired color
* @param green the green component of the desired color
* @param blue the blue component of the desired color
* @param opacity the alpha component or opacity of the desired color
* @return a fresh instance of the concrete color type
*/
protected abstract T create(int red, int green, int blue, int opacity);
private long getUniqueIdentifier(int r, int g, int b, int a) {
return ((a & 0xffL) << 48) | ((r & 0xffffL) << 32) | ((g & 0xffffL) << 16) | (b & 0xffffL);
}
}
}