001package squidpony.squidgrid.gui.gdx; 002 003import com.badlogic.gdx.graphics.Color; 004import com.badlogic.gdx.math.MathUtils; 005import squidpony.squidmath.Bresenham; 006import squidpony.squidmath.Coord3D; 007import squidpony.squidmath.RNG; 008 009import java.util.*; 010 011/** 012 * Provides utilities for working with colors as well as caching operations for 013 * color creation. 014 * 015 * All returned SColor objects are cached so multiple requests for the same 016 * SColor will not create duplicate long term objects. 017 * 018 * @see SquidColorCenter another technique for caching colors that allows filters. 019 * 020 * @author Eben Howard - http://squidpony.com - howard@squidpony.com 021 * @author Tommy Ettinger - responsible for mutilating this class to work on libGDX 022 */ 023public class SColorFactory { 024 025 private final TreeMap<String, SColor> nameLookup; 026 private final TreeMap<Integer, SColor> valueLookup; 027 private RNG rng; 028 private Map<Integer, SColor> colorBag; 029 private Map<String, ArrayList<SColor>> palettes; 030 private long floor = 1;//what multiple to floor rgb values to in order to reduce total colors 031 032 /** 033 * Constructs a new SColorFactory with an empty cache. 034 * <br> 035 * Implementation note: SColorFactory initially used static methods and was not intended to 036 * be constructed by users, but for compatibility with Android (where static instances may 037 * linger into another run of an application), it was reworked to require an object to be 038 * constructed and for that object to contain the cache, rather than a static instance. Code 039 * written for earlier versions of SquidLib may use static methods on SColorFactory; most 040 * code written in SquidLib 3.0.0 beta 2 and newer prefers the IColorCenter implementations 041 * such as SquidColorCenter when SColor features such as color names aren't required. As of 042 * the master revision after beta 3, SquidColorCenter implements most of SColorFactory's API, 043 * and also supports a Filter for automatic changes to requested colors.. 044 */ 045 public SColorFactory() { 046 047 nameLookup = new TreeMap<>(); 048 valueLookup = new TreeMap<>(); 049 rng = DefaultResources.getGuiRandom(); 050 colorBag = new HashMap<>(); 051 palettes = new HashMap<>(); 052 floor = 1; 053 054 } 055 056 /** 057 * Returns the SColor Constant who's name is the one provided. If one cannot 058 * be found then null is returned. 059 * 060 * This method constructs a list of the SColor constants the first time it 061 * is called. 062 * 063 * @param s the name 064 * @return the SColor by name s 065 */ 066 public SColor colorForName(String s) { 067 if (nameLookup.isEmpty()) { 068 for (SColor sc : SColor.FULL_PALETTE) { 069 nameLookup.put(sc.getName(), sc); 070 } 071 } 072 073 return nameLookup.get(s); 074 } 075 076 /** 077 * Returns the SColor who's value matches the one passed in. If no SColor 078 * Constant matches that value then a cached or new SColor is returned that 079 * matches the provided value. 080 * 081 * This method constructs a list of the SColor constants the first time it 082 * is called. 083 * 084 * @param rgb an int encoding 256 * 256 * red + 256 * green + blue 085 * @return the SColor with value rgb 086 */ 087 public SColor colorForValue(int rgb) { 088 if (valueLookup.isEmpty()) { 089 for (SColor sc : SColor.FULL_PALETTE) { 090 valueLookup.put(sc.toIntBits(), sc); 091 } 092 } 093 094 return valueLookup.containsKey(rgb) ? valueLookup.get(rgb) : asSColor(rgb); 095 } 096 097 /** 098 * Returns the number of SColor objects currently cached. 099 * 100 * @return 101 */ 102 public int quantityCached() { 103 return colorBag.size(); 104 } 105 106 /** 107 * Utility method to blend the two colors by the amount passed in as the 108 * coefficient. 109 * 110 * @param a 111 * @param b 112 * @param coef 113 * @return 114 */ 115 @SuppressWarnings("unused") 116 private int blend(int a, int b, double coef) { 117 coef = MathUtils.clamp(coef, 0, 1); 118 return (int) (a + (b - a) * coef); 119 } 120 /** 121 * Utility method to blend the two colors by the amount passed in as the 122 * coefficient. 123 * 124 * @param a 125 * @param b 126 * @param coef 127 * @return 128 */ 129 private float blend(float a, float b, double coef) { 130 float cf = MathUtils.clamp((float)coef, 0, 1); 131 return (a + (b - a) * cf); 132 } 133 134 /** 135 * Returns an SColor that is the given distance from the first color to the 136 * second color. 137 * 138 * @param color1 The first color 139 * @param color2 The second color 140 * @param coef The percent towards the second color, as 0.0 to 1.0 141 * @return 142 */ 143 public SColor blend(SColor color1, SColor color2, double coef) { 144 return asSColor(blend(color1.a, color2.a, coef), 145 blend(color1.r, color2.r, coef), 146 blend(color1.g, color2.g, coef), 147 blend(color1.b, color2.b, coef)); 148 } 149 150 /** 151 * Returns an SColor that is randomly chosen from the color line between the 152 * two provided colors from the two provided points. 153 * 154 * @param color1 155 * @param color2 156 * @param min The minimum percent towards the second color, as 0.0 to 1.0 157 * @param max The maximum percent towards the second color, as 0.0 to 1.0 158 * @return 159 */ 160 public SColor randomBlend(SColor color1, SColor color2, double min, double max) { 161 return blend(color1, color2, rng.between(min, max)); 162 } 163 164 /** 165 * Adds the two colors together. 166 * 167 * @param color1 168 * @param color2 169 * @return 170 */ 171 public SColor add(SColor color1, SColor color2) { 172 return asSColor(color1.a + color2.a, color1.r + color2.r, color1.g + color2.g, color1.b + color2.b); 173 } 174 175 /** 176 * Uses the second color as a light source, meaning that each of the red, 177 * green, and blue values of the first color are multiplied by the lighting 178 * color's percentage of full value (1.0). 179 * 180 * @param color 181 * @param light 182 * @return 183 */ 184 public SColor lightWith(SColor color, SColor light) { 185 return asSColor((color.a * light.a), (color.r * light.r), (color.g * light.g), (color.b * light.b)); 186 } 187 188 /** 189 * Clears the backing cache. 190 * 191 * Should only be used if an extreme number of colors are being created and 192 * then not reused, such as when blending different colors in different 193 * areas that will not be revisited. 194 */ 195 public void emptyCache() { 196 colorBag = new HashMap<>(); 197 } 198 199 /** 200 * Sets the value at which each of the red, green, and blue values will be 201 * set to the nearest lower multiple of. 202 * 203 * For example, a floor value of 5 would mean that each of those values 204 * would be considered the nearest lower multiple of 5 when building the 205 * colors. 206 * 207 * If the value passed in is less than 1, then the flooring value is set at 208 * 1. 209 * 210 * @param value used to determine the precision of rounding 211 */ 212 public void setFloor(int value) { 213 floor = Math.max(1, value); 214 } 215 216 /** 217 * Gets the value at which each of the red, green, and blue values will be 218 * set to the nearest lower multiple of. 219 * <br> 220 * For example, a floor value of 5 would mean that each of those values 221 * would be considered the nearest lower multiple of 5 when building the 222 * colors. 223 * 224 * @return the current floor value as a long, which defaults to 1. 225 */ 226 public long getFloor() { 227 return floor; 228 } 229 230 /** 231 * Returns the cached color that matches the desired rgb value. 232 * 233 * If the color is not already in the cache, it is created and added to the 234 * cache. 235 * 236 * This method does not check to see if the value is already available as a 237 * SColor constant. If such functionality is desired then please use 238 * colorForValue(int rgb) instead. 239 * 240 * @param argb 241 * @return 242 */ 243 public SColor asSColor(int argb) { 244 int working = argb; 245 if (floor != 1) {//need to convert to floored values 246 int r = (argb >>> 24) & 0xff; 247 r -= r % floor; 248 int g = (argb >> 16) & 0xff; 249 g -= g % floor; 250 int b = (argb >> 8) & 0xff; 251 b -= b % floor; 252 int a = (argb) & 0xff; 253 a -= a % floor; 254 255 //put back together 256 working = ((r & 0xFF) << 24) 257 | ((g & 0xFF) << 16) 258 | ((b & 0xFF) << 8) 259 | (a & 0xFF); 260 } 261 262 if (colorBag.containsKey(working)) { 263 return colorBag.get(working); 264 } else { 265 SColor color = new SColor(working); 266 colorBag.put(working, color); 267 return color; 268 } 269 } 270 /** 271 * Returns the cached color that matches the desired rgb value. 272 * 273 * If the color is not already in the cache, it is created and added to the 274 * cache. 275 * 276 * This method does not check to see if the value is already available as a 277 * SColor constant. If such functionality is desired then please use 278 * colorForValue(int rgb) instead. 279 * 280 * @param a 281 * @param r 282 * @param g 283 * @param b 284 * @return 285 */ 286 public SColor asSColor(float a, float r, float g, float b) { 287 int working = 0; 288 int aa = MathUtils.round(255 * a); 289 aa -= aa % floor; 290 int rr = MathUtils.round(255 * r); 291 rr -= rr % floor; 292 int gg = MathUtils.round(255 * g); 293 gg -= gg % floor; 294 int bb = MathUtils.round(255 * b); 295 bb -= bb % floor; 296 297 //put back together 298 working = ((aa & 0xFF) << 24) 299 | ((rr & 0xFF) << 16) 300 | ((gg & 0xFF) << 8) 301 | (bb & 0xFF); 302 303 304 if (colorBag.containsKey(working)) { 305 return colorBag.get(working); 306 } else { 307 SColor color = new SColor(working); 308 colorBag.put(working, color); 309 return color; 310 } 311 } 312 313 /** 314 * Returns an SColor that is opaque. 315 * 316 * @param r 317 * @param g 318 * @param b 319 * @return 320 */ 321 public SColor asSColor(int r, int g, int b) { 322 return asSColor(255, r, g, b); 323 } 324 325 /** 326 * Returns an SColor with the given values, with those values clamped 327 * between 0 and 255. 328 * 329 * @param a 330 * @param r 331 * @param g 332 * @param b 333 * @return 334 */ 335 public SColor asSColor(int a, int r, int g, int b) { 336 a = Math.min(a, 255); 337 a = Math.max(a, 0); 338 r = Math.min(r, 255); 339 r = Math.max(r, 0); 340 g = Math.min(g, 255); 341 g = Math.max(g, 0); 342 b = Math.min(b, 255); 343 b = Math.max(b, 0); 344 return asSColor((a << 24) | (r << 16) | (g << 8) | b); 345 } 346 347 /** 348 * Returns an SColor representation of the provided Color. If there is a 349 * named SColor constant that matches the value, then that constant is 350 * returned. 351 * 352 * @param color 353 * @return 354 */ 355 public SColor asSColor(Color color) { 356 return colorForValue(Color.rgba8888(color.a, color.r, color.g, color.b)); 357 } 358 359 /** 360 * Returns an SColor that is a slightly dimmer version of the provided 361 * color. 362 * 363 * @param color 364 * @return 365 */ 366 public SColor dim(SColor color) { 367 return blend(color, SColor.BLACK, 0.1); 368 } 369 370 /** 371 * Returns an SColor that is a somewhat dimmer version of the provided 372 * color. 373 * 374 * @param color 375 * @return 376 */ 377 public SColor dimmer(SColor color) { 378 return blend(color, SColor.BLACK, 0.3); 379 } 380 381 /** 382 * Returns an SColor that is a lot darker version of the provided color. 383 * 384 * @param color 385 * @return 386 */ 387 public SColor dimmest(SColor color) { 388 return blend(color, SColor.BLACK, 0.7); 389 } 390 391 /** 392 * Returns an SColor that is a slightly lighter version of the provided 393 * color. 394 * 395 * @param color 396 * @return 397 */ 398 public SColor light(SColor color) { 399 return blend(color, SColor.WHITE, 0.1); 400 } 401 402 /** 403 * Returns an SColor that is a somewhat lighter version of the provided 404 * color. 405 * 406 * @param color 407 * @return 408 */ 409 public SColor lighter(SColor color) { 410 return blend(color, SColor.WHITE, 0.3); 411 } 412 413 /** 414 * Returns an SColor that is a lot lighter version of the provided color. 415 * 416 * @param color 417 * @return 418 */ 419 public SColor lightest(SColor color) { 420 return blend(color, SColor.WHITE, 0.6); 421 } 422 423 /** 424 * Returns an SColor that is the fully desaturated (greyscale) version of 425 * the provided color. 426 * 427 * @param color 428 * @return 429 */ 430 public SColor desaturated(SColor color) { 431 int r = MathUtils.round(color.r * 255); 432 int g = MathUtils.round(color.g * 255); 433 int b = MathUtils.round(color.b * 255); 434 435 int average = (int) (r * 0.299 + g * 0.587 + b * 0.114); 436 437 return asSColor(average, average, average); 438 } 439 440 /** 441 * Returns an SColor that is the version of the provided color desaturated 442 * the given amount. 443 * 444 * @param color 445 * @param percent The percent to desaturate, from 0.0 for none to 1.0 for 446 * fully desaturated 447 * @return 448 */ 449 public SColor desaturate(SColor color, double percent) { 450 return blend(color, desaturated(color), percent); 451 } 452 453 /** 454 * Returns a list of colors starting at the first color and moving to the 455 * second color. The end point colors are included in the list. 456 * 457 * @param color1 starting color 458 * @param color2 ending color 459 * @return an ArrayList of SColor going from color1 to color2; the length should be no greater than 256. 460 */ 461 public ArrayList<SColor> asGradient(SColor color1, SColor color2) { 462 String name = paletteNamer(color1, color2); 463 if (palettes.containsKey(name)) { 464 return palettes.get(name); 465 } 466 467 //get the gradient 468 Queue<Coord3D> gradient = Bresenham.line3D(scolorToCoord3D(color1), scolorToCoord3D(color2)); 469 ArrayList<SColor> ret = new ArrayList<>(); 470 for (Coord3D coord : gradient) { 471 ret.add(coord3DToSColor(coord)); 472 } 473 474 palettes.put(name, ret); 475 return ret; 476 } 477 478 /** 479 * Returns the palette associate with the provided name, or null if there is 480 * no such palette. 481 * 482 * @param name 483 * @return 484 */ 485 public ArrayList<SColor> palette(String name) { 486 return palettes.get(name); 487 } 488 /** 489 * Returns the palette associate with the provided name, or null if there is 490 * no such palette. 491 * 492 * @param name 493 * @return 494 * @deprecated Prefer palette over this misspelled version. 495 */ 496 public ArrayList<SColor> pallet(String name) { 497 return palettes.get(name); 498 } 499 500 /** 501 * Returns the SColor that is the provided percent towards the end of the 502 * palette. Bounds are checked so as long as there is at least one color in 503 * the palette, values below 0 will return the first element and values 504 * above 1 will return the last element; 505 * 506 * If there is no palette keyed to the provided name, null is returned. 507 * 508 * @param name 509 * @param percent 510 * @return 511 */ 512 public SColor fromPalette(String name, float percent) { 513 ArrayList<SColor> list = palettes.get(name); 514 if (list == null) { 515 return null; 516 } 517 518 int index = Math.round(list.size() * percent);//find the index that's the given percent into the gradient 519 index = Math.min(index, list.size() - 1); 520 index = Math.max(index, 0); 521 return list.get(index); 522 } 523 /** 524 * Returns the SColor that is the provided percent towards the end of the 525 * palette. Bounds are checked so as long as there is at least one color in 526 * the palette, values below 0 will return the first element and values 527 * above 1 will return the last element; 528 * 529 * If there is no palette keyed to the provided name, null is returned. 530 * 531 * @param name 532 * @param percent 533 * @return 534 * 535 * @deprecated Prefer fromPalette over this misspelled version; they are equivalent. 536 */ 537 public SColor fromPallet(String name, float percent) { 538 ArrayList<SColor> list = palettes.get(name); 539 if (list == null) { 540 return null; 541 } 542 543 int index = Math.round(list.size() * percent);//find the index that's the given percent into the gradient 544 index = Math.min(index, list.size() - 1); 545 index = Math.max(index, 0); 546 return list.get(index); 547 } 548 549 /** 550 * Places the palette into the cache, along with each of the member colors. 551 * 552 * @param name 553 * @param palette 554 * 555 * @deprecated Prefer addPalette over this misspelled version; they are equivalent. 556 */ 557 public void addPallet(String name, ArrayList<SColor> palette) { 558 addPalette(name, palette); 559 } 560 561 /** 562 * Places the palette into the cache, along with each of the member colors. 563 * 564 * @param name 565 * @param palette 566 */ 567 public void addPalette(String name, ArrayList<SColor> palette) { 568 ArrayList<SColor> temp = new ArrayList<>(); 569 570 //make sure all the colors in the palette are also in the general color cache 571 for (SColor sc : palette) { 572 temp.add(asSColor(sc.a, sc.r, sc.g, sc.b)); 573 } 574 575 palettes.put(name, temp); 576 } 577 578 /** 579 * Converts the provided color into a three dimensional coordinate point for 580 * use in the Bresenham algorithms. 581 * 582 * @param color 583 * @return 584 */ 585 private Coord3D scolorToCoord3D(SColor color) { 586 return new Coord3D(MathUtils.floor(color.r * 255), MathUtils.floor(color.g * 255), MathUtils.floor(color.b * 255)); 587 } 588 589 /** 590 * Converts the provided three dimensional coordinate into a color for use 591 * in the Bresenham algorithms. 592 * 593 * @param coord 594 * @return 595 */ 596 private SColor coord3DToSColor(Coord3D coord) { 597 return asSColor(coord.x, coord.y, coord.z); 598 } 599 600 private String paletteNamer(SColor color1, SColor color2) { 601 return color1.getName() + " to " + color2.getName(); 602 } 603 604}