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}