将旋转图像堆叠成一个组合图像

Stacking rotated images into one combined image

我正在编写一个扑克游戏,目前正在尝试编写一个 class 来生成给定手牌的图像。我最初只是通过将 5 张卡片中的每一张的图像彼此并排组合来生成图像。这是结果:

然后我决定,将卡片堆叠在一起并呈扇形展开的手牌会更好,就像拿着一手卡片一样。

这是迄今为止我能做到的最好成绩:

正如您所看到的,最后 3 张牌看起来应该是这样,但是前三张牌在第三张牌的左侧被切掉了。

这是我目前的代码(它不是最干净的,因为我一直在努力让它工作,不管需要什么)

private static final int CARD_WIDTH = 500;
private static final int CARD_HEIGHT = 726;
private static final double ROTATION = 20.0;

public void createImage(HandOfCards hand) throws IOException {
    int handImageWidth = (int) (2 * (Math.sin(degreesToRadian(ROTATION)) * CARD_HEIGHT + Math.cos(degreesToRadian(ROTATION)) * CARD_WIDTH)- CARD_WIDTH);
    int handImageHeight = (int) (CARD_HEIGHT + Math.sin(degreesToRadian(ROTATION)) * CARD_WIDTH); 

    BufferedImage handImage = new BufferedImage(handImageWidth, handImageHeight, BufferedImage.TYPE_INT_ARGB);
    Graphics2D graphics = (Graphics2D) handImage.getGraphics();

    int xPos = handImageWidth / 2 - CARD_WIDTH / 2;
    int yPos = 0;
    int xAnchor = CARD_WIDTH; 
    int yAnchor = CARD_HEIGHT;

    double rotation = -ROTATION;        
    for (int i = 0; i < HandOfCards.HAND_SIZE; i++) {
        if (i == 3) xAnchor = 0;

        PlayingCard card = hand.getCard(i);
        BufferedImage cardImage = ImageIO.read(new File("cardImages/" + card + ".png"));

        AffineTransform transform = new AffineTransform();
        transform.rotate(degreesToRadian(rotation), xAnchor, yAnchor);
        AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
        cardImage = transformOp.filter(cardImage, null);

        graphics.drawImage(cardImage, xPos, yPos, null);
        rotation += ROTATION / 2;
    }

private double degreesToRadian(double degrees) {
    return (degrees * Math.PI) / 180.0;
}

编辑

为了让事情更清楚,这里是仅首先执行循环(仅绘制第一张卡片)并且背景着色以显示整个图像大小的结果。

您所观察到的行为的原因在 documentation of AffineTransformOp#filter:

中说明

The coordinates of the rectangle returned by getBounds2D(BufferedImage) are not necessarily the same as the coordinates of the BufferedImage returned by this method. If the upper-left corner coordinates of the rectangle are negative then this part of the rectangle is not drawn.

当你用像

这样的语句打印每张卡片的边界时
System.out.println("Bounds: "+transformOp.getBounds2D(cardImage));

你会看到边界是负的(正如人们在向左旋转卡片时所期望的那样)。

这可以通过调整 AffineTransform 始终导致 边界,并使用非 [=15] 调用 filter 方法来避免=] 目标图像(在你的情况下:包含手的图像 - 即所有卡片图像)。


(这个^才是问题的真正答案,剩下的部分可以忽略,或者被认为是我有太多空闲时间的证据)


话虽这么说,但我想提出一个不同的解决方案,因为当前的方法在不同层面上存在一些问题。


最高级别:为什么要创建此图像根本?我猜你正在实施纸牌游戏。在这样的游戏中,你可能会有数百个不同的"hands"。为什么要为 each 手创建 new 图像?

无需为每只手创建图像,您只需直接绘制 旋转图像即可。粗略地说:无需将图像绘制到新图像的 Graphics 中,您只需将它们绘制到 JPanelGraphics 中,您实际上就是在画手。

但考虑到区别仅在于您绘制的 Graphics 对象,这是以后可以轻松更改的内容(如果相应实施),也许您确实有理由这样做创建这些图像。


