Draw an image of growing size in Java - java

I have an application where I want to draw an image that grows in width over time. Specifically, the application listens to the microphone and computes a spectrogram every 6 milliseconds, and I want to draw the updated spectrogram when it comes in. I've been using java.awt.image.BufferedImage to draw the spectrograms, but only for pre-recorded files, so I have a fixed width for the image. What's the best way to do this for a streaming application, where I don't know a priori the width of the image?
One possibility is to just create a new BufferedImage with one extra pixel on the right and copy the data over, but that seems inefficient to do hundreds of times per second. Or I could start with a relatively large width and keep the right side blank until it fills up, and double the width when it does, similar to how an ArrayList amortizes its size - I would only have to copy the data a few times per second, or once every few seconds, or so. Is there a better option?

I would use a combination of what you suggest and an overriding component that only paints a subimage of the total image and returns an appropriate preferred size.
Here is a demo code which shows what I mean:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class TestRecording {
public static class MyPanel extends JPanel {
private BufferedImage buffer = new BufferedImage(3000, 100, BufferedImage.TYPE_INT_RGB);
private int width = 0;
private int lastY = 50;
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (width > 0) {
BufferedImage sub = buffer.getSubimage(0, 0, width, buffer.getHeight());
g.drawImage(sub, 0, Math.max(0, (getHeight() - buffer.getHeight()) / 2), this);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(width, 100);
}
protected void drawSomething() {
// Here need to handle growing image
Graphics g = buffer.getGraphics();
g.setColor(Color.GREEN);
int y = new Random().nextInt(buffer.getHeight());
g.drawLine(width, lastY, width + 1, y);
lastY = y;
width += 1;
Rectangle r = new Rectangle();
r.x = getWidth();
// Lame hack to auto-scroll to the end
scrollRectToVisible(r);
revalidate();
repaint();
}
}
protected void initUI() {
JFrame frame = new JFrame(TestRecording.class.getSimpleName());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final MyPanel p = new MyPanel();
JScrollPane scrollpane = new JScrollPane(p);
frame.add(scrollpane);
frame.setSize(400, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Timer t = new Timer(20, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
p.drawSomething();
}
});
t.start();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TestRecording().initUI();
}
});
}
}
And here is the result:

May be just scale the original image to get desired size.
Image has getScaledInstance(int width, int height, int hints) method

Do you have 1) gui where you draw it, or 2) do you need to realy output image file (or stream)?
if 1)
Draw only what is seen. Extend JComponent, I usually use JPanel and override paintComponent and paint what is visible.
To make it more efficient, create "tiles" - list of eg. BufferedImage of constant width and create them from incoming data and draw to gui only them.
2) Something similar, but you can use one image with relatively low width and draw new data to it. When full, "append" it to so far created "left" image (initially empty). This way you frequently modify small image and big not so frequently.
If end comes before filling whole right image, join only filled part with left.
You could try to gradually increase size of right image (*X, eg *1.5 or *2) to reduce number of joining images at cost of using more memory.
You could store those "images" as byte(or int) array (1D array representing 2D, but store it by columns, not by rows for more efficient modifications), this way you can store it bit more efficiently if you know that you need some unusual ammount of bits per pixels, because some colors will never be in result image.
If image gets too big, save it to disk and clear your left image and later join them together or use them separately.

Related

How to limit repainting to monitor refresh rate (vsync)

