7

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.

mKorbel
  • 108,320
  • 17
  • 126
  • 296
Niss36
  • 305
  • 3
  • 8
  • Not exactly sure if I'm correctly seeing what you describe, but try to turn on antialiasing `g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)` where `g` is an instance of `Graphics2D` – copeg Jul 29 '16 at 17:50
  • Thank you for your quick answer ! I have tried it right now, and it does work (as in, the border no longer changes when transparency kicks in). However, and I failed to mention this, the game I am working on features only pixel-art graphics and automatic anti-aliasing stands out too much, plus it does look very blurry because of the low resolution. I'll update my question to clarify – Niss36 Jul 29 '16 at 17:59
  • haven't really followed exactly what you are trying to do, but here is a posting that may (or may not) give you a different approach to use: http://stackoverflow.com/questions/15025092/border-with-rounded-corners-transparency – camickr Jul 29 '16 at 20:24
  • @camickr I am working on a game, and am trying to implement relatively simple text bubbles which fade out. I had already seen the question you've linked, and the reason why it doesn't fit my needs is that I don't want the bubbles to be actual Swing components, although it is definitely an interesting approach. I've already got a working solution which I marked as the accepted answer, too. – Niss36 Jul 29 '16 at 23:03

1 Answers1

3

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

copeg
  • 8,092
  • 17
  • 27
  • 1
    Thank you ! I have managed to port your answer to my specific case (Which differs a lot from the code I posted, actually) and made it work. However, it instanciates one BufferedImage per text bubble, which could maybe lead to performance issues with a lot of bubbles to display, but this is not a concern at the moment and I'll mark your answer as accepted, until someone comes up with a solution not involving additional BufferedImages. – Niss36 Jul 29 '16 at 18:52
  • You could use a single intermediate buffer for all your text bubbles. You can clear it each time, draw the bubble in full opacity and alphacomposite it on the component. Then the next bubble can use the same intermediary buffer – Falco Jul 29 '16 at 22:08
  • Could you elaborate a bit more please ? I'm not sure I understand what you mean. What I guess is something like a static BufferedImage the size of the scene to which a text bubble is drawn, then the image is drawn with alpha on the screen, then cleared, then repeat for the next bubble. – Niss36 Jul 29 '16 at 22:58