在最低级别:函数 degreesToRadian 应完全替换为 Math.toRadians


下面是一个示例,实现为 MCVE。 (这意味着它不使用 HandOfCardsPlayingCards 类。相反,它对 BufferedImage 对象的列表进行操作。这些图像实际上是从维基百科下载的,位于运行时间)。

这个例子的核心是RotatedPlayingCardsPainter。它允许您将(旋转的)卡片绘制成 Graphics2D。当您尝试时,这种方法的一个优点可能会变得很明显:您可以使用滑块动态更改卡片之间的 angle。 (在这里为您的游戏想象一些奇特的动画...)

(但如果你愿意,它还包含一个 createImage 方法,允许你创建一个图像,就像问题中最初所做的那样)

当您阅读代码时,您会看到 每个 卡片的 AffineTransform 实例是在 createTransform 方法中创建的。在那里,我添加了一些任意的、神奇的因素来稍微移动卡片,让它们看起来更 "fan-like" 。

比较这张图片,没有神奇的因素

具有神奇因素的那个:

我认为后者看起来更"realistic",但这可能是一个品味问题。


另一个旁注:直接绘制图像的一个缺点(与 AffineTransformOp 方法相比)是图像的边界可能看起来参差不齐,无论过滤和抗锯齿设置如何。这是因为在图像的边界没有任何东西可以插值。在给定的程序中,这是通过 addBorder 方法来规避的,该方法为图像添加了一个 1 像素的透明边框,以确保它看起来不错,并且当图像旋转时边框看起来很平滑。

代码如下:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;

public class RotatedPlayingCards
{
    public static void main(String[] args)
    {
        try
        {
            List<BufferedImage> images = loadTestImages();
            SwingUtilities.invokeLater(() -> createAndShowGui(images));
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

    }

    private static void createAndShowGui(List<BufferedImage> images)
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        RotatedPlayingCardsPanel cardsPanel = 
            new RotatedPlayingCardsPanel(images);

        JSlider angleDegSlider = new JSlider(0, 20, 10);
        angleDegSlider.addChangeListener(e -> {
            double rotationAngleRad = Math.toRadians(angleDegSlider.getValue());
            cardsPanel.setRotationAngleRad(rotationAngleRad);
        });
        JPanel controlPanel = new JPanel();
        controlPanel.add(angleDegSlider);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);

        f.getContentPane().add(cardsPanel, BorderLayout.CENTER);
        f.setSize(500,500);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static List<BufferedImage> loadTestImages() throws IOException
    {
        String basePath = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
        List<String> subPaths = Arrays.asList(
          "3/36/Playing_card_club_A.svg/480px-Playing_card_club_A.svg.png",
          "2/20/Playing_card_diamond_4.svg/480px-Playing_card_diamond_4.svg.png",
          "9/94/Playing_card_heart_7.svg/480px-Playing_card_heart_7.svg.png",
          "2/21/Playing_card_spade_8.svg/480px-Playing_card_spade_8.svg.png",
          "b/bd/Playing_card_spade_J.svg/480px-Playing_card_spade_J.svg.png",
          "0/0b/Playing_card_diamond_Q.svg/480px-Playing_card_diamond_Q.svg.png",
          "2/25/Playing_card_spade_A.svg/480px-Playing_card_spade_A.svg.png"
        );
        List<BufferedImage> result = new ArrayList<BufferedImage>();
        for (String subPath : subPaths)
        {
            String path = basePath + subPath;
            System.out.println("Loading "+path);
            BufferedImage image = ImageIO.read(new URL(path));

            image = scale(image, 0.3);
            image = addBorder(image);
            result.add(image);
        }
        return result;
    }

    // Scale the given image by the given factor
    private static BufferedImage scale(
        BufferedImage image, double factor) 
    {
        int w = (int)(image.getWidth() * factor);
        int h = (int)(image.getHeight() * factor);
        BufferedImage scaledImage = new BufferedImage(w, h, image.getType());
        Graphics2D g = scaledImage.createGraphics();
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(image, 0, 0, w, h, null);
        g.dispose();
        return scaledImage;
    }

