Niss36 Niss36 - 4 months ago 16
Java Question

Drawing a rounded rectangle with opacity on a BufferedImage

I have been trying to implement basic text bubbles for a small game I am developing. Not wanting to go too fancy, I started with a basic rounded rectangle with a border containing some text :

Basic Text Bubble

Then, I decided that text bubbles should fade out after a preset time. And this is where I stumbled upon a problem : when I tried to display the bubbles in a test window, everything worked fine, but when I displayed them in game, there was a distortion when the bubble faded out. I tested some more, debugged, and found the only difference between the two cases was that on the test window I drew the bubble with the paintComponent method's Graphics, while in game I used BufferedImages to simulate layers and used the graphics from image.createGraphics. I could then successfully replicate the bug :

Gif displaying the bug

Here, you see that when the bubble on the left is fading, its rounded corners change shape compared to before fading, whereas the bubble on the right's rounded corners do not change. Indeed, the left bubble is drawn on a BufferedImage which is then drawn on the panel, whereas the right bubble is directly drawn on the panel.

I have isolated the code which is needed to reproduce the problem :

public static void main(String[] args) {

JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setSize(400, 400);

JPanel panel = new JPanel() {

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

BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics graphics = image.createGraphics();

paintExampleBubble(graphics, 50, 50);

g.drawImage(image, 0, 0, this);

paintExampleBubble(g, 250, 50);
}
};

frame.getContentPane().add(panel);
frame.setVisible(true);
}

private static final Color background = new Color(1f, 1f, 1f, 0.5f);
private static final Color foreground = new Color(0f, 0f, 0f, 0.5f);
private static final int borderRadius = 16;
private static final int width = 100;
private static final int height = 50;

private static void paintExampleBubble(Graphics g, int x, int y) {

g.setColor(background);
g.fillRoundRect(x, y, width, height, borderRadius, borderRadius);
g.setColor(foreground);
g.drawRoundRect(x, y, width, height, borderRadius, borderRadius);
}


Here is the result the above code produces :

Minimal code bug reproduction image

Anyways, this shows that drawing to the BufferedImage is what causes the problem, however it is not an option to let go of BufferedImages at the moment.

I tried to debug the code to see what could cause this difference and only could notice that the graphics object uses different components to draw when transparency was involved, however that doesn't help me in solving my problem as, even if it was possible to force the graphics to do what I want them to, I'd rather avoid hacking if possible.

Does anyone know of a relatively simple and efficient way to solve this problem, or work around it ?

Anyways, thanks for taking the time to read this :)

PS : As this is the first time I ask a question, I may have missed some stuff, so feel free to tell me if that is the case ! It'd be much appreciated.

EDIT : As I said in the comments, the game is pixel-art based therefore I would rather not use anti-aliasing, but keep the basic pixelated look of rounded rectangles.

Answer

Here, you see that when the bubble on the left is fading, its rounded corners change shape compared to before fading, whereas the bubble on the right's rounded corners do not change. Indeed, the left bubble is drawn on a BufferedImage which is then drawn on the panel, whereas the right bubble is directly drawn on the panel.

Rather than redrawing the image each time with a different alpha value, create it once and use AlphaComposite to manage the transparency.

Below is an adaptation of your example with three 'bubbles': far left is drawing the image every time changing the foreground color, the two on the right use AlphaComposite (middle using an image that was created once, far right uses the JPanel Graphics directly).

public class Test {

    public static void main(String[] args) {

        JFrame frame = new JFrame("Test");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setSize(600, 200);
        final BufferedImage image = new BufferedImage(600, 200, BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = image.createGraphics();
        paintExampleBubble(graphics, 250, 50, foreground);
        graphics.dispose();
        final JPanel panel = new JPanel() {

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

                final BufferedImage i2 = new BufferedImage(600, 200, BufferedImage.TYPE_INT_ARGB);
                Graphics2D graphics = i2.createGraphics();
                paintExampleBubble(graphics, 50, 50, alphaForeground);
                graphics.dispose();
                g.drawImage(i2, 0, 0, this);
                //use Alpha Composite for transparency
                Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER , alpha );
                g2d.setComposite(comp);
                g2d.drawImage(image, 0, 0, this);

                paintExampleBubble(g2d, 450, 50, foreground);
            }
        };
        javax.swing.Timer timer = new javax.swing.Timer(100, new ActionListener(){

            @Override
            public void actionPerformed(ActionEvent e) {
                alpha -= 0.05;

                if ( alpha < 0 ){
                    alpha = 1.0f;
                }
                alphaForeground = new Color(0f, 0f, 0f, alpha);
                panel.repaint();
            }

        });
        timer.start();
        frame.getContentPane().add(panel);
        frame.setVisible(true);
    }

    private static float alpha = 1.0f;
    private static final Color background = new Color(1f, 1f, 1f, 1f);
    private static final Color foreground = new Color(0f, 0f, 0f, 1f);
    private static Color alphaForeground = new Color(0f, 0f, 0f, alpha);
    private static final int borderRadius = 16;
    private static final int width = 100;
    private static final int height = 50;

    private static void paintExampleBubble(Graphics g, int x, int y, Color color) {
        g.setColor(background);
        g.fillRoundRect(x, y, width, height, borderRadius, borderRadius);
        g.setColor(color);
        g.drawRoundRect(x, y, width, height, borderRadius, borderRadius);
    }
}

On my system I see distortion on far left (managing transparency with foretground color) but not with the AlphaComposite transparency

Comments