I have a weird issue that every other guide and answer seems to contradict, but the issue seems to be deeper. OS-level deep.
System details:
Ubuntu 18.04, Unity window manager, nVidia graphics (proprietary driver)
Tried with the following Java VMs:
-OpenJDK 11 & 17,
-Temurin 11 & 17,
-JBR-17
I have an application where I draw an image on a canvas, zoomed (so it's pixelated), and I can pan around and edit with the mouse (sort of like photoshop). I do this by defining a small rectangle in the image and drawing that to the entire panel (at 4K resolution):
g.drawImage(image,
x, // dst
y,
x + visibleImageWidth * blockSize,
y + visibleImageHeight * blockSize,
ul.x, // src
ul.y,
ul.x + visibleImageWidth,
ul.y + visibleImageHeight,
this);
This works fine statically, but when I start working it with the mouse, it progressively slows down to a crawl.
I analyzed this, and it seems that the mouse fires events at 1000Hz. Then paintComponent() somehow manages to finish within that same 1ms. The OS however chokes on the amount of visual data thrown at it, and every (visual) update takes longer than the last. As long as I keep dragging the mouse, the OS crawls to a complete stop. (It seems everything non-graphical still works at normal speed, e.g. my program keeps processing input) Also visual updates of other programs stop, so it's like the graphics card or driver chokes on the data and can't process/discard it fast enough. When I let go of the mouse it stays frozen until next visual update. Then all programs instantly update their visuals and everything is back to normal.
The (heavyweight) JPanel that I have doesn't collate repaint() calls where I expect it should. Every time I call it, it immediately (from my perspective) calls paintComponent(), which finishes within 1ms, before the next call to repaint().
* Why is Java so insistent on sending graphics data to the OS at such a ridiculous speed? My monitor runs at 60Hz, not 1000Hz.
I found a very dirty workaround that at least lets me use the program in any reasonable way at all: Adding
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}
at the end of the paintComponent() method. The program now works super smooth, without visible tearing or microstuttering (even though 10ms != 60Hz).
Why do I need this delay to, of all things, speed up graphics? How can I make Java respect the monitor's refresh rate in a 'neater' way?
[Edit] MSSCC
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class Temp extends JPanel {
public static void main(String... args) {
System.out.println(LocalDate.now());
SwingUtilities.invokeLater(() -> {
JFrame f = new JFrame();
f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.setContentPane(new Temp());
f.setExtendedState(Frame.MAXIMIZED_BOTH);
f.setVisible(true);
});
}
private final BufferedImage image = new BufferedImage(10000, 10000, BufferedImage.TYPE_INT_RGB);
public Temp() {
super(null);
addMouseMotionListener(new MouseAdapter() {
#Override
public void mouseDragged(MouseEvent e) {
int x = e.getX() / 5;
int y = e.getY() / 5;
int rgb = ThreadLocalRandom.current().nextInt(0xFFFFFF);
int[] pixels = new int[100];
Arrays.fill(pixels, rgb);
image.getRaster().setDataElements(x, y, 8, 8, pixels);
repaint();
((Frame)getTopLevelAncestor()).setTitle(" (" + x + ", " + y + ')');
}
});
}
#Override
public void paintComponent(Graphics g) {
g.drawImage(image,
0, 0, getWidth(), getHeight(),
0, 0, getWidth() / 5, getHeight() / 5,
null);
}
}
Back in the day I created a Paint widget to draw like you'd do in MS Paint - and I definitely did not experience mouse move events firing at the rate you are reporting. As #GilbertLeBlanc commented, it seems like you are probably in an infinite paint loop.
Nevertheless, if this isn't the issue, you can throttle event firing like this
public class Throttle extends MouseMotionAdapter {
public static final long THRESHHOLD = 30; //ms
private lastEvTime = 0;
public void mouseMoved(MouseEvent me) {
long time = System.currentTimeMillis();
if (time > (lastEvTime + THRESHHOLD)) {
lastEvTime = time;
//do graphical update
}
// else do nothing
}
}

How to dynamically center a Graphic

Although in this example, the X-Y values are hard-coded, lets assume the user entered the values dynamically and clicked a button to view the results on the screen.
It wouldn't make sense to calculate the frame based on the largest size as the Frame would be too large for the monitor.
What is required to take the X-Y values entered (not matter how large or small) and have the image appear centered within the frame?
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class ZoomToXY extends JPanel
{
int x = 0;
public void paint(Graphics g)
{
//Can't see this.
int[] xs2 = {5308, 5306, 4143, 4143, 4120, 4119, 4118, 4117, 4116, 4114, 4112};
int[] ys2 = {4474, 5329, 5306, 5171, 5171, 5173, 5175, 5177, 5179, 5181, 5182};
BasicStroke traceStroke = new BasicStroke (1); //Line thickness
Graphics2D gc = (Graphics2D) g.create();
gc.setColor(Color.RED);
gc.drawPolyline(xs2, ys2, 11);
gc.setStroke(traceStroke);
x++;
}
public static void main(String[] args)
{
JFrame frame = new JFrame();
frame.add(new ZoomToXY());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setBounds(20,20, 250,250);
frame.setVisible(true);
}
}
The reason we can't see the polygon or whatever you're making, is because it's outside the frame's bounds. Let's take a look.
frame.setBounds(20,20, 250,250);
This line indicates we will only see what's inside these bounds, though everything outside will also be drawn but not shown. Try drawing a rectangle inside the bounds and see.
g.fillRect(20, 20, 100, 100);
You will see a rectangle. But how can I solve this issue? Since having a frame being 5000px by 5000px isn't going to work on most monitors, either you work with smaller resolutions and therefore smaller coordinates, or you implement a camera. Having a camera you can have as big world as you want and being able to move around in it. But if your frame can only show 100 pixels and your polygon is 1000px, we will only see 10% of it, this problem can easily be solved with zooming. Here is a topic how to implement a gamecamera. With the gameCamera you can simply calculate the center of your image, then translate it, quite simple. If you need assistance just ask.
A frame that is 250x250 is quite small, consider it being a little bigger. Also why set the coordinates as (20, 20)? If you want to center the JFrame to the current monitor just call:
frame.setLocationRelativeTo(null);

