在BufferedImage上绘制具有不透明度的圆角矩形

7
我一直在尝试为我正在开发的小游戏实现基本的文字气泡。我开始时并不想太花哨,只用一个带有边框的基本圆角矩形来包含一些文本:
Basic Text Bubble

然后,我决定文本气泡在预设时间后应该淡出。这就是我遇到问题的地方:当我尝试在测试窗口中显示气泡时,一切都正常工作,但当我在游戏中显示它们时,当气泡淡出时会出现扭曲。我进行了更多的测试和调试,并发现两种情况之间唯一的区别是,在测试窗口上,我使用paintComponent方法的图形来绘制气泡,而在游戏中,我使用BufferedImages模拟图层,并使用来自image.createGraphics的图形。我成功地复制了这个错误:
Gif displaying the bug

在这里,您可以看到当左侧的气泡消失时,其圆角与消失前相比发生了形状变化,而右侧的气泡的圆角没有变化。的确,左侧的气泡是绘制在一个BufferedImage上,然后绘制在面板上,而右侧的气泡是直接绘制在面板上。我已经隔离出需要复制问题的代码:

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);
}

这是上述代码产生的结果:
Minimal code bug reproduction image 总之,这表明绘制到BufferedImage是导致问题的原因,但目前放弃BufferedImages不是一个选项。
我尝试调试代码以查看可能导致此差异的原因,并且只能注意到当涉及透明度时,图形对象使用不同的组件进行绘制,但是这并不能帮助我解决我的问题,因为即使可以强制图形执行我想要的操作,如果可能的话,我宁愿避免黑客攻击。
有人知道相对简单和高效的方法来解决这个问题或解决方法吗?
无论如何,感谢您抽出时间阅读这篇文章:)
PS:正如我在评论中所说,游戏是基于像素艺术的,因此我宁愿不使用反走样,而是保持圆角矩形的基本像素化外观。

我不确定我是否正确理解了您所描述的内容,但请尝试打开抗锯齿功能 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON),其中 gGraphics2D 的实例。 - copeg
谢谢您的快速回答!我现在已经尝试了,它确实有效(也就是说,当透明度生效时,边框不再改变)。然而,我没有提到的是,我正在开发的游戏只使用像素艺术图形,并且自动抗锯齿太过突出,因为分辨率太低,看起来非常模糊。我会更新我的问题以澄清。 - Niss36
我并没有完全明白你想做什么,但是这里有一个帖子,也许(或者也许不会)给你提供一个不同的方法来使用:https://dev59.com/ImUp5IYBdhLWcg3wz6AH - camickr
@camickr 我正在开发一个游戏,尝试实现相对简单的文本气泡并使其渐隐。我已经看过你提供的链接中的问题,但它不符合我的需求,因为我不想让气泡成为实际的Swing组件,尽管这绝对是一种有趣的方法。我已经有了一个可行的解决方案,并将其标记为接受的答案。 - Niss36
1个回答

3
这里,您可以看到左侧的气泡正在消失时,与消失前相比,其圆角形状发生了变化,而右侧的气泡的圆角没有改变。实际上,左侧的气泡是绘制在 BufferedImage 上,然后再绘制在面板上,而右侧的气泡直接绘制在面板上。
不要每次使用不同的 alpha 值重新绘制图像,而是创建一次并使用 AlphaComposite 来管理透明度。
下面是您的示例的改编,其中有三个“气泡”:最左边每次更改前景色都会重新绘制图像,右侧的两个使用 AlphaComposite(中间使用一次创建的图像,最右侧使用 JPanel Graphics 直接)。
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);
    }
}

在我的系统上,我看到左侧出现了扭曲(使用前景色管理透明度),但使用AlphaComposite透明度时没有出现这种情况。

1
谢谢!我已经成功将您的答案移植到我的特定情况中(实际上与我发布的代码有很大不同),并使其正常工作。但是,它为每个文本气泡实例化了一个BufferedImage,如果要显示很多气泡,则可能会导致性能问题,但目前这不是一个问题,我将接受您的答案,直到有人提出不涉及额外BufferedImages的解决方案。 - Niss36
你可以使用一个单独的中间缓冲区来存储所有的文本气泡。每次清空它,在完全不透明的情况下绘制气泡,并将其与组件进行合成。然后下一个气泡可以使用相同的中介缓冲区。 - Falco
请您再详细解释一下吗?我不确定我理解您的意思。我的猜测是创建一个静态的BufferedImage,大小与场景相同,在其上绘制一个文本气泡,然后将该图像带有alpha值地绘制在屏幕上,清除图像,接着为下一个气泡重复此过程。 - Niss36

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接