    // Add a 1-pixel transparent border to the given image, to avoid
    // aliasing artifacts when the image is rotated
    private static BufferedImage addBorder(
        BufferedImage image) 
    {
        int w = image.getWidth();
        int h = image.getHeight();
        BufferedImage result = new BufferedImage(w + 2, h + 2, image.getType());
        Graphics2D g = result.createGraphics();
        g.setColor(new Color(0,0,0,0));
        g.fillRect(0, 0, w + 2, h + 2);
        g.drawImage(image, 1, 1, w, h, null);
        g.dispose();
        return result;
    }    

}

class RotatedPlayingCardsPanel extends JPanel
{
    private List<BufferedImage> images;
    private double rotationAngleRad;

    public RotatedPlayingCardsPanel(List<BufferedImage> images)
    {
        this.images = images;
        this.rotationAngleRad = Math.toRadians(10);
    }

    public void setRotationAngleRad(double rotationAngleRad)
    {
        this.rotationAngleRad = rotationAngleRad;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);

        g.translate(200, 100);
        RotatedPlayingCardsPainter.drawImages(
            g, images, rotationAngleRad);
    }
}


class RotatedPlayingCardsPainter
{
    public static BufferedImage createImage(
        List<? extends BufferedImage> images, double rotationAngleRad)
    {
        Rectangle2D bounds = computeBounds(images, rotationAngleRad);
        BufferedImage image = new BufferedImage(
            (int)bounds.getWidth(), (int)bounds.getHeight(), 
            BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = image.createGraphics();
        graphics.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        graphics.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        graphics.translate(-bounds.getX(), -bounds.getY());
        drawImages(graphics, images, rotationAngleRad);
        graphics.dispose();
        return image;
    }

    public static Rectangle2D computeBounds(
        List<? extends BufferedImage> images, double rotationAngleRad)
    {
        Rectangle2D totalBounds = null;
        for (int i=0; i<images.size(); i++)
        {
            BufferedImage image = images.get(i);
            AffineTransform transform = createTransform(
                i, images.size(), image.getWidth(), image.getHeight(), 
                rotationAngleRad);
            Rectangle2D imageBounds = new Rectangle2D.Double(0.0, 0.0, 
                image.getWidth(), image.getHeight());
            Rectangle2D transformedBounds = 
                transform.createTransformedShape(imageBounds).getBounds();
            if (totalBounds == null)
            {
                totalBounds = transformedBounds;
            }
            else
            {
                Rectangle.union(transformedBounds, totalBounds, totalBounds);
            }
        }
        return totalBounds;
    }

    public static void drawImages(Graphics2D g, 
        List<? extends BufferedImage> images, double rotationAngleRad)
    {
        for (int i=0; i<images.size(); i++)
        {
            AffineTransform oldAt = g.getTransform();
            BufferedImage image = images.get(i);
            AffineTransform transform = createTransform(
                i, images.size(), image.getWidth(), image.getHeight(), 
                rotationAngleRad);
            g.transform(transform);
            g.drawImage(image, 0, 0, null);
            g.setTransform(oldAt);
        }
    }

    private static AffineTransform createTransform(
        int index, int total, double width, double height, 
        double rotationAngleRad)
    {
        double startAngleRad = (total - 1) * 0.5 * rotationAngleRad;
        double angleRad = index * rotationAngleRad - startAngleRad;
        AffineTransform transform = new AffineTransform();

        // A magic factor to shift the images slightly, to give 
        // them a more fan-like appearance. Just set it to 0.0
        // or remove it if you don't like it.
        double magicFactor = 0.2;

        double magicOffsetFactor = 
            (1.0 - index) * magicFactor * rotationAngleRad;
        double magicOffsetX = -width * magicOffsetFactor;
        double magicOffsetY = height * magicOffsetFactor;
        transform.translate(magicOffsetX, height + magicOffsetY);
        transform.rotate(angleRad);
        transform.translate(0, -height);
        return transform;
    }

}