将旋转图像堆叠成一个组合图像
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
中,您只需将它们绘制到 JPanel
的 Graphics
中,您实际上就是在画手。
但考虑到区别仅在于您绘制的 Graphics
对象,这是以后可以轻松更改的内容(如果相应实施),也许您确实有理由这样做创建这些图像。
在最低级别:函数 degreesToRadian
应完全替换为 Math.toRadians
。
下面是一个示例,实现为 MCVE。 (这意味着它不使用 HandOfCards
和 PlayingCards
类。相反,它对 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;
}
}
我正在编写一个扑克游戏,目前正在尝试编写一个 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
中,您只需将它们绘制到 JPanel
的 Graphics
中,您实际上就是在画手。
但考虑到区别仅在于您绘制的 Graphics
对象,这是以后可以轻松更改的内容(如果相应实施),也许您确实有理由这样做创建这些图像。
在最低级别:函数 degreesToRadian
应完全替换为 Math.toRadians
。
下面是一个示例,实现为 MCVE。 (这意味着它不使用 HandOfCards
和 PlayingCards
类。相反,它对 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;
}
}