001package squidpony; 002 003import squidpony.panel.IColoredString; 004import squidpony.squidmath.RNG; 005 006import java.util.HashMap; 007import java.util.Map; 008 009/** 010 * How to manage colors, making sure that a color is allocated at most once. 011 * 012 * <p> 013 * If you aren't using squidlib's gdx part, you should use this interface (and 014 * the {@link Skeleton} implementation), because it caches instances. 015 * </p> 016 * 017 * <p> 018 * If you are using squidlib's gdx part, you should use this interface (and the 019 * {@code SquidColorCenter} implementation) if: 020 * 021 * <ul> 022 * <li>You don't want to use preallocated instances (if you do, check out 023 * {@code squidpony.squidgrid.gui.Colors})</li> 024 * <li>You don't want to use named colors (if you do, check out 025 * {@code com.badlogic.gdx.graphics.Colors})</li> 026 * <li>You don't like libgdx's Color representation (components as floats 027 * in-between 0 and 1) but prefer components within 0 (inclusive) and 256 028 * (exclusive); and don't mind the overhead of switching the representations. My 029 * personal opinion is that the overhead doesn't matter w.r.t other intensive 030 * operations that we have in roguelikes (path finding).</li> 031 * </ul> 032 * 033 * @author smelC 034 * 035 * @param <T> 036 * The concrete type of colors 037 */ 038public interface IColorCenter<T> { 039 040 /** 041 * @param red 042 * The red component. For screen colors, in-between 0 (inclusive) 043 * and 256 (exclusive). 044 * @param green 045 * The green component. For screen colors, in-between 0 (inclusive) 046 * and 256 (exclusive). 047 * @param blue 048 * The blue component. For screen colors, in-between 0 (inclusive) 049 * and 256 (exclusive). 050 * @param opacity 051 * The alpha component. In-between 0 (inclusive) and 256 052 * (exclusive). Larger values mean more opacity; 0 is clear. 053 * @return A possibly transparent color. 054 */ 055 T get(int red, int green, int blue, int opacity); 056 057 /** 058 * @param red 059 * The red component. For screen colors, in-between 0 (inclusive) 060 * and 256 (exclusive). 061 * @param green 062 * The green component. For screen colors, in-between 0 (inclusive) 063 * and 256 (exclusive). 064 * @param blue 065 * The blue component. For screen colors, in-between 0 (inclusive) 066 * and 256 (exclusive). 067 * @return An opaque color. 068 */ 069 T get(int red, int green, int blue); 070 071 /** 072 * 073 * @param hue The hue of the desired color from 0.0 (red, inclusive) towards orange, then 074 * yellow, and eventually to purple before looping back to almost the same red 075 * (1.0, exclusive) 076 * @param saturation the saturation of the color from 0.0 (a grayscale color; inclusive) 077 * to 1.0 (a bright color, exclusive) 078 * @param value the value (essentially lightness) of the color from 0.0 (black, 079 * inclusive) to 1.0 (inclusive) for screen colors or arbitrarily high 080 * for HDR colors. 081 * @param opacity the alpha component as a float; 0.0f is clear, 1.0f is opaque. 082 * @return a possibly transparent color 083 */ 084 T getHSV(float hue, float saturation, float value, float opacity); 085 086 /** 087 * 088 * @param hue The hue of the desired color from 0.0 (red, inclusive) towards orange, then 089 * yellow, and eventually to purple before looping back to almost the same red 090 * (1.0, exclusive) 091 * @param saturation the saturation of the color from 0.0 (a grayscale color; inclusive) 092 * to 1.0 (a bright color, exclusive) 093 * @param value the value (essentially lightness) of the color from 0.0 (black, 094 * inclusive) to 1.0 (inclusive) for screen colors or arbitrarily high 095 * for HDR colors. 096 * @return an opaque color 097 */ 098 T getHSV(float hue, float saturation, float value); 099 100 /** 101 * @return Opaque white. 102 */ 103 T getWhite(); 104 105 /** 106 * @return Opaque black. 107 */ 108 T getBlack(); 109 110 /** 111 * @return The fully transparent color. 112 */ 113 T getTransparent(); 114 115 /** 116 * @param rng an RNG from SquidLib. 117 * @param opacity 118 * The alpha component. In-between 0 (inclusive) and 256 119 * (exclusive). Larger values mean more opacity; 0 is clear. 120 * @return A random color, except for the alpha component. 121 */ 122 T getRandom(RNG rng, int opacity); 123 124 /** 125 * @param c a concrete color 126 * @return The red component. For screen colors, in-between 0 (inclusive) and 256 (exclusive). 127 */ 128 int getRed(T c); 129 130 /** 131 * @param c a concrete color 132 * @return The green component. For screen colors, in-between 0 (inclusive) and 256 133 * (exclusive). 134 */ 135 int getGreen(T c); 136 137 /** 138 * @param c a concrete color 139 * @return The blue component. For screen colors, in-between 0 (inclusive) and 256 (exclusive). 140 */ 141 int getBlue(T c); 142 143 /** 144 * @param c a concrete color 145 * @return The alpha component. In-between 0 (inclusive) and 256 146 * (exclusive). 147 */ 148 int getAlpha(T c); 149 150 /** 151 * 152 * @param c a concrete color 153 * @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and 154 * eventually to purple before looping back to almost the same red (1.0, exclusive) 155 */ 156 float getHue(T c); 157 158 /** 159 * 160 * @param c a concrete color 161 * @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a 162 * bright color, exclusive) 163 */ 164 float getSaturation(T c); 165 166 /** 167 * 168 * @param c a concrete color 169 * @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to 170 * 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors. 171 */ 172 float getValue(T c); 173 174 /** 175 * @param c 176 * @return The color that {@code this} shows when {@code c} is requested. May be {@code c} itself. 177 */ 178 T filter(T c); 179 180 /** 181 * @param ics 182 * @return {@code ics} filtered according to {@link #filter(Object)}. May be 183 * {@code ics} itself if unchanged. 184 */ 185 IColoredString<T> filter(IColoredString<T> ics); 186 187 /** 188 * Gets a copy of t and modifies it to make a shade of gray with the same brightness. 189 * The doAlpha parameter causes the alpha to be considered in the calculation of brightness and also changes the 190 * returned alpha of the color. 191 * Not related to reified types or any usage of "reify." 192 * @param t a T to copy; only the copy will be modified 193 * @param doAlpha 194 * Whether to include (and hereby change) the alpha component. 195 * @return A monochromatic variation of {@code t}. 196 */ 197 T greify(/*@Nullable*/ T t, boolean doAlpha); 198 199 /** 200 * Gets the linear interpolation from Color start to Color end, changing by the fraction given by change. 201 * @param start the initial color T 202 * @param end the "target" color T 203 * @param change the degree to change closer to end; a change of 0.0f produces start, 1.0f produces end 204 * @return a new T between start and end 205 */ 206 T lerp(T start, T end, float change); 207 /** 208 * Gets a fully-desaturated version of the given color (keeping its brightness, but making it grayscale). 209 * Keeps alpha the same; if you want alpha to be considered (and brightness to be calculated differently), then 210 * you can use greify() in this class instead. 211 * @param color the color T to desaturate (will not be modified) 212 * @return the grayscale version of color 213 */ 214 T desaturated(T color); 215 216 /** 217 * Brings a color closer to grayscale by the specified degree and returns the new color (desaturated somewhat). 218 * Alpha is left unchanged. 219 * @param color the color T to desaturate 220 * @param degree a float between 0.0f and 1.0f; more makes it less colorful 221 * @return the desaturated (and if a filter is used, also filtered) new color T 222 */ 223 T desaturate(T color, float degree); 224 225 /** 226 * Fully saturates color (makes it a vivid color like red or green and less gray) and returns the modified copy. 227 * Leaves alpha unchanged. 228 * @param color the color T to saturate (will not be modified) 229 * @return the saturated version of color 230 */ 231 T saturated(T color); 232 233 234 /** 235 * Saturates color (makes it closer to a vivid color like red or green and less gray) by the specified degree and 236 * returns the new color (saturated somewhat). If this is called on a color that is very close to gray, this does 237 * not necessarily return a specific color, but most implementations will treat a hue of 0 as red. 238 * @param color the color T to saturate 239 * @param degree a float between 0.0f and 1.0f; more makes it more colorful 240 * @return the saturated (and if a filter is used, also filtered) new color 241 */ 242 public T saturate(T color, float degree); 243 244 /** 245 * A skeletal implementation of {@link IColorCenter}. 246 * 247 * @author smelC 248 * 249 * @param <T> a concrete color type 250 */ 251 abstract class Skeleton<T> implements IColorCenter<T> { 252 253 private final Map<Long, T> cache = new HashMap<>(256); 254 255 protected /*Nullable*/ IFilter<T> filter; 256 257 /** 258 * @param filter 259 * The filter to use, or {@code null} for no filter. 260 */ 261 protected Skeleton(/*Nullable*/ IFilter<T> filter) { 262 this.filter = filter; 263 } 264 265 /** 266 * It clears the cache. You may need to do this to limit the cache to the colors used in a specific section. 267 * This is also useful if a Filter changes what colors it should return on a frame-by-frame basis; in that case, 268 * you can call clearCache() at the start or end of a frame to ensure the next frame gets different colors. 269 */ 270 public void clearCache() 271 { 272 cache.clear(); 273 } 274 275 /** 276 * You may want to copy colors between IColorCenter instances that have different create() methods -- and as 277 * such, will have different values for the same keys in the cache. This allows you to copy the cache from other 278 * into this Skeleton, but using this Skeleton's create() method. 279 * @param other another Skeleton of the same type that will have its cache copied into this Skeleton 280 */ 281 public void copyCache(Skeleton<T> other) 282 { 283 for (Map.Entry<Long, T> k : other.cache.entrySet()) 284 { 285 cache.put(k.getKey(), create(getRed(k.getValue()), getGreen(k.getValue()), getBlue(k.getValue()), 286 getAlpha(k.getValue()))); 287 } 288 } 289 290 /** 291 * If you're changing the filter, you should likely call 292 * {@link #clearCache()}. 293 * 294 * @param filter 295 * The filter to use, or {@code null} to turn filtering OFF. 296 * @return {@code this} 297 */ 298 public Skeleton<T> setFilter(IFilter<T> filter) { 299 this.filter = filter; 300 return this; 301 } 302 303 @Override 304 public T get(int red, int green, int blue, int opacity) { 305 final Long value = getUniqueIdentifier((short)red, (short)green, (short)blue, (short)opacity); 306 T t = cache.get(value); 307 if (t == null) { 308 /* Miss */ 309 t = create(red, green, blue, opacity); 310 /* Put in cache */ 311 cache.put(value, t); 312 } 313 return t; 314 } 315 316 @Override 317 public T get(int red, int green, int blue) { 318 return get(red, green, blue, 255); 319 } 320 321 @Override 322 public T getHSV(float hue, float saturation, float value, float opacity) { 323 if ( saturation < 0.0001 ) //HSV from 0 to 1 324 { 325 return get(Math.round(value * 255), Math.round(value * 255), Math.round(value * 255), 326 Math.round(opacity * 255)); 327 } 328 else 329 { 330 float h = hue * 6f; 331 if ( h >= 6 ) h = 0; //H must be < 1 332 int i = (int)h; //Or ... var_i = floor( var_h ) 333 float a = value * ( 1 - saturation ); 334 float b = value * ( 1 - saturation * ( h - i ) ); 335 float c = value * ( 1 - saturation * ( 1 - ( h - i ) ) ); 336 337 switch (i) 338 { 339 case 0: return get(Math.round(value * 255), Math.round(c * 255), Math.round(a * 255), 340 Math.round(opacity * 255)); 341 case 1: return get(Math.round(b * 255), Math.round(value * 255), Math.round(a * 255), 342 Math.round(opacity * 255)); 343 case 2: return get(Math.round(a * 255), Math.round(value * 255), Math.round(c * 255), 344 Math.round(opacity * 255)); 345 case 3: return get(Math.round(a * 255), Math.round(b * 255), Math.round(value * 255), 346 Math.round(opacity * 255)); 347 case 4: return get(Math.round(c * 255), Math.round(a * 255), Math.round(value * 255), 348 Math.round(opacity * 255)); 349 default: return get(Math.round(value * 255), Math.round(a * 255), Math.round(b * 255), 350 Math.round(opacity * 255)); 351 } 352 } 353 } 354 355 @Override 356 public T getHSV(float hue, float saturation, float value) { 357 return getHSV(hue, saturation, value, 1.0f); 358 } 359 360 @Override 361 public T getWhite() { 362 return get(255, 255, 255, 255); 363 } 364 365 @Override 366 public T getBlack() { 367 return get(0, 0, 0, 255); 368 } 369 370 @Override 371 public T getTransparent() { 372 return get(0, 0, 0, 0); 373 } 374 375 @Override 376 public T getRandom(RNG rng, int opacity) { 377 return get(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256), opacity); 378 } 379 380 /** 381 * @param r the red component in 0.0 to 1.0 range, typically 382 * @param g the green component in 0.0 to 1.0 range, typically 383 * @param b the blue component in 0.0 to 1.0 range, typically 384 * @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a 385 * bright color, exclusive) 386 */ 387 public float getSaturation(float r, float g, float b) { 388 float min = Math.min(Math.min(r, g ), b); //Min. value of RGB 389 float max = Math.max(Math.max(r, g), b); //Min. value of RGB 390 float delta = max - min; //Delta RGB value 391 392 float saturation; 393 394 if ( delta < 0.0001f ) //This is a gray, no chroma... 395 { 396 saturation = 0; 397 } 398 else //Chromatic data... 399 { 400 saturation = delta / max; 401 } 402 return saturation; 403 } 404 /** 405 * @param c a concrete color 406 * @return the saturation of the color from 0.0 (a grayscale color; inclusive) to 1.0 (a 407 * bright color, exclusive) 408 */ 409 @Override 410 public float getSaturation(T c) { 411 return getSaturation(getRed(c) / 255f, getGreen(c) / 255f, getBlue(c) / 255f); 412 } 413 414 /** 415 * @param r the red component in 0.0 to 1.0 range, typically 416 * @param g the green component in 0.0 to 1.0 range, typically 417 * @param b the blue component in 0.0 to 1.0 range, typically 418 * @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to 419 * 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors. 420 */ 421 public float getValue(float r, float g, float b) 422 { 423 return Math.max(Math.max(r, g), b); 424 } 425 /** 426 * @param c a concrete color 427 * @return the value (essentially lightness) of the color from 0.0 (black, inclusive) to 428 * 1.0 (inclusive) for screen colors or arbitrarily high for HDR colors. 429 */ 430 @Override 431 public float getValue(T c) { 432 float r = getRed(c) / 255f; //RGB from 0 to 255 433 float g = getGreen(c) / 255f; 434 float b = getBlue(c) / 255f; 435 436 return Math.max(Math.max(r, g), b); 437 } 438 439 /** 440 * @param r the red component in 0.0 to 1.0 range, typically 441 * @param g the green component in 0.0 to 1.0 range, typically 442 * @param b the blue component in 0.0 to 1.0 range, typically 443 * @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and 444 * eventually to purple before looping back to almost the same red (1.0, exclusive) 445 */ 446 public float getHue(float r, float g, float b) { 447 float min = Math.min(Math.min(r, g ), b); //Min. value of RGB 448 float max = Math.max(Math.max(r, g), b); //Min. value of RGB 449 float delta = max - min; //Delta RGB value 450 451 float hue; 452 453 if ( delta < 0.0001f ) //This is a gray, no chroma... 454 { 455 hue = 0; //HSV results from 0 to 1 456 } 457 else //Chromatic data... 458 { 459 float rDelta = ( ( ( max - r ) / 6f ) + ( delta / 2f ) ) / delta; 460 float gDelta = ( ( ( max - g ) / 6f ) + ( delta / 2f ) ) / delta; 461 float bDelta = ( ( ( max - b ) / 6f ) + ( delta / 2f ) ) / delta; 462 463 if ( r == max ) hue = bDelta - gDelta; 464 else if ( g == max ) hue = ( 1f / 3f ) + rDelta - bDelta; 465 else hue = ( 2f / 3f ) + gDelta - rDelta; 466 467 if ( hue < 0 ) hue += 1f; 468 else if ( hue > 1 ) hue -= 1; 469 } 470 return hue; 471 } 472 473 /** 474 * @param c a concrete color 475 * @return The hue of the color from 0.0 (red, inclusive) towards orange, then yellow, and 476 * eventually to purple before looping back to almost the same red (1.0, exclusive) 477 */ 478 @Override 479 public float getHue(T c) { 480 return getHue(getRed(c) / 255f, getGreen(c) / 255f, getBlue(c) / 255f); 481 } 482 483 @Override 484 public T filter(T c) 485 { 486 return get(getRed(c), getGreen(c), getBlue(c), getAlpha(c)); 487 } 488 489 @Override 490 public IColoredString<T> filter(IColoredString<T> ics) { 491 /* 492 * It is common not to have a filter or to have the identity one. To 493 * avoid always copying strings in this case, we first roll over the 494 * string to see if there'll be a change. 495 * 496 * This is clearly a subjective design choice but my industry 497 * experience is that minimizing allocations is the thing to do for 498 * performances, hence I prefer iterating twice to do that. 499 */ 500 boolean change = false; 501 for (IColoredString.Bucket<T> bucket : ics) { 502 final T in = bucket.getColor(); 503 if (in == null) 504 continue; 505 final T out = filter(in); 506 if (in != out) { 507 change = true; 508 break; 509 } 510 } 511 512 if (change) { 513 final IColoredString<T> result = IColoredString.Impl.create(); 514 for (IColoredString.Bucket<T> bucket : ics) 515 result.append(bucket.getText(), filter(bucket.getColor())); 516 return result; 517 } else 518 /* Only one allocation: the iterator, yay \o/ */ 519 return ics; 520 } 521 /** 522 * Gets a copy of t and modifies it to make a shade of gray with the same brightness. 523 * The doAlpha parameter causes the alpha to be considered in the calculation of brightness and also changes the 524 * returned alpha of the color. 525 * Not related to reified types or any usage of "reify." 526 * @param t a T to copy; only the copy will be modified 527 * @param doAlpha 528 * Whether to include (and hereby change) the alpha component. 529 * @return A monochromatic variation of {@code t}. 530 */ 531 @Override 532 public T greify(T t, boolean doAlpha) { 533 if (t == null) 534 /* Cannot do */ 535 return null; 536 final int red = getRed(t); 537 final int green = getGreen(t); 538 final int blue = getBlue(t); 539 final int alpha = getAlpha(t); 540 final int rgb = red + green + blue; 541 final int mean; 542 final int newAlpha; 543 if (doAlpha) { 544 mean = (rgb + alpha) / 4; 545 newAlpha = mean; 546 } else { 547 mean = rgb / 3; 548 /* No change */ 549 newAlpha = alpha; 550 } 551 return get(mean, mean, mean, newAlpha); 552 } 553 554 /** 555 * Gets the linear interpolation from Color start to Color end, changing by the fraction given by change. 556 * This implementation tries to work with colors in a way that is as general as possible, using getRed() instead 557 * of some specific detail that depends on how a color is implemented. Other implementations that specialize in 558 * a specific type of color may be able to be more efficient. 559 * @param start the initial color T 560 * @param end the "target" color T 561 * @param change the degree to change closer to end; a change of 0.0f produces start, 1.0f produces end 562 * @return a new T between start and end 563 */ 564 public T lerp(T start, T end, float change) 565 { 566 if(start == null || end == null) 567 return null; 568 final int sr = getRed(start), sg = getGreen(start), sb = getBlue(start), sa = getAlpha(start), 569 er = getRed(end), eg = getGreen(end), eb = getBlue(end), ea = getAlpha(end); 570 return get( 571 (int)(sr + change * (er - sr)), 572 (int)(sg + change 573 * (eg - sg)), 574 (int)(sb + change * (eb - sb)), 575 (int)(sa + change * (ea - sa)) 576 ); 577 } 578 /** 579 * Gets a fully-desaturated version of the given color (keeping its brightness, but making it grayscale). 580 * Keeps alpha the same; if you want alpha to be considered (and brightness to be calculated differently), then 581 * you can use greify() in this class instead. 582 * @param color the color T to desaturate (will not be modified) 583 * @return the grayscale version of color 584 */ 585 public T desaturated(T color) 586 { 587 int f = (int)Math.min(255, getRed(color) * 0.299f + getGreen(color) * 0.587f + getBlue(color) * 0.114f); 588 return get(f, f, f, getAlpha(color)); 589 } 590 591 /** 592 * Brings a color closer to grayscale by the specified degree and returns the new color (desaturated somewhat). 593 * Alpha is left unchanged. 594 * @param color the color T to desaturate 595 * @param degree a float between 0.0f and 1.0f; more makes it less colorful 596 * @return the desaturated (and if a filter is used, also filtered) new color T 597 */ 598 public T desaturate(T color, float degree) 599 { 600 return lerp(color, desaturated(color), degree); 601 } 602 603 /** 604 * Fully saturates color (makes it a vivid color like red or green and less gray) and returns the modified copy. 605 * Leaves alpha unchanged. 606 * @param color the color T to saturate (will not be modified) 607 * @return the saturated version of color 608 */ 609 public T saturated(T color) 610 { 611 return getHSV(getHue(color), 1f, getValue(color), getAlpha(color)); 612 } 613 /** 614 * Saturates color (makes it closer to a vivid color like red or green and less gray) by the specified degree and 615 * returns the new color (saturated somewhat). If this is called on a color that is very close to gray, this is 616 * likely to produce a red hue by default (if there's no hue to make vivid, it needs to choose something). 617 * @param color the color T to saturate 618 * @param degree a float between 0.0f and 1.0f; more makes it more colorful 619 * @return the saturated (and if a filter is used, also filtered) new color 620 */ 621 public T saturate(T color, float degree) 622 { 623 return lerp(color, saturated(color), degree); 624 } 625 626 627 /** 628 * Create a concrete instance of the color type given as a type parameter. That's the 629 * place to use the {@link #filter}. 630 * 631 * @param red the red component of the desired color 632 * @param green the green component of the desired color 633 * @param blue the blue component of the desired color 634 * @param opacity the alpha component or opacity of the desired color 635 * @return a fresh instance of the concrete color type 636 */ 637 protected abstract T create(int red, int green, int blue, int opacity); 638 639 private long getUniqueIdentifier(short r, short g, short b, short a) { 640 return ((a & 0xffL) << 48) | ((r & 0xffffL) << 32) | ((g & 0xffffL) << 16) | (b & 0xffffL); 641 } 642 643 } 644}