JPanel paintComponent(...) doesnt work

My problem is easy to explain: i want a/some JPanels, added to a JFrame, to paint themself with an image. sadly the last thing does not work. for info: the image path is correct and the JPanel size is the same as the image size.
thx for help :P
package frames;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import world.Terrain;
public class PanelTerrain extends JPanel {
private Image img;
private int x;
private int y;
private Image imga;
public PanelTerrain(Terrain terra, int x, int y) {
imga = new ImageIcon(terra.getPath()).getImage();
this.x = x;
this.y = y;
this.setBounds(x, y, 8, 8);
//this.setBackground(terra.getColor());
}
public void changeTerrain(Terrain t)
{
this.setVisible(false);
this.setBackground(t.getColor());
this.setVisible(true);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(imga, x, y, this);
}
}
My first guess is that you're passing the wrong x and y.The x and y in g.drawImage are the coordinates of the top left corner, not the size of the image, so usually they are set directly at 0 (that means, g.draWimage(imga, x, y, this).
One very big problem is your possible misuse of bounds and x and y. You're calling setBounds on the JPanel (something to avoid) but then drawing the image at some x and y that in all likelihood is way beyond the bounded size of your JPanel. Don't do this. for more complete help, please create and post your Minimal, Complete, and Verifiable example.
Here's what I think that you should do instead -- assuming that you want a grid of images, some of say earth, some water, some grass, ...
Create ImageIcons for the base images, one Icon for grass, one for water, etc...
Create a JPanel that uses a GridLayout, and fill it with a grid of JLabels.
Place those same JLabels in a 2-dimensional array of JLabel.
Swap the label icons where and when you need to change the image by calling setIcon(newIcon) on the JLabel.
For example, please see this answer of mine to a similar question as well as the other answers to the same question.

Java Image Scaling Quality Lost in JAR File

