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