How do I increase the frequency JPanel is redrawn? - java

I have a simple program that draws the trajectory of a particle launched from the origin at a certain speed and angle. I created a subclass of JPanel to handle the drawing of this. My everytime my subclass is redrawn it takes the difference between the current time and the initial time(both in milliseconds), converts this to seconds, then finds the x and y coordinate of where the particle should be at that point in time, and finally takes those x and y coordinates and draws them on the screen. My problem is that my subclass seems to be redrawn at interval that seem long because there are only a few dots that are shown.
My drawing method:
private void doDrawing(Graphics g) {
Dimension size = getSize();
Insets insets = getInsets();
int w = size.width - insets.left - insets.right;
int h = size.height - insets.top - insets.bottom;
Graphics2D g2d = (Graphics2D) g;
g.drawString("Acceleration: -9.8m/s i", 0, 20);
StringBuilder b = new StringBuilder();
b.append("Current Velocity: ");
b.append(String.valueOf(sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000)).getMagnitude()));
b.append(" m/s at ");
b.append(String.valueOf(sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000)).getDirection().getDirectionDeg()));
b.append(" degrees");
g.drawString(b.toString(), 0, 30);
drawPreviousPoints(g2d);
drawCurrentPointAndAppend(g2d, w, h);
repaint();
}
private void drawCurrentPointAndAppend(Graphics2D g2d, int w, int h) {
g2d.setColor(Color.red);
double height = (length / w) * h;
Vector2D c = sim.getVector(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000));
double currentX = w
* ((sim.getX(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000))) / length);
double currentY = h
* (1 - ((sim.getY(tickSpeed
* ((System.currentTimeMillis() - initTime) / 1000))) / height));
g2d.drawLine((int) currentX, (int) currentY, (int) currentX,
(int) currentY);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_MITER));
g2d.drawLine((int) currentX, (int) (currentY),
(int) (currentX + w * (c.getX() / length)),
(int) (currentY + (h * -(c.getY() / height))));
xList.add(currentX);
yList.add(currentY);
}
private void drawPreviousPoints(Graphics2D g2d) {
g2d.setColor(Color.blue);
g2d.setStroke(new BasicStroke(7, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND));
if (!xList.isEmpty()) {
for (int i = 0; i < xList.size(); i++) {
g2d.drawLine(xList.get(i).intValue(), yList.get(i).intValue(),
xList.get(i).intValue(), yList.get(i).intValue());
}
}
}
tickSpeed is just a variable that I use to speed up or slow down the particle. It runs fine; however, the animation seems very choppy.
How do I fix this choppiness(make everything seem more "fluid")
Where should I call repaint()? Because I feel like calling it at the end of my drawing method isn't right.

An important rule of Swing- You don't control the paint process...
Don't perform these calculations within the paintComponent. The paintComponent is meant to paint the current state of the UI and may be called at any time for many reasons, most of which are outside your control.
Instead, consider using a javax.swing.Timer set to repeat at a regular interval (40ms is 25 ticks a second).
Set up a model which keeps track of the particles current been processed. When the timer ticks, calculate your particle positions and update them, then call repaint.
Within your paintComponent, simply paint the current state of your model.
Have a look at Concurrency in Swing and How to use Swing Timers for more details

The paint process is internally handled so you can not control the frequency of it's execution.
However, you can create separate threads or timers which can invoke processes at your desired frequency. Use the paint method only to render on your canvas, do other logic and processing in another function.

Related

Swing translate scale change order misplacement