I am coding a program which has an entire JPanel zoomed in with a JScrollPane as a map, but I also want to give users a scaled & minimized view of the whole map in the corner of the JScrollPane. I am using BlueJ, & (in BlueJ, at least) I have basically gotten what I want. To scale down the map, I create a single BufferedImage, bi, that is the size of the mini-map, then take its Graphics object, g, convert it to a Graphics2D object, g2, then give g2 a single set of RenderingHints, through:
g2.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
From there, I use the Graphics.scale(double sx, double sy) method on g2 to set the scale so that upon calling the paint(Graphics g) method of the large map with g2 as its parameter, it will draw a scaled version of the large map on bi that is the exact size of the mini-map. This happens 20 times per second (somewhat arbitrary, at that speed I think it looks best. The mini-map reflects what the user is doing in real-time & I think it looks laggy if done fewer than 20 times per second.)
On my Macbook Pro, The resulting look of this code is:
There is no noticeable lag with this code when I run the program, which has led me to keep the picture re-sampling at 20 times per second, since there appears to be plenty of CPU for this. However, upon exporting my project to a JAR file, the resulting look is this:
There still is little to no lag with the program running, but what the heck happened? So long as I run the code in BlueJ, it works - but in a JAR file, the mini-map looks horrible.
For clarification, this is the map to be scaled (which, ironically, is being scaled when I'm placing it in this post):
And here are the two BlueJ & JAR file mini-maps, side-by-side:
What could be the cause of this?
When I run "java -version", this is the output:
java version "1.8.0_31"
Java(TM) SE Runtime Environment (build 1.8.0_31-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode)
Tl;dr:
When I run my code in BlueJ, the small picture looks good. After exporting to a JAR file, it looks bad. What could be causing this?
EDIT: I am now including an SSCCE to give a better idea of what I mean.
import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import javax.swing.border.*;
public class ScaleTest
{
int scrollSpeed = 16, imageSize = 200;
int rows = 40, columns = 40;
JFrame f1 = new JFrame("ScaleTest: Full Size");
JScrollPane scrollPane = new JScrollPane();
JPanel f1Panel = new JPanel(new GridLayout(rows,columns,1,1));
JFrame f2 = new JFrame("ScaleTest: Scaled");
JPanel f2Panel = new JPanel()
{
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
g2.drawImage(bi,0,0,null);
}
};
BufferedImage bi = new BufferedImage(imageSize,imageSize,BufferedImage.TYPE_INT_ARGB);
public ScaleTest()
{
f1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f1.add(scrollPane);
scrollPane.setPreferredSize(new Dimension(600,600));
scrollPane.getHorizontalScrollBar().setUnitIncrement(scrollSpeed);
scrollPane.getVerticalScrollBar().setUnitIncrement(scrollSpeed);
scrollPane.setViewportView(f1Panel);
for (int i = 0; i < rows*columns; i++)
{
JPanel p = new JPanel();
p.setBackground(Color.WHITE);
p.setBorder(BorderFactory.createLineBorder(Color.BLACK,1));
p.setPreferredSize(new Dimension(100,100));
f1Panel.add(p);
}
f1.pack();
f2.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f2.setResizable(false);
f2.add(f2Panel);
f2Panel.setPreferredSize(new Dimension(imageSize,imageSize));
f2.pack();
Graphics2D g2 = (Graphics2D)bi.getGraphics();
g2.setRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
g2.scale(((double)imageSize)/f1Panel.getWidth(),((double)imageSize)/f1Panel.getHeight());
f1Panel.paint(g2);
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
}
public static void createAndShowGUI()
{
ScaleTest s = new ScaleTest();
s.f1.setVisible(true);
s.f2.setVisible(true);
}
}
In BlueJ the resulting scaled image is this:
Yet, in the JAR file that is produced from BlueJ, running the JAR produces this image:
What could be going on?
I would guess that your BlueJ using JDK1.6.0.
java version "1.6.0_45"
java version "1.8.0_31"
I found this answer on SE: https://stackoverflow.com/a/24746194/1695856 .
The upshot of it is that one way to improve the quality of the scaled down image is to blur it and scale it down in stages. In the following example I scaled the image down by 50% three times, and then by 80%, to get a final scale of 10% of the original.
The file referred to (map.png) was the large image you posted earlier. Setting the Antialiasing rendering hint is of no use when drawing scaled images as it only has an effect on lines, ovals, rectangles etc, but not images, from what I can tell. The Interpolation rendering hint helps a little, but relying on any rendering hint is not always going to work as they may not be implemented the same (or at all!) on different platforms.
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class Rescale {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
private ConvolveOp cop;
#Override
public void run() {
JFrame frame = new JFrame("Map");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try {
BufferedImage im = ImageIO.read(new File("map.png"));
cop = new ConvolveOp(new Kernel(3, 3, new float[] { 0f, 1f / 5f, 0f, 1f / 5f, 1f / 5f, 1f / 5f, 0f, 1f / 5f, 0f }),
ConvolveOp.EDGE_NO_OP, null);
for (int i = 0; i < 3; i++) {
im = getScaled(im, 0.5);
}
im = getScaled(im, 0.8f);
JLabel lab = new JLabel(new ImageIcon(im));
frame.setContentPane(lab);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException e) {
e.printStackTrace();
}
}
private BufferedImage getScaled(BufferedImage im, double scale) {
BufferedImage nim;
Dimension dim = new Dimension((int) (im.getWidth() * scale), (int) (im.getHeight() * scale));
nim = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) nim.getGraphics();
g.scale(scale, scale);
g.drawImage(im, cop, 0, 0);
g.dispose();
return nim;
}
});
}
}
I hope this helps.

Scale images as a single surface in Java 2D API

