Reputation:
I am creating a retro arcade game in Java. The screen resolution for the game is 304 x 256, which I want to keep to preserve the retro characteristics of the game (visuals, animations, font blockiness, etc.).
But when I render this on a large desktop display, it is too small, as one would expect.
I'd like to be able to scale the window up say by a constant factor, without having to code the various paint(Graphics)
methods to be knowledgeable about the fact that there's a scale-up. That is, I'd like the rendering code believe that the screen is 304 x 256. I also don't want to have to change my desktop resolution or go into full screen exclusive mode. Just want a big window with scaled up pixels, essentially.
I'd be looking for something along the following lines:
scale(myJFrame, 4);
and have all the contents automatically scale up.
UPDATE: Regarding input, my game happens to use keyboard input, so I don't myself need the inverse transform that trashgod describes. Still I can imagine that others would need that, so I think it's an appropriate suggestion.
Upvotes: 2
Views: 1557
Reputation:
Thanks to trashgod for pointing me in the right direction with his two answers. I was able to combine elements of both answers to arrive at something that works for what I need to do.
So first, my goal was to scale up an entire UI rather than scaling up a single icon or other simple component. By "an entire UI" I specifically mean a JPanel
containing multiple custom child components laid out using a BorderLayout
. There are no JButtons
or any other interactive Swing components, and no mouse input (it's all keyboard-based input), so really I just need to scale a 304 x 256 JPanel
up by a factor of 3 or 4.
Here's what I did:
package bb.view;
import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import static bb.BBConfig.SCREEN_WIDTH_PX; // 304
import static bb.BBConfig.SCREEN_HEIGHT_PX; // 256
public class Resizer extends JPanel {
private static final int K = 3;
private static final Dimension PREF_SIZE =
new Dimension(K * SCREEN_WIDTH_PX, K * SCREEN_HEIGHT_PX);
private static final AffineTransform SCALE_XFORM =
AffineTransform.getScaleInstance(K, K);
public Resizer(JComponent component) {
setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
add(component);
}
@Override
public Dimension getPreferredSize() {
return PREF_SIZE;
}
@Override
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setTransform(SCALE_XFORM);
super.paint(g2);
}
}
Some important elements of the solution:
FlowLayout
here shrinkwraps the child component, which is what I want. (Thanks trashgod for that.) That is, I don't want the child to expand to fill the Resizer
preferred size, because that wrecks the child component's layout. Specifically it was creating this huge gap between the child component's CENTER
and SOUTH
regions.FlowLayout
with left alignment and hgap, vgap = 0. That way my scale transform would have the scaled up version anchored in the upper left corner too.AffineTransform
to accomplish the scaling. (Again thanks trashgod.)paint()
instead of paintComponent()
because the Resizer
is simply a wrapper. I don't want to paint a border. I basically want to intercept the paint()
call, inserting the scale transform and then letting the JPanel.paint()
do whatever it would normally do.BufferedImage
.The end result is that the UI is large, but the all the code other than this Resizer
thinks the UI is 304 x 256.
Upvotes: 2
Reputation: 205885
One approach, suggested here, is to rely on the graphics context's scale()
method and construct to an inverse transform to convert between mouse coordinates and image coordinates. In the example below, note how the original image is 256 x 256, while the displayed image is scaled by SCALE = 2.0
. The mouse is hovering over the center the image; the tooltip shows an arbitrary point in the display and the center point (127, 127) in the original.
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
/** @see https://stackoverflow.com/a/2244285/230513 */
public class InverseTransform {
private static final double SCALE = 2.0;
public static void main(String[] args) {
JFrame frame = new JFrame("Inverse Test");
BufferedImage image = getImage(256, 'F');
AffineTransform at = new AffineTransform();
at.scale(SCALE, SCALE);
frame.add(new ImageView(image, at));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
private static BufferedImage getImage(int size, char c) {
final Font font = new Font("Serif", Font.BOLD, size);
BufferedImage bi = new BufferedImage(
size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setPaint(Color.white);
g2d.fillRect(0, 0, size, size);
g2d.setPaint(Color.blue);
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int x = (size - fm.charWidth(c)) / 2;
int y = fm.getAscent() + fm.getDescent() / 4;
g2d.drawString(String.valueOf(c), x, y);
g2d.setPaint(Color.black);
g2d.drawLine(0, y, size, y);
g2d.drawLine(x, 0, x, size);
g2d.fillOval(x - 3, y - 3, 6, 6);
g2d.drawRect(0, 0, size - 1, size - 1);
g2d.dispose();
return bi;
}
private static class ImageView extends JPanel {
private BufferedImage image;
private AffineTransform at;
private AffineTransform inverse;
private Graphics2D canvas;
private Point oldPt = new Point();
private Point newPt;
@Override
public Dimension getPreferredSize() {
return new Dimension( // arbitrary multiple of SCALE
(int)(image.getWidth() * SCALE * 1.25),
(int)(image.getHeight() * SCALE * 1.25));
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
try {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
inverse = g2d.getTransform();
inverse.invert();
g2d.translate(this.getWidth() / 2, this.getHeight() / 2);
g2d.transform(at);
g2d.translate(-image.getWidth() / 2, -image.getHeight() / 2);
inverse.concatenate(g2d.getTransform());
g2d.drawImage(image, 0, 0, this);
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace(System.err);
}
}
ImageView(final BufferedImage image, final AffineTransform at) {
this.setBackground(Color.lightGray);
this.image = image;
this.at = at;
this.canvas = image.createGraphics();
this.canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
this.canvas.setColor(Color.BLACK);
this.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
Point m = e.getPoint();
Point i = e.getPoint();
try {
inverse.inverseTransform(m, i);
setToolTipText("<html>Mouse: " + m.x + "," + m.y
+ "<br>Inverse: " + i.x + "," + i.y + "</html>");
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
});
}
}
}
Upvotes: 2
Reputation: 205885
One approach, suggested here, is to rely on drawImage()
to scale an image of the content. Your game would render itself in the graphics context of a BufferedImage
, rather than your implementation of paintComponent()
. If the game includes mouse interaction, you'll have to scale the mouse coordinates as shown. In the variation below, I've given the CENTER
panel a preferred size that is a multiple of SCALE = 8
and added the original as an icon in the WEST
of a BorderLayout
. As the default, CENTER
, ignores a component's preferred size, you may want to add it to a (possibly nested) panel having FlowLayout
. Resize the frame to see the effect.
f.setLayout(new FlowLayout());
f.add(new Grid(NAME));
//f.add(new JLabel(ICON), BorderLayout.WEST);
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;
/**
* @see https://stackoverflow.com/a/44373975/230513
* @see http://stackoverflow.com/questions/2900801
*/
public class Grid extends JPanel implements MouseMotionListener {
private static final String NAME = "OptionPane.informationIcon";
private static final Icon ICON = UIManager.getIcon(NAME);
private static final int SCALE = 8;
private final BufferedImage image;
private int imgW, imgH, paneW, paneH;
public Grid(String name) {
super(true);
imgW = ICON.getIconWidth();
imgH = ICON.getIconHeight();
image = new BufferedImage(imgW, imgH, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D) image.getGraphics();
ICON.paintIcon(null, g2d, 0, 0);
g2d.dispose();
this.addMouseMotionListener(this);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(imgW * SCALE, imgH * SCALE);
}
@Override
protected void paintComponent(Graphics g) {
paneW = this.getWidth();
paneH = this.getHeight();
g.drawImage(image, 0, 0, paneW, paneH, null);
}
@Override
public void mouseMoved(MouseEvent e) {
Point p = e.getPoint();
int x = p.x * imgW / paneW;
int y = p.y * imgH / paneH;
int c = image.getRGB(x, y);
this.setToolTipText(x + "," + y + ": "
+ String.format("%08X", c));
}
@Override
public void mouseDragged(MouseEvent e) {
}
private static void create() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new Grid(NAME));
f.add(new JLabel(ICON), BorderLayout.WEST);
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
create();
}
});
}
}
Upvotes: 2