squidpony.IColorCenter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of squidlib-util Show documentation
Show all versions of squidlib-util Show documentation
SquidLib platform-independent logic and utility code. Please refer to
https://github.com/SquidPony/SquidLib .
package squidpony;
import squidpony.panel.IColoredString;
import squidpony.squidmath.CrossHash;
import squidpony.squidmath.IRNG;
import squidpony.squidmath.UnorderedSet;
import java.util.ArrayList;
/**
* How to manage colors, making sure that a color is allocated at most once.
*
*
* If you aren't using squidlib's gdx part, you should use this interface (and
* the {@link Skeleton} implementation), because it caches instances.
*
*
*
* If you are using squidlib's gdx part, you should use this interface (and the
* {@code SquidColorCenter} implementation) if:
*
*
* - You don't want to use preallocated instances (if you do, check out
* {@code squidpony.squidgrid.gui.Colors})
* - You don't want to use named colors (if you do, check out
* {@code com.badlogic.gdx.graphics.Colors})
* - 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).
*
*
* @author smelC
*
* @param
* The concrete type of colors
*/
public interface IColorCenter {
/**
* @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). Values outside this range should be treated as wrapping
* around, so 1.1f and -0.9f would be the same as 0.1f .
* @param saturation the saturation of the color from 0.0 (a grayscale color; inclusive)
* to 1.0 (a bright color, inclusive)
* @param value the value (essentially lightness) of the color from 0.0 (black,
* inclusive) to 1.0 (very bright, inclusive).
* @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 (very bright, inclusive).
* @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(IRNG 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 (very bright, inclusive).
*/
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 filter(IColoredString 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 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 gradient(T fromColor, T toColor, int steps);
/**
* A skeletal implementation of {@link IColorCenter}.
*
* @author smelC
*
* @param a concrete color type
*/
abstract class Skeleton implements IColorCenter {
@SuppressWarnings("unchecked")
protected class GranularHasher implements CrossHash.IHasher{
int[] feed = new int[4];
@Override
public int hash(final Object data) {
if(data == null) return 0;
final long y = (data == feed)
? getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3])
: getUniqueIdentifier((T)data);
final int x = (int)(y ^ y >>> 32) * 0x62BD5;
return x ^ ((x << 17) | (x >>> 15)) ^ ((x << 9) | (x >>> 23));
}
@Override
public boolean areEqual(final Object left, final Object right) {
if(left == right) return true;
if(left != null && right != null)
{
if(left == feed)
{
return getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3]) == getUniqueIdentifier((T)right);
// final long f = getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3]);
// final long i = getUniqueIdentifier((T)right);
// System.out.printf("left 0x%016X, right 0x%016X\n", f, i);
// return f == i;
}
else if(right == feed)
{
return getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3]) == getUniqueIdentifier((T)left);
// final long f = getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3]);
// final long i = getUniqueIdentifier((T)left);
// System.out.printf("left 0x%016X, right 0x%016X\n", i, f);
// return f == i;
}
else
{
return (getUniqueIdentifier((T)left) == getUniqueIdentifier((T)right));
}
}
return false;
// return (left == right) || (left != null && right != null &&
// ((left == feed && getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3]) == getUniqueIdentifier((T)right)) ||
// (right == feed && getUniqueIdentifier((T)left) == getUniqueIdentifier(feed[0], feed[1], feed[2], feed[3])) ||
// (left != feed && right != feed && getUniqueIdentifier((T)left) == getUniqueIdentifier((T)right))));
}
}
protected GranularHasher theHasher = new GranularHasher();
private final UnorderedSet cache = new UnorderedSet<>(256, theHasher);
protected /*Nullable*/ IFilter filter;
/**
* @param filter
* The filter to use, or {@code null} for no filter.
*/
protected Skeleton(/*Nullable*/ IFilter 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 other)
{
cache.addAll(other.cache);
}
/**
* 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 setFilter(IFilter filter) {
this.filter = filter;
return this;
}
@SuppressWarnings("SuspiciousMethodCalls")
@Override
public T get(int red, int green, int blue, int opacity) {
theHasher.feed[0] = red;
theHasher.feed[1] = green;
theHasher.feed[2] = blue;
theHasher.feed[3] = opacity;
T t;
if (cache.contains(theHasher.feed)) {
t = cache.get(theHasher.feed);
}
else
{
/* Miss */
t = create(red, green, blue, opacity);
/* Put in cache */
cache.add(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.0001f ) //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) % 1f) * 6f; // allows negative hue to wrap
int i = (int)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(IRNG 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 (very bright, inclusive).
*/
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 (very bright, inclusive).
*/
@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 ? null : get(getRed(c), getGreen(c), getBlue(c), getAlpha(c));
}
@Override
public IColoredString filter(IColoredString 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 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 result = IColoredString.Impl.create();
for (IColoredString.Bucket 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 gradient(T fromColor, T toColor) {
return gradient(fromColor, toColor, 16);
}
@Override
public ArrayList gradient(T fromColor, T toColor, int steps) {
ArrayList 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);
protected long getUniqueIdentifier(int r, int g, int b, int a) {
return ((a & 0xffL) << 48) | ((r & 0xffL) << 32) | ((g & 0xffL) << 16) | (b & 0xffL);
}
protected long getUniqueIdentifier(T item)
{
return ((getAlpha(item) & 0xffL) << 48) | ((getRed(item) & 0xffL) << 32)
| ((getGreen(item) & 0xffL) << 16) | (getBlue(item) & 0xffL);
}
}
}