Java: Smooth Color Transition

I am trying to make a health bar, and as what might be original, it will start out green, and after losing health you find that it will turn yellow, then orange, then red.. or something relative to that.

I tried using the method provided in this link: https://stackoverflow.com/questions/19841477/java-smooth-color-transition

The result from that link was this code, just a test from value 100 to 0, but it ended in an IllegalArgumentException at normally Red and Green, and my guess for reason is it being over the value of 255.

Color to = Color.red;
Color base = Color.green;
int red = (int)Math.abs((100 * to.getRed()) + ((1 - 100) * base.getRed()));
int green = (int)Math.abs((100 * to.getGreen()) + ((1 - 100) * base.getGreen()));
int blue = (int)Math.abs((100 * to.getBlue()) + ((1 - 100) * base.getBlue()));
setForeground(new Color(red, green, blue));

It didn't really work, and I have absolutely no idea how I can get it to transition the way I wish it to.

So within my HealthBar class, I have an update() method

public void update() {
    if (getValue() < 10) setForeground(Color.red);
    else if (getValue() < 25) setForeground(Color.orange);
    else if (getValue() < 60) setForeground(Color.yellow);
    else setForeground(Color.green);
}

This code does a basic transition at certain points.

I need to create fields to use certain colors at certain values of the health bar, so now I have this..

if (getValue() < 10) {
    Color to = Color.black;
    // Color current = getForeground() ?
    Color from = Color.red;
    // ?
}

I am just going to use an example for the last bit.

So I know that I am going to have a color that I am going to, and a color that is a base. I am not so sure if I need a color for current. The problem that I see now is for the steps of the transition because each transition has a different amount of steps.

Summary and Question
I don't know how to achieve what I am attempting, all I know for sure is I need a to and base color, and I provided a link to an answer that I saw, but I couldn't really figure it out. With the information given, how could I get it to transition colors ?


Solution 1:

I spent a lot of time trying to find/create a blending algorithm that worked for me, this is basically what I was able to hobble together.

I've used this approach to generate a gradient transition mixing multiple colors, as demonstrated here

Basically, this approach allows you to set up a series of colors and percentage marks so that you gain much greater control over which points the colors transition between.

Blend

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.text.NumberFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ColorFading {

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

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

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

    public class FadePane extends JPanel {

        private final float[] fractions = new float[]{0f, 0.5f, 1f};
        private final Color[] colors = new Color[]{Color.RED, Color.YELLOW, Color.GREEN};
        private float progress = 1f;
        private JSlider slider;

        public FadePane() {
            slider = new JSlider(0, 100);
            setLayout(new BorderLayout());
            add(slider, BorderLayout.SOUTH);

            slider.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    progress = ((float)slider.getValue() / 100f);
                    repaint();
                }
            });
            slider.setValue(100);
        }

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

        @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 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 2:

OK, before Mad posted his answer (and 1+ to it), I was working on this too, so I might as well post what I came up with....

import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EnumMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.event.*;

@SuppressWarnings("serial")
public class ColorTransition extends JPanel {

   private static final int TRANSITION_DELAY = 30;
   private static final int PREF_W = 800;
   private static final int PREF_H = 600;
   private RgbSliderPanel rgbSliderPanel1 = new RgbSliderPanel("Color 1");
   private RgbSliderPanel rgbSliderPanel2 = new RgbSliderPanel("Color 2");
   private Color background1;
   private Color background2;
   private JButton button = new JButton(new ButtonAction("Push Me"));

   public ColorTransition() {
      setBackground(Color.black);

      add(rgbSliderPanel1.getMainPanel());
      add(rgbSliderPanel2.getMainPanel());

      add(button);

      rgbSliderPanel1.addPropertyChangeListener(new PropertyChangeListener() {

         @Override
         public void propertyChange(PropertyChangeEvent evt) {
            if (RgbSliderPanel.COLOR.equals(evt.getPropertyName())) {
               setBackground(rgbSliderPanel1.calculateColor());
            }
         }
      });
   }

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

