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}