There is a method called scale(double sx, double sy) in Graphics2D in Java. But this method seems like to scale images as separate surfaces rather than a single surface. As a result, scaled images have sharp corners if original images have no extra width and height. The following screenshot demonstrates the problem:
Here is the code:
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class TestJava {
static int scale = 10;
public static class Test extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
BufferedImage img = null;
try {
img = ImageIO.read(new File("Sprite.png"));
} catch (IOException e) {
e.printStackTrace();
}
g2.drawImage(img, null, 5, 5);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(300, 350);
frame.setVisible(true);
}
}
One possible solution to the problem is original images having extra width and height (in this case "Sprite.png"). But this does not seem to be a good way to eliminate the problem. So I am seeking for a programmatic way in Java to solve this problem rather than using an image editor. What is the way to do so?
In your example it's not the image you scale, but you set a scaling transformation on the Graphics2D object which will be applied on all operations performed on that graphics context.
If you want to scale an image, you have 2 options. All I write below uses java.awt.Image, but since BufferedImage extends Image, all this applies to BufferedImage as well.
1. Image.getScaledInstance()
You can use the Image.getScaledInstance(int width, int height, int hints) method. The 3rd parameter (the hints) tells what scaling algorithm you want to use which will affect the "quality" of the scaled image. Possible values are:
SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE, SCALE_AREA_AVERAGING
Try the SCALE_AREA_AVERAGING and the SCALE_SMOOTH for nicer scaled images.
// Scaled 3 times:
Image img2 = img.getScaledInstance(img.getWidth(null)*3, img.getHeight(null)*3,
Image.SCALE_AREA_AVERAGING);
// Tip: you should cache the scaled image and not scale it in the paint() method!
// To draw it at x=100, y=200
g2.drawImage(img2, 100, 200, null);
2. Graphics.drawImage()
You can use different Graphics.drawImage() overloads where you can specify the size of the scaled image. You can "control" the image quality with the KEY_INTERPOLATION rendering hint. It has 3 possible values:
VALUE_INTERPOLATION_NEAREST_NEIGHBOR, VALUE_INTERPOLATION_BILINEAR,
VALUE_INTERPOLATION_BICUBIC
The VALUE_INTERPOLATION_BILINEAR uses a bilinear interpolation algorithm of the 4 nearest pixels. The VALUE_INTERPOLATION_BICUBIC uses a cubic interpolation of the 9 nearby pixels.
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// To draw image scaled 3 times, x=100, y=200:
g2.drawImage(img, 100, 200, img.getWidth(null)*3, img.getHeight(null)*3, null);
Removing sharp edges
If you want to avoid sharp edges around the image, you should write a loop to go over the pixels at the edge of the image, and set some kind of transparency, e.g. alpha=0.5 (or alpha=128). You might also do this on multiple rows/columns, e.g. 0.8 alpha for the edge, 0.5 alpha for the 2nd line and 0.3 alpha for the 3rd line.
An interesting question (+1). I think that it is not trivial to find a good solution for this: The interpolation when scaling up the image always happens inside the image, and I can not imagine a way to make it blur the scaled pixels outside the image.
This leads to fairly simple solution: One could add a 1-pixel-margin around the whole image. In fact, this is the programmatic way of the solution that you proposed yourself. The resuld would look like this:
(the left one is the original, and the right one has the additional 1-pixel-border)
Here as a MCVE, based on your example
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class ScaledPaint
{
static int scale = 10;
public static class Test extends JPanel
{
BufferedImage image = createTestImage();
BufferedImage imageWithMargin = addMargin(image);
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.drawImage(image, 5, 5, null);
g2.drawImage(imageWithMargin, 30, 5, null);
}
}
private static BufferedImage createTestImage()
{
BufferedImage image =
new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.RED);
g.drawOval(0, 0, 19, 19);
g.dispose();
return image;
}
private static BufferedImage addMargin(BufferedImage image)
{
return addMargin(image, 1, 1, 1, 1);
}
private static BufferedImage addMargin(BufferedImage image,
int left, int right, int top, int bottom)
{
BufferedImage newImage =
new BufferedImage(
image.getWidth() + left + right,
image.getHeight() + top + bottom,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, left, top, null);
g.dispose();
return newImage;
}
private static BufferedImage convertToARGB(BufferedImage image)
{
BufferedImage newImage =
new BufferedImage(image.getWidth(), image.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return newImage;
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(600, 350);
frame.setVisible(true);
}
}
But...
... one problem with this approach can already be seen in the screenshot: The image becomes larger. And you'll have to take this into account when painting the image. So if your original sprites all had a nice, predefined, easy-to-handle size like 16x32, they will afterwards have a size of 18x34, which is rather odd for a tile. This may not a problem, depending on how you are handling your tile sizes. But if it is a problem, one could think about possible solutions. One solution might be to ...
take the 16x32 input image
create a 16x32 output image
paint the 16x32 intput image into the region (1,1)-(15,31) of the output image
But considering the fact that in sprites of this size, every single pixel may be important, this may have undesirable effects as well...
An aside: Altough I assume that the code that you posted was only intended as a MCVE, I'd like to point out (for others who might read this question and the code) :
You shoud NOT load images in the paintComponent method
For efficient painting, any PNG that is loaded (particularly when it contains transparency) should be converted into an image with a known type. This can be done with the convertToARGB method in my code snippet.

Categories

Resources