   @Override
   public void setEnabled(boolean enabled) {
      super.setEnabled(enabled);
      button.setEnabled(enabled);
      rgbSliderPanel1.setEnabled(enabled);
      rgbSliderPanel2.setEnabled(enabled);
   }

   private class ButtonAction extends AbstractAction {

      public ButtonAction(String name) {
         super(name);
      }

      @Override
      public void actionPerformed(ActionEvent e) {
         ColorTransition.this.setEnabled(false);
         background1 = rgbSliderPanel1.calculateColor();
         background2 = rgbSliderPanel2.calculateColor();

         setBackground(background1);

         Timer timer = new Timer(TRANSITION_DELAY, new TransitionListener());
         timer.start();
      }

      private class TransitionListener implements ActionListener {
         private int index = 0;

         @Override
         public void actionPerformed(ActionEvent e) {
            if (index > 100) {
               ((Timer) e.getSource()).stop();
               ColorTransition.this.setEnabled(true);
            } else {
               int r = (int) (background2.getRed() * index / 100.0 + background1
                     .getRed() * (100 - index) / 100.0);
               int g = (int) (background2.getGreen() * index / 100.0 + background1
                     .getGreen() * (100 - index) / 100.0);
               int b = (int) (background2.getBlue() * index / 100.0 + background1
                     .getBlue() * (100 - index) / 100.0);
               setBackground(new Color(r, g, b));
            }
            index++;
         }
      }
   }

   private static void createAndShowGui() {
      JFrame frame = new JFrame("ColorTransition");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(new ColorTransition());
      frame.pack();
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

enum Rgb {
   RED("Red"), GREEN("Green"), BLUE("Blue");
   private String name;

   private Rgb(String name) {
      this.name = name;
   }

   public String getName() {
      return name;
   }
}

class RgbSliderPanel {
   public static final String COLOR = "color";
   private JPanel mainPanel = new JPanel();
   private SwingPropertyChangeSupport propertyChangeSupport = new SwingPropertyChangeSupport(
         this);
   private Map<Rgb, JSlider> colorSliderMap = new EnumMap<>(Rgb.class);
   private String name;
   protected Color color;

   public RgbSliderPanel(String name) {
      this.name = name;
      mainPanel.setBorder(BorderFactory.createTitledBorder(name));
      //mainPanel.setOpaque(false);
      mainPanel.setLayout(new GridLayout(0, 1));
      for (Rgb rgb : Rgb.values()) {
         JSlider colorSlider = new JSlider(0, 255, 0);
         colorSliderMap.put(rgb, colorSlider);
         mainPanel.add(colorSlider);
         colorSlider.setBorder(BorderFactory.createTitledBorder(rgb.getName()));
         colorSlider.setPaintTicks(true);
         colorSlider.setPaintTrack(true);
         colorSlider.setMajorTickSpacing(50);
         colorSlider.setMinorTickSpacing(10);
         colorSlider.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
               Color oldValue = color;
               Color newValue = calculateColor();
               color = newValue;
               propertyChangeSupport.firePropertyChange(COLOR, oldValue,
                     newValue);
            }
         });

      }
   }

   public JComponent getMainPanel() {
      return mainPanel;
   }

   public void setEnabled(boolean enabled) {
      for (JSlider slider : colorSliderMap.values()) {
         slider.setEnabled(enabled);
      }
   }

   public Color calculateColor() {
      int r = colorSliderMap.get(Rgb.RED).getValue();
      int g = colorSliderMap.get(Rgb.GREEN).getValue();
      int b = colorSliderMap.get(Rgb.BLUE).getValue();
      return new Color(r, g, b);
   }

   public String getName() {
      return name;
   }

   public void addPropertyChangeListener(PropertyChangeListener listener) {
      propertyChangeSupport.addPropertyChangeListener(listener);
   }

   public void removePropertyChangeListener(PropertyChangeListener listener) {
      propertyChangeSupport.removePropertyChangeListener(listener);
   }
}