I've found something weird when splitting a translate operation around a scaling one with Java Swing. Maybe I'm doing something stupid but I'm not sure where.
In the first version I center the image, scale it and then translate it to the desired position.
In the second version I directly scale the image and then translate to the desired position compensating for having a non centered image.
The two solutions should be equivalent. Also this is important when considering rotations around a point and motion in another.. I've code that does that too... but why this does not work?
Here are the two versions of the code. They are supposed to do the exact same thing but they are not. Here are the screenshots:
First produces: screenshot1
Second produces: screenshot2
I think that the two translation operations in draw1 surrounding the scale operation should be equivalent to the scale translate operation in draw2.
Any suggestion?
MCVE:
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.net.URL;
public class Asteroid extends JComponent implements ActionListener {
public static final Dimension FRAME_SIZE = new Dimension(640, 480);
public double x = 200;
public double y = 200;
public int radius = 40;
private AffineTransform bgTransfo;
private final BufferedImage im2;
private JCheckBox draw1Check = new JCheckBox("Draw 1", true);
Asteroid() {
BufferedImage img = null;
try {
img = ImageIO.read(new URL("https://i.stack.imgur.com/CWJdo.png"));
} catch (Exception e) {
e.printStackTrace();
}
im2 = img;
initUI();
}
private final void initUI() {
draw1Check.addActionListener(this);
JFrame frame = new JFrame("FrameDemo");
frame.add(BorderLayout.CENTER, this);
frame.add(BorderLayout.PAGE_START, draw1Check);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
Asteroid asteroid = new Asteroid();
}
#Override
public Dimension getPreferredSize() {
return FRAME_SIZE;
}
#Override
public void paintComponent(Graphics g0) {
Graphics2D g = (Graphics2D) g0;
g.setColor(Color.white);
g.fillRect(0, 0, 640, 480);
if (draw1Check.isSelected()) {
draw1(g);
} else {
draw2(g);
}
}
public void draw1(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//scaling
bgTransfo.scale(stretchx, stretchy);
//translation
bgTransfo.translate(x / stretchx, y / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
public void draw2(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//scale
bgTransfo.scale(stretchx, stretchy);
//translate and center
bgTransfo.translate((x - radius) / stretchx, (y - radius) / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
}
Not sure if this question is still really open. Anyway here is my answer.
I think the crucial part to understand this behavior is the difference between AffineTransform.concatenate and AffineTransform.preConcatenate methods. The thing is that resulting transformation depends on the order the sub-transformations are applied.
To quote the concatenate JavaDoc
Concatenates an AffineTransform Tx to this AffineTransform Cx in the most commonly useful way to provide a new user space that is mapped to the former user space by Tx. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by Tx and then transforming the result by the original transform Cx like this: Cx'(p) = Cx(Tx(p))
compare this with preConcatenate:
Concatenates an AffineTransform Tx to this AffineTransform Cx in a less commonly used way such that Tx modifies the coordinate transformation relative to the absolute pixel space rather than relative to the existing user space. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by the original transform Cx and then transforming the result by Tx like this: Cx'(p) = Tx(Cx(p))
The scale and translate methods are effectively concatenate. Lets call 3 transformations in your draw1 method C (center), S (scale), and T (translate). So your compound transformation is effectively C(S(T(p))). Particularly it means that S is applied to the T but not to the C so your C does not really center the image. A simple fix would be to change the order of S and C but I think that a more proper fix would be something like this:
public void draw3(Graphics2D g) {
//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
AffineTransform bgTransfo = new AffineTransform();
//translation
bgTransfo.translate(x, y);
//scaling
bgTransfo.scale(stretchx, stretchy);
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
I think the big advantage of this method is that you don't have to re-calculate the T using stretchx/stretchy

Resizable according to screen resolution, Java Application

I am just about to have this app ready to distribute. However i still haven't found out a way to make a app suitable for whatever screen. In fact I am using pngs file for background and buttons (mainly) into JLabels, and this is a huge problem i think. What i would like to achieve is to have the JFrame and its components resizable but maintaining the proportions, just like for a responsive website!
Other ways i was planning of creating pngs of the originals but smaller and then create new frames and while booting the app ask the system what frame should suite the screen best and use it (but i know i would not be great and then of course os sometimes use screen resolution in different ways making is appear smaller or larger, if you know what i mean).
(I am using Netbeans).
Thank you a lot, i am looking foreword to discuss with you this issue that i am sure will concern many others.
Toolkit.getDefaultToolkit() has methods that provide you with what you need, including getting the current screen size:
private void makeFrameFullSize(JFrame aFrame)
{
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
aFrame.setSize(screenSize.width, screenSize.height);
}
This post discusses Resize image while keeping aspect ratio in Java.
Now that you have the screen size, you can calculate the ratio of image height/width to current frame size and scale on the % difference of current frame size to full screen size.
private Dimension get getFrameToScreenRatio(Frame aFrame){
Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
return dimension.setSize(aFrame.getWidth()/dimension.getWidth(), aFrame.getHeight()/dimension.getHeight());
}
Here is an example utility class that can scale an image to fit a canvas (like a background image) and scales it to fit:
package net.codejava.graphics;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
/**
* This utility class draws and scales an image to fit canvas of a component.
* if the image is smaller than the canvas, it is kept as it is.
*
* #author www.codejava.net
*
*/
public class ImageDrawer {
public static void drawScaledImage(Image image, Component canvas, Graphics g) {
int imgWidth = image.getWidth(null);
int imgHeight = image.getHeight(null);
double imgAspect = (double) imgHeight / imgWidth;
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
double canvasAspect = (double) canvasHeight / canvasWidth;
int x1 = 0; // top left X position
int y1 = 0; // top left Y position
int x2 = 0; // bottom right X position
int y2 = 0; // bottom right Y position
if (imgWidth < canvasWidth && imgHeight < canvasHeight) {
// the image is smaller than the canvas
x1 = (canvasWidth - imgWidth) / 2;
y1 = (canvasHeight - imgHeight) / 2;
x2 = imgWidth + x1;
y2 = imgHeight + y1;
} else {
if (canvasAspect > imgAspect) {
y1 = canvasHeight;
// keep image aspect ratio
canvasHeight = (int) (canvasWidth * imgAspect);
y1 = (y1 - canvasHeight) / 2;
} else {
x1 = canvasWidth;
// keep image aspect ratio
canvasWidth = (int) (canvasHeight / imgAspect);
x1 = (x1 - canvasWidth) / 2;
}
x2 = canvasWidth + x1;
y2 = canvasHeight + y1;
}
g.drawImage(image, x1, y1, x2, y2, 0, 0, imgWidth, imgHeight, null);
}

Way to repaint this JPanel after given count

Hello I would like to prevent graphics drawing and drawing again but I don't know how to do, I just want my panel delete all painted graphics and restart with same code. I tried some methods posted here but no one does the job.
public class Main extends JPanel implements ActionListener {
Timer timer;
private double angle = 444;
private double scale = 1;
private double delta = 0.0001;
RoundRectangle2D.Float r = new RoundRectangle2D.Float();
int counter = 0;
public Main() {
timer = new Timer(55, this);
timer.start();
}
public void paint(Graphics g) {
counter++;
int h = getHeight();
int w = getWidth();
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(new Color(randomNumber(0, 155), randomNumber(0, 255),randomNumber(0, 155), randomNumber(0, 255)));
drawCircles(g2d, getWidth()/2, getHeight()/2, 250);
if(counter > 200){
g2d.clearRect (0, 0, getWidth(), getHeight());
super.paintComponent(g2d);
counter = 0;
}
}
public int randomNumber(int min, int max) {
int c = new Random().nextInt((max - min) + 1);
return c;
}
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setUndecorated(true);
Dimension dim = new Dimension(Toolkit.getDefaultToolkit()
.getScreenSize().width, Toolkit.getDefaultToolkit()
.getScreenSize().height);
frame.setSize(dim);
frame.setLocation(0, 0);
frame.setBackground(new Color(0, 0, 0, 255));
frame.add(new Main());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
void drawCircles(Graphics graphics, int xMid, int yMid, int radius) {
// end recursion
if(radius < 5)
return;
// Draw circle
// start recursion
//left
drawCircles(graphics, xMid-radius, yMid, radius / 2);
((Graphics2D) graphics).rotate(angle);
graphics.drawOval(xMid - radius, yMid - radius, radius * 2, radius * 2);
//right
drawCircles(graphics, xMid+radius, yMid, radius / 2);
graphics.drawOval(xMid - radius, yMid - radius, radius * 2, radius * 2);
((Graphics2D) graphics).rotate(angle);
((Graphics2D) graphics).rotate(angle);
((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
((Graphics2D) graphics).setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
}
public void actionPerformed(ActionEvent e) {
if (scale < 0.01)
delta = -delta;
else if (scale > 0.99)
delta = -delta;
scale += delta;
angle += 0.001;
repaint();
}
}
I am not sure I understand you fully, but you can use a JToggleButton (for example) where is the toggle button is down it prevents drawing. I can see something like this inside your drawCircles() method:
void drawCircles(Graphics graphics, int xMid, int yMid, int radius)
{
if(!toggleBtn.isSelected() // the toggle button is pressed
{
// draw something
}
}
In your example, you are drawing two circles and two ovals. If I understood you correctly, you want to be able to pause in the middle of the method, for example, and only draw the first circle. Then, at some point, you want to continue drawing the two ovals and the remaining circle. Unfortunately, you cannot do that. You cannot stop (or pause) a method in the middle of it.
Methods have to execute to completion (whether to the end, or an exception is thrown). However, you can create some kind of task to draw ONE shape (for example, a circle). If you create multiple tasks, you can draw many circles. To accomplish this, you will need to learn about Concurrency and probably about Java Tasks. You can have these tasks execute in some kind of order and because of concurrency, you could pause and resume these drawing tasks the way I think you would want.

JFrame duplicates drawings when resizing

I wrote a code in Java (using swing) which draws few polygons on a panel.
public MyClass extends JPanel
The code is very simple (but long) and basically adds few Polygons, then adds few points to each polygon and then draw them on the screen (with drawPolygon).
My problem is when I run the program, I can't see the drawings on the panel.
After a while, I figure out that when I re-size my frame, I can suddenly see the drawing but it duplicates itself many times (depends how much I re-size). If I play enough time with the resizing, I get:
java.lang.OutOfMemoryError: Java heap space
Also, myPolygon.invalidate() doesn't help.
When using setResizable(false) I can't see my drawing at all.
Does anyone have a solution?
Duplicate Image Screenshot:1
To start with, in your paintComponent method, don't call
setPreferredSize(new Dimension(500,500));
setVisible(true);
validate();
This will request a repaint, cause paintComponent to be recalled and you'll end up in a nasty loop, consuming your CPU and (as you have found out), your memory.
IF you can get away with it, you're better off to draw the polygon to a buffer and draw the buffer to the screen on each iteration of the paintComponent. This will be faster in the long run...
// Create a field
private BufferedImage buffer;
// Call this when you need to change the polygon some how...
protected void createBuffer() {
// You need to determine the width and height values ;)
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
int xoffset=5;//Multiply in order to "zoom" the picture
int offset=0;//moves shape to the right
p.addPoint(40*xoffset-offset, 30*xoffset-offset);
p.addPoint(50*xoffset-offset,30*xoffset-offset);
p.addPoint(57*xoffset-offset,37*xoffset-offset);
p.addPoint(57*xoffset-offset,47*xoffset-offset);
p.addPoint(50*xoffset-offset,54*xoffset-offset);
p.addPoint(40*xoffset-offset,54*xoffset-offset);
p.addPoint(33*xoffset-offset,47*xoffset-offset);
p.addPoint(33*xoffset-offset, 37*xoffset-offset);
g.drawPolygon(p);
g.dispose();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (buffer != null) {
Graphics2D g2d = (Graphics2D)g;
g2d.drawImage(buffer, translateX, translateY, this);
}
}
UPDATE
So, anyway, the other fun things you're doing are...
Creating a static reference to your Polygon. Hope you weren't intending to have more the one on the screen at a time ;)
Add new points to an already existing polygon (each time paintComponent was called)
Translating the polygon each time paintComponent was called
Try something like this instead
public class RoundTop extends JPanel {
//Polygons declarations
private Polygon p = new Polygon();
//Translate variables;
private int translateX = 10;
private int translateY = 10;
public RoundTop() {
int xoffset = 5;//Multiply in order to "zoom" the picture
int offset = 0;//moves shape to the right
p.addPoint(40 * xoffset - offset, 30 * xoffset - offset);
p.addPoint(50 * xoffset - offset, 30 * xoffset - offset);
p.addPoint(57 * xoffset - offset, 37 * xoffset - offset);
p.addPoint(57 * xoffset - offset, 47 * xoffset - offset);
p.addPoint(50 * xoffset - offset, 54 * xoffset - offset);
p.addPoint(40 * xoffset - offset, 54 * xoffset - offset);
p.addPoint(33 * xoffset - offset, 47 * xoffset - offset);
p.addPoint(33 * xoffset - offset, 37 * xoffset - offset);
p.translate(translateX, translateY);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.drawPolygon(p);
g2d.dispose();
}
}

Java - Does subpixel line accuracy require an AffineTransform?

I've never worked with Java drawing methods before, so I decided to dive in and create an analog clock as a PoC. In addition to the hands, I draw a clock face that includes tick marks for minutes/hours. I use simple sin/cos calculations to determine the position of the lines around the circle.
However, I've noticed that since the minute tick-marks are very short, the angle of the lines looks wrong. I'm certain this is because both Graphics2D.drawLine() and Line2D.double() methods cannot draw with subpixel accuracy.
I know I can draw lines originating from the center and masking it out with a circle (to create longer, more accurate lines), but that seems like such an inelegant and costly solution. I've done some research on how to do this, but the best answer I've come across is to use an AffineTransform. I assume I could use an AffineTransform with rotation only, as opposed to having to perform a supersampling.
Is this the only/best method of drawing with sub-pixel accuracy? Or is there a potentially faster solution?
Edit: I am already setting a RenderingHint to the Graphics2D object.
As requested, here is a little bit of the code (not fully optimized as this was just a PoC):
diameter = Math.max(Math.min(pnlOuter.getSize().getWidth(),
pnlOuter.getSize().getHeight()) - 2, MIN_DIAMETER);
for (double radTick = 0d; radTick < 360d; radTick += 6d) {
g2d.draw(new Line2D.Double(
(diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.1d,
(diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.1d,
(diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.05d,
(diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.05d));
} // End for(radTick)
Here's a screenshot of the drawing. It may be somewhat difficult to see, but look at the tick mark for 59 minutes. It is perfectly vertical.
Line2D.double() methods cannot draw
with subpixel accuracy.
Wrong, using RenderingHints.VALUE_STROKE_PURE the Graphics2D object can draw "subpixel" accuracy with the shape Line2D.
I assume I could use an
AffineTransform with rotation only, as
opposed to having to perform a
supersampling. Is this the only/best
method of drawing with sub-pixel
accuracy? Or is there a potentially
faster solution?
I think you are missing somthing here. The Graphics2D object already holds a AffineTransform and it is using it for all drawing actions and its cheap performance wise.
But to get back to you what is missing from your code - this is missing:
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
Below is a self contained example that produces this picture:
public static void main(String[] args) throws Exception {
final JFrame frame = new JFrame("Test");
frame.add(new JComponent() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
System.out.println(g2d.getTransform());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
double dia = Math.min(getWidth(), getHeight()) - 2;
for (int i = 0; i < 60 ; i++) {
double angle = 2 * Math.PI * i / 60;
g2d.draw(new Line2D.Double(
(dia / 2) + Math.cos(angle) * dia / 2.1d,
(dia / 2) + Math.sin(angle) * dia / 2.1d,
(dia / 2) + Math.cos(angle) * dia / 2.05d,
(dia / 2) + Math.sin(angle) * dia / 2.05d));
}
g2d.draw(new Ellipse2D.Double(1, 1, dia - 1, dia - 1));
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setVisible(true);
}

Categories

Resources