I'm creating some custom Swing components that I'd like to have fade from one color to another. At the moment I'm converting from RGB to HSB then incrementing through the Hue value and converting back to RGB before painting, work's fine.

However, this cycles through all the colors (i.e. attempting to fade from blue to green cycles through yellow, orange, red etc). Is there a decent algorithm/method to fade directly from one color into another?

Edit: I already had it updating via a Swing Timer (I try to steer clear of touching components with Threads like the plague). I'll have a go this evening with your suggestions, THANKS GUYS!


Solution 1:

Based on this example, the Queue<Color> below cycles from Color.green to Color.blue and back to Color.green again in N = 32 steps. Note that Color.green is numerically less than Color.blue in the HSB model. See also this related example using HSB.

enter image description here

public Flash(JComponent component) {
    this.component = component;
    float gHue = Color.RGBtoHSB(0, 1, 0, null)[0];
    float bHue = Color.RGBtoHSB(0, 0, 1, null)[0];
    for (int i = 0; i < N; i++) {
        clut.add(Color.getHSBColor(gHue + (i * (bHue - gHue) / N), 1, 1));
    }
    for (int i = 0; i < N; i++) {
        clut.add(Color.getHSBColor(bHue - (i * (bHue - gHue) / N), 1, 1));
    }
}

Solution 2:

I use a combination of approaches to achieve the same result.

Baiscally I use a simular API interface as the LinearGradientPaint, where I supply an array of fractions and an array of colors, then based a float percentage, I calculate the resulting blend color.

This allows me to produce a number of, effective, results through the same algorithm.

enter image description here

While this example is designed to demonstrate a blending of a range of colors, you could simply supply two colors and a fraction of {0f, 1f} for two colors

This allows me to effectively do color animation as well.

public class ColorFade {

    public static void main(String[] args) {
        new ColorFade();
    }

    public ColorFade() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException ex) {
                } catch (InstantiationException ex) {
                } catch (IllegalAccessException ex) {
                } catch (UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
//                frame.add(new FadePane());
                frame.add(new ColorFadePane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class FadePane extends JPanel {

        private float[] fractions = new float[]{0f, 0.25f, 0.5f, 1f};
        private Color[] colors = new Color[]{Color.GREEN, Color.BLUE, Color.YELLOW, Color.RED};
        private float direction = 0.05f;
        private float progress = 0f;

        public FadePane() {
            Timer timer = new Timer(125, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (progress + direction > 1f) {
                        direction = -0.05f;
                    } else if (progress + direction < 0f) {
                        direction = 0.05f;
                    }
                    progress += direction;
                    repaint();
                }
            });
            timer.setCoalesce(true);
            timer.setRepeats(true);
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 100);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            int width = getWidth();
            int height = getHeight();
            Color startColor = blendColors(fractions, colors, progress);
            g2d.setColor(startColor);
            g2d.fillRect(0, 0, width, height);
            g2d.dispose();
        }
    }

    public class ColorFadePane extends JPanel {

        private float[] fractions = new float[]{0f, 0.25f, 0.5f, 1f};
        private Color[] colors = new Color[]{Color.GREEN, Color.BLUE, Color.YELLOW, Color.RED};

        public ColorFadePane() {
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 100);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();
            int width = getWidth();
            int height = getHeight();
            int bandWidth = width / 100;
            for (int index = 0; index < 100; index++) {
                float progress = (float)index / (float)100;
                Color color = blendColors(fractions, colors, progress);

                int x = bandWidth * index;
                int y = 0;
                g2d.setColor(color);
                g2d.fillRect(x, y, bandWidth, height);
            }
            g2d.dispose();
        }
    }

    public static Color blendColors(float[] fractions, Color[] colors, float progress) {
        Color color = null;
        if (fractions != null) {
            if (colors != null) {
                if (fractions.length == colors.length) {
                    int[] indicies = getFractionIndicies(fractions, progress);

                    float[] range = new float[]{fractions[indicies[0]], fractions[indicies[1]]};
                    Color[] colorRange = new Color[]{colors[indicies[0]], colors[indicies[1]]};

                    float max = range[1] - range[0];
                    float value = progress - range[0];
                    float weight = value / max;

                    color = blend(colorRange[0], colorRange[1], 1f - weight);
                } else {
                    throw new IllegalArgumentException("Fractions and colours must have equal number of elements");
                }
            } else {
                throw new IllegalArgumentException("Colours can't be null");
            }
        } else {
            throw new IllegalArgumentException("Fractions can't be null");
        }
        return color;
    }

    public static int[] getFractionIndicies(float[] fractions, float progress) {
        int[] range = new int[2];

        int startPoint = 0;
        while (startPoint < fractions.length && fractions[startPoint] <= progress) {
            startPoint++;
        }

        if (startPoint >= fractions.length) {
            startPoint = fractions.length - 1;
        }

        range[0] = startPoint - 1;
        range[1] = startPoint;

        return range;
    }

    public static Color blend(Color color1, Color color2, double ratio) {
        float r = (float) ratio;
        float ir = (float) 1.0 - r;

        float rgb1[] = new float[3];
        float rgb2[] = new float[3];

        color1.getColorComponents(rgb1);
        color2.getColorComponents(rgb2);

        float red = rgb1[0] * r + rgb2[0] * ir;
        float green = rgb1[1] * r + rgb2[1] * ir;
        float blue = rgb1[2] * r + rgb2[2] * ir;

        if (red < 0) {
            red = 0;
        } else if (red > 255) {
            red = 255;
        }
        if (green < 0) {
            green = 0;
        } else if (green > 255) {
            green = 255;
        }
        if (blue < 0) {
            blue = 0;
        } else if (blue > 255) {
            blue = 255;
        }

        Color color = null;
        try {
            color = new Color(red, green, blue);
        } catch (IllegalArgumentException exp) {
            NumberFormat nf = NumberFormat.getNumberInstance();
            System.out.println(nf.format(red) + "; " + nf.format(green) + "; " + nf.format(blue));
            exp.printStackTrace();
        }
        return color;
    }
}

Solution 3:

The easiest way would be to interpolate between each of the RGB values. This would be the same for all languages -- the python code would look like:

steps = 10

rgb1 = [ 'AA', '08', 'C3' ]
rgb2 = [ '03', '88', '1C' ]

h1 = map( lambda s: int( '0x'+s, 0 ), rgb1 )
h2 = map( lambda s: int( '0x'+s, 0 ), rgb2 )

inc = [0, 0, 0]
for i in range(0,3):
    inc[i] = ( h2[i] - h1[i] ) / ( steps - 1 )

for i in range(0,steps-1):
    print '<span style="background: #%02x%02x%02x"> &nbsp; %i &nbsp; </span>' % ( 
            h1[0] + i * inc[0],
            h1[1] + i * inc[1],
            h1[2] + i * inc[2],
            i+1 )

print '<span style="background: #%02x%02x%02x"> &nbsp; %i &nbsp; </span>' % ( 
        h2[0], h2[1], h2[2], steps )