Search code examples
javacolors

How do I generate a random color in a specific range?


I have looked around different sources:

However, none of these answers is what I am searching for.

What I am trying to do is to get a randomized color in range of 2 colors, let's say for example purple and pink. With purple being #6A0DAD and pink being #FFC0CB, I want to grab a color in the range of these 2 colors, but a random one. So I'd get for example #D982B5, which is magenta-pink.

I have no clue so far where to get started other than making a randomizer.


Solution

  • Tl;dr:

    It's not that simple. Color is what you perceive when you see visible light. What makes a color are different wavelengths of light. To represent color by numbers, we use different color spaces, e.g. RGB, CMYK, YCbCr and so on. All these color spaces do simplifications to represent a physical (or biological) property.

    When you say "color between", it could mean many things: A wavelength between two wavelengths. A hue that is between two other hues in a specific color space. A color that you "expect" to be between two colors by your experience and perception. A color that results from mixing two colors. To mix colors, a good approach is to use additive mixing, but in a color space. However, in terms of "a color between", this could be flawed, depending on what you want.

    Here are some related posts that are worth reading:


    The longer version:

    The first thing that comes to mind is to simply take each channel in RGB of both colors and to generate a random value in-between the values of the corresponding channel: E.g. you take the colors #6A0DAD and #FFC0CB. The values for the red channels are 106 and 255. So the value of the red channel for the new color would be a number between 106 and 255. This naive approach has a problem: The result for the number between #6A0DAD, purple, and #FFC0CB, pink, could be #6AC0AD, which is petrol color. Petrol is definitely not a color that you would perceive as a color "between purple and pink".

    The cause of it is that the RGB color space has the hue of a color represented by all three channels: it's the balance of the channels that makes up the hue. When we generate a random number within the corresponding range for each channel for the new color, the hue of the resulting color might be something completely different, because it might result from a completely different balance of the channels.

    Another approach would be to change the color space we are working in to one that represents hue by a single channel. One such color space is HSL/HSV. What we do is: Convert both numbers to the HSB equivalent. Then generate random numbers for each channel like we did in RGB. Notice that the balance of the channels doesn't matter as the hue is represented by a single channel. Then take the result and convert it back to RGB.

    Here a simple demonstration in Java:

    import java.awt.Color;
    import java.awt.GridLayout;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    
    class Main {
        
        
        static int[] purple = {106, 13, 173};
        static int[] pink = {255, 192, 203};
        
        
        static float randomBetween(float a, float b) {
            float min = Math.min(a, b);
            float max = Math.max(a, b);
            return min + (max - min) * (float) Math.random();
        }
        
        
        static Color colorBetween(int[] a, int[] b) {
            float[] a_hsb = Color.RGBtoHSB(a[0], a[1], a[2], null);
            float[] b_hsb = Color.RGBtoHSB(b[0], b[1], b[2], null);
            float[] between_hsb = {
                    randomBetween(a_hsb[0], b_hsb[0]),
                    randomBetween(a_hsb[1], b_hsb[1]),
                    randomBetween(a_hsb[2], b_hsb[2])
            };
            return new Color(
                    Color.HSBtoRGB(
                            between_hsb[0],
                            between_hsb[1],
                            between_hsb[2]));
        }
        
        
        public static void main(String args[]) {
            Color purple_color = new Color(purple[0], purple[1], purple[2]);
            Color pink_color = new Color(pink[0], pink[1], pink[2]);
            Color between_color = colorBetween(purple, pink);
            
            JFrame frame = new JFrame();
            GridLayout layout = new GridLayout(2, 2);
            frame.setLayout(layout);
            
            JLabel purple_label = new JLabel();
            purple_label.setBackground(purple_color);
            purple_label.setOpaque(true);
            frame.add(purple_label);
            
            JLabel pink_label = new JLabel();
            pink_label.setBackground(pink_color);
            pink_label.setOpaque(true);
            frame.add(pink_label);
            
            JLabel between_label = new JLabel();
            between_label.setBackground(between_color);
            between_label.setOpaque(true);
            frame.add(between_label);
            
            frame.addMouseListener(new MouseAdapter() {  
                public void mouseClicked(MouseEvent e) {  
                   between_label.setBackground(colorBetween(purple, pink));
                }  
            }); 
            
            frame.setSize(1000, 1000);
            frame.setVisible(true);
        }
    }
    
    

    Click on the frame to recreate the new color. In the upper left is the purple, in the upper right the pink and the result is bottom left.

    a b

    As you should see, the result is definitely a color that you would call at least "in the same color palette". But this has still an issue: We are also adjusting the brightness and saturation in HSB. The pink color that we have initially has a very low saturation but the purple a very high one. The issue is that the result could be the same pink but highly saturated, which looks very reddish. It doesn't "seem" right.

    Another approach could be to come back to RGB and to respect the balance of the channels. And actually, this is nothing new. It's called blending, which is a common technique in rendering to display transparency.

    Here the same demonstration but with an adjusted colorBetween method:

    import java.awt.Color;
    import java.awt.GridLayout;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    
    class Main {
        
        
        static int[] purple = {106, 13, 173};
        static int[] pink = {255, 192, 203};
        
        
        static Color colorBetween(int[] a, int[] b) {
            double c_a = Math.random();
            double c_b = 1.0 - c_a;
            
            int[] blend = {
                    (int) (c_a * a[0] + c_b * b[0]),
                    (int) (c_a * a[1] + c_b * b[1]),
                    (int) (c_a * a[2] + c_b * b[2]),
            };
            
            return new Color(blend[0], blend[1], blend[2]);
        }
        
        
        public static void main(String args[]) {
            Color purple_color = new Color(purple[0], purple[1], purple[2]);
            Color pink_color = new Color(pink[0], pink[1], pink[2]);
            Color between_color = colorBetween(purple, pink);
            
            JFrame frame = new JFrame();
            GridLayout layout = new GridLayout(2, 2);
            frame.setLayout(layout);
            
            JLabel purple_label = new JLabel();
            purple_label.setBackground(purple_color);
            purple_label.setOpaque(true);
            frame.add(purple_label);
            
            JLabel pink_label = new JLabel();
            pink_label.setBackground(pink_color);
            pink_label.setOpaque(true);
            frame.add(pink_label);
            
            JLabel between_label = new JLabel();
            between_label.setBackground(between_color);
            between_label.setOpaque(true);
            frame.add(between_label);
            
            frame.addMouseListener(new MouseAdapter() {  
                public void mouseClicked(MouseEvent e) {  
                   between_label.setBackground(colorBetween(purple, pink));
                }  
            }); 
            
            frame.setSize(1000, 1000);
            frame.setVisible(true);
        }
    }
    
    

    c

    Notice that the result is never a highly saturated pink anymore that "seems wrong". What you do is simply using a random factor c_a that is between 0.0 and 1.0 and a factor c_b that is 1.0 - c_a. You multiply each channel of color a by c_a, do the same with b and c_b and add the results. This is simple scaling of a color by a constant factor, which has the effect of the balance of the channels being unchanged. After adding the results, you have a new color balance that is a blend of a and b.

    Another method is a subtractive approach: Multiple each channel of color a by the corresponding channels of color b and divide by 255.

    Or an another additive approach: Add the channels together and take it as the result if it is <= 255, otherwise take 255.

    ... and so on and so on ...

    Notice that all these methods work relatively well when the initial two colors are close to another. It's getting difficult in some other cases: The colors red, (255, 0, 0), and cyan, (0, 255, 255), are complementary colors. An additive approach will results in white. A subtractive approach will result in black. For a color "in-between", let's look at hue (the HSB/HSL image from Wikipedia):

    hue

    You can see that the color red is on the left and on the right. The simplification made by the color space is now troublesome when we want to pick a hue in-between. Do we pick a color between 0° and 180° or do we pick a color between 180° and 360°? The first example that I've shown will pick a color from the lower half. You can see well why two colors that are close to another will yield a good result and colors that are far away will yield a bad result.

    A perfect example for that: (255, 0, 0) and (255, 0, 50), which are red and pink, will result in any color: green, blue, yellow and so on. What you can do to solve it is to look at the distance and to add 360° if needed. E.g. color a with the hue 10° and color b with the hue 350° would become color a with 370° and b with still 350°. The randomly picked hue in this range modulo 360 is the hue of the new color.

    My suggestion is to use the blending method, as it yields a result from an additive approach, which is closer to how visible light works, and does not increase the brightness unproportionally like some other methods would.